리눅스 Tips, 리눅스 C/C++ 프로그래밍, 모바일 클라우드 동향 및 테스트 등

2012년 12월 5일 수요일

리눅스 시스템 프로그래밍 : 03 입력과 출력(2)


5. 파일에서 읽기

 파일을 성공적으로 열었다면, 이제 읽거나 쓰는 등의 작업을 하면 된다. 여기에서는 파일을 읽는 법에 대해서 알아보도록 할 것이다. 우선 다음과 같은 내용의 샘플 파일을 하나 준비하도록 하자. 우리는 아래의 파일의 내용을 읽어서 화면에 출력하는 프로그램을 작성하는 것으로 파일 읽는 법에 대해서 배울 것이다. 파일의 이름은 fly.txt로 하겠다.

Fly me to the moon And let me play among the stars
Let me see what spring is like on Jupiter and Mars
In other words hold my hand
In other words darling kiss me

 - 'fly me to the moon' 가사 중 일부분이다.

 파일을 읽기 위해서는 당연히 읽기 전용 혹은 읽기/쓰기 상태로 파일을 열어야 한다. 여기에서는 읽기 전용 모드로 파일을 열도록 할 것이다. 만들어진 파일을 여는 것이기 때문에 O_CREAT는 필요 없으며, 파일을 생성하는 것이 아니기 때문에 모드 인자 역시 필요 없다. open 함수는 다음과 같이 사용할 수 있을 것이다.

  fd = open("fly.txt", O_RDONLY);
  if (fd < 0)
  {
    ...
  }

 성공적으로 파일을 열었다면 fd는 0보다 큰 수가 리턴 되었을 것이고, - 대부분의 경우 2보다 큰 수가 리턴 될 것이다. 이유는 아래에서 설명할 것이다 - 리턴 받은 정수를 파일 지정 번호로 사용하게 된다. 우리는 이 파일 지정 번호를 이용해서 파일의 내용을 읽게 된다.

   5.1 read 시스템 콜

 열린 파일로부터 데이터를 읽기 위해서 제공하는 시스템 함수read 이다. 이 함수는 인자로 주어진 파일 지정 번호가 가리키는 파일로부터, 지정된 크기 만큼의 데이터를 읽게 된다. 다음은 read 함수의 원형이다.

#include <unistd.h>

size_t read(int fd, void *buf, size_t count);
 
  1. fd : open 으로 열린 파일을 가리키는 파일 지정 번호
  2. buf : 읽은 데이터를 저장할 공간
  3. count : 읽을 데이터의 크기로 byte 단위

 함수는 단순하며 직관적이다. read 함수는 성공적으로 실행될 경우 0보다 큰 수를 리턴 한다. 파일의 끝에 다다라서 더 이상 읽을 데이터가 없다면 0을 리턴 한다. read 함수를 이용하는 일반적인 방법은 루프를 돌면서 리턴 값이 0이 될 때까지 - 즉 파일의 끝을 만날 때까지 - 데이터를 읽는 것이다. 다음과 같은 형태로 사용할 수 있을 것이다.

int readn = 0;
int fd;
char buf[80];
fd = open(...);

