Search
⏭️

get_next_line

Created
2021/02/13
tag
42서울
42Seoul
get_next_line
gnl
fd
fcntl
static

Subjects

1. 파일 디스크립터 (fd: File Descriptor)

1) 파일 디스크립터란?

흔히 유닉스 내에 존재하는 모든 것은 파일이라고 한다. 일반적인 정규 파일 (Regular File)에서부터 디렉토리 (Directory), 소켓 (Socket), 파이프 (pipe), 블록 디바이스, 캐릭터 디바이스 등등 모든 객체들은 파일로써 관리된다. 유닉스 시스템에서는 프로세스가 위와 같은 파일들을 접근할 때에 파일 디스크립터 (File Descriptor)라는 개념을 이용하도록 되어 있다.
응용 프로세스가 파일을 열거나 생성 하게 되면 정수로 된 파일 디스크립터를 얻게 되는데, 이 파일 디스크립터읽기 (read 함수), 쓰기 (write 함수), 파일 동작 제어 (fcntl 함수), 파일 닫기(close 함수)등의 모든 파일에 대한 동작을 수행할 수 있도록 FD Table을 참조하는데 사용되는 index이다.
단순히 index 값만으로 파일을 제어하고 조작할 수 있다는 것이 이해하기 어려울 수 있다. 이와 같은 동작이 가능한 이유는 파일 디스크립터를 이용하는 함수들이 내부적으로는 시스템 콜을 호출하며 여러 작업들을 수행하기 때문이다. 이 때 이용되는 테이블들이 3개가 있는데, FD Table, File Table, VFS inode Cache이다. 제시된 테이블들이 무엇이며, 어떤 흐름으로 이용되는지 이해해야 파일 디스크립터를 이용하는 동작들에 대해서 이해할 수 있다.

1. FD Table

FD TableFile Descriptor Table이며, 머신 단위가 아니라 프로세스 단위로 할당되는 테이블이다. 즉, 각 프로세스들은 저마다의 FD Table을 보유하고 있으며, FD Table의 각 index에는 File Table을 참조할 수 있도록 File Table에 대한 포인터를 보유하고 있다. 프로세스가 생성되면 기본적으로 0번, 1번, 2index가 활성화 되므로, FD Table의 각 index에 해당되는 공간에는 각각 표준 입력, 표준 출력, 표준 오류에 해당되는 File Table의 주소를 갖고 있게 된다.
Linux에서의 Kernel을 뜯어보면 <linux/file.h>에 다음과 같이 fdtable이라는 구조체가 정의되어 있다. 아래의 멤버 변수 중에서 fd가 곧 파일 디스크립터라는 index를 이용하는 배열인데, 이는 file이라는 구조체의 주소에 대한 배열이므로 이중 포인터로 선언된 것을 볼 수 있다.
file이라는 구조체는 Linux에서의 Kernel에서 사용되고 <linux/fs.h>에 정의되어 있으므로 File Table을 다룰 때 확인할 것이다. 이와 같은 헤더들은 일반적으로 사용되는 헤더가 아니라, 운영체제를 구성하는 일부이므로 Kernel을 이루는 코드를 통해서만 확인할 수 있다.
struct fdtable { unsigned int max_fds; int max_fdset; struct file ** fd; /* current fd array */ fd_set *close_on_exec; fd_set *open_fds; struct rcu_head rcu; struct files_struct *free_files; struct fdtable *next; };
C
복사
기본적으로 동일 프로세스 내에서는 동일한 파일 디스크립터를 이용할 수 없지만, FD Table이 프로세스 단위로 할당된다는 것은 서로 다른 프로세스라면 중복된 파일 디스크립터를 이용할 수 있다는 것을 의미한다. 예를 들어, a라는 프로세스가 FD Table3index를 사용하고 있다고 해서 b라는 프로세스가 FD Table3index를 이용할 수 없다는 것이 아니라는 말이다. 따라서 파일 디스크립터의 할당은 머신 단위가 아니라 프로세스 단위라는 것에 유의해야 한다.
또한 프로세스마다 서로 다른 FD Table을 이용하므로, 각 FD Table이 갖고 있는 File Table의 주소는 서로 다를 수 있다. 예를 들어 a 프로세스가 FD Table3index를 사용하고 있을 때 aFile을 이용하고 있었고 b 프로세스가 FD Table3index를 사용하고 있을 때 bFile을 이용하고 있었다면, 서로 다른 File Table의 주소가 기록되어 있을 것이다.

2. File Table

