본문 바로가기

프로그래밍(TA, AA)/C C++

[시스템프로그래밍] 저수준 파일 입출력

파일은 관련 있는 데이터의 집합으로 하드 디스크 같은 저장 장치에 일정한 형태로 저장됩니다. 파일은 데이터를 저장하는 데는 물론, 데이터를 전송하거나 장치에 접근하는 데도 사용합니다. 유닉스에서 파일은 크게 일반 파일과 특수 파일로 구분할 수 있습니다.


 종류

 용도

 일반 파일

 텍스트 바이너리 형태의 데이터를 저장하는 파일

 특수 파일

 데이터 전송, 장치 접근에 사용하는 파일


특수 파일의 생성과 삭제 및 입출력은 특수 파일별로 약간씩 차이가 있습니다. 특수 파일에 대한 입출력 방법은 다른 장에서 살펴보고, 이 장에서는 일반 파일을 중심으로 데이터를 쓰거나 읽는 방법을 알아봅니다.


유닉스에서 파일을 읽고 쓰는 방법은 저수준 파일 입출력과 고수준 파일 입출력으로 구분할 수 있습니다. 저수준 파일 입출력은 유닉스 커널의 시스템을 호출하여 파일 입출력을 수행합니다. 저수준 파일 입출력은 시스템 호출을 이용하므로 파일에 좀 더 빠르게 접근할 수 있다는 장점이 있습니다. 또한 저수준 파일 입출력은 바이트 단위로 파일의 내용을 다루므로, 일반 파일뿐만 아니라 특수 파일도 읽고 쓸 수 있습니다. 하지만 바이트 단위로만 입출력을 수행하므로, 이를 이용해서 응용 프로그램을 작성하려면 바이트를 적당한 형태의 데이터로 변환하는 함수 등 여러 가지 추가적인 기능을 구현해야 하는 단점이 있습니다.


저수준 파일 입출력의 불편함을 해결하기 위해 제공하는 것이 바로 고수준 파일 입출력입니다. 고수준 파일 입출력은 C 언어의 표준 함수로 제공되며, 데이터를 바이트 단위로 한정하지 않고 버퍼를 이용해 한꺼번에 일긱와 쓰기를 수행합니다. 또한 다양한 입출력 데이터 변환 기능도 이미 구현되어 있어 데이터형에 따라 편리하게 이용할 수 있습니다. 고수준 파일 입출력을 표준 입출력 라이브러리라고도 합니다.


저수준 파일 입출력은 열린 파일을 참조하는 데 파일 기술자를 사용하는 반면, 고수준 파일 입출력은 파일 포인터를 사용합니다. 파일 포인터는 열린 파일의 특성에 관한 정보를 저장하는 구조체를 가리키는 포인터입니다. 이 파일 포인터 구조체의 항목 중 하나가 파일 기술자입니다. 파일 포인터에 관한 자세한 내용은 다음 절에서 다루게 됩니다.


저수준 파일 입출력과 고수준 파일 입출력을 비교하면 아래와 같습니다. 주요 함수를 보면 open, close, read, write 등 비슷한 함수명이 있습니다. 실제로 데이터를 읽고 쓰는 함수는 저수준 파일 입출력의 경우 read, write 뿐이지만, 고수준 파일 입출력은 fread, fwrite, fgets, fputs, fprintf, fscanf 등 다양한 함수가 있습니다. 고수준 파일 입출력이 사용자의 편의를 고려하고 있음을 알 수 있습니다.


고수준 파일 입출력 함수는 C 언어의 표준 함수로 제공되기 때문에 일반적으로 C 언어 과정에서 다루게 됩니다. 따라서 저수준 파일 입출력에 비해 친숙할 것입니다. 이 장에서는 저수준 파일 입출력 함수를 중심으로 다룹니다. 고수준 파일 입출력 함수는 상대적으로 가볍게 정리하고, 저수준 파일 입출력 함수와의 차이를 살펴 봅니다.


 

