본문 바로가기
Computer Science/UNIX & Linux

[UNIX/Linux] ep3) 저수준 파일 입출력

by 클레어몬트 2024. 9. 30.

ㅁ파일(file): 관련 있는 데이터의 집합으로, 저장 장치에 일정한 형태로 저장
데이터를 저장하는 데는 물론 데이터를 전송하거나 장치에 접근하는 데도 사용
특수 파일의 생성과 삭제 및 입출력은 특수 파일별로 약간씩 차이가 있다
 
 
ㅁ저수준 파일 입출력: 시스템 호출(System Call)
• 리눅스 커널의 시스템 호출을 이용해 파일 입출력을 수행
• 시스템 호출을 이용하므로 파일에 좀 더 빠르게 접근할 수 있는 장점
• 또한 바이트 단위로 파일의 내용을 다루므로 일반 파일뿐만 아니라 특수 파일도 읽고 쓸 수 있음
• 바이트 단위로만 입출력을 수행 가능하므로 응용 프로그램 작성 시 다른 추가기능을 함수로 추가 구현 해야 함
• 열린 파일을 참조할 때 파일 기술자(fd) 사용
 
 
ㅁ고수준 파일 입출력: C언어 표준 함수
• 저수준 파일 입출력의 불편함을 해결하기 위해 제공
• C언어의 표준 함수로 제공
• 데이터를 바이트 단위로 한정하지 않고 버퍼를 이용해 한꺼번에 읽기와 쓰기를 수행
• 다양한 입출력 데이터 변환 기능도 이미 구현되어 있어 자료형에 따라 편리하게 이용할 수 있음
• 열린 파일을 참조할 때 파일 포인터(fp) 사용
 

scanf()는 고수준 파일 입출력이다(버퍼 그릇을 사용)

 
 

[리눅스 저수준 파일 입출력 함수]

ㅇ파일 기술자(fd): 현재 열려 있는 파일을 구분할 목적으로 시스템에서 붙여놓은 번호
• 저수준 파일 입출력에서는 열린 파일을 참조하는 데 사용하는 지시자 역할을 수행
• 파일 기술자(fd)는 정숫값으로, open() 함수를 사용해 파일을 열 때 부여
• 프로세스가 파일을 열 때 파일 기술자에는 0번부터 순서대로 가장 작은 번호가 자동 할당
• 0번부터 시작하여 0번, 1번, 2번 파일 기술자에는 기본적인 용도가 지정되어 있음
- 0번: 표준 입력
- 1번: 표준 출력
- 2번: 표준 오류 출력

 
 
ㅇ int open(const char* pathname, int flags): 파일 열기
ㅇ int open(const char* pathname, int flags, mode_t mode): 파일 열기 + mode 값으로 파일 생성까지 가능
- pathname : 열려는 파일이 있는 경로
- flags : 파일 상태 플래그
- mode : 접근 권한
pathname에 지정한 파일을 flags에 지정한 상태 플래그의 값에 따라 열고 파일 기술자(fd)를 반환

man 명령으로 확인할 수 있다

 
쓰기 전용으로 열 때(이미 파일이 있는 경우)

O_WRONLY | O_TRUNC // 비트 OR 연산

 
 
쓰기 전용으로 열 때(파일이 없는 경우)

O_WRONLY | O_CREAT | O_TRUNC

 
 
읽기/쓰기/추가용으로 열 때

O_RDWR | O_APPEND

 
 
상수를 이용해 0644 권한을 지정

mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;

 
 
※ 요즘은 잘 사용하지 않고 mode 인자가 있는 open() 함수를 사용한다
ㅇ int creat(const char* pathname, mode_t mode): 파일 생성
- pathname : 파일을 생성할 경로
- mode : 접근 권한
open() 함수에 파일 생성 기능이 없던 구버전 유닉스에서 사용하던 것
open() 함수와 달리 플래그를 지정하는 부분이 없다
creat() 함수로 파일을 생성하면 파일 기술자(fd)가 리턴되므로 별도로 open() 함수를 호출해 파일을 열 필요가 없음
 

creat(pathname, mode);
open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);
// 이 두 개는 완전히 같은 의미이다

 
 
ㅇ int close(int fd): 파일 닫기
- fd : 파일 기술자
한 프로세스가 열 수 있는 파일의 개수에는 제한이 있으므로, 파일 입출력 작업을 완료하면 반드시 파일을 닫아야 한다
성공하면 0을 반환, 실패하면 -1을 리턴하고 오류 코드를 외부 변수 errno에 저장
 
 
[open() 함수, close() 함수 예시 문제들]

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

