[Linux] 파일 접근 프리미티브

파일 접근 프리미티브

파일

: 연속된 바이트의 나열. 특정한 포맷이 정해져 있지는 않음(각 응용 프로그램에서 해석)

: 보조저장장치 뿐만 아니라 외부 입출력 장치에 대한 인터페이스 (e. 장치 파일)

 

시스템 호출(system call)

: 커널에 서비스 요청을 위한 인터페이스. 응용 프로그램은 시스템 호출을 통해 커널에 서비스를 요청

 

파일 접근 프리미티브(file access primitives)

: 커널에서 파일을 다루기 위해 제공되는 I/O 설비에 직접 접근을 제공하는 시스템 호출 함수들의 집합

- 리눅스 I/O를 위한 기초적 요소를 형성. 다른 파일 접근 방식은 이에 기반함

시스템 호출 함수 용도
open 파일을 열거나 빈 파일 생성
creat 빈 파일 생성
close 열린 파일을 닫음
read 파일로부터 정보를 추출
write 파일에 정보를 기록
lseek 파일 안에 지정된 바이트로 이동
unlink 파일을 제거
remove 파일을 제거
fcntl 한 파일에 연관된 속성 제어

 

파일 열기: open()

: 파일을 사용하기 위해서는 먼저 open() 시스템 호출 함수를 이용해 접근할 파일을 열어야

- 파일 디스크립터(file descriptor) : 열린 파일에 대한 식별 번호. 0 이상의 양의 정수값

#include <sys/types.h>  // 다양한 자료형 정의
#include <sys/stat.h>   // 파일의 상태 및 정보
#include <fcntl.h>      // file control header

int open(const char* path, int olfag, [mode_t mode]);
// 파일 열기에 성공하면 파일 디스크립터를, 실패하면 -1을 반환

 

open()의 기본 플래그

: open()의 두 번째 인수 oflag<fcntl.h>에 정의된 정수 상수 사용

  • O_RDONLY : 읽기 전용으로 파일 열기. read() 호출 사용 가능
  • O_WRONLY : 쓰기 전용으로 파일 열기. write() 호출 사용 가능
  • O_RDWR : 읽고 쓰기용으로 파일 열기. read(), write() 호출 사용 가능

 

파일 닫기: close()

: 파일 사용을 마치면 close() 시스템 호출 함수를 이용해 열린 파일을 닫아야 함

- why) ① 한 프로세스 내에서 동시에 열 수 있는 파일 수에 제한이 있음 / ② 안전하게 파일의 사용을 마침

#include <unistd.h>

int close(int fd);
// 파일 닫기에 성공하면 0, 실패하면 -1을 반환

실습(파일 열고 닫기): open.c, mycat.c

open.c

#include <fctl.h>    // open(), O_WRONLY
#include <unistd.h>  // close()
#include <stdio.h>
#include <stdlib.h>  // exit()

int main(int arg, char* argv[])
{
    int fd;
    if ((fd = open(argv[1], O_RDONLY)) == -1) {
        printf("파일 열기 오류\n");
        exit(-1);
    }
    
    printf("파일 %s 열기 성공: %d\n", argv[1], fd);
    
    close(fd);
    
    exit(0);
}

실습 결과

~/fio$ gcc -o open open.c
~/fio$ ls -F
cmd_arg*  cmd_arg.c  open*  open.c  readme
~/fio$ ./open abcd
파일 열기 오류
~/fio$ ./open readme
파일 readme 열기 성공: 3
~/fio$

 

mycat.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define BUFSIZE 1024

int main(int argc, char* argv[]) // ./mycat readme
{
	char buf[BUFSIZE];
	int fd;
	ssize_t nread;

	if ((fd = open(argv[1], O_RDONLY)) == -1 ) {
		perror(argv[1]);
		exit(1);
	}

	while ((nread = read(fd, buf, BUFSIZE) > 0)) { /* fd로부터 buf에 읽어들이기 */
		/* buf의 내용을 표준출력에 쓰기 */
		write(1, buf, nread);
	}

	/* 파일 닫기 */
	close(fd);

	exit(0);
}

open()의 추가 플래그

