제임스딘딘의
Tech & Life

개발자의 기록 노트/C

[C] 저수준 파일 입출력

제임스-딘딘 2011. 10. 3. 06:49

저수준 화일 입출력 함수들

저수준의 화일 입출력에서는 FILE이란 구조 대신 간단하게 각 화일마다 번호를 사용하는데, 이를 화일 식별자(file descriptor), 또는 핸들(handle)이라고 한다. 이 핸들은 0 이상의 값을 가지고 있는데 실제로 0과 1, 2 는 고정된 의미(핸들 0은 표준 입력을 위한 번호이며 1은 표준 출력, 그리고 2는 표준 에러로 사용)를 갖고 있어서 화일을 처음 열게 되면 그 화일의 핸들은 3이 된다.

윈도우즈 상에서는 저수준 입출력 함수에 모두 '_'를 붙인다.
예) _open, _write, _O_RDONLY, _close ...

open 함수

저수준의 화일 입출력에서도 화일을 사용하기 위해서는 화일을 먼저 열어야 하며, 이때 다음과 같이 open 함수를 사용한다.

int fd;                                                                               
fd = open("화일 이름 , 액세스 방식\[, 모드\]);

위에서 [CLang: ]는 역시 생략할 수 있는 부분을 의미하며, 화일 이름은 fopen과 같이 열 화일의 이름이고 액세스 방식은 이 화일을 어떻게 열 것인가인데, fopen과는 달리 다음과 같은 형태로 사용한다.

모드

내 용

O_RDONLY

0x0001

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

O_WRONLY

0x0002

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

O_RDWR

0x0004

읽고 쓰기 위해 화일을 연다.

O_CREAT

0x0100

화일이 없을 경우 새로운 화일을 만든다.

O_TRUNC

0x0200

현재 있는 화일의 내용을 0으로(제거) 한다.

O_EXCL

0x0400

O_CREAT과 함께 사용하며, 화일이 없을 경우에만 연다.

O_APPEND

0x0800

화일을 쓰기용으로 열고 화일 포인터를 화일의 끝에 위치시킨다.

O_TEXT

0x4000

화일을 텍스트 형식으로 연다.

O_BINARY

0x8000

화일을 이진 형식으로 연다.

위의 O_로 시작하는 것들은 모두 상수로 이의 정의는 fcntl.h(이것은 file control의 약자)에 들어있기 때문에 open 문을 사용하려면 반드시 fcntl.h를 포함하여야 한다.
위의 것들은 액세스 방식의 한 조건들로 여러 개를 동시에 같이 사용할 수 있으며 이 때에는 각 조건들을 '|'(비트 연산자 OR)를 이용해서 묶으면 된다.
data.dat 란 화일을 읽기 전용으로 열고자 할 때에는 다음과 같이 하면 된다.

fd = open("data.dat", O_RDONLY);

그리고 기존의 화일이 있으며, 이를 지우고 쓰기 전용으로 열 때에는 다음과 같이 하면 된다.

fd = open("data.dat", O_WRONLY \| O_TRUNC);

O_TRUNC이 추가 되었는데, 만약 이를 써 주지 않으면 쓰기 전용이라도 화일의 내용은 없어지지 않는다. 반면에 화일의 끝에 추가하고자 할 때에는 다음과 같이 O_APPEND를 써 주면 된다.

fd = open("data.dat", O_WRONLY \| O_APPEND);

fopen 문과 다른 것은 해당 화일이 없는 경우 화일이 만들어지지 않는다는 것이다. 만약 화일이 없을 때 화일을 만들도록 하고 싶으면 다음과 같이 O_CREAT를 지정하여야 하며, 이 때에는 세번재 인자가 필요하다.

fd = open("data.dat", O_WRONLY \| O_CREAT \| O_TRUNC, 모드);                           
                /\*  화일이 있으면 그 내용을 지우고 없으면 생성, fopen의 "w"와 같음  \*/
fd = open("data.dat", O_WRONLY \| O_CREAT \| O_APPEND, 모드);                          
             /\*  화일이 있으면 그 끝으로 이동하고 없으면 생성, fopen의 "a"와 같음  \*/

세번째 인자인 모드에는 액세스 방식에서 O_CREAT 플래그가 지정된 경우, sys\stat.h(TC의 경우)에 정의되어 있는 다음 기호 중 하나가 사용된다(일반적으로 8진수 0700을 주면 된다).

S_IWRITE /* 써 넣기 가능 */
S_IREAD /* 읽어 내기 가능 */
S_IREAD | S_IWRITE /* 읽기 / 쓰기 가능 */

읽기용 화일에서는 모드를 생략해도 화일의 사용에는 별 지장이 없다.

그밖에 O_RDWR은 읽고 쓰고자 할 때 사용하며 O_EXCL은 O_CREAT하고만 같이 사용하는데, 지정한 화일이 있으면 에러가 난다. 즉 화일이 없는 상태에서 새로 만들고자 할 때에는 O_EXCL 과 O_CREAT를 같이 사용하면 된다.

open이 제대로 화일을 열게 되면 음이 아닌 정수값을 반환하는데, 그 화일의 핸들을 계산 값으로 산출하게 되며 에러 발생의 경우 -1을 계산 결과로 산출하게 된다. 따라서 다음과 같이 항상 조사하는 것이 필요하다.

if ((fd = open("data.dat", O_RDONLY)) == \-1) {                                 
   fprintf(stderr,"Error: Cannot open data.dat\n");                              
    :

read 함수

화일을 열었으면 이를 읽고 써야 하는데, 읽을 때에는 다음과 같이 read 란 함수를 사용한다.

int n;                                                                                
 char \*buf;                                                                           
 int size;                                                                             
 int fd;                                                                               
 n = read(fd, buf, size);

여기서 fd는 open에서 넘겨준 화일의 번호이고 buf는 읽어들일 데이터를 저장할 공간(버퍼)을 가리키는 포인터이다. 그리고 size는 몇 개의 바이트를 읽어들일 것인가, 그 크기를 나타낸다. 즉 read는 화일 fd로부터 size 만큼의 바이트를 읽어들여 이를 buf가 가리키는 곳에 저장하게 된다.

화일 내에 size 만큼의 데이터가 있지 않을 수도 있는데, 이를 조사하기 위해 read는 자신이 실제로 읽어들인 바이트의 수를 계산 결과로 산출하고 있다. 따라서 read를 사용할 때에는 항상 위의 n 값을 잘 조사하여야 하며, 이것이 실제 읽어들인 데이터의 수이기 때문에 buf가 가리키는 공간에는 바로 n 바이트가 존재하게 된다. 이 n이 0이라는 것은 읽어들인 데이터가 없다, 즉 EOF라는 의미이며 이 값이 -1이면 이는 읽을 때 에러가 발생한 것을 의미한다.

예를 들어 data.dat란 화일로부터 80 바이트의 데이터를 읽어들이고자 할 때에는 다음과 같이 사용한다.

int fd;                                                                              
 char buf\[80\];                                                                       
 int n;                                                                                
 fd = open("data.dat", O_RDONLY);                                                      
 n = (fd, buf, 80);        /\*  두번째 인자(buf)에 들어가는 값은 포인터이어야 한다  \*/

이때 n이 80이면 잘 읽은 것이고 다른 값이면 데이터가 부족하거나 뭔가 잘못된 것이다. 일반적으로 읽을 데이터의 크기는 다르지만 버퍼의 크기는 보통 고정적으로 사용하는데 stdio.h에 정의되어 있는 BUFSIZ를 사용하는 것이 관례로 되어 있다. 이 크기는 한 화일로부터 가져올 수 있는 데이터의 최적치이기 때문에 많은 양의 데이터를 읽거나 쓸 때에는 이 것을 사용하는 것이 성능을 최대한 높일 수 있다.

다음은 read 함수의 한 예로서

size file(1) ... fine(n)

과 같이 사용하여 각 화일들의 크기를 계산하여 출력하는 프로그램이다.

#include <stdio.h>
#include <fcntl.h>
main(int argc, char *argv[]) {
  int fd;
  long int size;
  int n;
  char buf[BUFSIZ];
  if (argc == 1) {
    fprintf(stderr,"USAGE: %s file(1) file(2) ... file(n)\n",*argv);
    return (0);
    }
  while (--argc > 0) {
    if ((fd = open(*++argv, O_RDONLY | O_BINARY)) == -1) {
      fprintf(stderr,"Error: Cannot open %s\n",*argv);
      continue;
      }
    size = 0l;
    while ((n = read(fd, buf, BUFSIZ)) > 0)
      size += n;
    if (n == 0)
      fprintf(stdout,"%s: %ld bytes.\n",*argv,size);
    else
      fprintf(stderr,"Error in reading %s\n",*argv);
    }
  close(fd);          /*  핸들이 다루는 화일을 닫는다. 되돌림 값은 화일 닫기가  */
  }                   /*  성공하면 0, 실패하면 -1의 값을 되돌린다  */
결과

C:\TC>size tc.exe tcc.exe 뇌
tc.exe: 290249 bytes.
tcc.exe: 179917 bytes.

write 함수

저수준의 화일 입출력에서 화일로부터 읽어들이는 것은 이 read 밖에 없다. 즉 문자 하나씩 밖에 읽어들이지 않기 때문에 만약 정수값을 읽어들이고자 할 때에는 읽어들인 것을 정수로 변환하여야 한다.

따라서 복잡한 포맷으로 된 데이터를 읽어들일 때에는 고수준의 화일 입출력을 사용하는 것이 좋다.

read 와 반대로 화일에 쓰고자 할 때는 다음과 같이 write이라는 함수를 사용한다.

int fd;                                                                               
 char \*buf;                                                                           
 int size;                                                                             
 int n;                                                                                
 n = write(fd, buf, size);

fd는 open에 의해 넘겨 받은 화일의 핸들이며 buf는 출력할 데이터가 들어 있는 곳을 가리키는 포인터이고, size는 출력할 데이터의 크기(바이트수)를 의미한다. 즉 fd가 나타내는 화일에 size만큼의 바이트를 buf로부터 가져와 출력하라는 의미가 된다. 이때 write 함수는 실제로 쓴 바이트의 수를 계산 결과로 산출하게 된다. 이는 항상 size와 같지는 않은데, 예를 들어 디스크가 꽉차서 더 이상 쓸 공간이 없게 되면 지정한 크기 보다 적은 수를 쓰게되므로 n이 size보다 작을 수 있다. 그리고 에러가 발생한 경우에는 read와 같이 -1을 계산 결과로 산출한다.

다음은 read와 write 함수를 사용하여 한 화일을 똑같이 복사하는 프로그램이다.

#include
#include
main(int argc, char *argv[]) {
  int fd1, fd2;
  char buf[BUFSIZ];
  int n;
  if (argc != 3) {
    fprintf(stderr,"USAGE: %s sourcefile objectfile\n",*argv);
    return (1);
    }
  if ((fd1 = open(*(argv+1), O_RDONLY | O_BINARY)) < 0) {
    fprintf(stderr,"Error: Cannot open %s\n",*(argv+1));
    return (2);
    }
  if ((fd2 = open(*(argv+2), O_WRONLY | O_TRUNC | O_CREAT | O_BINARY, 0700)) < 0) {
    fprintf(stderr,"Error: Cannot create %s\n",*(argv+2));
    return (3);
    }
  printf("FileCopy %s to %s\n\n",*(argv+1),*(argv+2));
  while ((n = read(fd1, buf, BUFSIZ)) > 0)
    if (write(fd2, buf, n) != n) {    /*  read와 마찬가지로 저수준의 화일 입출력에서 출력 함수는 write 밖에 없다.
                                          따라서 문자 데이터를 출력하기에는 괜찮지만 정수나 실수값을 출력하고자 할 때에는
                                          이를 ASCII 코드 형태로, 즉 문자 형태로 변환한 다음 출력하여야 하기 때문에
                                          그러한 경우에는 고수준의 입출력 함수를 사용하는 것이 더 편리하다  */
      fprintf(stderr,"Error in writing %s\n",*(argv+2));
      return (4);
      }
  if (n < 0)
    fprintf(stderr,"Error in reading %s\n",*(argv+1));
  close(fd1);
  close(fd2);
  }
 
결과

C:\TC>slcopy tc.exe ec.exe 뇌
FileCopy tc.exe to ec.exe