FD Table이 프로세스 단위로 할당되었다면, File Table은 머신 단위의 할당으로 생성된다. 머신 단위의 전역적으로 관리되는 이 테이블은 <linux/file.h>files_struct라는 구조체로 그 형태를 유지하고 있으며, 이 때 테이블의 각 entry<linux/fs.h>file이라는 구조체로 관리된다. 이 때 files_struct 내에 단순히 file이라는 구조체의 배열이 있을 것 같지만, 아래와 같이 files_struct를 보면 그렇지 않은 것을 확인할 수 있다. files_struct라는 하나의 큰 구조체는 각 프로세스들이 이용하는 fdtable의 배열로 유지되고 있으며, 이 때 하나의 fdtable에서 파일 디스크립터를 이용하여 file이라는 entry에 접근하게 되는 구조를 갖고 있다.
struct files_struct { /* * read mostly part */ atomic_t count; struct fdtable *fdt; struct fdtable fdtab; /* * written part on a separate cache line in SMP */ spinlock_t file_lock ____cacheline_aligned_in_smp; int next_fd; struct embedded_fd_set close_on_exec_init; struct embedded_fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; };
C
복사
이 때 파일에 대한 특정 동작을 수행하도록 만들기 위해선 파일에 대한 정보를 알고 있어야 하는데, <linux/fs.h>file 구조체 내에는 해당 파일에 대한 직접적인 정보를 담고 있지 않다. 이에 대해서 잠깐 짚고 넘어가보자면, 유닉스에서 파일들을 관리할 때는 inode라고 하는 구조체를 이용하는데, 이 구조체에는 파일에 대한 모든 정보이 기록되어 있다. 따라서 file 구조체에도 파일에 대한 직접적인 정보를 기록해두게 되면 inode의 역할과 중복되어 그 존재 의의가 무색해지기 때문에, file 구조체에는 프로세스의 FD Table에서 파일 디스크립터를 얻을 때 사용되었던 제어 정보들 위주로 구성되어 있는 것을 확인할 수 있다.
struct file { /* * fu_list becomes invalid after file_free is called and queued via * fu_rcuhead for RCU freeing */ union { struct list_head fu_list; struct rcu_head fu_rcuhead; } f_u; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; const struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; struct fown_struct f_owner; unsigned int f_uid, f_gid; struct file_ra_state f_ra; unsigned long f_version; void *f_security; /* needed for tty driver, and maybe others */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct list_head f_ep_links; spinlock_t f_ep_lock; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; };
C
복사
inode란? 파일을 기술하는 디스크 상의 데이터 구조로서 파일의 데이터 블록이 디스크 상의 어느 주소에 위치하고 있는가와 같은 파일에 대한 중요한 정보를 갖고 있다. 각각의 inode들은 고유 번호(inode number)를 가지고 있어서 파일을 식별할때 사용한다. 터미널에서 ls -i 옵션으로 inode number를 확인할 수 있다.

3. VFS inode Cache

실제로 파일에 대한 정보들을 이용하고 이를 조작해야 하는 경우에는 inode를 직접 활용해야 한다는 것인데, 위의 file 구조체를 살펴보면 그 어디에도 inode와 관련된 멤버 변수가 없는 것을 확인할 수 있다. 오래된 Kernel의 코드를 보면 file 구조체 내에 직접적으로 inode라는 구조체가 포인터로 존재하기도 하지만, 현재 확인하고 있는 Kernel의 경우에는 아래와 같이 <linux/fs.h>address_space라는 구조체 내에 inode 구조체의 주소를 둘 수 있도록 만들어져 있는 것을 확인할 수 있다.
struct address_space { struct inode *host; /* owner: inode, block_device */ struct radix_tree_root page_tree; /* radix tree of all pages */ rwlock_t tree_lock; /* and rwlock protecting it */ unsigned int i_mmap_writable;/* count VM_SHARED mappings */ struct prio_tree_root i_mmap; /* tree of private and shared mappings */ struct list_head i_mmap_nonlinear;/*list VM_NONLINEAR mappings */ spinlock_t i_mmap_lock; /* protect tree, count, list */ unsigned int truncate_count; /* Cover race condition with truncate */ unsigned long nrpages; /* number of total pages */ pgoff_t writeback_index;/* writeback starts here */ const struct address_space_operations *a_ops; /* methods */ unsigned long flags; /* error bits/gfp mask */ struct backing_dev_info *backing_dev_info; /* device readahead, etc */ spinlock_t private_lock; /* for use by the address_space */ struct list_head private_list; /* ditto */ struct address_space *assoc_mapping; /* ditto */ } __attribute__((aligned(sizeof(long))));
C
복사
위와 같이 각각의 file이라는 구조체들이 내부적으로 inode의 주소를 기록하는 행위에 대해서 생각해보자. 이와 같은 행위가 가능하기 위해선 파일에 대한 특정 동작 수행을 위해 파일을 여는 행위부터 시작되며, 해당 파일과 일치하는 inode를 전체적으로 찾아내는 과정이 요구된다. 한 프로세스에서 각각 다른 파일 디스크립터로 동일한 파일을 열 수 있었고, 이러한 행위는 여러 프로세스에서 수행될 수 있었다. 만일 중복되는 파일들을 사용해야 하는 경우에 반복적으로 inode를 전체적으로 탐색하게 되면 파일 시스템에 대한 액세스 성능이 굉장히 좋지 않으므로, 이를 극복하기 위해 파일을 열 때마다 사용된 inode를 기록하는 행위가 발생한다. 이 때 기록되는 공간이 VFS inode Cache이며, 내부적으로 기록하며 유지하는 inode는 고유하다. 따라서 파일이 열리게 되면 VFS inode Cache를 먼저 탐색한 후에 전체 탐색에 들어가게 된다.
VFS inode Cache에서 유지하는 entry들은 고유하다고 했기 때문에, 이는 해쉬 테이블로 구현되어 있다.
VFS inode CacheFile Table의 각 entry의 기록에도 도움이 되지만, 현재 열려 있는 파일들의 묶음이라는 점에서 운영체제의 파일 관리 측면에서도 많은 도움이 된다.