저수준 파일 입출력 

고수준 파일 입출력

파일 지시자

 int fd

 FILE *fp;

특징

 - 훨씬 빠르다

 - 바이트 단위로 읽고 쓴다

 - 특수 파일에 대한 접근이 가능하다

 - 사용하기 쉽다.

 - 버퍼 단위로 읽고 쓴다.

 - 데이터의 입출력 동기화가 쉽다.

 - 여러 가지 형식을 지원한다.

주요 함수 

 open, close, read, write, dup, dup2, fcntl, lseek, fsync

 fopen, fclose, fread, fwrite, fputs, fgets, fprintf, fscanf, freopen, fseek


저수준 파일 입출력에서 사용하는 파일 기술자와 고수준 파일 입출력에서 사용하는 파일 포인터를 상호 변환할 수 있습니다. 파일 기술자로부터 파일 포인터를 생성하는데 fdopen 함수를 사용하며, 파일 포인터로부터 파일 기술자 정보를 추출하는 데는 fileno 함수를 사용합니다.


프로그램을 수행하는 과정에서 데이터를 파일에 임시로 저장해야 할 때가 있는데, 기존 파일과 중복되지 않는 임시 파일명을 만들어주는 함수로 tmpnam, tmpname, mktemp가 있습니다. 이들 함수는 파일명만 리턴하므로 저수준이나 고수준 파일 생성 함수로 파일을 실제 생성해야 사용해야 합니다. 

 

 

저수준 파일 입출력


저수준 파일 입출력은 바이트 단위로 입출력을 수행하므로 해당 함수 자체의 구조는 대부분 단순합니다. 저수준 파일 입출력을 이용하면 일반 파일뿐만 아니라 특수 파일도 읽고 쓸 수 있으므로, 파일 입출력의 기본이 된다고 할 수 있습니다. 이 절에서는 저수준 파일 입출력 함수를 사용해서 파일을 읽고 쓰는 방법을 배웁니다.


 

파일 기술자


모든 저수준 파일 입출력 함수는 파일 기술자를 사용합니다. 파일 기술자는 현재 열려있는 파일을 구분할 목적으로 유직스가 붙여놓은 번호로, 저수준 파일 입출력에서 열린 파일을 참조하는 데 사용하는 지시자 역할을 합니다. 파일 기술자는 정수값으로, open 함수를 사용해 파일을 열었을 때 부여됩니다.

 

파일 기술자는 0번부터 시작합니다. 이 중 0, 1, 2번 파일 기술자는 기본적인 용도가 지정되어 있습니다. 0번은 표준 입력을 의미하며, 1번은 표준 출력, 2번은 표준 오류 출력을 의미합니다. 표준 입력, 표준 출력, 표준 오류 출력은 유닉스에서 입출력 방향 전환을 배울 때 학습했을 것입니다. 보통 0번 파일 기술자는 키보드, 1번과 2번 파일 기술자는 모니터 화면을 의미합니다. 물론 0번, 1번, 2번 파일 기술자를 다른 파일에 지정할 수도 있습니다. 이에 관해서는 조금 뒤에 살펴보겠습니다.

 

프로세스는 동작하면서 필요에 따라 파일을 열고 닫습니다. 한 프로세스가 동시에 열 수 있는 파일의 개수에는 제한이 있습니다. 이 개수를 변경하려면 유닉스 커널의 설정을 바꾼 후 재부팅해야 합니다. 프로세스가 파일을 열 때 파일 기술자는 0번부터 시작해 가장 작은 번호가 자동으로 할당됩니다. 프로세스가 처음 동작할 때 세 파일 기술자(0, 1, 2)는 기본적으로 할당됩니다. 따라서 프로세스가 파일을 처음 열면 3번 파일 기술자가 할당됩니다. 물론 0번이나 1번, 2번 파일 기술자를 닫아놓았을 경우에는 이들 번호도 할당될 수 있습니다. 0~2번 파일 기술자가 자동으로 할당되고, 3번부터 open 함수에 대응해 할당된 모습을 나타낸 것입니다.
 



