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

[UNIX/Linux] ep4) 고수준 파일 입출력

by 클레어몬트 2024. 10. 7.

https://claremont.tistory.com/entry/UNIXLinux-ep3-%EC%A0%80%EC%88%98%EC%A4%80-%ED%8C%8C%EC%9D%BC-%EC%9E%85%EC%B6%9C%EB%A0%A5

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

ㅁ파일(file): 관련 있는 데이터의 집합으로, 저장 장치에 일정한 형태로 저장데이터를 저장하는 데는 물론 데이터를 전송하거나 장치에 접근하는 데도 사용특수 파일의 생성과 삭제 및 입출력은

claremont.tistory.com

https://claremont.tistory.com/entry/UNIXLinux-ep3-%EC%A0%80%EC%88%98%EC%A4%80-%ED%8C%8C%EC%9D%BC-%EC%9E%85%EC%B6%9C%EB%A0%A5-%ED%95%A8%EC%88%98-%EC%8B%A4%EC%8A%B5

[UNIX/Linux] ep3+) 저수준 파일 입출력 함수 실습

ex1. 저수준 파일 입출력을 이용해 파일을 복사하는 프로그램을 작성하시오. 이때 파일명은 명령행 인자로 받는다../ex test.txt test.bak#include #include #include #include int main(int argc, char* argv[]) { if (argc !=

claremont.tistory.com

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

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

 
 

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

열린 파일을 가리킬 때 고수준 파일 입출력에서는 파일 포인터(fp)를 사용한다
ㅇ파일 포인터(fp): 디스크에서 메모리로 읽어온 파일의 위치(주소)에 관한 정보를 담고 있는 포인터
• 파일 기술자는 정수형이지만 파일 포인터는 시스템 헤더 파일에 정의되어 있는 FILE 포인터
• 플랫폼 독립적인 구조이므로 어느 플랫폼에서든 동일한 동작을 수행한다는 장점

 
 
ㅇ FILE* fopen(const char* pathname, const char* mode): 파일 열기
- pathname : 파일의 경로
- mode : 파일 열기 모드
pathname으로 지정한 파일을 mode에서 지정한 모드로 열고 파일 포인터를 반환
성공하면 열린 파일의 주소를 파일 포인터로 반환, 실패하면 0을 반환

FILE* fp;
fp = fopen("test.txt", "r");

이 경우에는 test.txt 파일을 읽기 전용(r)으로 열고 파일 포인터 주소를 파일 포인터 변수인 fp에 저장한다
 
[두 번째 인자인 mode에 사용할 수 있는 값]
 

b: binary file

 
 
ㅇ int fclose(FILE* stream): 파일 닫기
- stream : fopen()에서 반환한 파일 포인터(fp)
fopen() 함수에서 반환한 파일 포인터를 인자로 지정한 후 메모리에 있는 파일 내용을 디스크에 저장하고 종료
성공하면 0을 반환, 실패하면 EOF(-1)를 반환

FILE* fp;
fp = fopen("test.txt", "r");
fclose(fp);

 
 
('문자' 기반 입력 함수)
ㅇ int fgetc(FILE* stream): 파일 포인터가 가리키는 파일로부터 문자 한 개를 unsigned char 형태로 읽어옴
ㅇ int getc(FILE* stream): 매크로로 구현되어 있어 실행 속도는 약간 빠르지만 실행 코드가 확장되므로 메모리를 조금 더 차지, 그 외에는 fgetc() 함수와 동일한 기능을 수행
ㅇ int getchar(void): 표준 입력에서 문자 한 개를 읽어오는 매크로로, getc(stdin)과 같음
ㅇ int getw(FILE* stream): 파일에서 워드 단위로 읽음, 워드의 크기는 int형의 크기로 시스템에 따라 달라짐
- stream : 파일 포인터(fp)
 
 
('문자' 기반 출력 함수)
ㅇ int fputc(int c, FILE* stream): 인자로 받은 int형 데이터 c를 unsigned char로 변환해 파일에 사용
ㅇ int putc(int c, FILE* stream): fputc() 함수와 같은 동작을 수행하며 getc()와 마찬가지로 매크로로 구현되어있음
ㅇ int putchar(int c): 표준 출력으로 한 문자를 출력하는 매크로, putc(c, stdout)와 같음
ㅇ int putw(int w, FILE* stream): 워드 단위로 파일에 출력, 워드의 크기는 int형의 크기로 시스템에 따라 달라짐
- c, w : 출력할 문자
- stream : 파일 포인터(fp)

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