4. 전체적인 구조

2) 파일 디스크립터의 처리 흐름

fd = open("test.txt", O_RDONLY); // Bunch of Code Execution close(fd);
C
복사
위와 같은 구문이 내부적으로 어떻게 처리되는지 파일 디스크립터에서 설명된 것들을 이용하여 흐름을 확인해보자. 단, test.txt는 기존에 이미 존재하는 파일로써 inode를 유지하고 있고, 호출 구문 이전에 열려있는 파일은 하나도 없다고 전제하여 VFS inode Cache는 비어있다고 간주한다. openclose와 같이 파일에 대한 동작을 야기하는 함수에 대해서는 시스템 콜이 호출되며, 위와 같은 상황에 대해서는 Kernel 내에서 다음과 같은 순서로 처리된다.
[1] VFS inode Cache가 비어있기 때문에 디스크 내에서 test.txt를 찾고 그 inode 정보를 취득한다.
[2] inode에서 test.txt의 접근 권한을 확인하여, open 함수에서 사용된 모드가 접근 권한에 위배되지 않는지 확인한다.
접근 권한에 위배된다면, 즉시 -1 값을 index로 반환한다.
[3] VFS inode Cache의 새로운 entry로써, test.txt에 해당하는 inode를 채워 넣는다.
[4] test.txt에 해당하는 File Tableentry를 생성하고, 이에 따라 file 구조체의 값들을 초기 값으로 설정한다.
설정 값들 중에서 f_pos는 파일이 읽고 쓸 위치에 대한 index이다. O_APPEND 모드로 open 함수를 호출했다면 f_pos는 곧 파일의 크기와 동일하게 설정되고, 그렇지 않다면 0으로 설정된다.
설정 값들 중에서 f_flagsopen 함수에서 사용된 모드 값이 기록된다. Read-Only로 파일을 열었기 때문에 이에 대한 값으로 f_flags가 설정된다.
설정 값들 중에서 f_countFile Tableentry를 참조하는 파일 디스크립터의 수를 의미한다. 현재는 해당 entry에 대해 최초로 참조하는 파일 디스크립터가 생길 예정이므로 1로 설정된다. 동일한 파일을 여는 경우에도 별도의 File Tableentry로 기록이 되기 때문에 대체적으로 f_count1이 되지만, 예외적으로 파일 디스크립터를 복제하는 dup 혹은 dup2 함수의 호출로 동일한 entry를 참조할 수 있기 때문에 이 경우에 f_count가 증가하게 된다. 즉, 동일 entry를 지칭하는 파일 디스크립터의 수에 따라서 f_count가 증가하고 감소한다.
[5] test.txtinode를 참조하는 File Tableentry가 생겼으므로, 해당 inode 구조체 내에 있는 i_count의 참조 값을 수정한다.
i_count라는 설정 값은 File Tableentry들이 inode를 참조할 때마다 i_count는 증가하게 되고, inode의 참조를 끊어 낼 때마다 i_count는 감소하게 된다. 현재의 경우에는 최초로 inode를 참조하는 상태이므로 i_count의 값은 1이 된다.
[6] 프로세스의 FD Table을 처음부터 탐색하여 사용되고 있지 않은 영역에 File Tableentry 주소를 포인터로 기록한 후, open 함수의 반환 값으로 해당 영역의 index를 반환한다.
기본적으로 0, 1, 2index에 해당하는 영역은 stdin, stdout, stderr에 대한 스트림 파일을 참조하고 있기 때문에 그 이후의 값이 반환된다.
[7] 주석 처리 된 부분에 대해서 수행한다.
[8] FD Tablefd에 해당하는 index의 영역에서 File Tableentry 주소를 지운다.
[9] File Tableentry를 참조하는 파일 디스크립터가 없어졌으므로, f_count의 값이 감소한다.
현재의 경우에는 별도의 복제 과정을 거치지 않았다는 것을 가정하기 때문에 1이었던 f_count의 값이 0이 된다. f_count의 값이 0이 되었기 때문에 File Table에서 해당 entry를 삭제한다.
[10] test.txtinode를 참조하는 File Tableentry가 사라졌기 때문에, 해당 inode 구조체 내에 있는 i_count의 값을 감소한다.
현재의 경우에는 1이었던 i_count의 값이 0으로 감소했으므로 VFS inode Cache에서 test.txtinode에 해당하는 entry를 삭제한다.