파일 생성과 열고 닫기


파일을 읽고 쓰려면 먼저 파일을 열어야 합니다. 파일을 연다는 것은 파일의 내용을 읽거나, 파일에 내용을 쓸 수 있는 상태로 변경하는 일을 의미합니다. 파일에 대한 작업을 완료하면 파일을 닫아야 합니다. 그래야 파일의 내용을 하드 디스크에 온전히 기록하고 파일이 사용한 버퍼 등을 반납할 수 있습니다.


솔라리스 10에서 프로세스가 열 수 있는 최대 파일 개수는 기본으로 256개입니다. 이 최대 개수는 /etc/system 파일에서 'set rlim_fd_cur=1024' 형태로 수정할 수 있습니다. /etc/system 파일은 부팅할 때 한 번만 읽어오므로 파일의 내용을 수정한 후에는 재부팅해야 적용됩니다. /etc/system 파일에 오류가 있을 경우 시스템을 부팅할 수 없으므로, 이 파일을 수정할 때는 반드시 사본을 만들어놓고 작업해야 합니다. /etc/system 파일에 오류가 있어 부팅이 안될때는 boot -a로 부팅해 복사해둔 파일을 지정하면 정상적으로 부팅할 수 있습니다. 현재 커널의 설정을 확인할 때는 mdb 명령을 사용합니다.


이와 관련 하여, 시스템 운영중에 리눅스에서는 소켓을 사용하는 multi-threaded 프로그램 환경에서는 서비스 요청이 동시에 많이 발생하면, 서비스를 처리하는 accept() 함수가 에러를 발생시키기도 합니다.


"error=24, Too many open files"


이런 경우 동시에 open() 할 수 있는 파일 갯수의 제한 때문에 변경을 해줄 필요가 있습니다. system resource를 제한할 수 있는 경우는 아래와 같습니다. (per user 또는 per process 기준)


1. 한 프로세스에 의해 만들어질 수 있는 core file size

2. 한 프로세스의 Heap (process data segment)

3. 한 프로세스에 의해 만들어질 수 있는 file size

4. 한 프로세스가 생성할 수 있는 file descriptors

5. 한 프로세스의 process stack segments

6. 한 프로세스가 초당 사용할 수 있는 cpu time

7. 한 프로세스의 Virtual memory (mapped address space)


man getrlimit을 입력하여 resource definition 정보를 볼 수 있습니다.



위의 7가지 경우중 4번 file descriptors만이 kernel tunable 변수로서 조정이 가능합니다. 즉 /etc/system 파일을 사용하는 경우입니다.

os별로 limit 값을 보면, 아래와 같습니다.


--------------------------------------------------------------------------------- 

 os ver  default(soft limit)  default(hard limit) max(stdio) max(select()) 

--------------------------------------------------------------------------------- 

 5.4~5.6          64        1024          256        1024 

 5.7(32bit)      64        1024          256        65535 

 5.7(64bit)      64        1024          256        65535 

 5.8(32bit)      256        1024          256        65535 

 5.8(64bit)      256        1024          256        65535 

 5.9(32bit)      256      65535          256        65535 

 5.9(64bit)      256        65535          256        65535 

--------------------------------------------------------------------------------- 



Barkeley stdio.h를 이용하는 경우(fopen/fclose/fread/fwrite) per-process 당 open할 수 있는 maximum fd 갯수는 256입니다. DB서버나 웹서버가 아닌 대부분의 application에서는 256보다 크게 변경할 필요가 전혀 없습니다. 때때로 이 값이 1024보다 크게 설정했을 경우, select.h 함수에 선언되어 있는 fd_set과 같은 structure 경우는 maximum fd를 1023으로 간주하기 때문에 이 범위를 벗어나는 경우 memory space의 문제가 야기됩니다. 위의 fd_set structure는 select() 함수에 의해서 사용됩니다.


