JHB의 프로그래밍 삽질기

FILE Input/Output System Call Function 본문

PROGRAMMING/Linux

FILE Input/Output System Call Function

roter 2008.11.27 14:17

1. 유닉스에서의 파일


유닉스에서의 파일이 가지는 의미는 타 OS들과는 다른 독특한 의미를 지닌다.

"유닉스 시스템에서는 모든것이 파일로 돌아간다" 란 말이있다.

장치나 혹은 네트웍통신을 위한 소켓,또한 파이프, FIFO등을 다룰때도

파일개념이(실지로 파일이다.)적용되기 때문이다.

그만큼 파일이 중요하기 때문에 우리는 가장 처음 LOW LEVEL(저수준) 파일입

출력을 보겠다.


2. 왜 LOW LEVEL File IO 인가?

일반적으로 C프로그래밍을 하면서 printf()나 scanf()등의

입축력 함수들을 많이 다뤄봤을것이다(이들을 표준 라이브러리라 한다).

하지만 우리가 여기서 습득할 함수들은 일명 System Call 함수 즉, 커널에서

제공해주는 저수준의 함수이다. 이말은 그만큼 더 커널,시스템에 근접해서

프로그래밍한다를 뜻한다.

그냥 표준라이브러리를 써도 잘 되는데 굳이 왜 Low Level 을 익혀야하는가

에 대해서 반 하는 사람도 있을건데 가장 쉬운예를 들어보겠다.

우리가 어떠한 프로그램을 짜는데 당연히 잘 작동할것이라 생각하고 소스에

자신있게 써넣은 printf()(가상의 예이다)가 문제를 일으켰을땐 어떻게 할것인가?

혹은 더이상의 성능향상이 되질 않는다 했을땐 어떻게 할것인가?

표준 라이브러리 함수를 쓰는것 밖에 모른다면 이런 문제는 해결하기 어려울것이다.

하지만 시스템콜 함수를 이용하면 더욱성능이 좋은 새로운 printf()를 만들수 있을것이고,

또한 문제점도 무난히 해결할수 있을 것이다. 헛소리니까 너무 유심히 듣진말고.,

자! 더이상의 강조는 시간낭비일것 같다. 다음으로 넘어가자.

 
 

3. 파일 디스크립터(File Descriptor)

 

우선은 이 파일 디스크립터란 놈에 대해서 분명히 알아둬야 한다.

OS가 각파일들에 입출력을 수행할때 각 파일들을 구분할 무언가가 있어야 할것이다.

그 구분할 무언가가 파일 디스크립터이다. 이놈은 int형 즉 정수형 숫자다.

쉽게 말해 우리가 프로그램을 짜면서 파일을 다룰려고 함수를 이용해서 파일을 딱 열면

프로세스는 그 파일을 딴 파일들과 구분할수 있게 번호를 붙인다는 말이다.

하지만 시스템내 모든 파일에 이 일련번호(디스크립터)를 붙이는게 아니라,

지금 사용할 파일에만 붙인다.

주절 주절 설명보다는 역시 소스를 쳐보고 컴파일해보고 그 결과를 확인해 보면서 이해

하는게 더 쉬울것 같다. 소스를 보자.


[descriptor_test.c]

#include <stdio.h>

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

int main(void)
{

       int fd_1;
       int fd_2;

       fd_1 = open("test.txt",O_RDWR| O_CREAT | O_TRUNC,00644);
       fd_2 = open("test2.txt",O_RDWR| O_CREAT | O_TRUNC,00644);

       printf("test.txt 파일의 일련번호 : %d\n",fd_1);
       printf("test2.txt 파일의 일련번호 : %d\n",fd_2);

       close(fd_1);
       close(fd_2);

       return 0;
}

실행결과

test.txt 파일의 일련번호 : 3

test2.txt 파일의 일련번호 : 4

open() 함수를 이용 파일을 생성(존재할시 읽기,쓰기 모드로 읽음) 해서