플래그 설명
O_CREAT 해당 파일이 없는 경우에 생성
O_TRUNC 파일이 이미 있는 경우 내용을 지움
O_EXCL O_CREAT와 함께 사용. 해당 파일이 이미 있으면 오류 발생
O_APPEND 데이터를 쓰면 파일 끝에 추가
O_NONBLOCK 넌-블로킹 모드로 입출력
O_SYNC write() 호출시 디스크에 물리적으로 쓴 후 반환
블로킹(Blocking) vs. 논블로킹(Non-blocking)

기본모드(Blocking) : 데이터를 읽으려는데 데이터가 없으면 들어올 때까지 프로세스를 재우고 대기 ➡ I/O가 완료될 때까지 함수(read, write)에서 빠져나오지 못함

O_NONBLOCK (non-blocking) : 데이터가 없으면 대기하지 않고 즉시 에러(-1)를 반환. 즉, 함수 호출 시 즉시 결과(성공 혹은 실패)를 받고 다음 코드를 실행

 

파일의 생성

: open()을 이용해 쓰기 모드로 연다

플래그 설명
O_WRONLY | O_CREAT 파일 존재 시 시작 위치부터 overwrite
O_WRONLY | O_CREAT | O_TRUNC 파일 존재 시 대상 파일의 내용을 지우고 크기를 0으로
O_WRONLY | O_CREAT | O_EXCL 파일이 존재하면 오류 발생
O_WRONLY | O_CREAT | O_APEND 기존 파일 끝에 추가

 

open()의 모드

: open()의 세 번째 인수 mode는 선택적으로 사용

  - 생성되는 파일의 접근 권한 지정할 때(O_CREAT 플래그와 함께 사용됨)

  - 설정값: <sys/stat.h>에 정의된 접근권한에 대한 상수 이용 (8진수 표기법 e. 0644:-rw-r--r--)

 

파일 생성의 다른 방법

: 일반적인 저장용 파일 생성 후 열기

- open(path, O_WRONLY|O_CREAT|O_TRUNC, mode); 를 이용한 경우와 동일

#include <sys/types.h>   // 다양한 자료형 정의
#include <sys/stat.h>    // 파일의 상태 및 정보
#include <fcntl.h>       // file control header)

int creat(const char* path, mode_t mode);
// 파일 열기에 성공하면 파일 디스크립터를, 실패하면 -1을 반환

실습(파일생성): create.c

#include <fcntl.h>   // open(), creat(), O_WRONLY
#include <unistd.h>  // close()
#include <stdio.h>
#include <stdlib.h>  // exit()

#define PERMS 0644

int main(int argc, char* argv[])
{
    int fd;

    if ((fd = creat(argv[1], 0600)) == -1) {
        printf("파일 열기 오류\n");
        exit(-1);
    }
    
    printf("파일 %s 열기 성공: %d\n", argv[1], fd);
    
    close(fd);
    
    exit(0);
}

파일 읽기: read()

: fd가 나타내는 파일에서 nbytes 만큼 한번에 읽어 buf에 저장

   - 읽을 데이터가 nbytes보다 적으면 있는 만큼만 읽음

   - read() 호출 때마다 파일 위치 포인터읽은 바이트 만큼 이동

#include <unistd.h>

ssize_t read(int fd, void* buf, size_t nbytes);
// 파일 읽기에 성공하면 읽은 바이트 수 반환
// 파일 끝을 만나면 0을, 실패하면 -1을 반환

 

파일 위치 포인터(file position pointer)

: 파일 내에 읽거나 쓸 현재 파일 위치(current file position)를 가리킴

- 기본적으로 파일 열기 시 시작점을 가리킴 (O_APPEND 플래그 사용시 마지막 위치로 자동 갱신)

- 읽기 쓰기 동작 시 자동으로 갱신

 

파일 읽기: write()

: fd가 나타내는 파일에 buf에 있는 nbytes 만큼의 데이터를 저장.

- write() 호출 때마다 파일 위치 포인터마지막으로 쓰인 바이트 바로 뒤로 이동됨

#include <unistd.h>