solaris 7,8 등의 경우 현재 돌고 있는 프로세스에 대한 hard/soft limit 값을 변경할 수 있습니다. file descriptor를 변경하는 3가지 방법이 있습니다.


1) ulimit을 이용하는 방법

이 방법은 현재 쉘에서 soft limit 값만을 변경해 줍니다. 변경 범위는 hard limit 값 < 변경할 값 < default 값(soft limit값)입니다.

# ulimit -n <변경할 값>


2) setrlimit 함수를 사용하는 방법

이 방법은 ahrd and soft limit 모두 변경을 합니다. 단, hard limit 변경을 위해서 root 권한이 필요합니다.


3) /etc/system 파일에 넣어주는 방법

이 방법은 hard and soft limit 모두 변경합니다.


set rlim_fd_cur=4096 //모든 쉘에서 사용하는 기본값으로 Soft Limit 값.

set rlim_fd_max=4096 //한 프로세스가 open할 수 있는 파일의 갯수 Hard Limit 값


# /etc/system

set rlim_fd_max=8192

set rlim_fd_cur=8192


# ulimit -a

core file size          (blocks, -c) 0

data seg size           (kbytes, -d) unlimited

scheduling priority             (-e) 0

file size               (blocks, -f) unlimited

pending signals                 (-i) 15737

max locked memory       (kbytes, -l) 64

max memory size         (kbytes, -m) unlimited

open files                      (-n) 1024

pipe size            (512 bytes, -p) 8

POSIX message queues     (bytes, -q) 819200

real-time priority              (-r) 0

stack size              (kbytes, -s) 8192

cpu time               (seconds, -t) unlimited

max user processes              (-u) 15737

virtual memory          (kbytes, -v) unlimited

file locks                      (-x) unlimited


rlim_fd_max값만 확인하는 방법입니다.

# ulimit -Hn

4096



파일 열기: open(2)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int oflag [, mode_t mode]);

파일을 여는 데는 open 함수를 사용합니다. open 함수는 path에 지정한 파일을 oflag에 지정한 상태 플래그의 값에 따라 열고 파일 기술자를 반환합니다.


파일의 상태를 조정하는 oflag 값은 <sys/fcntl.h>(/usr/include/sys/fcntl.h) 파일에 #defined O_RDWR 2와 같이 정의되어 있습니다. oflag의 주요 플래그는 다음과 같습니다.


 종류

 기능

 O_RDONLY

 파일을 읽기 전용으로 연다.

 O_WRONLY

 파일을 쓰기 전용으로 연다.

 O_RDWR

 파일을 일기/쓰기 용으로 연다.

 O_CREAT

 파일이 없으면 생성한다. 파일을 생성할 권한은 당연히 있어야 한다. 파일이 이미 있다면 아무 의미 없는 옵션이다.

 O_EXCL

 O_CREAT 옵션과 함께 사용할 경우 기존에 없는 파일이면 파일을 생성하지만, 이미 있으면 파일을 생성하지 않고 오류 메시지를 출력한다.

 O_APPEND

 이 옵션을 지정하면 파일의 맨 끝에 내용을 추가한다.

 O_TRUNC

 파일을 생성할 때 이미 있는 파일이고, 쓰기 옵션으로 열었으면 내용을 모두 지우고 파일의 길이를 0으로 변경한다.

 O_NONBLOCK/O_NDELAY

 비블로킹(non-blocking) 입출력 옵션으로 파일을 읽거나 쓰고 난 후의 동작에 영향을 준다. 디스크의 파일 입출력보다는 FIFO 같은 특수 파일의 입출력에 의미가 있다. 디스크인 경우 읽거나 쓸 데이터가 없으면 -1을 리턴한다.

 O_SYNC/ODSYNC

 파일에 쓰기 동작을 할 때 보통 버퍼에만 쓰고 나중에 디스크와 같은 저장 장치로 옮겨 쓰는데, 이 옵션이 설정되어 있으면 저장 장치에 쓰기를 마쳐야 쓰기 동작을 완료한다. O_SYNC 플래그는 파일의 수정 시각 속성도 수정할 때까지 기다린다. 이 옵션을 설정하면 프로그램의 실행 속도는 느려질 수 있지만 디스크에 확실하게 저장됨을 보장한다.