3) 파일 디스크립터 제한

운영체제에서 사용할 수 있는 파일 디스크립터는 제한되어 있다. 이 값은 사용하는 운영체제 혹은 머신에 따라서도 달라질 수 있다. 또한 얼마만큼의 파일 디스크립터를 가용할지에 대해서도 권한이 있는 사용자에 한하여 그 값을 변경하는 것이 가능하다. 따라서 시스템 상에서 정확히 요구하는 최대치까지만 파일 디스크립터를 제한하기 위해선 사전에 정의된 값을 이용하는 것은 좋은 선택지가 아니다.
시스템 상에서 정의된 최대치를 알아내기 위해선 다음과 같은 함수를 이용하며, <unistd.h>의 포함이 필요하다. name에는 시스템 상에서 어떤 정보를 알아낼 것인지 항목에 따라 정의된 매크로 값을 인자로 넣는다. 현재 상황처럼 파일 디스크립터를 알아내기 위해선 _SC_OPEN_MAX라는 값을 name으로 이용하면 된다. 그 외의 설정 값을 알아보고 싶다면 아래 링크를 참고하도록 하자.
long sysconf(int name);
C
복사
#include <stdio.h> #include <unistd.h> int main(void) { long limit; limit = sysconf(_SC_OPEN_MAX); printf("%ld File Descriptors can be used\n", limit); return (0); }
C
복사
위 코드에서 sysconf를 통해 얻어낸 값은, getconf 명령어로 알아낸 현재 시스템 상에서 설정된 한 프로세스에서 열 수 있는 최대 파일의 수와 동일한 것을 확인할 수 있다.
만일 위와 같은 함수를 이용할 수 없다면, 차선책으로 OPEN_MAX 혹은 FOPEN_MAX 등의 매크로 값을 이용할 수 있다. 이는 시스템 상에서 정의된 값이 아니라 시스템 및 운영체제에 따라 언어에서 정의된 값이므로 정확한 수치를 나타내지 않는다는 것을 유의해야 한다. 예를 들어 시스템 상에서는 한 프로세스 당 2048개의 파일 디스크립터를 둘 수 있도록 설정되어 있는데 사용되는 매크로 값이 그 이상인 경우에는 문제가 될 수 있다. 따라서 현재 사용하고 있는 파일 디스크립터의 값에 대해 확인하는 절차가 반드시 포함되어야 한다.

4) 파일 디스크립터 확인

int open(const char *filename, int flags, [mode_t mode]);
C
복사
open 함수는 filename에 해당하는 파일을 열고, 해당 파일을 참조할 수 있는 파일 디스크립터를 반환한다. 현재 이용 가능한 파일 디스크립터들 중 가장 작은 값을 반환 받아 index로 이용하게 된다. open 함수는 <fcntl.h>에 위치한다.
int close(int fd);
C
복사
close 함수는 open 함수를 통해 반환 받은 파일 디스크립터를 닫는 역할을 수행한다. 정확히는 File Tableentry 주소를 지워 참조할 수 없게 만들어, entryf_count를 하나 감소시키도록 만든다. close 함수는 <unistd.h>에 위치한다.
아래 코드를 실행해보면 파일 디스크립터3부터 부여되는 것을 확인할 수 있다. 출력된 3표준 입력, 표준 출력, 표준 오류를 지칭하는 index의 바로 다음 값이다.
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main(void) { int fd; fd = open("test.txt", O_RDONLY); if (fd == -1) return (1); printf("fd : %d\n", fd); close(fd); return (0); }
C
복사