int main() {
    FILE* rfp;
    FILE* wfp;
    int c;

    // 읽어올 파일을 읽기 전용으로 열기
    if ((rfp = fopen("linux.txt", "r")) == NULL) {
        perror("fopen: linux.txt");
        exit(1);
    }
    // 출력할 파일을 실행
    if ((wfp = fopen("linux.out", "w")) == NULL) {
        perror("fopen: linux.out");
        exit(1);
    }
    // EOF를 만날 때까지 한 문자씩 읽어들여 파일에 기록
    while ((c = fgetc(rfp)) != EOF) { 
        fputc(c, wfp);
    }
    // 모든 작업을 완료하면 열려 있는 두 파일의 파일 포인터(fp)를 사용해 닫아준다
    fclose(rfp);
    fclose(wfp);

    return 0;
}
linux.txt 파일과 linux.out 파일의 내용이 같음을 알 수 있다

 
 
('문자열' 기반 입력 함수)
ㅇ char* gets(const char* s): get string
- s : 문자열을 저장한 버퍼의 시작 주소
표준 입력에서 문자열을 엔터키(개행문자)를 입력하거나 파일의 끝(EOF)을 만날 때까지 읽음
읽어 들인 문자열의 끝에서 개행 문자(엔터키 값)를 제외하고 널 문자(‘\0’)를 채워 인자 s가 가리키는 영역에 저장하고 반환
(메모리의 크기를 알 수 없기 때문에 s가 가득 찬 후에도 계속 읽을 수 있다)
 
ㅇ char* fgets(char* s, int size, FILE* stream): file get string
- s : 문자열을 저장한 버퍼의 시작 주소
- size : 버퍼의 크기
- stream : 파일 포인터(fp)
파일 포인터가 가리키는 파일에서 변수 size에 지정한 길이보다 하나 적게 문자열을 읽어 s에 저장
size의 길이만큼 읽는 도중에 개행 문자('\n')를 만나거나 파일의 끝(EOF)을 만나면 해당 지점까지만 읽어온다
gets() 함수와 달리 개행 문자도 s에 저장하고, 버퍼의 마지막 문자 다음에 널 문자를 저장한다
 
 
('문자열' 기반 출력 함수)
ㅇ int puts(const char* s): put string
- s : 문자열 주소
s가 가리키는 문자열을 표준 출력으로 출력
※ 개행 문자를 추가해서 출력한다
성공하면 음수가 아닌 수를 반환, 파일의 끝이면 에러를 반환
 
ㅇ int fputs(const char* s, FILE* stream): file put string
- s : 문자열 주소
- stream : 파일 포인터(fp)
s가 가리키는 문자열을 파일 포인터가 가리키는 파일로 출력
※ 출력할 때 개행 문자를 추가하지 않는다
성공하면 음수가 아닌 수를 반환, 파일의 끝이면 에러를 반환

// 앞의 문자 기반 입출력 함수와 흐름이 같다
#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE* rfp;
    FILE* wfp;
    int c;

    if ((rfp = fopen("linux.txt", "r")) == NULL) {
        perror("fopen: linux.txt");
        exit(1);
    }
    if ((wfp = fopen("linux.out", "w")) == NULL) {
        perror("fopen: linux.out");
        exit(1);
    }
    while ((c = fgetc(rfp)) != EOF) {
        fputc(c, wfp);
    }
    fclose(rfp);
    fclose(wfp);
    
    return 0;
}
linux.txt 파일과 linux.out 파일의 내용이 같음을 알 수 있다

 
 
(버퍼 기반 입력 함수)
ㅇ size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream)
- ptr : 버퍼 주소
- size : 버퍼 크기
- nmemb : 읽어올 항목 수
- stream : 파일 포인터(fp)
항목의 크기가 size인 데이터를 nmemb에서 지정한 개수만큼 읽어 ptr이 가리키는 버퍼에 저장
성공하면 읽어온 항목 수를 반환, 읽을 항목이 없으면 0을 반환, 파일의 끝을 만나면 EOF를 반환

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