플래그를 OR(|) 연산자로 연결해 지정할 수 있습니다. 예를 들어, 다음과 같이 파일의 용도에 따라 플래그를 지정하면 됩니다.


- 쓰기 전용으로 열 때, 이미 파일이 있는 경우 (O_WRONLY | O_TRUNC)

- 쓰기 전용으로 열 때, 파일이 없는 경우 (O_WRONLY | O_CREAT | O_TRUNC)

- 읽기/쓰기/추가용으로 열 때 (O_RDWR | O_APPEND)


mode는 파일의 접근 권한을 설정하는 것으로, O_CREAT 플래그를 지정해 파일을 생성할 때만 사용합니다. mode는 일반적으로 파일 권한을 설정하듯이 0644와 같이 지정할 수도 있지만, <sys/stat.h> 파일에 정의된 플래그를 사용할 수도 있습니다.


 플래그

 모드

 설명

 S_IRWXU

 0700 

 소유자 읽기/쓰기/실행 권한

 S_IRUSR

 0400

 소유자 읽기 권한

 S_IWUSR

 0200

 소유자 쓰기 권한

 S_IXUSR

 0100

 소유자 실행 권한

 S_IRWXG

 0070

 그룹 읽기/쓰기/실행 권한

 S_IRGRP

 0040

 그룹 읽기 권한

 S_IWGRP

 0020

 그룹 쓰기 권한

 S_IXGRP

 0010

 그룹 실행 권한

 S_IRWXO

 0007

 기타 사용자 읽기/쓰기/실행 권한

 S_IROTH

 0004

 기타 사용자 읽기 권한

 S_IWOTH

 0002

 기타 사용자 쓰기 권한

 S_IXOTH

 0001

 기타 사용자 실행 권한


위에 표를 기준으로 0644 권한을 지정하려면 다음과 같이 OR 연산자로 묶어줍니다.


mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;


open 함수는 파일 열기에 성공하면 파일 기술자를 리턴합니다. 파일 열기에 실패하면 -1을 리턴하고, 외부 변수 errno에 실패한 이유를 설명하는 오류 코드를 저장합니다. 이 오류 코드를 perror 함수로 출력하면 해당 메시지를 확인할 수 있습니다.



파일 생성: creat(2)

#include <sys/stat.h>
#include <fcntl.h>

int creat(const char *path, mode_t mode);


파일을 생성하는 전용 함수로 creat가 있는데, open 함수에 파일 생성 기능이 없었던 구번전 유닉스에서 사용하던 것입니다. creat 함수는 open 함수와 달리 옵션을 지정하는 부분이 없습니다.


아래에서 creat 함수와 open 함수는 같은 의미입니다.


creat(path, mode);

open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);


creat 함수로 파일을 생성하면 파일 기술자가 리턴되므로, 별도로 open 함수를 호출해 파일을 열 필요가 없습니다.



파일 닫기: close(2)

#include <unistd.h>

int close(int fildes);

파일을 닫는 데는 close 함수를 사용합니다. 파일 입출력 작업을 모두 완료하면 반드시 파일을 닫아야 합니다. 앞서 설명한 것처럼 한 프로세스가 열수 있는 파일의 개수에 제한이 있으므로, 제대로 닫지 않으면 최대 허용 개수를 초과해 더 이상 파일을 열지 못할 수 있습니다.