int main() {
    int fd;
    mode_t mode;

    mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; // 비트 OR 연산

    fd = open("test.txt", O_CREAT, mode); // test.txt 파일을 12행에서 지정한 권한(0644)으로 생성
    if (fd == -1) {
        perror("creat");
        exit(1);
    }
    close(fd);

    return 0;
}
// 실행 결과: 접근 권한이 0644인 test.txt 파일을 생성

접근 권한이 0644

 
 

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

int main() {
    int fd;

    // O_EXCL 플래그를 같이 지정
    // 이 경우 test.txt 파일이 이미 있으면 오류 메시지를 출력
    fd = open("test.txt", O_CREAT | O_EXCL, 0644); 
    if (fd == -1) {
        perror("Excl");
        exit(1);
    }
    close(fd);

    return 0;
}
// 실행 결과: 파일이 이미 있을 경우 "Excl: File exists" 오류 메시지 출력
// test.txt 파일은 앞선 예제에서 이미 생성했으므로 오류가 발생한 것
// 따라서 파일을 지운 후 다시 실행하면 정상적으로 파일을 생성

 
 
+
16행에서 파일을 닫기 전에 다음과 같은 printf()문을 추가해 파일 기술자(fd)를 출력

printf("test.txt: fd = %d\n", fd);

 
 

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

int main() {
    int fd;

    close(0); // 0번 파일 기술자를 닫음
    // 남아있는 파일 기술자(fd) 중 가장 작은 숫자가 0이다
    // 따라서 test.txt 파일을 열고 할당되는 파일 기술자는 실행 결과와 같이 0이 된다
    fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(1);
    }
    printf("test.txt : fd = %d\n", fd);
    close(fd);

    return 0;
}

 
 
ㅇ ssize_t read(int fd, void* buf, size_t count): 파일 읽기
- fd : 파일 기술자
- buf : 파일에 기록할 데이터를 저장한 메모리 영역
- count : buf의 크기(기록할 데이터의 크기)
파일 기술자가 가리키는 파일에서 count에 지정한 크기만큼 바이트를 읽어 buf로 지정한 메모리 영역에 저장한다
성공하면 실제로 읽은 바이트 수를 반환, 실패하면 -1을 반환
(만일 리턴값이 0이면 파일의 끝에 도달해 더 이상 읽을 내용이 없음을 의미)
+ 파일에 저장된 데이터가 텍스트든 이미지든 상관없이 무조건 바이트 단위로 읽는다

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

int main() {
    int fd, n;
    char buf[10];
    
    fd = open("linux.txt", O_RDONLY); // 파일을 읽기 전용으로 실행
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    // 5바이트를 읽어 buf에 저장하고 read() 함수의 반환값을 저장
    n = read(fd, buf, 5); 
    if (n == -1) {
        perror("read");
        exit(1);
    }
    buf[n] = '\0'; // %s 출력을 위해 null 문자를 수동으로 추가해줘야 한다
    printf("n=%d, buf=%s\n", n, buf);
    close(fd);

    return 0;
}
// 처음 파일의 오프셋은 'L'을 나타내지만 (오프셋 0)
// read() 함수를 실행한 다음에는 5바이트 이동해 'S' 앞의 공백 문자(' ')를 가리킴 (오프셋 5)
// 따라서 계속 읽어오면 ' '부터 읽음

linux.txt 파일에 어떤 데이터가 담겨 있는지 확인하자

 
 
read() 함수와 인자의 구조는 같지만 의미가 다르다! 파일 기술자(fd)는 쓰기를 수행할 파일을 가리키고, buf는 파일에 기록할 데이터를 저장하고 있는 메모리 영역을 가리킨다
ㅇ ssize_t write(int fd, const void* buf, size_t count): 파일 쓰기
- fd : 파일 기술자
- buf : 파일에 기록할 데이터를 저장한 메모리 영역
- count : buf의 크기(기록할 데이터의 크기)
buf가 가리키는 메모리 영역에서 count로 지정한 크기만큼 읽어 파일에 쓰기를 수행
성공하면 쓰기를 수행한 바이트 수를 반환, 실패하면 –1을 반환

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