memset(buf, 0x00, 80);
while( (readn = read(fd, buf, 79) )
{
  // 읽은 데이터가 있는 buf를 이용해서 필요한 작업을 한다.
  memset(buf, 0x00, 80);
}

 주의해야 할 점은 데이터가 저장되는 buf를 memset 함수를 이용해서 초기화 시켜줘야 한다는 점이다. read 함수는 count만큼 데이터를 읽어들여서 buf에 복사하기만 할 뿐, 내용을 초기화 시키기 않기 때문이다. 예를 들어 이전에 79byte를 읽어들였고, 이번에 읽어들인 데이터가 20byte였다면, 21byte 이후에 이전 데이터가 그대로 남아 있어서 잘못 처리할 수 있기 때문이다. 물론 read 의 리턴 값을 이용해서 20byte를 읽어 왔다는 것을 알 수 있으므로 주의해서 처리하면 되긴 하겠지만 실수할 만한 여지는 미리 제거하는게 좋을 것이다.

 아래의 프로그램을 실행시켜 보기 바란다. 프로그램의 이름은 fly.c로 하자.

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

#define MAXLEN 80
int main()
{
  int fd;
  int readn = 0;
  char buf[MAXLEN];
  fd = open("fly.txt", O_RDONLY);
  if (fd < 0)
  {
    perror("file open error:");
    return 1;
  }
  memset(buf, 0x00, MAXLEN);
  while( (readn = read(fd, buf, MAXLEN-1 )) > 0)
  {
    printf("%s", buf);
  }
}
 
 프로그램을 실행시켜 보면, 마지막에 다음과 같이 이전에 읽어들인 값이 출력되는 것을 볼 수 있을 것이다.

Fly me to the moon And let me play among the stars
Let me see what spring is like on Jupiter and Mars
In other words hold my hand
In other words darling kiss me
on Jupiter and Mars
In other words hold my hand
...

 이제 21줄 다음에 memset(buf, 0x00, MAXLEN); 을 추가하고나서 다시 실행시켜 보도록 하자. 문제 없이 깔끔하게 출력되는걸 확인할 수 있을 것이다.

 또 하나 코드에서 궁금한 점이 있을 것이다. read 에서 버퍼의 최대 크기인 MAXLEN 만큼을 읽어들이지 않고 MAXLEN-1 만큼을 읽어들이는 점이다. 이는 역시 버퍼의 크기를 넘어서서 데이터를 읽어버리는 만약의 실수를 막기 위함이다. printf 함수의 경우 널문자('\0')를 만나기 전까지 데이터를 읽어들이게 된다. 버퍼를 가득채워서 읽어들였는데, 버퍼메모리 영역의 마지막이 '\0'이 아닐 경우 끝이 아니라고 판단해서, '\0'을 만날때까지 메모리영역을 벗어나서 계속 읽어 버리는 문제가 발생할 수 있기 때문이다. 그러하니 버퍼의 마지막 라인을 '\0'으로 만들어 버리는게 깔끔하다.

  01 ...  89 80
 +-------+--+--+---------------------------+
 | ..... |  |  | '\0'이 아닌 알 수 없는 값 |
 +-------+--+--+---------------------------+
 |<--- buf --->|

 이 경우에도 읽어들인 데이터의 크기를 알 수 있기 때문에 snprintf()와 같은 함수를 이용해서 문제를 해결할 수 있을 것이다. 그렇지만 가능한 문제 발생 여지를 없애는 쪽으로 코딩을 하는게 좋을 것이다.

 그렇다고 해서, 모든 경우에 있어서 문제가 되는건 아니다. 파일로부터 읽어들일 데이터가 char, int 와 같은 원시데이터 타입일 경우에는 크기가 명확히 명시되므로 위에서와 같은 초기화 관련된 문제가 발생하지 않는다.

   5.2 버퍼 공간의 크기

 buffer가 사용되는 일반적인 이유는 잡음을 없애고 성능을 높이기 위함이다. 읽어들일 데이터가 1024 만큼이 있다고 가정해보자. 버퍼의 크기를 1로 잡았다면, read 함수를 1024번 호출해야 될것이다. 만약 버퍼의 크기를 512로 잡는다면, 단 2번만 read 함수를 호출하면 될 것이다. 후자가 더 효율적일 거라는 것은 분명하다.

 그렇다고 해서 무작정 메모리를 크게 잡는 것도 낭비다. 시간과 비용이 관련된 대부분의 현상이 그렇듯이 어느정도 크기가 지나면 성능의 증가폭이 줄어드는 지점이 오기 때문이다. 적당한 선에서 트레이드오프 해야할 필요가 있다.

 어떤 데이터를 처리하느냐에 따라 다르겠지만 512byte나 1024byte정도의 크기로 하는게 무난하다고 알려져 있다.

6. 파일에 쓰기

 파일에 데이터를 쓰기 위해서는 쓰기 전용 혹은 읽기/쓰기 가능 모드로 열어야 한다. 리눅스 커널은 쓰기 요청을 위한 write 함수를 제공한다.

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
 
 이 함수는 count 크기 만큼의 buf 에 있는 내용을 파일 지정 번호 fd 가 가리키는 파일에 쓸 것을 커널에 요청한다. 성공한 경우 쓴 byte 크기 만큼을 리턴한다.

 다음은 data.txt 파일을 열어서 int형 데이터를 쓰는 프로그램이다. 이 프로그램의 이름은 write.c로 하겠다.

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

int main()
{
    int fd;
    int i;
    int wdata = 0;
    int wsize = 0;
    fd = open("data.txt", O_CREAT|O_WRONLY);
    if (fd < 0)
    {
        perror("file open error");
        return 1;
    }

    for (i = 0; i < 100; i++)
    {
        wdata = i * 2;
        wsize = write(fd, (void *)&wdata, sizeof(int));
        printf("Write %d (%d byte)\n", wdata, wsize);
    }
    close(fd);
}

 22줄을 주의깊게 살펴보도록 하자. fd 에 int형 변수, wdata 값을 쓰려고 하고 있다. wdata 는 int형 데이터이기 때문에, void *형으로 형 변환을 했다. 마지막으로 sizeof 함수를 이용해서 쓰고자 하는 데이터의 크기를 구해서, write의 3번째 인자로 되돌려 줬다. int 데이터 타입의 크기는 4byte라는 것을 이미 알고 있기 때문에, sizeof를 쓰지 않고 4를 직접 명시해도 될 것이다.

 그러나 어떤 운영체제와 컴파일러의 환경에 따라서 int가 2byte 혹은 8byte가 되는 경우도 있다. 그러므로 이식성을 고려한다면 sizeof 함수를 이용해서 데이터 타입의 크기를 얻어내는 방법을 사용하는 것을 권장한다.

 프로그램을 컴파일 하고 실행시키면 다음과 같은 결과를 볼 수 있을 것이다.

# gcc -o write write.c
# ./write
Write 0 (4 byte)
Write 2 (4 byte)
Write 4 (4 byte)
Write 6 (4 byte)
Write 8 (4 byte)
... 
 
 ls로 data.txt 파일이 생성된 것을 볼 수 있을 것이다. 이 파일의 내용을 살펴보기 위해서 vi 로 열어도 내용을 알아볼 수는 없을 것이다. 이 파일의 내용은 ASCII printable 데이터 - 흔히 말하는 문자데이터 - 가 아니기 때문이다.

7. 파일 닫기

 열린 파일을 더 이상 쓰지 않는다면, 닫아주어야 한다. 그렇지 않을 경우 프로그램이 종료될 때까지, 계속 남아서 컴퓨터 시스템의 자원을 소비하게 된다. 파일의 종료는 close 함수를 이용하면 된다.

#include <unistd.h>

int close(int fd);


 - 다음 포스트에 이어서...

댓글 없음:

댓글 쓰기