5) 파일 디스크립터 변경

일반적으로 파일 디스크립터를 한 번에 변경할 수 있는 함수는 존재하지 않는다. 이 때문에 현재 파일에 대한 파일 디스크립터를 변경하고 싶다면 파일 디스크립터를 복제하는 행위로 이와 비슷하게 유도할 수 있다. 파일 디스크립터의 복제는 대체적으로 표준 입력을 나타내는 0index 혹은 표준 출력을 의미하는 1index가 기존의 파일 디스크립터가 참조하는 파일을 참조하도록 만들면서, 0번과 1index를 이용하더라도 표준 입력과 표준 출력에 대해 사용하지 않도록 만드는데 이용된다. 이와 같은 과정은 pipe 함수를 이용할 때 자주 볼 수 있다. 즉, 복제 행위는 파일 디스크립터를 변경시키는 것이 주된 목적이 아니라는 점을 명심하고, 여기서는 파일 디스크립터의 복제에 대한 심도 있는 이해보다는 이를 이용하여 파일 디스크립터를 변경하는 방법에 대해서만 간단히 알아볼 것이다.
dup 함수를 이용하게 되면 현재 이용하고 있지 않은 파일 디스크립터 중에서 가장 작은 값을 이용하도록 되어 있고, dup2의 경우에는 사용자가 지정한 파일 디스크립터를 이용하도록 되어 있다. 이 때 dup2에서 사용자가 지정한 파일 디스크립터가 이미 사용 중이라면, 사전에 먼저 열려있던 파일을 닫아서 파일 디스크립터를 해제한 후에 해당 번호를 할당하게 된다.
아래 예시에서는 dup2 함수를 이용하여 사용자가 원하는 파일 디스크립터를 할당할 수 있도록 만들었다.
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main(void) { int fd1; int fd2; fd1 = open("test.txt", O_RDONLY); if (fd1 == -1) return (1); printf("fd1\t: %d\n", fd1); printf("\nDuplication\n\n"); fd2 = dup2(fd1, 42); printf("fd1\t: %d\n", fd1); printf("fd2\t: %d\n", fd2); close(fd1); close(fd2); return (0); }
C
복사

2. File Control을 위한 함수들

1) open 함수 그리고 create, close

Linux, Unix 계열의 시스템에서 프로세스가 파일을 열 때 open 함수 혹은 openat 함수를 사용할 수 있다.
int open(const char *filename, int flag); int open(const char *filename, int flag, [mode_t mode]); int openat(int dirfd, const char *filename, int flag); int openat(int dirfd, const char *filename, int flag, [mode_t mode]);
C
복사
매개 변수로 filename (절대 경로 혹은 상대 경로), flag, mode 등을 받고 파일 디스트립터를 반환한다. 이때 에러가 나면 -1을 반환한다. 즉, open 관련 함수는 filename에 명시된 파일을 flag에서 설정한 모드로 열어서 파일 디스크립터를 반환하는 함수이다.

openat 함수와 open 함수의 차이