int main() {
    int rfd, wfd, n;
    char buf[10];

    // 파일을 읽기 전용으로 열고, 파일 기술자를 rfd에 저장
    rfd = open("linux.txt", O_RDONLY); 
    if (rfd == -1) {
        perror("open linux.txt");
        exit(1);
    }

    // 파일을 쓰기 전용으로 생성하고, 기존 파일이 있으면 내용을 비움
    wfd = open("linux.bak", O_CREAT | O_WRONLY | O_TRUNC, 0644); 
    if (wfd == -1) {
        perror("open linux.bak");
        exit(1);
    }

    /*
        while 문을 사용해 linux.txt 파일을 6바이트씩 읽고 출력
        read() 함수가 리턴한 값이 buf에 저장된 데이터의 크기이므로
        write() 함수의 세 번째 인자(n)로 사용할 수 있다
        만약, write() 함수의 리턴값이 출력할 데이터의 크기인 n과 다르면 
        쓰기 동작에 문제가 있다는 의미이므로 오류 메시지를 출력
    */
    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; 
}
// linux.bak 파일이 생성되고 데이터가 저장
// 즉, linux.txt 파일이 linux.bak 파일로 복사

 
 
ㅇ off_t lseek(int fd, off_t offset, int whence): 파일 오프셋 위치 지정
- fd : 파일 기술자
- offset : 이동할 오프셋 위치
- whence : 오프셋의 기준 위치 (whence는 어떤 약자가 아니라, 고어로 사용되는 영어 단어이다)
lseek() 함수는 파일 기술자(fd)가 가리키는 파일에서 offset으로 지정한 크기만큼 오프셋을 이동시킨다
이때 offset의 값은 whence의 값을 기준으로 해석한다

 
파일의 시작에서 다섯 번째 위치로 이동

lseek(fd, 5, SEEK_SET);

 
파일의 끝에서 0번째이므로 파일 끝으로 이동

lseek(fd, 0, SEEK_END);

 
현재 위치를 기준으로 0만큼 이동한 값을 구해 파일 오프셋 위치를 확인

cur_offset = lseek(fd, 0, SEEK_CUR);

 
 

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

int main() {
    int fd, n;
    off_t start, cur;
    char buf[256];


    // linux.txt 파일을 읽기 전용으로 실행
    fd = open("linux.txt", O_RDONLY);
    if (fd == -1) {
        perror("open linux.txt");
        exit(1);
    }
    
    // 파일의 현재 오프셋 위치를 파악해보면 0임을 알 수 있음 (읽기 전용으로 열었기 때문)
    start = lseek(fd, 0, SEEK_CUR);
    n = read(fd, buf, 255);
    buf[n] = '\0';
    printf("Offset start=%d, n=%d, Read Str=%s", (int)start, n, buf);
    cur = lseek(fd, 0, SEEK_CUR); // 파일에서 데이터를 읽은 후 현재 위치를 확인 (이 예제에서는 오프셋이 start + n 위치로 이동)
    printf("Offset cur=%d\n", (int)cur);

    // 파일의 시작을 기준으로 오프셋이 6인 위치로 이동한 후 데이터를 읽고 null 값을 출력
    start = lseek(fd, 6, SEEK_SET);
    n = read(fd, buf, 255);
    buf[n] = '\0';
    printf("Offset start=%d, Read Str=%s", (int)start, buf);
    close(fd);

    return 0;
}
// 오프셋의 위치에 따라 읽어온 데이터가 다름을 알 수 있음

 
 
(추가)
ㅇO_APPEND
• open이 성공하면 파일의 마지막 바이트 바로 뒤에 위치
• 그 이후의 write는 전부 파일의 끝에 자료를 추가하게 됨
[파일의 끝에 자료를 추가하는 방법]
1. lseek 사용
lseek(fd, 0, SEEK_END)
write(fd, buf, BUFSIZE)
 
2. O_APPEND
open("filename", O_WRONLY | O_APPEND)
write(fd, buf, BUFSIZE)
 
 
ㅇ표준 입력(0), 표준 출력(1), 표준 오류(2)
• redirection < >
- prog_name < infile
파일기술자(fd) 0으로부터 읽어 들일 때, infile로부터 자료를 읽음
- prog_name > outputfile
출력을 outputfile로 변경
- prog_name < infile > outputfile
 
• pipe
- prog1 | prog2

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define SIZE 512

int main(void) {
    ssize_t nread;
    char buf[SIZE];

    while ((nread = read(0, buf, SIZE)) > 0) {
        write(1, buf, nread);
    }

    return 0;
}

 
 
 
ㅇ int dup(int oldfd): 파일 기술자(fd) 복사
- oldfd : 복사할 파일 기술자
기존 파일 기술자를 인자로 받아 새로운 파일 기술자를 반환한다
(이때 새로 할당되는 파일 기술자는 현재 할당할 수 있는 파일 기술자 중 가장 작은 값으로 자동 할당)
※ 파일 기술자의 복사는 입출력 방향 전환에 많이 사용

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