close 함수는 파일을 성공적으로 닫으면 0을 리턴합니다. 파일 닫기에 실패하면 -1을 리턴하고, 오류 코드를 외부 변수 errno에 저장합니다. open 함수를 사용해 unix.txt 파일을 생성해보겠습니다. O_EXCL 플래그를 지정해보겠습니다. O_EXCL 플래그는 파일이 이미 있으면 해당 파일을 생성하지 않고 오류 메시지를 출력합니다.


#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(void) {

        int fd;
        mode_t mode;

        mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
        // 소유자 읽기 권한 | 소유자 쓰기 권한 | 그룹 읽기 권한 | 기타 사용자 읽기 권한

        fd = open("unix.txt", O_CREAT | O_EXCL, mode);
        printf("unix.txt : fd = %d\n", fd);
        if(fd == -1) {
                perror("Excl");
                exit(1);
        }
        close(fd);

        return 0;

}


unix.txt 파일의 파일 기술자가 3임을 알 수 있는데, 이는 위의 프로그램에서 처음으로 연파일이기 때문에 기본적으로 할당되는 0, 1, 2번 외에 가장 작은 숫자가 할당된 것입니다. 하지만 기본적으로 할당된 0~2번을 닫고 파일을 열면 결과가 달라집니다.



파일 읽기와 쓰기


파일의 내용을 읽으려면 read 함수를 사용하며, 파일에 내용을 쓰려면 write 함수를 사용합니다. read와 write 함수의 리턴값의 데이터형인 ssize_t는 <sys/types.h> 파일에 int(환경에 따라 long)으로 정의되어 있습니다.


파일 읽기: read(2)

#include <unistd.h>

ssize_t read(int fildes, void *buf, size_t nbytes);


read 함수는 파일 기술자가 가리키는 파일에서 nbytes로 지정한 크기만큼 바이트를 읽어서 buf로 지정한 메모리 영역에 저장합니다. read 함수는 실제로 읽어온 바이트 수를 리턴하며, 오류가 발생히면 -1을 리턴합니다. 만일 리턴값이 0이면 파일의 끝에 도달해 더 이상 읽을 내용이 없음을 의미합니다. 파일을 열면 읽어올 위치를 나타내는 오프셋이 파일의 시작을 가리키지만, read 함수를 실행할 때마다 읽어온 크기만큼 오프셋이 이동해 다음 읽어올 위치를 가리킵니다. 이 함수는 파일에 저장된 데이터가 텍스트든 이미지든 상관없이 무조건 바이트 단위로 읽어옵니다. 읽어온 데이터를 종류에 따라 처리하는 작업은 순전히 프로그래머의 몫입니다.


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

int main(void) {
        int fd, n;
        char buf[10];

        fd = open("unix.txt", O_RDONLY);
        if(fd == -1) {
                perror("Open");
                exit(1);
        }

        n = read(fd, buf, 23);
        if(n == -1) {
                perror("Read");
                exit(1);
        }

        buf[n] = '\0';
        printf("n=%d, buf=%s\n", n, buf);
        close(fd);

        return 0;
}


파일 쓰기: write(2)

#include <unistd.h>

ssize_t write(int fildes, const void *buf, size_t nbytes);

write 함수는 read 함수와 인자의 구조는 같지만 의미가 다릅니다. 파일 기술자는 쓰기를 수행할 파일을 가리키고, buf는 파일에 기록할 데이터를 저장하고 있는 메모리 영역을 가리킵니다. buf가 가리키는 메모리 영역에서 nbytes로 지정한 크기만큼 읽어 파일에 쓰기를 수행합니다. write 함수도 실제로 쓰기를 수행한 바이트 수를 리턴하며, 오류가 발생하면 -1을 리턴하게 됩니다.


파일을 읽어서 다른 파일에 쓰기를 수행해 보겠습니다. 이때 파일 기술자는 2개가 필요합니다.


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