open은 해당 경로의 파일을 flag 옵션을 적용하여 파일 디스크립터를 반환한다면, openatopen과 동일한 작업을 수행하지만 dirfd값을 추가로 받아 경로를 이용하는 방식이다. openat 함수가 도입된 데에는 다음과 같은 문제를 해결하기 위함이다.
멀티 쓰레드 환경에서 상대 경로를 다루기 쉽게 해준다. 같은 프로세스에 있는 쓰레드들은 CWD(Current Working Directory)를 공유한다.
TOCTTOU(time-of-check-to-time-of-use) 문제를 해결하기 위해서 사용된다.
openat 함수는? 관건은 filename절대 경로인지 상대 경로인지에 따라 달라진다. 1. 경로로 주어진 인자가 절대 경로라면, dirfd는 무시된다. 2. 경로로 주어진 인자가 상대 경로라면, FD Tabledirfd 에 해당하는 항목을 찾아 나온 디렉토리를 기준으로 인자를 붙여 찾아간다. 3. 경로로 주어진 인자가 상대 경로이면서 dirfd 값이 현재 작업 디렉토리를 의미하는 AT_FDCWD로 되어 있다면, 현재 작업 디렉토리를 기준으로 인자를 붙여 찾아간다. 이 때 dirfdAT_FDCWD라는 특수 값이 존재하며 이 때는 open과 동일하게 동작한다. (open 역시 CWD 기준으로 동작한다.) (openat의 매개 변수 dirfd의 이름에서도 유추할 수 있지만, 현재 FD Table에 기록되어 있는 디렉토리를 이용하기 위함이므로 해당 디렉토리fd를 이용한다. 매개 변수로 사용하는 dirfd 값을 얻기 위해 디렉토리 구조체를 인자로 사용하는 int dirfd(DIR *dirp) 함수를 주로 이용한다.)
Search
oflag
기준
Necessary
파일을 쓰기 전용으로 연다. (Write Only)
Necessary
파일을 쓰기와 읽기용으로 연다. (Read & Write)
Necessary
파일을 실행 전용으로 연다. (Execute Only)
Necessary
디렉토리 파일을 탐색 전용으로 연다. (Search Only)
Optional
파일의 끝부분 (EOF)에 write하도록 설정한다.
Optional
FD_CLOEXEC 플래그를 설정한 채 파일을 연다. (exec류의 함수를 수행하고 나면 fd가 닫긴다.)
Optional
파일이 없으면 생성한다. 이 플래그를 명시하면, open 함수에 Permission 정보를 추가로 더 받아야 한다. 파일이 존재하면 연다.
Optional
path에 해당하는 파일이 디렉토리가 아니면 에러를 발생한다.
Optional
O_CREAT 플래그와 같이 사용한다 파일이 이미 존재하면 에러를 발생한다.
Optional
path에 해당하는 파일이 터미널 장치인 경우, 해당 장치를 현재 프로세스의 컨트롤링 터미널로 할당하지 않는다.
Optional
path에 해당하는 파일이 심볼릭 링크면 에러를 발생한다.
Optional
FIFO, Block Device, Charactoer Device에 대해 논 블록킹 방식으로 read 함수와 write 함수를 수행하도록 기본 설정을 세팅한다.
Optional
path에 해당하는 파일에 write 함수를 사용할 경우 실제 물리적인 I/O가 끝날 때까지 기다리도록 설정한다
Optional
파일이 이미 존재하고 write-only, read-write모드로 열 수 있는 경우, 파일 사이즈를 0으로 초기화시킨다
Optional
write 함수 수행시 파일의 데이터 부분에 실제 물리적인 I/O가 끝나기를 기다린다. 파일의 설정이나 Attribute부분에 대한 업데이트는 기다리지 않는다.
Optional
read 함수 수행시 커널에 해당 파일의 offset에 대한 write 함수의 pending이 있으면 그 write 함수의 수행이 끝나기를 기다린다.

creat 함수

새로운 파일 생성은 creat 함수를 이용할 수 있다.
int creat(const char *filename, [mode_t mode]);
C
복사
open 함수와 마찬가지로 성공하면 파일 디스크립터를, 실패하면 -1을 반환한다.
creat 함수는 open 함수로도 구현할 수 있다.
open(filename, O_WRONLY | O_CREAT | O_TRUNC, [mode]);
C
복사
creat 함수의 최대 단점은 write 모드로만 열린다는 것이다. 다시 읽기 위해서는 creat 함수로 파일을 만든 후, close 함수로 닫고 O_RDONLY로 읽는 과정이 필요한 것이다. 따라서 아래의 코드처럼 사용하는 것이 더 낫다.
open(filename, O_RDWR | O_CREAT | O_TRUNC, [mode]);
C
복사

close 함수

open 함수로 연 파일은 close 함수로 닫을 수 있다.
int close(int fd);
C
복사
정상적으로 종료되면 0을, 실패하면 -1을 반환한다.
파일을 닫으면, 프로세스가 파일에 설정했던 Record Lock(레코드 잠금)도 자동으로 잠금 해제된다. 또 프로세스가 종료되면 프로세스가 열어놨던 파일들은 close 함수로 닫기게 된다.
Record Lock(레코드 잠금)이란? 멀티 쓰레드 프로그램에서, 여러 쓰레드가 하나의 파일에 동시에 접근할 경우 파일 잠금이 필요할 수 있다. 한번에 하나의 쓰레드만 파일에 읽기및 쓰기를 해야 하는 경우가 있을 수 있기 때문이다. 잠금이라는 행동의 주체는 운영체제이다. 잠금에 대한 기록이 운영체제에서 관리, 기록을 한다고 보면 된다. (Record Lock 자체는 <fcntl.h>fcntl 함수를 통해 이뤄지며, 이 때 flock이라고 하는 잠금을 위한 구조체를 이용한다.)

2) read 함수