그 리턴값을 int형 변수인 fd_1,fd_2에 저장했다.

그리고 그 fd_1,fd_2 의 값을 출력해보니 3과 4가 나왔다.

프로세스가 test.txt에 대해서 입출력을 하고자할때 메모리에 어느부분인지

인식하는데 3번이란 일련번호가 저장되어있는 fd_1을 쓰게 될것이다.

즉, 앞서 설명과 같이 test.txt 파일에 3번이란 일련번호(디스크립터)를 달아

준것이다.

그리고 test.txt가 3번, test2.txt가 4번을 부여받았는데, 그럼 0,1,2 는 멀까?

그것은 바로,


디스크립터

의미

시스템 콜 상수

표준 라이브러리 상수

0

표준입력

STDIN_FILENO

stdin

1

표준출력

STDOUT_FILENO

stdout

2

표준에러

STDERR_FILENO

stderr


이다. 이들도 파일이자 디스크립터 이다.

프로그램이 실행되면서 OS가 자동으로 저 3개의 파일에 대한 디스크립터를

할당해 버리는 것이다. 그래서 위의 프로그램에서 3번부터 디스크립터가

부여 된것이다. 우리가 입출력시킬 파일은 test.txt ,test2.txt

이지만, 프로그램을 표현하는건 OS이다. 키보드입력을 받아야하고(stdin),

모니터로 출력해야하고(stdout), 비상시 에러처리(stderr)는 해야될것 아닌가?

음,. 당연한 소리 같다.

자,그럼 여기서 쓰인 첫번째 시스템콜 함수인 open()과 close()를 자세히 살펴보자.


4. open() , close()

open()의 함수원형

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open(const char *pathname, int flags);

int open(const char *pathname, int flags, mode_t mode);

각각의 인자는 (사용할파일명, 플래그, 권한설정) 이다.

두번째 인자인 플래그는 파일의 설정에 해당된다. 각각의 설정을 살펴보자.

이놈들은 |(or) 로 여러경우를 적용시키는것이 가능하다.


옵션

의미

O_RDONLY

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

O_WRONLY

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

O_RDWR

읽기겸 쓰기용 으로 파일을 연다.

위 옵션들은 반드시 하나만 들어가야한다.중첩해서 쓸수 없다.

O_CREAT

첫번째 인자에 해당하는 파일이 없는경우 파일을 생성한다.

이경우 3번째인자를 확인하여 해당퍼미션을 적용한다.

O_EXCL

파일을 생성할경우 그 파일이 존재하면, 에러를 발생시킨다.

O_CREAT와 함께 써야한다.

O_TRUNC

O_RDONLY,O_RDWR 일 경우, 파일의 길이를 0으로 만든다.

O_APPEND

파일에 쓰기를 할때 항상 파일의 끝에서부터 쓰게한다.

O_SYNC

파일에 데이터를 쓸때마다 물리적디스크(하드)에도 동일하게 적용한다.

파일버퍼와 물리디스크의 동기화란 말이다. 뒤에서 자세히 알아보자.


위의 예제에서는 읽기쓰기모드로 열거나,만들거나,그파일의 내용을 지워버린다를 뜻한다.

세번째 인자인 권한설정은 퍼미션에 관련된다.

chmod 명령어로 퍼미션을 설정할때 777, 644 등 숫자를 썻을것이다.

그걸 앞에 숫자 0을 두개 붙여서 00777, 00644 식으로 넣으면된다.

파일을 생성시킬때만 이옵션을 참고해서 퍼미션을 설정한다.

리턴값은 성공했을시 할당된 파일디스크립터를 int 형 정수로 반환하고(위의 예제에서

int형 변수로 받았다).실패했을시는 -1을 리턴한다.


close()함수의 원형

#include <unistd.h>

int close(int filedes);


close()는 할당받은 파일 디스크립터를 다시 반환하는 것이다.