int main() {
    FILE* rfp;
    char buf[BUFSIZ];
    int n;

    if ((rfp = fopen("linux.txt", "r")) == NULL) {
        perror("fopen: linux.txt");
        exit(1);
    }
    // 항목의 크기를 sizeof(char)의 2배로 하고 한 번에 읽어올 개수를 4로 지정 
    // char의 크기는 1이므로 실제로는 8(= 2 × 4)바이트씩 읽어옴 
    while ((n = fread(buf, sizeof(char) * 2, 4, rfp)) > 0) {
        buf[8] = '\0'; // 문자열 출력 용도
        printf("n=%d, buf=%s\n", n, buf);
    }
    fclose(rfp);

    return 0;
}
// 실행 결과: 한 번에 8바이트씩 끊어서 읽어온다
// 리턴값을 저장한 n의 출력값을 보면 한 번에 4개 항목을 읽었음을 알 수 있다

 
 
ㅇ size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream)
- ptr : 버퍼 주소
- size : 항목의 크기
- nmemb : 항목 수
- stream : 파일 포인터(fp)
항목의 크기가 size인 데이터를 nmemb에서 지정한 개수만큼 ptr에서 읽어 stream으로 지정한 파일에 출력
성공하면 출력한 항목의 수를 반환, 오류가 발생하면 EOF를 반환

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

int main() {
    FILE* rfp;
    FILE* wfp;
    char buf[BUFSIZ];
    int n;

    if ((rfp = fopen("linux.txt", "r")) == NULL) {
        perror("fopen: linux.txt");
        exit(1);
    }
    // 출력할 파일을 추가로 실행
    // 실행하면 실행 결과 파일 linux.out에 덧붙여 출력
    if ((wfp = fopen("linux.out", "a")) == NULL) {
        perror("fopen: linux.out");
        exit(1);
    }
    // 항목의 크기를 sizeof(char)의 2배로 하고 한 번에 읽어올 개수를 4로 지정
    // char의 크기는 1이므로 실제로는 2 × 4 = 8바이트씩 읽음
    while ((n = fread(buf, sizeof(char) * 2, 4, rfp)) > 0) {
        // 항목의 크기가 sizeof(char) × 2인 항목을 
        // fread() 함수가 리턴한 항목 수인 만큼씩 wfp가 가리키는 파일에 출력
        write(buf, sizeof(char) * 2, n, wfp);
        // fwrite() 함수는 출력할 때 별도로 개행 문자를 추가 하지 않음
    }
    fclose(rfp);
    fclose(wfp);

    return 0;
}
// 실행 결과: 앞선 문자열 기반 입출력 예제 실행 결과 파일(linux.out)에 덧붙어 출력됨

 
 
(형식 기반 입력 함수)
ㅇ int scanf(const char* format, ...): 표준 입력으로 입력
ㅇ int fscanf(FILE* stream, const char* format, ...): 지정 파일에서의 입력
- format : 입력 형식(%d, %s 등)
- stream : 파일 포인터(fp)
성공하면 읽은 항목의 개수를 리턴, 입력값이 형식에 맞지 않거나 너무 빨리 EOF에 도달하면 0을 리턴
(형식에 맞는지 확인하기 전에 파일의 끝을 만나면 EOF를 리턴)

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