ssize_t write(int fd, void* buf, size_t nbytes);
// 파일 쓰기가 성공하면 실제 저장된 바이트 수 반환
// 실패하면 -1을 반환

실습: mycp.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define BUFSIZE 1024
#define PERMS 0644

int main(int argc, char* argv[])
{
	char buf[BUFSIZE];
	int fd_r, fd_w;
	ssize_t nread;

	if (argc != 3) {
		fprintf(stderr, "usage: %s file1 file2\n", argv[0]);
		exit(1);
	}

	if ((fd_r = open(argv[1], O_RDONLY))/* argv[1]을 읽기 모드로 열기 */ == -1) {
		perror(argv[1]);
		exit(2);
	}

	if ((fd_w = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC))/* argv[2]를 쓰기 모드로 열기 */ == -1) {
		perror(argv[2]);
		exit(3);
	}


	while ((nread = read(fd_r, buf, BUFSIZE)) > 0) {
		
		if (write(fd_w, buf, nread) < nread) {
			close(fd_r);
			close(fd_w);

			perror("File write error");
			exit(4);
		}
	}

	/* 모든 파일 닫기 */
	close(fd_r);
	close(fd_w);

	if (nread == -1) {
		perror("File read error");
		exit(5);
	}

	exit(0);
}

파일 디스크립터의 복제: dup(), dup2()

: 열려 있는 한 파일을 공유하기 위해 사용. 추후 파일 입출력 리다이렉션 시 사용

#include <unistd.h>

int dup(int oldfd);
// oldfd에 대한 복제본인 새로운 파일 디스크립터를 생성하여 반환
// 실패하면 -1을 반환

int dup2(int oldfd, int newfd);
// oldfd을 newfd에 복제하고 복제된 새로운 파일 디스크립터를 반환
// 실패하면 -1을 반환

실습: dup.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define FILENAME "dup_test"

int main(void)
{
	int fd, fd_cp;

	if ((fd = creat(FILENAME, 0600)) == -1)
		perror(FILENAME);

	write(fd, "Hello", 6);

	fd_cp = dup(fd);

	write(fd_cp, "World\n", 6);

	exit(0);
}

실행결과

minjeong@ubuntu:~/Documents/test$ gcc -o dup dup.c
minjeong@ubuntu:~/Documents/test$ ./dup
minjeong@ubuntu:~/Documents/test$ ls
dup  dup.c  dup_test  helloworld  helloworld.c
minjeong@ubuntu:~/Documents/test$ cat dup_test
HelloWorld

파일 임의 접근

파일 위치 포인터의 이동: lseek()

: 파일 위치 포인터를 whence를 기준으로 offset만큼 이동한 곳으로 변경

  • offset : 양수이면 파일 끝 쪽으로, 음수이면 파일 앞쪽으로
  • whence: SEEK_SET(파일의 시작), SEEK_CUR(현재 위치), SEEK_END(파일의 끝)

#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
// 이동에 성공하면 현재 위치를, 실패하면 -1을 반환

 

 

파일 위치 포인터의 이동 예(1/3) : 기본 이동

  • lseek(fd, 0L, SEEK_SET); : 파일 시작 위치로 이동(rewind)
  • lseek(fd, 100L, SEEK_SET); : 파일 시작 위치에서 100바이트 떨어진 위치로 이동
  • lseek(fd, 0L, SEEK_END); : 파일 끝 위치로 이동(append)

 

파일 위치 포인터의 이동 예(2/3) : 레코드 단위 이동(구조체 변수 record 가정)

  • lseek(fd, n * sizeof(record), SEEK_SET); : n + 1 번째 레코드 시작 위치로
  • lseek(fd, sizeof(record), SEEK_CUR); : 다음 레코드 시작 위치로
  • lseek(fd, -sizeof(record), SEEK_CUR); : 전 레코드 시작위치로

 

파일 위치 포인터의 이동 예(3/3) : 파일의 끝 너머로 이동

  • lseek(fd, sizeof(record), SEEK_END); : 파일의 끝에서 한 레코드 다음 위치로

실습: lseek_test.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