인자에는 파일디스크립터를 받은 int형 변수를 넣으면 된다.

리턴값은 성공했을시 0, 실패시 -1을 리턴한다.

굳이 close()를 쓰지않아도, 프로그램이 종료되면 커널이 알아서

해제를 해주지만, 꼭 하는 습관을 들이도록하자. 프로그램이 가동중에

특정 디스크립터를 해제해야 하는 경우도 생길수 있으니까 말이다.

 


5. lseek(),read(),write()


일단 이 세가지 함수가 머하는 놈들인지 알아보자.

lseek() 부터 보면,

프로세스에서 파일을 열면 커널은 파일의 정보중에 파일옵셋

(file offset)이라는 것을 설정한다. 파일옵셋은 현재 파일을 읽거나 쓰거나

할때 그 파일속의 위치를 저장하고 있는 값이다.

이 파일의 어느부분에서 쓸 위치를 지정해주는 놈이바로 lseek() 이다.


lseek()의 함수 원형

#include <sys/types.h>

#include <unistd.h>

off_t lseek(int filedes, off_t offset, int fromwhere);


인자는 (파일 디스크립터,이동할 바이트 수,어디서부터)이다.

offset은 fromwhere값의 영향을 받는데 즉,fromwhere에서 부터 offset바이트 수만큼

파일옵셋을 이동시키겠다 란 뜻이된다.

3번째 인자인 fromwhere에 들어갈 설정들


옵션

의미

SEEK_SET

파일의 첫부분

SEEK_CUR

현재 파일옵셋 위치

SEEK_END

파일의 끝


리턴값은 정상동작일때 파일옵셋의 값을 리턴한다(파일의 시작점부터).

에러시 역시 -1을 리턴한다.

예제 소스를 보자.

[lseek_test.c]

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

int main(void)
{

       int fd_1;
       char msg[] = "123456789";
       char ch;

       fd_1 = open("test3.txt",O_RDWR| O_CREAT | O_TRUNC,00644); //파일 오픈

       write(fd_1, msg, strlen(msg)); //파일에 해당 문자열 저장

       lseek(fd_1, -1L , SEEK_CUR);  //파일의 현재위치에서 한바이트 뒤로이동
       read(fd_1, &ch, 1);  //변수 ch에 현위치 문자 저장
       printf("\n현제 파일옵셋위치의 문자 : %c\n",ch); //결과 출력

       lseek(fd_1, 1L, SEEK_SET); //파일의 첫지점에서 한바이트 이동
       read(fd_1, &ch, 1);  //변수 ch에 현위치 문자 저장
       printf("\n한바이트 이동 : %c\n", ch); //결과 출력

       lseek(fd_1,3L, SEEK_SET); //파일의 첫지점에서 세바이트 이동
       read(fd_1, &ch, 1); //변수 ch에 현위치 문자 저장
       printf("\n세바이트 이동 : %c\n", ch); //결과 출력

       close(fd_1); //파일 닫기

       return 0;

}


실행결과

현제 파일옵셋위치의 문자 : 9

한바이트 이동 : 2

세바이트 이동 : 4



 

파일을 오픈하고 write()를 이용, 변수 msg에 들어있는 스트링을 파일에 저장한다.

이때 파일옵셋의 위치는 EOF(파일 맨끝)이다.

그래서 lseek(fd_1, -1L , SEEK_CUR)하고 값을 출력시키면 9가 나오는 것이다.

lseek()에서 SEEK_SET 옵션은 파일의 첫지점으로 파일옵셋을 이동시킨다했는데,

위 예제의 문자열 "123456789" 에서 '1'을 가르키게 된다.

1부터 한바이트 이동해보라 당연히 2가 나오지 않는가?

잘 이해가 가지 않는 다면 위 예제소스를 요리저리 바꿔보며 테스트해보길 바란다.

자, 이제 위 소스에 갑자기 등장한 read() 와 write() 함수를 살펴보자.

