Search
Duplicate
🗃️

리눅스가 파일을 다루는 방식

간단소개
파일 디스크립터 및 파일 입출력 함수에 대해 정리해 보았다!
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
unix
File I/O
태그
File I/O
Scrap
8 more properties
libft에서 인자로 받는 fd를 활용해 함수를 구현할 때 등장하는 File Descriptor
get_next_line에서 파일을 읽기 위해 필요했던 함수 openread
두 과제를 거치면서 파일 디스크립터 및 파일 입출력 함수에 대해 공부한 내용을 조금이나마 정리해 보았다.
리눅스에서 파일에 접근하는 가장 기본적인 방법은 “시스템 콜”을 활용하는 것이다.
시스템 콜(System Call) 이란? 운영체제는 커널 모드와 사용자 모드로 나뉘어 구동된다. 시스템 콜은 커널 모드의 기능을 사용자 모드가 사용할 수 있도록, 즉 프로세스가 하드웨어에 직접 접근해서 필요한 기능을 사용할 수 있게 해준다.
파일을 다루는 데 사용되는 3개의 시스템 콜을 알아보자

open() 시스템 콜

읽기 및 쓰기를 위해 파일을 여는 함수이다. <fcntl.h> 헤더파일에 존재한다.
프로토타입
int open(const char *path, int oflag);
C
path : 열고자 하는 파일의 경로 이름으로, 절대경로와 상대경로 모두 가능하다.
oflag : 파일을 읽기용으로 열 것인지, 쓰기용으로 열 것인지를 결정한다.
O_RDONLY
읽기 전용으로 열기
O_WRONLY
쓰기 전용으로 열기
O_RDWR
읽기 및 쓰기 전용으로 열기
이 외에도 열기 동작을 설정할 수 있는 옵션들이 존재하며 |(OR) 연산을 통해 사용할 수 있다.
mode : 세 번째 매개변수로 파일의 접근 권한을 설정하기도 한다.
사용하기
open("./test.txt", O_RDONLY);
C
이렇게 test.txt 파일을 읽기 전용으로 열 수 있다.
open("./test.txt", O_WRONLY | O_CREAT);
C
이렇게 oflag로 여러 개의 옵션을 설정할 수도 있다. 파일을 쓰기 전용으로 여는 옵션 뿐만 아니라, 만약 test.txt 파일이 없다면 새로운 파일을 생성하라는 옵션도 함께 설정되어 있는 모습이다.
open() 시스템 콜이 호출된 이후 어떤 일이 일어날까
시스템은 open() 시스템 콜을 통해 열린 파일의 정보를 어딘가에 저장해야 하고, 이후에 읽거나 쓰기 위해 파일에 접근할 수 있어야 한다. 이를 가능하게 하기 위해 어떤 일들이 일어나는 걸까?
우선 파일 정보를 저장하는 세 가지 테이블에 대해 알아보자!
Inode Table
프로세스가 파일을 열면 커널은 Inode Table에 빈 엔트리를 할당하고, 여기에 해당 파일의 속성(파일의 종류, 권한, 크기, 데이터 블록을 가리키는 포인터 등)을 저장한다. Inode Table에서는 하나의 파일에 대해 하나의 엔트리만 존재할 수 있기 때문에, 같은 파일은 하나의 엔트리를 공유한다.
(Open) File Table
모든 열린 파일들을 관리하기 위한 테이블이다. 파일이 열릴 때마다 무조건 하나의 엔트리가 생성되므로, 같은 파일에 대해 여러 개의 엔트리가 존재할 수 있다. 엔트리에는 읽기 및 쓰기 전용에 대한 정보와 Inode table을 가리키는 포인터 등이 저장된다.
File Descriptor Table
각 프로세스에서 현재 사용 중인 파일을 관리하기 위한 테이블이다. File Table에 대한 포인터를 저장하는 배열으로, 파일 디스크립터 값이 이 배열의 인덱스이다.
이미지 출처 : File descriptor - Wikiwand
open() 을 통해 파일을 열었을 때, 시스템 콜은 커널 내에서 다음 과정을 거친다.
1.
Inode Table의 빈 엔트리를 채운다. inode 정보가 이미 있으면 해당 엔트리를 사용한다.
2.
File Table에서 새로운 엔트리를 할당하고 정보를 채운다.
3.
File Descriptor Table을 처음부터 탐색해서 사용되지 않는 영역에 File Table 엔트리 포인터를 저장하고, 그 인덱스를 반환한다.
결국 open() 시스템 콜은 연 파일의 파일 디스크립터 값을 반환한다. (오류 발생 시 -1 반환)
이 파일 디스크립터 값을 가지고 read()write() 를 활용해 파일을 읽고 쓸 수 있게 될 것이다!