파일을 열고 난 후 데이터를 읽어올 때에는 read 함수를 사용한다. read 함수는 파일에서 n 만큼 읽어와서 buf에 그 값을 할당하게 된다. 함수를 정상적으로 수행한 경우에는 읽은 바이트 수를, 그렇지 않다면 -1을 반환한다.
ssize_t read(int fd, void *buf, size_t n);
C
복사
size_t와 ssize_t란? size_t는 객체의 크기를 나타내기 위한 type으로 보통의 32 bit 머신에서는 32 bit 크기를 가지며 unsigned int로 되어있다. sizeof라는 연산자가 반환하는 값을 담기 위한 type으로 보면 되고, 많은 I/O 함수에서 사용된다. ssize_t는 부호가 있는 형태로 객체의 크기를 나타내기 위한 type으로 보통의 32 bit 머신에서는 32 bit 크기를 가지며 int로 되어있다. I/O 함수의 반환값으로 많이 사용되는데 그 이유는 해당 I/O 함수의 실패를 알려주기 위해서이다. 따라서 함수 수행 중에 오류가 발생했을 경우 -1을 반환하면서 해당 I/O 함수의 실패를 알려줄 수 있다.
아래 코드를 통해 파일을 읽을 수 있다.
//text.txt abcdefg
C
복사
// read.c #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main(void) { int fd; char buf[100]; fd = open("./text.txt", O_RDONLY); if (fd < 0) printf("file open error"); else { read(fd, buf, sizeof(buf)); printf("%s", buf); close(fd); } return (0); }
C
복사

3) write 함수

파일을 열고 난 후 파일에 값을 기록할 때는 write 함수를 사용한다. 함수 정상적으로 수행한 경우에는 기록한 바이트 수를, 그렇지 않다면 -1을 반환한다.
ssize_t write(int fd, const void *buf, size_t n);
C
복사

3. 프로그램과 프로세스에 대하여

1) Program (프로그램)이란?

일반적으로 프로그램이라 함은 컴퓨터에서 실행되는 명령어 모음이 들어 있는 덩어리라고 볼 수 있다. 이 때의 덩어리는 CodeData로 나뉜다.
프로그램 자체로써는 아무런 의미가 없고, 그것을 더블 클릭 등으로 실행했을 때 의미를 갖게 된다. 프로그램을 실행하면 프로그램 안에 있는 명령어를 한 줄씩 수행하면서 프로그램이 뭔가 실행하는 등의 상태를 갖게 된다. 이렇게 프로그램이 상태(State)를 갖고 있는 것을 프로세스라고 한다.

2) Process (프로세스)란?

프로세스는 운영체제에 의해서 관리 되며, 프로세스 별로 사용 가능한 메모리 영역이 존재한다. 프로세스에 할당된 메모리 공간은 독립적이기 때문에, 기본적으로 서로 다른 프로세스는 각 프로세스가 사용하는 메모리 공간을 읽고 쓰는 것이 불가능하다. 프로그램이 프로세스로써 돌아가게 되면 프로그램의 CodeData는 프로세스 메모리로 불러들여 진다. 이 때 프로세스는 Code Segment, Data Segment, Heap, Stack의 구조로 메모리 레이아웃을 가진다. 프로세스가 생성되었을 때 운영체제가 HeapStack으로 직접 나누어 공간을 할당하는 것이 아니라는 점을 명심해야 한다. 운영체제는 프로세스가 생성되었을 때 사용할 공간 자체를 부여하기만 할 뿐, 메모리 레이아웃을 갖는 것은 프로세스에 대한 기본 설정과 구동 환경에 따라서 결정된다.
프로그램은 HDD, SSD와 같은 곳에 존재하고, 프로세스는 RAM에 존재한다.
일반적인 Stack의 크기는 수십KB ~ 수십MB 정도이다. 이 크기는 시스템에 따라 상이하며, 지역 변수를 이용할 때 Stack을 이용하기 때문에 큰 배열을 지역 변수로 선언하면 Stack 용량 부족 문제를 겪을 수 있다. 지역 변수 이외에도 Stack에는 함수의 호출 기록 및 매개 변수 등에 대해 보유하게 되며, 이들은 모두 중괄호의 Block 단위로 관리되어 중괄호를 마치면 Stack에서 사용되었던 모든 것들이 Stack Frame을 벗겨내면서 해제된다. 따라서 이를 해결하기 위해선 시스템 설정으로 Stack의 크기를 강제로 늘려주거나, 메모리 이용을 Stack을 피하면 된다. 이는 정적 변수 혹은 전역 변수 로 선언하여 Data Segment에 위치시키거나 Heap으로부터 메모리를 할당 받으면 된다는 것이다.

3) Thread (쓰레드)란?