int main() {
    char* filename = "test.txt";
    char* data = "Hello, this is a test file for lseek!";

    // 파일 생성 및 데이터 쓰기
    int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        printf("Failed to open file: %s\n", strerror(errno));
        exit(1);
    }

    // 데이터 쓰기
    write(fd, data, strlen(data));

    /* 파일의 시작으로 이동 (SEEK_SET) */
    printf("1. Moved to the beginning of the file: %ld\n", (long) lseek(fd, 0L, SEEK_SET));

    /* 현재 위치에서 5바이트 앞으로 이동 (SEEK_CUR) */
    printf("2. Moved 5 bytes from the current position: %ld\n", (long) lseek(fd, -5L, SEEK_CUR));

    char buff;
    read(fd, &buff, 1);
    printf("3. Read data at current position: %c\n", buff);

    /* 파일 끝에서 10바이트 뒤로 이동 (SEEK_END) */
    printf("4. Moved 10 bytes back from the end of the file: %ld\n", (long) lseek(fd, 10L, SEEK_END));

    // 현재 위치에서 데이터 읽기
    char buffer[100] = { '\0' };
    read(fd, buffer, 10);
    printf("5. Read data at current position: %s\n", buffer);

    // 파일 닫기
    close(fd);

    return 0;
}

 

실습하기: 임의 접근을 이용한 학생 레코드 검색

개요

: 학생 정보를 레코드 단위로 파일 입출력

- 임의 접근 프리미티브를 이용해 각 레코드에 접근

- 학번(id), 이름(name), 점수(score) 저장 : 구조체 타입으로 정의해 사용 (student.h)

#ifndef _STUDENT_H_
#define _STUDENT_H_

#define MAX 24
#define START_ID 1001001

typedef struct student {
	char	name[MAX];
	int		id;
	double	gpa;     // Grade Point Average
} STUD_T;

#endif

 

 

레코드 수정 과정 (in stud_create.c)

: 파일에서 해당 레코드를 읽어 화면에 표시 후 원래 위치에 수정된 레코드 기록

- lseek()를 이용해 레코드 크기 만큼 이동 필요

 

makefile

CC = gcc
CFLAGS = -Wall
TARGETS = stud_create stud_list stud_search stud_update

all: $(TARGETS)

stud_create: stud_create.c student.h
	$(CC) $(CFLAGS) -o stud_create stud_create.c

stud_list: stud_list.c student.h
	$(CC) $(CFLAGS) -o stud_list stud_list.c

stud_search: stud_search.c student.h
	$(CC) $(CFLAGS) -o stud_search stud_search.c

stud_update: stud_update.c student.h
	$(CC) $(CFLAGS) -o stud_update stud_update.c

clean:
	rm -f $(TARGETS) mydb

 

stud_create.c : 키보드로 입력(^d로 종료)한 데이터를 파일에 저장

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#include "student.h"

int main(int argc, char* argv[])  // ./create mydb
{
	int fd;
	STUD_T rec;

	if (argc < 2) {
		fprintf(stderr, "usage: %s file\n", argv[0]);
		exit(1);
	}

	/* 첫번째 명령행 인자로 전달된 이름으로 쓰기 용도의 파일 열기 */
	if ((fd = open(argv[1], O_WRONLY | O_CREAT, 0644)) == -1) {
		perror(argv[1]);
		exit(2);
	}

	printf("%-9s %-13s %-4s\n", "ID", "NAME", "GPA");
	while (scanf("%d %s %lf", &rec.id, rec.name, &rec.gpa) == 3) {
		/* 해당 학번의 학생 정보 위치로 파일 포인터 이동 */
		lseek(fd, (rec.id - START_ID) * sizeof(rec), SEEK_SET);

		/* fd가 가리키는 파일에 rec 저장 */
		write(fd, &rec, sizeof(rec));
	}

	/* 파일 닫기 */
	close(fd);

	return 0;
}

 

stud_list.c : 파일에 저장된 전체 학생 정보 일괄 조회

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#include "student.h"