int main() {
    FILE* rfp;
    int id, s1, s2, s3, s4, n;

    if ((rfp = fopen("linux.dat", "r")) == NULL) {
        perror("fopen: linux.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;
}

 
 
(형식 기반 출력 함수)
ㅇ int printf(const char* format, ...): 표준 출력으로 출력
ㅇ int fprintf(FILE* stream, const char* format, ...): 지정한 파일로 출력
- stream : 파일 포인터(fp)
- format : 출력 형식
성공하면 출력한 문자 수를 반환, 실패하면 EOF를 반환
(fprintf() 함수도 printf() 함수가 사용하는 형식 지정 방법을 그대로 사용)

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

int main() {
    FILE* rfp;
    FILE* wfp;
    int id, s1, s2, s3, s4, n;

    if ((rfp = fopen("linux.dat", "r")) == NULL) {
        perror("fopen: linux.dat");
        exit(1);
    }
    // linux.scr 파일을 쓰기 모드로 생성
    if ((wfp = fopen("linux.scr", "w")) == NULL) {
        perror("fopen: linux.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;
}
// 실행 결과: linux.scr 파일에 학번과 평균이 저장

 
 
ㅇ int fseek(FILE* stream, long offset, int whence): 파일 오프셋 이동
- stream : 파일 포인터(fp)
- offset : 이동할 오프셋
- whence : 오프셋의 기준 위치
lseek() 함수와 유사하게 동작
fseek() 함수는 stream이 가리키는 파일에서 offset에 지정한 크기만큼 오프셋을 이동시킨다
성공하면 0을, 실패하면 EOF를 반환

cur = fseek(fp, 0, SEEK_CUR); // 잘못된 예시
cur = ftell(fp);  // 올바른 예시

 
whence는 offset 값을 해석하는 방법을 결정하는 상수로, lseek() 함수에서와 같은 의미이다
[whence에 사용할 수 있는 값]
• SEEK_SET : 파일의 시작을 기준으로 계산
• SEEK_CUR : 현재 위치를 기준으로 계산
• SEEK_END : 파일의 끝을 기준으로 계산
 
 
ㅇ long ftell(FILE* stream): 현재 오프셋 구하기
- stream : 파일 포인터(fp)
인자로 지정한 파일의 현재 오프셋을 반환한다
(ftell() 함수가 리턴하는 오프셋은 파일의 시작에서 현재 오프셋 위치까지의 바이트 수)
 
수행에 실패하면 EOF를 리턴, 따라서 현재 오프셋 값을 구하려면 ftell() 함수를 다음과 같이 사용해야 한다

long cur;
cur = ftell(fp);

 
 
ㅇ void rewind(FILE* stream): 처음 위치로 오프셋 이동
- stream : 파일 포인터(fp)
오프셋 위치를 파일의 시작으로 즉시 이동시킨다

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

int main() {
    FILE *fp;
    int n;
    long cur;
    char buf[BUFSIZ];

    if ((fp = fopen("linux.txt", "r")) == NULL) {
        perror("fopen: linux.txt");
        exit(1);
    }

    cur = ftell(fp); // 현재 오프셋 확인
    printf("Offset cur=%d\n", (int)cur);

    n = fread(buf, sizeof(char), 5, fp);
    buf[n] = '\0';
    printf("-- Read Str=%s\n", buf);

    fseek(fp, 1, SEEK_CUR); // 현재 오프셋을 기준으로 1만큼 이동
    // 18행에서 5바이트를 읽어 현재 오프셋이 5였는데, 1만큼 이동시켜 오프셋이 6으로 이동
    // "Linux SystemProgramming"에서 'S'에 해당하는 위치로, 33행의 경우 ‘P’로 이동

    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);

    fseek(fp, 1, SEEK_CUR); // 현재 오프셋을 기준으로 1만큼 이동

    cur = ftell(fp); // 현재 오프셋 확인
    printf("Offset cur=%d\n", (int)cur);

    n = fread(buf, sizeof(char), 11, fp);
    buf[n] = '\0';
    printf("-- Read Str=%s\n", buf);

    rewind(fp); // 오프셋을 다시 시작 위치로 이동
    cur = ftell(fp); // 현재 오프셋 확인
    printf("Rewind Offset cur=%d\n", (int)cur);

    fclose(fp);

    return 0;
}
// 실행 결과: 오프셋이 이동하면서 단어별로 출력되는 것을 확인할 수 있음
linux.txt 파일 안 데이터: Linux SystemProgramming

 
 
ㅇ int fflush(FILE* stream): 파일과 디스크 동기화 함수
- stream : 파일 포인터(fp)
버퍼에 있는 데이터를 파일에 기록한다
파일을 읽기 전용으로 연 경우 버퍼에 있는 내용을 모두 비운다
파일 포인터가 NULL이면 쓰기 전용으로 연 모든 파일에 데이터를 사용한다
 
 
 
 
 
(참고) fd 와 fp 간의 변환
저수준 파일 입출력: 열린 파일을 가리킬 때 파일 기술자(fd)를 사용
고수준 파일 입출력: 파일 포인터(fp)를 사용
 
파일 기술자에서 파일 포인터를 생성하려면 fdopen() 함수를 사용
파일 포인터에서 파일 기술자 정보를 추출하려면 fileno() 함수를 사용

 
 
 
 
 
 
 
 
참고 및 출처: 시스템 프로그래밍 리눅스&유닉스(이종원)