프로세스를 처리하는 기본 단위는 쓰레드이다. 이 때, 프로세스 내에는 여러 쓰레드가 있을 수 있으며, 한 프로세스 안에 속한 여러 쓰레드들은 프로세스 안의 Heap, Code Segment, Data Segment를 공유하며 Stack만 별도로 가진다. (이 말은 각 쓰레드에서 실행되는 함수와 지역 변수들이 쓰레드 마다 별도로 관리된다는 의미이다.) 프로세스 내에는 하나의 쓰레드만 둔채로 실행이 될 수도 있는데, 이를 싱글 쓰레드 프로그램이라고 한다. 또한 이렇게 싱글 쓰레드로 작동하도록 프로그램을 설계하고 구현하는 것을 싱글 쓰레드 모델이라고 한다. 싱글 쓰레드 프로그램멀티 쓰레드 프로그램은 기본적으로 프로세스를 처리하는 메인 쓰레드가 존재한다.
프로세스가 갖는 전반적인 메모리 레이아웃은 아래의 글을 통해 살펴보자. 이를 통해 static 변수가 어느 메모리 레이아웃에 존재하는지 알 수 있다.
메모리 레이아웃에 대한 이해가 되었다면, static이라는 키워드에 대해서 알아볼 필요가 있다.

4. static 변수

1) static 변수란?

static 변수는 Global, Local 어느 것으로 이용이 가능하다.
전역이든 지역이든 static 변수는 Data Segment에 위치한다.

2) 외부 정적 변수

전역으로 선언된 static 변수는 외부 정적 변수라고도 불리며, 별도의 초기화 구문이 없다면 0으로 초기화된다. 이 때는 Data SegmentBSS 영역에 위치하여 0으로 초기화된다. 초기화 구문 존재 시에는 Data SegmentData 영역에 위치한다. 초기화 구문에는 함수 호출 구문을 이용하지 않도록 주의해야 한다. 함수 호출은 런 타임에 이뤄지므로 런 타임 이전에 그 값이 결정되는 static 변수에는 이용할 수 없다.

3) 내부 정적 변수

특정 함수나 클래스 내부에 선언된 지역 변수내부 정적 변수라고도 불리며, 외부 정적 변수와 마찬가지로 별도의 초기화 구문이 없으면 0으로 초기화 된다. 이 때는 Data SegmentBSS 영역에 위치하여 0으로 초기화된다. 초기화 구문 존재 시에는 Data SegmentData 영역에 위치한다. 초기화 구문에는 함수 호출 구문을 이용하지 않도록 주의해야 한다. 함수 호출은 런 타임에 이뤄지므로 런 타임 이전에 그 값이 결정되는 static 변수에는 이용할 수 없다.
static 변수를 함수 혹은 클래스에 대해서 내부 정적 변수로 이용하는 경우에 각 함수 별 혹은 클래스 별로 공유되는 일종의 공유 변수로 이용된다. 아래 코드를 살펴보자.
#include <stdio.h> void increase_num(void) { static int num = 4; printf("%d\n", num); ++num; } int main(void) { increase_num(); increase_num(); increase_num(); return (0); }
C
복사
위의 경우 static int num이라는 내부 정적 변수의 초기 값 결정은 프로그램을 생성할 때 이뤄지며 그 값이 4가 된다. 이 때 static int numincrease_num이라는 함수의 지역 변수처럼 보여 Stack에 위치할 것 같지만, 초기화 구문이 존재하기 때문에 Data 영역에 위치하게 된다. 이에 따라 알 수 있는 것은 increase_num 함수 내에서 static int num = 4라는 구문은 매 함수 실행마다 이뤄지는 것은 아니라는 것이다. 또한 내부 정적 변수는 특정 함수 혹은 클래스 간 공유되어 사용된다고 했기 때문에 위 main 함수의 실행 결과는 4, 5, 6가 된다.

4) 주의할 점

다른 소스 파일에 존재하는 전역 static 변수 (외부 정적 변수)는 참조 할 수 없다. 즉, extern이 불가능 하고 이를 시도하면 컴파일 오류가 발생한다.
increase_num.c
#include <stdio.h> static int num = 4; void increase_num(void) { printf("%d\n", num); ++num; }
C
복사
main.c
#include <stdio.h> extern int num; // 컴파일 에러 void increase_num(void); int main(void) { increase_num(); increase_num(); increase_num(); printf("%d\n", num); return (0); }
C
복사
위와 같이 extern을 받게 되면 아래 사진과 같은 오류를 볼 수 있다.

static 변수는 매개 변수로 사용할 수 없다.

매개 변수에 static을 붙이더라도 매개 변수는 static으로써의 역할을 수행할 수 없다. 즉, 값이 유지 되지 않는다. 아래와 같이 매개 변수로 사용하려고 하면 오류가 나는 것을 볼 수 있다.

5. Reference