int main(int argc, char* argv[])
{
	int fd;
	STUD_T rec;

	if (argc < 2) {
		fprintf(stderr, "usage: %s file\n", argv[0]);
		exit(1);
	}

	/* 첫번째 명령행 인자로 전달된 이름으로 읽기 용도의 파일 열기 */
	if ((fd = open(argv[1], O_RDONLY)) == -1) {
		perror(argv[1]);
		exit(2);
	}

	while ( 1 ) {
		/* fd가 가리키는 파일로부터 하나의 레코드를 rec에 읽어 들이기 */
		if (read(fd, &rec, sizeof(rec)) <= 0)
			break;

		/*rec의 id필드가 0이면*/
		if (rec.id == 0)
			continue;

		printf("ID: %d\t NAME: %-10s\t GPA: %.1f\n", rec.id, rec.name, rec.gpa);
	}

	/* 파일 닫기 */
	close(fd);
	return 0;
}

 

stud_search.c : 학번을 기준으로 특정 학생 정보 조회

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#include "student.h"

int main(int argc, char* argv[])
{
	int fd;
	STUD_T rec;
	char c;

	if (argc < 2) {
		fprintf(stderr, "usage: %s file\n", argv[0]);
		exit(1);
	}

	/* 첫번째 명령행 인자로 전달된 이름으로 읽기 용도의 파일 열기 */
	if ((fd = open(argv[1], O_RDONLY)) == -1) {
		perror(argv[1]);
		exit(2);
	}

	do {
		int id;
		printf("\nEnter student ID to search: ");
		if (scanf("%d", &id) == 1) {
			/* 해당 학번의 학생 정보 위치로 파일 포인터 이동 */;
			lseek(fd, (id - START_ID) * sizeof(rec), SEEK_SET);

			/* fd가 가리키는 파일로부터 하나의 레코드를 rec에 읽어 들이기 */
			if ((read(fd, &rec, sizeof(rec)) > 0) && (rec.id != 0))
				printf("ID: %d\t Name: %-10s\t GPA: %.1f\n", rec.id, rec.name, rec.gpa);
			else
				printf("Record %d not found\n", id);
		}
		else
			printf("Input error");

		printf("Continue? (Y/N) ");
		scanf(" %c", &c);
	} while (c == 'Y' || c == 'y');

	/* 파일 닫기 */
	close(fd);
	
	return 0;
}

 

stud_update.c : O_RDWR 플래그로 파일을 열어서 → 학번을 기준으로 특정 학생의 정보를 보여준 후 → 새 성적을 입력받아 그 학생의 성적 정보를 갱신

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#include "student.h"

int main(int argc, char* argv[])
{
	int fd;
	STUD_T rec;
	char c;

	if (argc < 2) {
		fprintf(stderr, "usage: %s file\n", argv[0]);
		exit(1);
	}

	/* 첫번째 명령행 인자로 전달된 이름으로 읽기/쓰기 용도의 파일 열기 */
	if ((fd = open(argv[1], O_RDWR)) == -1) {
		perror(argv[1]);
		exit(2);
	}

	do {
		int id;
		printf("\nEnter student ID to modify: ");
		if (scanf("%d", &id) == 1) {
			/* 해당 학번의 학생 정보 위치로 파일 포인터 이동 */
			lseek(fd, (id - START_ID) * sizeof(rec), SEEK_SET);

			/* fd가 가리키는 파일로부터 하나의 레코드를 rec에 읽어 들이기 */
			if ((read(fd, &rec, sizeof(rec)) > 0) && (rec.id != 0)) {
				printf("ID: %d\t Name %s\t GPA %.1f\n", rec.id, rec.name, rec.gpa);

				printf("Enter new gpa: ");
				scanf("%lf", &rec.gpa);

				/* 이전 레코드 위치로 파일 포인터 이동 */
				lseek(fd, (long) - sizeof(rec), SEEK_CUR);
				/* fd가 가리키는 파일에 rec 저장 */
				write(fd, &rec, sizeof(rec));
			}
			else
				printf("Record %d not found\n", id);
		}
		else
			printf("Input error\n");

		printf("Continue? (Y/N) ");
		scanf(" %c", &c);
	} while (c == 'Y' || c == 'y');

	/* 파일 닫기 */
	close(fd);
	return 0;
}