read() 시스템 콜

파일의 내용을 읽어들이는 함수이다. <unistd.h> 헤더파일에 존재한다.
프로토타입
ssize_t read(int fd, void *buf, size_t nbyte);
C
fd : 읽고자 하는 파일의 파일 디스크립터 값
buf : 읽어들인 내용을 저장할 곳
nbyte : 읽어들일 바이트 수
사용하기
read(fd, buffer, 10);
C
fd가 가리키는 파일의 내용을 buffer 라는 공간에 10 바이트만큼 읽어들인다.
#include <tcntl.h> #include <unistd.h> #include <stdio.h> #define BUFFER_SIZE 42 int main(void) { char buf[BUFFER_SIZE + 1]; int fd; ssize_t read_byte; fd = open("./test.txt", O_RDONLY); while ((read_size = read(fd, buf, BUFFER_SIZE)) > 0) { buf[read_size] = '\0'; printf("%s\n", buffer); } close(fd); return (0); }
C
read() 시스템 콜에서 특별한 점은 한 번 호출하고 다음 번에 또 호출했을 때, 저번에 읽어들인 내용의 다음부터 이어서 읽어준다는 것이다. 따라서 읽어들일 바이트 수가 파일 내용의 전체 바이트 수보다 적더라도, 반복적으로 read()를 호출함으로써 파일의 내용을 모두 읽을 수 있게 된다.
read() 시스템 콜은 정상적으로 작동된 경우 읽어온 바이트 수를 리턴한다. (오류 발생 시 -1 반환)

write() 시스템 콜

파일에 내용을 입력하는 함수이다. <unistd.h> 헤더 파일에 존재한다.
프로토타입
ssize_t write(int fd, const void *buf, size_t nbyte);
C
fd : 쓰고자 하는 파일의 파일 디스크립터 값
buf : 파일에 입력할 내용을 저장한 곳
nbyte : 입력할 바이트 수
write() 사용하기
write(fd, buffer, 10);
C
fd가 가리키는 파일에 buffer가 저장한 내용을 10 바이트만큼 입력한다.
#include <tcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main(void) { int fd; const char *buf; ssize_t write_byte; fd = open("./test.txt", O_WRONLY); buf = "Hello, World!" write_byte = write(fd, buf, strlen(buf)); if (write_byte == -1) printf("ERROR\n"); else printf("Success!\n"); return (0); }
C
 read() 시스템 콜은 파일의 내용을 정해진 바이트 수만큼 부분적으로 읽어오지만, write() 시스템 콜은 에러가 발생하지 않는 한 작업을 완전하게 보장받을 수 있기 때문에 굳이 반복적으로 호출할 필요는 없다. 하지만 특수한 파일(소켓, 파이프 등)의 경우, 반복되는 write() 호출이 에러를 잡아낼 수도 있기 때문에 상황에 따라서 루프가 필요할 수 있다고 한다!
write() 시스템 콜은 정상적으로 작동된 경우 쓰기에 성공한 바이트 수를 리턴한다. (오류 발생 시 -1 반환)
참고한 자료
잘못된 부분이 있다면 알려주세요