2. 파일 입출력
2-1. 개요
파일은 관련있는 데이터의 집합이다. 하드디스크같은 저장장치에 일정한 형태로 저장된다.
파일은 데이터를 저장하는데는 물론, 데이터를 전송하거나 장치에 접근하는데도 사용한다.
유닉스에서 파일은 일반파일(regular file)과 특수파일(special file)로 구분된다.
종류 | 용도 |
---|---|
일반파일 | 텍스트 바이너리 형태의 데이터를 저장하는 파일 |
특수파일 | 데이터 전송, 장치 접근에 사용되는 파일 |
유닉스에서 파일을 읽고 쓰는 방법은 low-level 파일과 high-level 파일 입출력으로 구분할 수 있다.
저수준 파일 입출력
유닉스 커널의 시스템 호출을 사용하여 파일 입출력을 수행한다.
시스템 호출을 이용하기 때문에 좀더 빠르게 파일에 접근 가능하다.
바이트단위로 파일의 내용을 다루므로 특수 파일도 읽고 쓸 수 있다.
바이트단위로 입출력을 수행하기때문에 응용 프로그램 작성시 변환 함수등의 추가 기능을 구현해야한다.
열린 파일을 참조하는데에 파일 기술자(file descriptor)를 사용한다.
파일 지시자 : int fd
주요 함수 : open, close, read, write, dup, dup2, fcntl, lseek, fsync
고수준 파일 입출력
C언어 표준 함수로 제공된다.
데이터를 바이트 단위로 한정짓지 않고 버퍼를 이용해 한번에 읽기쓰기를 수행한다.
다양한 입출력 변환 기능이 구현되어 있어 데이터형에 따라 이용 가능하다.
고수준 파일 입출력을 표준 입출력 라이브러리(standard input/output library)라고 한다.
열린 파일 참조시 파일 포인터(file pointer)를 사용한다. 파일 포인터란 열린 파일의 특성에 대한 정보를 저장하는 구조체(struct)를 가리키는 포인터이다. 이 파일 포인터 구조체의 항목 중 하나가 파일 기술자이다.
파일 지시자 : FILE *fp;
주요 함수 : fopen, fclose, fread, fwrite, fputs, fgets, fprintf, fscanf, freopen, fseek
2-2. 저수준 파일 입출력
저수준 파일 입출력은 바이트 단위로 입출력을 수행하므로 함수 자체 구조는 단순한 편이다. 특수 파일도 읽고 쓸 수 있으므로 파일 입출력의 기본이 된다.
파일 기술자(file descriptor)
파일 기술자는 현재 열려 있는 파일을 구분할 목적으로 유닉스가 붙여놓은 번호이다. 저수준 파일 입출력에서 열린 파일을 참조하는데 사용하는 지시자 역할을 한다. 파일 기술자는 정수값으로, open함수를 사용해 파일을 열었을 때 부여된다.
<파일 기술자 0~2 의 용도>
- 0 : 표준 입력
- 1 : 표준 출력
2 : 표준 오류 출력
프로세스는 동작하면서 필요에 따라 파일을 열고 닫는다. 한 프로세스가 동시에 열 수 있는 파일의 개수에는 제한이 있다. 이 개수를 변경하려면 유닉스 커널의 설정을 바꾼 후 재부팅해야한다.
솔라리스 10에서 한 프로세스가 열수 있는 최대 파일개수는 256개이다.
/etc/system 파일에서 'set rlim_fd_cur = 1024' 형태로 수정할 수 있다.
이 파일은 부팅시 한번만 읽으므로 수정후 재부팅해야한다. 이 파일이 오류가 나면 부팅이 안되므로 사본을 꼭 챙겨야한다.
파일에 오류가 나서 부팅이 안될 때는 'boot -a'로 부팅해서 복사해둔 파일을 지정하면 부팅이 가능하다.
# mdb -k
>rlim_fd_cur/D (D: 결과값을 10진수로 출력)
rlim_fd_cur: 256
프로세스가 파일을 열때 파일 기술자는 0번부터 시작해서 가장 작은 번호가 자동으로 할당된다.
프로세스가 처음 동작할 때 0,1,2는 기본으로 할당된다.
프로세스가 파일을 처음 열면 3번 기술자가 할당된다.
파일 생성과 열고 닫기
- 파일 열기: open(2)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int oflag [, mode_t mode]);
[parmeter 설명]
path : 열려는 파일이 있는 경로
oflag : 파일 상태 플래그. <sys/fcntl.h>(/usr/include/sys/fcntl.h)파일에 #define O_RDWR 2와 같이 정의되어있다.
플래그를 OR(|)연산자로 연결해 지정할 수 있다.
mode : 접근 권한(O_CREAT플래그를 지정해 파일을 생성할 때만 사용한다. 파일 권한을 설정하듯이 지정할 수 있으나
<sys/stat.h>에 정의된 플래그를 사용할 수 있다.
0644 권한 = S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH;
[return 값]
success : 파일 기술자
fail : -1 및 외부변수 errno에 실패사유 코드 저장한다. perror()로 출력하면 메세지 확인 가능.
파일 생성 : creat(2)
파일을 생성하는 전용함수인데, open함수에 파일 생성 기능이 없었던 구버전 유닉스에서 사용되던 것이다. 옵션을 지정하는 부분이 없다.
creat(path, mode); == open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); 같은 의미.
creat함수로 파일을 생성하면 파일 기술자가 리턴되기 때문에 open함수 호출할 필요가 없다.
파일 닫기 : close(2)
파일을 닫는데 사용하며 파일 입출력 작업이 끝나면 반드시 닫아야한다. 한 프로세스가 열 수 있는 파일의 개수에 제한이 있기 때문이다. 파라미터로 파일 기술자를 넘겨줘야한다.
성공적으로 닫으면 0을 리턴하고 실패하면 -1과 오류코드를 errno에 저장한다.
- 새파일 생성하는 프로그램 제작
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.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, mode);
if(fd == -1) {
perror("Creat");
exit(1);
}
close(fd);
return 0;
}
<실행>
#gcc -o ex2_1.out ex2_1.c
#ex2_1.out
#ls -l unix.txt
-rw-r--r-- 1 root other 0 1월5일 12:13 unix.txt
mode 인수를 'O_CREAT | O_EXCL'로 지정하면 파일이 이미 있으면 생성하지 않고 오류 메세지를 출력하게 된다.
fd 변수(파일기술자)는 프로그램에서 처음으로 파일을 열면 3이 할당되고 해당 함수에서
close(0);
open()를 호출하기 전에 구문을 추가하면 새로 생성된 파일의 파일기술자는 0이 된다.
가장 작은 파일 기술자 번호를 할당하기 때문이다.
파일 읽기와 쓰기
read와 write 함수의 리턴값의 데이터형인 ssize_t는
파일 읽기 : read(2)
이 함수는 파일 기술자가 가리키는 파일에서 지정한 크기만큼 바이트를 읽어서 buf로 지정한 메모리 영역에 저장한다. 리턴값이 0이면 더이상 읽을 내용이 없다는 의미이다. 파일을 열면 읽어올 위치를 나타내는 오프셋이 파일의 시작을 가리키지만, read함수를 실행할 때마다 읽어온 크기만큼 오프셋이 이동해 다음 읽어올 위치를 가리킨다. 무조건 바이트 단위로 읽는다.
#include <unistd.h>
ssize_t read(int fildes, void *buf, size_t nbytes);
[인자 설명]
fildes : 파일기술자
buf : 바이트를 저장할 메모리 영역의 시작 주소
nbytes : 읽어올 바이트수
<파일 읽기 : ex2_4.c>
#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, 6);
if(n == -1){
perror("Read");
exit(1);
}
buf[n] = '\0';
//read함수는 읽어온 데이터 끝에 자동으로 널을 추가하지 않으므로 널 문자를 추가한다. buf를 문자열로 출력하려면 널을 추가해야한다.
printf("n=%d, buf=%s\n", n, buf);
close(fd);
return 0;
}
<실행 결과>
#ex2_4.out
n=6, buf= Unix S
파일 쓰기 : write(2)
write함수는 read함수와 인자의 구조는 같지만 뜻이 틀리다. 파일 기술자는 쓰기를 수행할 파일을 가리키고, buf는 파일에 기록할 데이터를 저장하고 있는 메모리 영역을 가리킨다.
buf가 가리키는 메모리 영역에서 nbytes로 지정한 크기만큼 읽어 파일에 쓰기를 수행한다.
write 함수도 실제로 쓰기를 수행한 바이트 수를 리턴하며 오류가 발생하면 -1을 리턴한다.
<파일 읽고 쓰기 : ex2_5.c>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void){
int rfd, wfd, n;
char buf[10];
rfd =open("unix.txt",O_RDONLY);
if(rfd == -1){
perror("Open unix.txt");
exit(1);
}
wfd = open("unix.bak", O_CREAT | O_TRUNC, 0644);
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;
}
파일 오프셋 지정
파일의 내용을 읽거나 쓰면 현재 읽을 위치나 쓸 위치를 알려주는 offset이 자동으로 변경된다.
오프셋은 파일 시작 지점에서 현재까지의 바이트 수이다.
파일을 열면 읽어올 위치를 나타내는 오프셋이 파일의 시작인 0이지만, read 함수를 실행할 때마다 읽어온 크기만큼 오프셋이 이동해 다음 읽어올 위치를 가리킨다.
write 함수도 파일에 내용을 쓸 때마다 오프셋이 변형되며, 다음에 쓸 위치를 가리킨다.
한 파일에서 파일 오프셋은 오직 하나이다. 파일을 쓰기/읽기 모드로 열었을 때 파일 읽기 오프셋과 쓰기 오프셋이 별도로 있는게 아니다.
오프셋을 원하는 위치로 바꾸고 위치를 확인하려면 lseek함수를 사용한다.
- 파일 오프셋 위치 지정 : lseek(2)
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fildes, off_t offset, int whence);
*parameter information
1. fildes : 파일 기술자
2. offset : 이동할 오프셋 위치
3. whence : 오프셋의 기준 위치
lseek 함수는 파일 기술자가 가리키는 파일에서 offset으로 지정한 크기만큼 오프셋을 이동시킨다. 이때 offset의 값은 whence의 값을 기준으로 해석한다.
- whence 값
값 | 설명 |
---|---|
SEEK_SET | 파일의 시작 기준 |
SEEK_CUR | 현재 위치 기준 |
SEEK_END | 파일의 끝 기준 |
lseek(fd, 5, SEEK_SET); 파일의 시작에서 5번째 위치로 이동
lseek(fd, 0, SEEK_END); 파일의 끝에서 0번째 이므로 파일의 끝으로 이동
파일 오프셋의 현재 위치를 알려면 어떻게 할까? 다음과 같이 현재 위치를 기준으로 0만큼 이동한 값을 구하면 된다.
cur_offset = lseek(fd, 0, SEEK_CUR);
<lseek함수를 이용해 파일에서 데이터를 읽어들이는 위치를 임의로 변경하는 예제>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void){
int fd, n;
off_T start, cur;
char buf[256];
fd = open("unix.txt", O_RONLY);
if(fd == -1){
peeror("Open unix.txt");
exit(1);
}
start = lseek(fd, 0, SEEK_CUR);
n = read(fd, buf, 255);
buf[n] = '\0';
printf("Offset start=%d, Read Str=%s, n=%d\n", (int)start,buf, n);
cur = lseek(fd, 0, SEEK_CUR);
printf("offset cur=%d\n", (int)cur);
start = lseek(fd, 5, SEEK_SET);
n = read(fd, buf, 255);
buf[n]='\0';
printf("Offset start=", Read Str=%s", (int)start, buf);
close(fd);
return 0;
}
<결과>
# ex2_6.out
Offset start=0, ReadStr=Unix System Programing
,n =24
Offset cur=24
Offset start=5, Read Str=System Programming
파일 기술자 복사
파일을 열면 파일 기술자가 할당된다. 이 파일 기술자를 복사해 같은 파일을 가리키는 두번째 파일 기술자를 생성할 수 있다. 파일 기술자를 복사하는데는 dup함수와 dup2 함수를 사용한다.
- 파일 기술자 복사하기 : dup(2)
#include <unistd.h>
int dup(int fildes);
dup함수는 기존 파일 기술자를 인자로 받아 새로운 파일 기술자를 리턴한다. 새로 할당되는 파일 기술자는 현재 할당할 수 있는 파일 기술자중 가장 작은 값으로 자동 할당된다. 파일 기술자의 복사는 입출력 반환 전환에서 많이 사용된다.
<출력 방향을 표준 출력에서 파일로 전환하는 예제>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void){
int fd, fd1;
fd = open("tmp.aaa", O_CREAT|O_WRONLY|O_TRUNC, 0644);
if( fd == -1){
perror("Create tmp.aaa");
exit(1);
}
close(1);
fd1 = dup(fd);
printf("DUP FD=%d\n", fd1);
printf("Standard Output Redirection\n");
close(fd);
return 0;
}
<결과>
# ex2_7.out
# cat temp.aaa
DUP FD=1
Standard Output Redirection
- 파일 기술자 복사 : dup2(3)
#include <unistd.h>
int dup2(int fildes, int fildes2);
*parameter information
1. fildes : 파일 기술자
2. fildes2 : 파일 기술자를 복사할 곳
dup함수는 새로운 파일 기술자를 자동으로 할당한다. dup2함수는 새로운 파일 기술자를 지정할 수 있게 해준다. dup2 함수는 파일 기술자 fildes를 fildes2로 복사한다.
<dup2 함수 사용>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int fd;
fd = open("tmp.bbb", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if(fd == -1) {
perror("Create tmp.bbb");
exit(1);
}
dup2(fd,1);
printf("DUP2: Standard Output Redirection \n");
close(fd);
return 0;
}
<실행 및 결과>
#ex2_8.out
#cat tmp.bbb
#DUP2 : Standard output Redirection
파일 기술자 제어
현재 열려있는 파일에 대한 파일 기술자의 속성을 확인하고 제어할 수 있다.
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fildes, int cmd, /* arg */ ...);
*parameter information
1. fildes : 파일기술자
2. cmd : 명령
3. arg : cmd에 따라 필요시 지정하는 인자들
fcntl함수는 파일 기술자가 가리키는 파일에 cmd로 지정한 명령을 수행한다. cmd의 종류에 따라 인자를 지정할 수도 있다.
fcntl함수의 두번째 인자인 command는 fcntl.h에 정의되어있다. 다음 두가지를 주로 사용한다.
- F_GETFL : 상태 플래그 정보를 읽어온다.
- F_SETFL : 상태 플래그 정보를 설정한다. 설정할 수 있는 플래그는 대부분 open함수에서 지정하는 플래그다.
<fcntl함수를 사용해 상태플래그를 읽어서 출력하고 수정하는 함수>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.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, "aaaaa", 12) != 12) perror("write");
close(fd);
return 0;
}
<결과>
#cat unix.txt
Unix System Programming
#ex2_9.out
#cat unix.txt
Unix System Programming
aaaaa
파일 삭제: unlink(2)
#include <unistd.h>
int unlink(const char *path);
* path : 삭제할 파일의 경로
이 함수는 지정한 파일의 inode에서 링크 수를 감소시킨다. 링크수가 0이 되면 path에 지정된 파일이 삭제된다. 이 함수를 사용하려면 해당 프로세스는 파일이 위치한 디렉토리에 쓰기 권한이 있어야 한다.
이 함수는 파일, 디렉토리도 삭제한다.
같은 기능의 함수는 remove(3)가 있다. remove()는 디렉토리 삭제시 디렉토리 내부가 비어있어야만 삭제된다.
#include <stdio.h>
int remove(const char *path);
<unlink 함수 사용>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int cnt;
cnt = unlink("tmp.aaa");
if(cnt == -1){
perror("Unlink tmp.aaa");
exit(1);
}
printf("Unlink tmp.aaa success!! \n");
return 0;
}
파일과 디스크 동기화 함수: fsync(3)
이 함수는 메모리에 위치하는 파일의 내용을 디스크로 보내 메모리와 디스크의 내용을 동기화한다. 메모리의 내용이 디스크로 모두 기록되기 전에는 리턴하지 않는다.
#include <unistd.h>
int fsync(int fildes);
2-3. 고수준 파일 입출력
고수준 파일 입출력은 standard IO library라고도 하며 C언어의 표준 함수로 제공된다. 저수준 파일 입출력은 바이트단위로만 입출력을 수행하므로, 읽어들인 데이터를 가공하는 추가적인 작업이 필요하다. 고수준 파일 입출력은 문자 단위, 행단위, 버퍼단위, 형식 기반 입출력 등 편리한 기능을 제공하므로 프로그램 용도에 맞게 적당한 함수를 선택하면 된다.
파일 포인터
저수준 파일 입출력에서는 열린 파일을 가리키는데 파일 기술자를 사용하는 한편, 고수준 파일 입출력에서는 file pointer를 사용한다. 파일 포인터는 디스크에서 메모리로 읽어온 파일의 위치에 관한 정보를 담고 있는 포인터이다. 파일 기술자는 정수형이지만, 파일 포인터는 시스템 헤더 파일에 정의되어있는 FILE* 형이다.
파일 포인터는 FILE 구조체를 가리키는 주소이다. 이 구조체에는 파일기술자, 버퍼주소, 버퍼내 문자 등이 있다. 이 파일 기술자를 이용해 파일 포인터와 파일 기술자 사이에서 변환할 수 있다.
파일 포인터는 플랫폼 독립적인 구조이므로 이를 사용하면 어느 플랫폼에서든 동일한 동작을 수행한다는 장점이 있다.
파일 열기/닫기
- 파일 열기 : fopen(3)
고수준 파일 입출력에서 파일을 여는 함수이다. 이 함수는 파일 포인터를 리턴한다.
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
1. filename :파일 경로
2. mode : 파일 열기 모드
fopen함수는 filename 으로 지정한 파일을 지정한 모드로 열고 파일 포인터를 리턴한다.
모드 | 의미 |
---|---|
r | 읽기 전용으로 텍스트 파일을 연다 |
w | 새로 쓰기용으로 텍스트 파일을 연다. 기존 내용은 삭제된다. |
a | 추가용으로 텍스트 파일을 연다 |
rb | 읽기전용으로 바이너리 파일을 연다 |
wb | 새로 쓰기용으로 바이너리 파일을 연다. 기존 내용은 삭제된다. |
ab | 추가용으로 바이너리 파일을 연다 |
r+ | 읽기와 쓰기용으로 텍스트 파일을 연다 |
w+ | 쓰기와 읽기용으로 텍스트 파일을 연다 |
이밖에도 a+, rb+ ... 등등이 있다.
fopen함수는 수행실패시 0을 리턴한다.
사용 예는 다음과 같다.
FILE *fp;
fp = fopen("unix.txt", "r");
파일 닫기 : fclose(3)
fopen함수로 연 파일을 사용하고 나면 닫아야한다.
#include <stdio.h>
int fclose(FILE *stream);
stream : fopen에서 리턴한 파일 포인터
fclose함수는 fopen에서 리턴한 파일 포인터를 인자로 지정한 후 메모리에 있던 파일 내용을 디스크에 저장하고 종료한다. 파일 닫기 정상 종료시 0을 리턴하고, 오류가 생기면 EOF(-1)을 리턴한다.
사용예시
FILE *fp;
fp = fopen("unix.txt", "r");
fclose(fp);
문자 기반 입출력
고수준 파일 입출력에서 가장 간단한 방법은 저수준 파일 입출력처럼 바이트 단위로 입출력하는 것이다. 문자 기반 입출력은 데이터를 byte stream으로 이해하고 한바이트씩 처리하는 함수를 제공한다.
문자 기반 입력 함수 : fgetc(3), getc(3), getchar(3), getw(3)
파일에서 바이트를 읽어오는 함수와 매크로는 다음과 같다.
#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
int getw(FILE *stream);
* stream : 파일 포인터
fgetc함수는 파일 포인터가 가리키는 파일로부터 문자 한개를 unsigned char 형태로 읽어온다 .
getc함수는 매크로로 구현되어있고 fgetc함수와 동일한 기능을 수행한다. getc함수는 실행속도가 약간 빠르지만, 실행코드가 확장되므로 메모리를 조금더 차지한다.
getchar함수는 표준 입력에서 문자한개를 읽어오는 매크로이다.
#define getchar() getc(stdin)
getw함수는 파일에서 word단위로 읽어온다. word의 크기는 int형의 크기. 시스템에 따라 다르다.
오류 발생시 EOF(-1)을 리턴한다.
이들 함수형의 리턴형이 int형으로 정의된 이유는 char형으로 처리할 경우 값의 범위 제한으로 인해 EOF와 십진수 255구별할 수 없기 때문이다. EOF는 -1로 정의된 상수이다. 이를 2진수로 표현하면 11111111, 십진수 255도 char로 이해하면 11111111이 된다.
- 문자기반 출력함수 문자를 파일에 출력하는 함수와 매크로이다.
#include <stdio.h>
int fputc(int c, *stream);
int putc(int c, *stream);
int putchar(int c);
int putw(int w, FILE *stream);
* c,w: 출력할 문자
* stream : 파일 포인터
fpuc함수는 이자로 받은 int데이터 c를 unsigned char로 변환해 파일에 쓴다.
putc는 fputc와 같은 동작을 수행하는 매크로로 구현되어있다.
putchar 는 표준출력으로 한문자를 출력하는 매크로이다.
` #define putchar(c) putc(c, stdout)
putw함수는 word단위로 파일에 출력한다. 워드의 크기는 int형의ㅏ 크기이다.
<문자 기반 입출력 함수를 사용해 한 파일의 내용을 다른 파일로 복사하기>
#include <stdlib.h>
#include <stdio.h>
int main(void){
FILE *rfp, *wfp;
int c;
if((rfp = fopen("unix.txt", "r")) == NULL) {
perror("fopen: unix.txt");
exit(1);
}
if((wfp =fopen("unix.out", "w")) == NULL) {
perror("fopen:unix.out");
exit(1);
}
while ((c= fgetc(rfp)) != EOF) {
fputc(c, wfp);
}
fclose(rfp);
fclose(wfp);
return 0;
}
문자열 기반 입출력
고수준 파일 입출력에서는 한번에 여러문자, 즉 문자열을 입출력하는 방법을 제공한다.
- 문자열 기반 함수 : gets(3), fgets(3)
#include <studio.h>
char *gets(char *s);
char *fgets(char *s, int n, FILE *stream);
* s: 문자열을 저장한 버퍼의 시작주소
* n: 버퍼의 크기
* stream : 파일 포인터
gets 함수는 표준 입력에서 문자열을 읽어들인다.
문자열은 엔터키를 입력하거나 파일의 끝을 만날때까지 읽는다. gets는 읽어들인 문자열의 끝에서 개행 문자(엔터키값)을 제외하고 널 문자('\0')을 채워 s가 가리키는 영역에 저장하고 리턴한다.
이함수는 인자 s가 확보하고 있는 메모리의 크기를 알수 없어서 s가 가득찬 후에도 계속 읽어들일 수 있다. 이때문에 보안침해가 발생할 수 있으므로 사용하지 않는게 좋다.
fgets 함수는 파일 포인터가 가리키는 파일에서 정수 n으로 지정한 길이보다 하나 적게 문자열을 읽어 s에 저장한다.
n의 길이만큼 읽는 도중에 개행문자를 만들거나, 파일의 끝을 만나면 해당 지점까지만 읽어온다.
fgets는 gets와 달리 개행문자도 s에 저장하고 널문자로 마친다.
두함수 모두 오류인 경우와 파일의 끝에 도달할 경우 읽어올 수 없으므로 NULL을 반환한다.
정상적으로 입력을 수행하면 s의 시작주소를 리턴한다.
- 문자열 기반 출력 함수 : puts(3), fputs(3)
#include <stdio.h>
int puts(const char *s);
int fputs(const char *s, FILE *Stream);
puts함수는 s가 가리키는 문자열을 표준 출력으로 출력한다. 이때 개행문자를 추가해 출력한다. fputs함수는 s가 가리키는 문자열을 파일 포인터가 가리키는 파일로 출력한다. fputs는 출력할 때 개행문자를 추가하지 않는다.
<unix.out 파일의 끝부분에 내용을 추가하는 함수>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
FILE *rfp, *wfp;
char buf[BUFSIZ];
if((rfp = fopen("unix.txt", "r")) == NULL) {
perror("fopen:unix.txt");
exit(1);
}
if((wfp = fopen("unix.out", "a")) ==NULL) {
perror("fopen:unix.out");
exit(1);
}
while(fgets(buf,BUFSIZ, rfp) != NULL){
fputs(buf, wfp);
}
fclose(rfp);
fclose(wfp);
return 0;
}
버퍼 기반 입출력
고수준 파일 입출력에서는 한번에 입출력하는 버퍼의 크기를 지정해 버퍼 단위로 파일 입출력을 수행하는 방법을 제공한다.
- 버퍼 기반 입력 함수 : fread(3)
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
*ptr: 버퍼 주소
*size: 버퍼 크기
*nitems : 읽어올 항목수
*stream : 파일 포인터
fread함수는 항목의 크기가 size인 데이터를 nitems에 지정한 개수만큼 읽어서 ptr이 가리키는 버퍼에 저장한다. 성공적으로 수행시 읽어온 항목수를 리턴한다. 읽을 항목이 없으면 0, 파일의 끝을 만나면 EOF를 리턴한다.
<fread함수를 사용해 unix.txt 파일을 읽어 표준 출력으로 출력하기>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
FILE *rfp;
char buf[BUFSIZ];
int n;
if((rfp = fopen("unix.txt", "r")) == NULL) {
perror("fopen: unix.txt");
exit(1);
}
while((n=fread(buf, sizeof(char)*2, 3, rfp)) >0 ) {
buf[6] = '\0';
printf("n=%d, buf=%s\n", n, buf);
}
fclose(rfp);
return 0;
}
- 버퍼 기반 출력 함수 : fwrite(3)
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream);
* ptr : 버퍼주소
* size : 항목의 크기
* nitems : 항목의 수
* stream : 파일 포인터
fwrite함수는 항목의 크기가 size인 데이터를 nitems에서 지정한 개수만큼 ptr에서 읽어 stream으로 지정한 파일에 리턴한다. 성공적으로 수행하면 출력한 항목의 수를, 오류가 발생하면 EOF를 리턴한다.
<fwrite함수를 이용한 unixt.out에 읽어온 내용 출력하기>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
FILE *rfp, *wfp;
char buf[BUFSIZ];
int n;
if((rfp = fopen("unix.txt", "r")) == NULL) {
perror("fopen:unix.txt");
exit(1);
}
if((wfp = fopen("unix.out", "a")) == NULL){
perror("fopen:unix.out");
exit(1);
}
while((n = fread(buf, sizeof(char)*2, 3, rfp)) > 0 ){
fwrite(buf, sizeof(char)*2, n, wfp);
}
fclose(rfp);
fclose(wfp);
return 0;
}
형식 기반 입출력
fgets와 같은 함수는 문자열의 형식을 고려하지 않고 한행씩 읽어온다. 고수준 파일 입출력은 이와 같이 정해진 형식이 있는 파일을 읽어들일 때 유용하게 사용할 수 잇는 형식기반 입출력 함수를 제공한다.
#include <stdio.h>
int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict stream, const char *restrict format, ...);
*format: 입력 형식(%d, %s ..)
*stream : 파일 포인터
<return>
*성공: 읽어온 항목개수 / 실패 : 0 또는 EOF
형식기반 파일 입출력은 C언에서 다루는 scanf함수이다. 이 함수는 표준입력에서 입력을 받고 fscanf는 지정한 파일에서 입력을 받는다. fscanf도 scanf가 사용하는 형식지정방법을 그대로 사용한다.
<성적 데이터 파일을 읽어서 평균 구하기>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
FILE *rfp;
int id s1, s2 s3, s4, n;
if((rfp == fopen("unix.dat", "r")) == NULL) {
perror("fopen : unix.dat");
exit(1);
}
printf("학점 평균 \n");
while((n = fscanf(rfp, "%d %d %d %d %d", &id, &s1, &s2, &s3, &s4)) != EOF) {
printf("%d : %d\n", id, (s1+s2+s3+s4)/4);
}
fclose(rfp);
return 0;
}
<결과>
학점 평균
2009001 : 87
2008002 : 86
- 형식기반 출력 함수 : printf(3), fprintf(3)
#include <stdio.h>
int printf(const char *restrict format, /* args .. * /);
int fprintf(FILE *restrict stream, const char *restric format, /*args */ ...);
* stream 파일 포인터
* format 출력형식
* args 출력 변수
* 성공시 출력한 문자수 리턴, 실패시 EOF
형식 기반 파일 출력도 C언어에서 다룬 printf 함수를 기반으로 한다. printf함수는 표준 출력으로 출력하고 fprintf는 지정한 파일로 출력한다. 출력 대상이 파일이기 때문에 첫번째 인자가 파일 포인터가 된다는 점이 다르다.
<읽어들인 내용을 파일로 출력하기>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
FILE *rfp, *wfp;
int id, s1, s2, s3, s4, n;
if((rfp = fopen("unix.dat", "r")) == NULL){
perror("fopen: unix.dat");
exit(1);
}
if((wfp = fopen("unix.scr", "W")) == NULL) {
perror("fopen : unix.scr");
exit(1);
}
fprintf(wfp, "학번 평균 \n");
while((n=fscanf(rfp, "%d %d %d %d %d", &id,&s1,&s2,&s3,&s4)) != EOF){
fprintf(wfp, "%d : %d\n", id, (s1+s2+s3+s4)/4);
}
fclose(rfp);
fclose(wfp);
return 0;
}
파일 오프셋 지정
저수준 파일 입출력에서는 lseek함수는 파일의 내용을 읽거나 쓰면 현재 읽을 위치나 쓸 위치를 알려주는 오프셋을 지정하는 기능을 제공한다. 고수준 파일 입출력에서는 다양한 함수를 제공한다.
- 파일 오프셋 지정 : fseek(3)
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
*steam : 파일 포인터
*offset : 이동할 오프셋
*whence: 오프셋의 기준위치
* 성공시 0, 실패시 EOF
fseek함수는 lseek함수와 유사하며, stream이 가리키는 파일에 offset에 지정한 크기만큼 오프셋을 이동시킨다. whence는 offset값을 해석하는 방법을 결정하는 상수이다. lseek에서와 같은 의미를 지닌다.
- SEEK_SET : 파일의 시작기준
- SEEK_CUR : 현재 위치 기준
- SEEK_END : 파일의 끝 기준
- 현재 오프셋 구하기 : ftell(3)
#include <stdio.h>
long ftell(FILE *stream);
수행에 실패하면 EOF 반환
사용예)
long cur;
cur = ftell(fp);
ftell함수는 인자로 지정한 파일의 현재 오프셋을 리턴한다. ftell이 리턴하는 오프셋은 파일의 시작에서 현재 오프셋 위치까지의 바이트함수이다.
처음 위치로 오프셋 이동 : rewind(3)
이함수는 오프셋을 파일의 시작으로 즉시 이동시킨다.
#include <stdio.h>
void rewind(FILE *stream);
- 오프셋의 저장과 이동 : fsetpos(3), fgetpos(3)
#include <stdio.h>
int fsetpos(FILE *stream, const fpos_t *pos);
int fgetpos(FILE *stream, fpos_t *pos);
*stream 파일포인터
*pos 오프셋을 저장하고 있는 영역주소(fsetpos), 오프셋을 저장할 영역주소(fgetpos)
이함수들은 ANSI C에서 새로 정의한 함수들이다. fgetpos함수는 파일의 현재 오프셋을 pos가 가리키는 영역에 저장한다. fsetpos함수는 pos가 가리키는 영역의 값으로 파일 오프셋을 변경한다. 두 함수 모두 성공하면 0, 실패하면 다른 값을 리턴.
<파일 오프셋을 이동하며 파일에 있는 3단어를 분리해서 읽어보기>
#include <stdlib.h>
#include <stdio.h>
int main(void){
FILE *fp;
int n;
long cur;
char buf[BUFSIZ];
if((fp==fopen("unix.txt", "r")) == NULL) {
perror("fopen: unix.txt");
exit(1);
}
cur = ftell(fp);
printf("Offset cur=%d\n", (int)cur);
n = fread(buf, sizeof(char), 4, fp);
buf[n] = '/0';
printf("-- Read Str = %s\n", buf);
fseek(fp, 1, SEEK_CUR);
cur = ftell(fp);
printf("Offset cur=%d\n", (int)cur);
n = fread(buf, sizeof(char), 6, fp);
buf[n] ='\0';
printf("--Read Str=%s\n", buf);
cur = 12;
fsetpos(fp, &cur);
fgetpos(fp, &cur);
printf("Offset cur=%d\n", (int)cur);
n= fread(buf, sizeof(char),13,fp);
buf[n]='\0';
printf("--Read Str=%s\n", buf);
fclose(fp);
return 0;
}
파일과 디스크 동기화 함수
고수준 파일 입출력 함수는 기본적으로 버퍼의 내용을 디스크로 옮겨온다. 이를 항상 보장할 수 없으므로 필요에 따라 강제로 수행해야한다. 고수준 파일 입출력에서는 fflush(3)함수를 제공한다.
#include <stdio.h>
int fflush(FILE *steam);
이 함수는 버퍼에 있는 데이터를 파일에 기록한다. 파일을 읽기 전용으로 열면 버퍼의 내용을 모두 지운다. 파일 포인터가 NULL이면 쓰기 전용으로 연 모든 파일에 데이터를 쓴다.
2-4. 파일 기술자와 파일 포인터 간 변환
저수준 파일 입출력에서는 열린 파일을 가리킬 때 파일 기술자를 사용하며, 고수준 파일 입출력에서는 파일 포인터를 사용한다. 파일 기술자와 파일 포인터를 상호 변환시킬 수 있다.
파일 기술자에서 파일 포인터를 생성하려면 fdopen(3)함수를 사용하며, 파일 포인터로부터 파일 기술자 정보를 추출하려면 fileno(3)함수를 사용한다.
- 파일 포인터 생성 : fdopen(3)
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);
* mode : 열기 모드
* 성공시 파일포인터 리턴, 실패하면 Null 포인터 리턴함.
fdopen은 파일 기술자와 읽기/쓰기 모드 인자를 받아 파일 포인터를 생성한다. mode에 값을 지정할 때는 파일 기술자를 열때와 같은 종류의 값으로 해야한다.
저수준에서는 읽기모드(O_RDONLY)로 열었는데 고수준으로 변환하면서 쓰기(w)로 하면 정상작동이 안된다.
<fdopen함수 사용>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
FILE *fp;
int fd;
char str[BUFSIZ];
fd = open("unix.txt", O_RDONLY);
if(fd == -1){
perror("open");
exit(1);
}
fp= fdopen(fd, "r");
fgets(str, BUFSIZ , fp);
printf("Read : %s\n", str);
fclose(fp);
return 0;
}
*파일 기술자 생성 : fileno(3) 이 함수는 파일 포인터를 인자로 받아 파일 기술자를 리턴한다.
#include <stdio.h>
int fileno(FILE *stream);
<fopen함수로 파일을 열고 리턴받은 포인터를 fileno함수로 전달해 파일 기술자를 추출한다.>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
int main(void) {
FILE *fp;
int fd, n;
char str[BUFSIZ];
fp = fopen("unix.txt", "r");
if(fp==NULL){
perror("fopen");
exit(1);
}
fd =fileno(fp);
printf("fd : %d\n", fd);
n= read(fd, str, BUFSIZ);
str[n]='\n';
printf("Read : %s\n", str);
close(fd);
return 0;
}
2-5. 임시파일 사용
프로그램을 수행하는 과정에서 데이터를 파일에 임시에 저장해야할 때가 있다. 그런데 같은 프로그램을 여러 사용자가 동시에 사용하는 경우 임시 파일명이 동일하면 문제가 발생할 수 있다. 임시 파일이 중복되지 않도록 생성하는 방법이 필요하다.
임시 파일명 생성: tmpnam, tempnam, mktemp
기존 파일과 중복되지 않는 임시 파일명을 만들어주는 함수. 이들은 파일명만 리턴하므로 저수준이나 고수준 파일 생성함수로 파일을 생성해 사용해야한다 .
#includ <stdio.h>
char *tmpnam(char *s);
*s 파일명을 저장할 버퍼의 시작주소
* 이 함수는 임시파일명을 시스템이 알아서 지정한다.
인자가 있는 경우 인자가 가리키는 곳에 임시 파일명을 저장하고,
인자가 없으면 임시파일명을 리턴한다.
#include <stdio.h>
char *tempnam(const char *dir, const char *pfx);
*dir: 임시파일명의 디렉토리 / pfx : 임시파일명의 접두어
*이 함수는 임시 파일명에 사용할 디렉토리와 접두어를 지정가능하며
생성된 파일명을 리턴한다.
접두어는 5글자만 허용한다.
*사용예)
char *fname;
fname = tempnam("/tmp", "zzz");
#include <stdio.h>
char *mktemp(char *template);
*template : 임시 파일명의 템플릿
* 이 함수는 인자로 임시 파일의 템플릿을 받아 이를 임시 파일명으로
변환해 리턴한다.
인자로 지정하는 템플릿은 대문자 'X'6개로 마치도록 지정해줘야한다.
이 X 부분을 다른 문자로 대체해 임시파일명을 만든다.
한 프로세스에서 한 템플릿당 26개의 유일한 임시파일을 만들수 있고
생성이 실패하면 널 문자열을 리턴한다.
<응용>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char *fname;
char fntmp[BUFSIZ];
char template[32];
fname = tmpnam(NULL);
printf("1. TMP file name(tmpnam) : %s\n", fname);
tmpnam(fntmp);
printf("2. TMP file name(tmpname) : %s\n", fntmp);
fname = tempnam("/temp", "haaaa");
printf("3. TMP file name(tempnam) : %s\n", fname);
strcpy(template, "tmp/aaaaaXXXXXXX");
fname = mktemp(template);
printf("4. TMP file name(mktemp) : %s\n", fname);
return 0;
}
임시 파일의 파일 포인터 생성
위 함수들은 임시파일명만 생성하고, 해당 임시파일을 대상으로 입출력 수행시 파일을 열어야한다. 대부분의 경우 프로그램을 수행할 때 임시 파일명을 반드시 알아야하는 것은 아니다. 임시 파일명은 알 필요없고 단지 임시 파일에 대한 파일 포인터만 필요하면 tmpfile 함수를 사용하면 된다.
tmpfile함수는 자동으로 w+모드로 열린 파일 포인터를 리턴한다.
#include <stdio.h>
FILE *tmpfile();
<응용>
#include <stdio.h>
int main(void) {
FILE *fp;
fp = tmpfile();
fputs("unix system", fp);
flcose(fp);
return 0;
}