int main(void) {

    int rfd, wfd, n;
    char buf[10];
    mode_t mode;

    mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;

    rfd = open("unix.txt", O_RDONLY);
    if(rfd == -1) {
        perror("Open unix.txt");
        exit(1);
    }

    wfd = open("unix.bak", O_CREAT | O_WRONLY | O_TRUNC, mode);
    if (wfd == -1) {
        perror("Open unix.bak");
        exit(1);
    }

    while((n = read(rfd, buf, 6)) > 0)
        if(write(wfd, buf, n) != n) perror("Write");

    if(n == -1) perror("Read");

    close(rfd);
    close(wfd);

    return 0;

}

while문을 사용해 unix.txt 파일을 6바이트씩 읽고 출력합니다. 22행에서 read 함수가 리턴한 값이 buf에 저장된 데이터의 크기이므로, 이를 23행에서 write 함수의 세 번째 인자로 사용할 수 있습니다. write 함수의 리턴값이 출력할 데이터의 크기인 n과 다르면 쓰기 동작에 문제가 있다는 의미이므로 오류 메시지를 출력하도록 했습니다.



파일 오프셋 지정


파일의 내용을 읽거나 쓰면 현재 읽을 위치나 쓸 위치를 알려주는 오프셋이 자동으로 변경됩니다. 오프셋은 파일의 시작 지점에서 현재 위치까지의 바이트 수입니다. 파일을 열면 읽어올 위치를 나타내는 오프셋이 파일의 시작인 0이지만, rad 함수를 실행할 때마다 읽어온 크기만큼 오프셋이 이동해 다음 읽어올 위치를 가리킵니다. 이와 마찬가지로 write 함수도 파일에 내용을 쓸 때마다 오프셋이 변경되며, 다음에 쓸 위치를 가리킵니다. 한 파일에서 파일 오프셋은 오직 하나입니다. 다시 말해, 파일을 읽기/쓰기 모드로 열었을 때 파일 읽기 오프셋과 쓰기 오프셋이 별도로 있는 것이 아니므로 주의해야 합니다.


오프셋을 원하는 위치로 바꾸고, 위치를 확인하려면 lseek 함수를 사용합니다.


파일 오프셋 위치 지정: lseek(2)

#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fildes, off_t offset, int whence);


lseek 함수는 파일 기술자가 가리키는 파일에서 offset으로 지정한 크기만큼 오프셋을 이동시킵니다. 이때 offset의 값은 whence의 값을 기준으로 해석합니다. whence 부분에 사용할 수 있는 값은 다음과 같습니다.


SEEK_SET(파일의 시작 기준) | SEEK_CUR(현재 위치 기준) | SEEK_END(파일의 끝 기준)



파일 오프셋의 현재 위치를 알려면 어떻게 할까요? 다음과 같이 현재 위치를 기준으로 0만큼 이동한 값을 구하면 됩니다. 또한 뒤에 WHENCE값을 사용한 예들입니다.

cur_offset = lseek(fd, 0, SEEK_CUR);
lseek(fd, 5, SEEK_SET);  // 파일의 시작에서 5번째 위치로 이동
lseek(fd, 0, SEEK_END); // 파일의 끝에서 0번째이므로 파일의 끝으로 이동

반대 방향으로 오프셋을 이동하려면 offset 값을 음수로 지정하면 됩니다. lseek 함수는 실행에 성공하면 새로운 오프셋을 리턴하고, 실패하면 -1을 리턴합니다. lseek 함수 리턴값의 데이터형인 off_t는 <sys/types.h> 파일에 long으로 정의되어 있습니다.



파일 기술자 복사


파잉을 열면 파일 기술자가 할당도비니다. 이 파일 기술자를 복사해 같은 파일을 가리키는 두번째 파일 기술자를 생성할 수 있습니다. 파일 기술자를 복사하는 데는 dup 함수와 dup2 함수를 사용합니다.