read()는 파일에서 데이터를 읽는 함수이다.

함수원형부터 살펴보자.

read()의 함수원형

#include <unistd.h>

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


 

read()는 현재 파일옵셋 위치에서 부터 nbytes 만큼의 데이터를 읽어

buf에 저장하는 함수이다.

그리고 읽은 바이트 만큼 파일옵셋을 이동시킨다.(중요하다!!)

인자를 살펴보면 (파일 디스크립터,저장받을 버퍼,얼마만큼의 바이트) 이다.

버퍼의 형태를 보면 void* 인데, 이는 어떤 형태의 자료형으로라도 받을수

있다는 것을 뜻한다. 구조체도 들어갈수있다. 편리하지 않은가?

리턴값은 읽어들인 만큼의 바이트 수를 리턴한다. 따라서 보통 리턴값과

세번째인자인 nbytes의 크기는 같다. 하지만, nbytes만큼 읽을려고 했는데

EOF(파일끝)을 만나면, 거기까지 읽어서 버퍼에 저장하고 그만큼의 값을

리턴한다.에러가 아니라는 말이다.

현재 파일옵셋위치가 파일의 끝에 있으면 0을 리턴하고 에러는 -1을 리턴한다.

일반적으로 read()를 사용하는 법은 저밑에 예제 소스에서 자세히 다룰것이지만,

간략하게 살펴보겠다.


 

int fd_1; //파일 디스크립터

char char_buf[128]; // 받을 버퍼

//....파일 open 했다치고,

if(read(fd_1,char_buf,sizeof(char_buf)) < 0){

       printf("read() 쓰다 에러가 났어요.\n");

}


 

이런식으로 사용한다.

다음 write() 함수를 보자.

write()도 read()와 비슷하다. 다만 동작이 반대일 뿐! 함수원형부터 살펴보자.

write()함수 원형

#include <unistd.h>

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


 

인자들은 read()와 같다. 역시나 현재 파일옵셋 위치부터 nbytes만큼 쓰는! 놈이다.

그리고 역시나 읽은 바이트 만큼 파일옵셋을 이동시킨다.(역시나 중요하다!!)

리턴값은 쓴만큼의 바이트수를 리턴하고, 에러는 역시 -1 을 리턴한다.

read()와 다른점은 쓴만큼의 바이트수와 3번째인자인 nbytes의 값이 같아야지

정상동작이 된다. 다르다면 에러인 -1을 리턴한다.

write()에서 정말 중요한건,"123456789" 이란 문자열이 파일에 있다할때,

현재 파일옵셋이 3에 위치해있다. 근데 우리는 whatsay란 문자를 지금 위치서

부터 write()할것이다. 그러면 결과값은 "12whatsay3456789" 가 아니라

그 위치부터 위에 덮어써서 "12whatsay" 가 된다는 것이다.

write()를 사용하는 일반적인 방법은


 

int fd_1; //파일 디스크립터

int buf_len; //버퍼 길이

char char_buf[] = "123456789"; // 쓸 버퍼

//....파일 open 했다치고,

buf_len = strlen(char_buf);

if(write(fd_1,char_buf,buf_len) != buf_len){

       printf("write() 쓰다 에러가 났어요.\n");

}


 

혹은 read() 처럼,


 

if(write(fd_1,char_buf,buf_len) < 0){

       printf("write() 쓰다 에러가 났어요.\n");

}

자, 우리는 시스템 콜 펑션으로 파일을 열고, 옵셋설정하고, 읽고, 쓰고, 닫고

하는법을 습득했다. 배운걸 정리하는 예제를 하나 쓰고 다음강좌에서

파일입출력시 커널의 상태에 대해 알아보도록 하겠다.

[fileio_test.c]

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

typedef struct
{
       char id[16];
       char email[32];
       int age;
}user;