int main() {
    int fd, fd1;

    // tmp.aaa 파일을 생성
    fd = open("tmp.aaa", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd == -1) {
        perror("Create tmp.aaa");
        exit(1);
    }
    close(1); // 파일 기술자(fd) 1번을 닫음 (1번은 표준 출력이므로 화면으로 출력되는 것을 닫은 것)

    // 파일 기술자 fd가 가리키는 파일에 새로운 파일 기술자가 지정
    // 15행에서 1번을 닫았기 때문에 현재 가장 작은 값은 1이다
    // 따라서 이 값이 새로운 파일 기술자로 할당
    fd1 = dup(fd); 
    printf("DUP FD=%d\n", fd1);
    printf("Standard Output Redirection\n");
    close(fd);

    return 0;
}
// 실행 결과: 21행과 22행에서 출력한 내용이 tmp.aaa 파일에 저장

 
 
ㅇ int dup2(int oldfd, int newfd): 파일 기술자(fd) 복사2
- oldfd : 복사할 파일 기술자
- newfd : 파일 기술자를 복사할 곳
dup() 함수와 달리 새로운 파일 기술자를 지정할 수 있게 해줌
파일 기술자 oldfd를 newfd로 복사

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

int main() {
    int fd;

    fd = open("tmp.bbb", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd == -1) {
        perror("Create tmp.bbb");
        exit(1);
    }

    // 기존 파일 기술자(fd)를 1번 파일 기술자로 복사
    // 이제 1번 파일 기술자로 출력하면 더 이상 표준 출력(화면)이 아닌 fd가 가리키는 파일로 출력
    dup2(fd, 1);
    printf("DUP2: Standard Output Redirection\n");
    close(fd);

    return 0;
}

 
 
int fcntl(int fd, int cmd, ... /* arg */ ): 파일 기술자(fd) 제어
- fd : 파일 기술자
- cmd : 명령
- arg : cmd에 따라 필요시 지정하는 인자들
파일 기술자가 가리키는 파일에 cmd로 지정한 명령을 수행
(cmd의 종류에 따라 인자를 지정해야 할 수도 있다)
 
※ fcntl() 함수의 두 번째 인자인 명령은 fcntl.h 파일에 정의되어 있으며 다음 두 가지를 자주 사용
- F_GETFL : 상태 플래그 정보를 읽어옴
- F_SETFL : 상태 플래그 정보를 설정, 설정할 수 있는 것은 대부분 open() 함수에서 지정하는 플래그

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

int main() {
    int fd, flags;

    // linux.txt 파일을 읽기/쓰기용으로 실행
    fd = open("linux.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(1);
    }

    // F_GETFL 명령을 설정해 fcntl() 함수를 호출하면 현재 설정된 플래그 값이 반환
    if ((flags = fcntl(fd, F_GETFL)) == -1) {
        perror("fcntl");
        exit(1);
    }

    flags |= O_APPEND; // 반환된 플래그 값에 변경할 플래그를 OR로 연결해 새로운 플래그로 저장

    // 저장된 플래그를 F_SETFL 명령으로 설정해 fcntl() 함수를 호출하면 기존 플래그가 새로운 플래그로 변경
    if (fcntl(fd, F_SETFL, flags) == -1) {
        perror("fcntl");
        exit(1);
    }

    // 파일 설정을 O_APPEND로 변경했기 때문에 파일에 출력하면 내용이 파일 끝부분에 추가
    // 실행 결과를 확인해보자
    if (write(fd, "Hanbit Academy\n", 15) != 15) {
        perror("write");
    }
    close(fd);

    return 0;
}

파일 끝부분에 내용이 추가

 
 
ㅇ int remove(const char* pathname): 파일 삭제
 path에 지정한 파일이나 디렉터리를 삭제
(내부적으로 삭제 대상이 파일이면 unlink() 함수를 호출 / 삭제 대상이 디렉터리면 rmdir() 함수를 호출)
※ 디렉터리는 비어 있을 때만 삭제

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

int main() {
    int ret;

    // remove() 함수를 이용해 tmp.bbb 파일을 삭제
    ret = remove("tmp.bbb");
    if (ret == -1) {
        perror("Remove tmp.bbb");
        exit(1);
    }
    printf("Remove tmp.bbb success!!!\n");

    return 0;
}

tmp.bbb 파일이 삭제된 모습

 
 
ㅇ int fsync(int fd): 파일과 디스크 동기화
- fd : 디스크로 저장할 파일의 파일 기술자
메모리에 위치하고 있는 파일의 내용을 디스크로 보내 메모리와 디스크의 내용을 동기화
(메모리의 내용이 디스크로 모두 기록되기 전에는 반환하지 않는다)
 
 
 
 
 
 
 
참고 및 출처: 시스템 프로그래밍 리눅스&유닉스(이종원)