파일 기술자 복사하기 : dup(2)

#include <unistd.h>

int dup(int fildes);

dup 함수는 기존 파일 기술자를 인자로 받아 새로운 파일 기술자를 리턴합니다. 이때 새로 할당되는 파일 기술자는 현재 할당할 수 있는 파일 기술자 중 가장 작은 값으로 자동할당됩니다. 파일 기술자의 복사는 입출력 방향 전환에서 많이 사용합니다.  



파일 기술자 복사하기: dup2(3)

#include <unistd.h>

int dup2(int fildes, int fildes2);

dup 함수는 새로운 파일 기술자를 자동으로 할당합니다. 이와 달리 dup2 함수는 새로운 파일 기술자를 지정할 수 있게 해줍니다. dup2 함수는 파일 기술자 fildes를 fildes2로 복사합니다.



파일 기술자 제어


현재 열려 있는 파일에 대한 파일 기술자의 속성을 확인하고 제어할 수 있습니다. fcntl(2) 함수를 사용하면 파일을 열때 설정한 플래그들을 조정할 수 있습니다.


#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fildes, int cmd, /* arg */ ...);


fcntl 함수는 파일 기술자가 가리키는 파일에 cmd로 지정한 명령을 수행합니다. cmd의 종류에 따라 인자를 지정해야 할 수도 있습니다. fcntl 함수의 두 번째 인자인 명령은 <fcntl.h> 파일에 정의되어 있으며 다음 두 가지를 자주 사용합니다.


F_GETFL : 상태 플래그 정보를 읽어온다.

F_SETFL : 상태 플래그 정보를 설정한다. 설정할 수 있는 플래그는 대부분 open 함수에서 지정하는 플래그다.


fnctl 함수를 사용해 상태 플래그를 읽어서 출력하고 수정해보겠습니다.


#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    int fd, flags;

    fd = open("unix.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    if((flags = fcntl(fd, F_GETFL)) == -1) {
        perror("fcntl");
        exit(1);
    }

    flags |= O_APPEND;

    if(fcntl(fd, F_SETFL, flags) == -1) {
        perror("fcntl");
        exit(1);
    }

    if(write(fd, "Hanbit Media", 12) != 12) perror("write");

    printf("before close");

    close(fd);

    return 0;
}

F_GETFL 명령을 설정해 fcntl 함수를 호출하면 현재 설정된 플래그 값이 리턴됩니다. 리턴된 플래그 값에 변경할 플래그를 OR로 연결해 새로운 플래그로 저장합니다. 지정된 플래그를 F_SETFL 명령으로 설정해 fcntl 함수를 호출하면 기존 플래그가 새로운 플래그로 변경됩니다. 파일의 설정을 O_APPEND로 변경했기 때문에 파일에 출력하면 내용이 파일의 끝부분에 추가됩니다.



파일 삭제


파일을 삭제하려면 unlink(2) 함수를 사용합니다.

#include <unistd.h>

int unlink(const char *path);

unlink 함수를 사용해 tmp.aaa를 삭제해 보겠습니다. 

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

int main(void) {
    int cnt;

    // 리턴 결과값
    cnt = unlink("tmp.aaa");
    if(cnt == -1) {
        perror("Unlink tmp.aa");
        exit(1);
    }

    printf("Unlink tmp.aaa success!!!\n");

    return 0;
}

unlink 함수를 이용해 tmp.aaa 파일을 삭제합니다. 파일을 성공적으로 삭제하면 "Unlink tmp.aaa success!!!"라는 메시지를 출력합니다.



파일과 디스크 동기화 함수


fsync(3) 함수는 메모리에 위치하고 있는 파일의 내용을 디스크로 보내 메모리와 디스크의 내용을 동기화합니다. fsync 함수는 메모리의 내용이 디스크로 모두 기록되기 전에는 리턴하지 않습니다.

#include <unistd.h>

int fsync(int fildes);