int main(void)
{
       user on_user[] = {
               {"whatsay","
sf2000@nownuri.net",22},
               {"foxdirector","
foxdirector@hotmail.com",28},
               {"kkaedong","
kkaedong@hotmail.com",28}
       };

       user on_user2[] = {
               {"setset","
setset@tegd.net",25},
               {"asdasd","
asdasd@asd.net",26},
               {"ertret","
ertret@ret.ert",27}
       };

       user cp_user[6];

       int fd,i,user_count;
       off_t temp_offset; //옵셋저장 할 임시 변수

       user_count = sizeof(on_user) / sizeof(on_user[0]); //구조체에 몇명의 정보인가?

       if((fd = open("test4.txt",O_RDWR | O_CREAT | O_TRUNC, 00644)) < 0){



 

       //파일 오픈
               perror("open() failed\n");
               exit(1);
       }

       if((write(fd,on_user,sizeof(on_user))) < 0){        //파일에 on_user구조체 정보 씀
               perror("write() failed\n");
               exit(1);
       }
       else{
               printf("파일쓰기 성공\n");
       }

       if((temp_offset = lseek(fd,0L,SEEK_CUR)) < 0){ //현재 옵셋위치 저장
               perror("offset copy failed\n");
               exit(1);
       }

       if((lseek(fd, 0L, SEEK_SET)) < 0){        //파일옵셋을 파일의 맨처음으로 옮김
               perror("lseek() failed\n");
               exit(1);
       }

       if((read(fd,cp_user,sizeof(cp_user))) < 0){          //파일에서 읽어서 cp_user 구조체에 복사
               perror("read() failed\n");
               exit(1);
       }

       printf("[파일 정보]\n");
       for(i = 0; i<user_count ; i++){ //파일내용 출력

               printf("ID : %s, E-Mail : %s, Age : %d \n"                     ,cp_user[i].id,cp_user[i].email,cp_user[i].age);
       }

       if((lseek(fd,temp_offset,SEEK_SET)) < 0){         //위의 저장한 옵셋으로 파일옵셋 옮김
               perror("lseek() failed\n");
               exit(1);
       }


       if((write(fd,on_user2,sizeof(on_user2))) < 0){         //그 파일옵셋서 부터 on_user2구조체 내용 씀
               perror("write() failed\n");
               exit(1);
       }

       if((lseek(fd, 0L, SEEK_SET)) < 0){         //파일옵셋을 파일의 맨 처음으로 옮김
               perror("lseek() failed\n");
               exit(1);
       }

       if((read(fd,cp_user,sizeof(cp_user))) < 0){         //파일에서 읽어서 cp_user 구조체에 복사
               perror("read() failed\n");
               exit(1);
       }

       user_count = sizeof(cp_user) / sizeof(cp_user[0]);         //user 갯수 개산

       printf("\n[수정된 파일 정보]\n");
       for(i = 0; i < user_count; i++){ //수정된 파일 내용 출력

               printf("ID : %s, E-Mail : %s, Age : %d \n"                    ,cp_user[i].id,cp_user[i].email,cp_user[i].age);
       }

       close(fd); //파일디스크립터 닫음
       return 0;
}

실행결과

파일쓰기 성공

[파일 정보]

ID : whatsay, E-Mail : sf2000@nownuri.net, Age : 22

ID : foxdirector, E-Mail : foxdirector@hotmail.com, Age : 28

ID : kkaedong, E-Mail : kkaedong@hotmail.com, Age : 28

[수정된 파일 정보]

ID : whatsay, E-Mail : sf2000@nownuri.net, Age : 22

ID : foxdirector, E-Mail : foxdirector@hotmail.com, Age : 28

ID : kkaedong, E-Mail : kkaedong@hotmail.com, Age : 28

ID : setset, E-Mail : setset@tegd.net, Age : 25

ID : asdasd, E-Mail : asdasd@asd.net, Age : 26

ID : ertret, E-Mail : ertret@ret.ert, Age : 27


0 Comments
댓글쓰기 폼