[C] 저수준 파일 입출력 함수: 사용법과 예제

C 저수준 파일 입출력은 시스템 호출을 사용하여 파일을 읽고 쓸 수 있는 강력한 기능을 제공합니다.
이는 그 어떤 파일 입출력 함수보다 빠르게 접근할 수 있으며 대용량 파일 다루기에 최적화 됩니다.

저수준 입출력

  • 파일 및 장치에 직접 액세스할 수 있습니다.
  • 복잡하며 버퍼 관리는 프로그래머가 직접 관리 해야 합니다.
  • I/O 기능을 사용할 때 로우 레벨 I/O는 하이 레벨 I/O에 비해 더 빠릅니다.

파일 디스크립터 ( File Descriptor) 를 사용하여 파일의 상태를 추적합니다.

파일 디스크립터 ( File Descriptor )

파일 디스크립터는 파일을 열 때 필요합니다. 쓰기 작업이나 읽기 작업을 수행할 때, 새 파일을 만들어서 쓸 수도 있고, 기존 파일을 사용하면서 이전 내용을 삭제할 수도 있습니다. 읽기 작업이나 쓰기 작업을 수행하기 위해 특정 권한이 필요합니다.

파일에 쓰기 작업을 할 때, 시스템은 해당 파일의 권한을 확인하고 권한이 있다면 시스템은 “파일 디스크립터”라 불리는 음수가 아닌 정수를 반환합니다. 저수준 I/O 함수에서 파일 디스크립터는 이후의 모든 읽기 또는 쓰기 작업을 식별하는 데 사용됩니다.

저수준 I/O 함수는 다음과 같은 용도로 사용됩니다:

  • 파일과 장치에 직접 접근하기 위해
  • 이진 파일을 대량으로 읽기 위해
  • 빠르고 효율적인 I/O 작업을 수행하기 위해

파일을 열고 읽고 쓰는 기본적인 예제를 보여드립니다.

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

int main() {
    int file_descriptor;
    char data[] = "Hello, File I/O!";

    // 파일 열기 - 쓰기 모드
    file_descriptor = open("output.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);

    if (file_descriptor == -1) {
        printf("파일 열기 실패\n");
        return 1;
    }

    // 파일에 데이터 쓰기
    if (write(file_descriptor, data, sizeof(data)) == -1) {
        printf("파일 쓰기 실패\n");
        return 1;
    }

    // 파일 닫기
    if (close(file_descriptor) == -1) {
        printf("파일 닫기 실패\n");
        return 1;
    }

    printf("파일 쓰기 완료\n");
    return 0;
}

이 코드는 “output.txt” 파일을 쓰기 모드로 열고, “Hello, File I/O!”를 파일에 씁니다. 파일을 열 때 open 함수를 사용하고, 쓰기는 write 함수를 사용합니다. 마지막으로 close 함수를 통해 파일을 닫습니다.

저수준 파일 입출력은 좀 더 복잡하고 다양한 기능을 제공합니다.

아래 각 함수들을 설명함으로 자세하게 다뤄 보겠습니다.

C의 저수준 I/O 시스템은 파일과 장치에 접근할 수 있는 함수를 제공합니다.

  • Open()
  • Close()
  • Read()
  • Write()

저수준 파일 입출력 함수들

open() 함수

C 프로그래밍에서 open() 함수는 파일을 열 때 사용됩니다. 이 함수는 파일 디스크립터를 반환하는데, 파일 디스크립터란 파일을 식별하는 번호입니다. 이 함수는 다양한 모드로 파일을 열 수 있도록 해줍니다.

open() 함수의 기본 형식은 다음과 같습니다:

int open(const char *path, int oflag [, mode_t mode]);

여기서 filename은 열고자 하는 파일의 이름이고, flags는 파일을 어떤 모드로 열지에 대한 옵션을 나타냅니다. flags에는 여러 옵션을 함께 사용할 수 있으며, 주요 옵션으로는 다음과 같은 것들이 있습니다:

  • O_RDONLY: 읽기 전용으로 파일을 엽니다.
  • O_WRONLY: 쓰기 전용으로 파일을 엽니다.
  • O_RDWR: 읽기와 쓰기 모두 가능한 상태로 파일을 엽니다.
  • O_CREAT: 파일이 존재하지 않을 경우 새로 생성합니다.
  • O_TRUNC: 파일을 열 때 이미 존재하는 경우 내용을 삭제합니다.
  • O_APPEND: 파일을 열 때 이미 존재하는 경우 파일의 끝에 추가합니다.

예를 들어, 파일을 쓰기 전용으로 열고자 할 때는 다음과 같이 open() 함수를 사용할 수 있습니다:

int file_descriptor = open("파일명.txt", O_WRONLY | O_CREAT);

이렇게 파일을 열면 파일 디스크립터를 반환하여 파일에 대한 작업을 수행할 수 있습니다. 이후에는 해당 파일 디스크립터를 이용하여 read()write() 함수 등을 사용하여 파일을 읽거나 쓸 수 있습니다.

close() 함수

close() 함수는 파일 디스크립터를 닫을 때 사용됩니다. 파일을 열었을 때 반환된 파일 디스크립터는 작업을 마친 후에는 반드시 닫아주어야 합니다. 파일을 닫지 않으면 시스템 자원이 낭비될 수 있습니다.

close() 함수는 다음과 같은 형식을 갖습니다:

int close(int file_descriptor);

여기서 file_descriptor는 닫으려는 파일의 파일 디스크립터를 나타냅니다. 이 함수를 호출하면 해당 파일 디스크립터와 연결된 파일이 닫히게 됩니다.

예를 들어, 파일을 열고 작업을 마친 후에는 다음과 같이 close() 함수를 사용하여 파일을 닫을 수 있습니다:

int file_descriptor = open("파일명.txt", O_WRONLY | O_CREAT);
// 파일 작업 수행
close(file_descriptor); // 파일 닫기

close() 함수를 통해 파일을 닫으면 해당 파일 디스크립터는 더 이상 사용할 수 없게 되며, 시스템 리소스도 반환됩니다. 파일을 올바르게 닫아주는 것은 프로그램의 안정성과 시스템 자원의 효율적인 관리에 중요한 역할을 합니다.

read() 함수

read() 함수는 파일에서 데이터를 읽을 때 사용됩니다. 이 함수는 파일 디스크립터에서 지정한 위치부터 데이터를 읽어와 지정한 버퍼에 저장합니다.

read() 함수의 기본 형식은 다음과 같습니다:

ssize_t read(int file_descriptor, void *buffer, size_t count);

여기서 file_descriptor는 데이터를 읽을 파일의 파일 디스크립터를 나타내며, buffer는 데이터를 저장할 버퍼의 위치를 나타냅니다. count는 읽고자 하는 데이터의 바이트 수를 나타냅니다.

예를 들어, 파일에서 데이터를 읽어와 버퍼에 저장하고자 할 때는 다음과 같이 read() 함수를 사용할 수 있습니다:

int file_descriptor = open("파일명.txt", O_RDONLY);
char buffer[100]; // 데이터를 저장할 버퍼
ssize_t bytes_read;

bytes_read = read(file_descriptor, buffer, sizeof(buffer));
if (bytes_read == -1) {
    printf("파일 읽기 오류\n");
    return 1;
}

// 읽은 데이터 출력
printf("읽은 데이터: %s\n", buffer);

close(file_descriptor); // 파일 닫기

read() 함수는 파일 디스크립터에서 count만큼의 데이터를 읽어와서 buffer에 저장하고, 실제로 읽은 바이트 수를 반환합니다. 이렇게 읽은 데이터는 후속 작업에서 활용될 수 있습니다. 파일을 읽을 때는 파일이 성공적으로 열렸는지와 읽기 작업이 성공적으로 수행되었는지를 항상 확인해야 합니다.

write() 함수

write() 함수는 파일에 데이터를 쓸 때 사용됩니다. 이 함수는 파일 디스크립터에 지정한 버퍼의 데이터를 씁니다.

write() 함수의 기본 형식은 다음과 같습니다:

ssize_t write(int file_descriptor, const void *buffer, size_t count);

여기서 file_descriptor는 데이터를 쓸 파일의 파일 디스크립터를 나타내며, buffer는 쓸 데이터가 담긴 버퍼를 가리킵니다. count는 쓰고자 하는 데이터의 바이트 수를 나타냅니다.

예를 들어, 파일에 데이터를 쓰고자 할 때는 다음과 같이 write() 함수를 사용할 수 있습니다:

int file_descriptor = open("파일명.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
char data[] = "파일에 쓰는 예제입니다."; // 쓸 데이터
ssize_t bytes_written;

bytes_written = write(file_descriptor, data, strlen(data));
if (bytes_written == -1) {
    printf("파일 쓰기 오류\n");
    return 1;
}

close(file_descriptor); // 파일 닫기

write() 함수는 버퍼에 있는 데이터를 파일 디스크립터로 지정된 파일에 씁니다. 이 때 count에 명시한 만큼의 바이트 수를 씁니다. 함수는 실제로 쓴 바이트 수를 반환하며, 이를 통해 쓰기 작업이 성공적으로 수행되었는지 확인할 수 있습니다. 파일을 쓸 때는 파일이 성공적으로 열렸는지와 쓰기 작업이 성공적으로 수행되었는지를 항상 확인해야 합니다.

lseek() 함수

lseek() 함수는 파일의 오프셋(위치)을 이동시킬 때 사용됩니다. 이 함수를 이용하면 파일에서 원하는 위치로 이동하여 데이터를 읽거나 쓸 수 있습니다.

lseek() 함수의 기본 형식은 다음과 같습니다:

off_t lseek(int file_descriptor, off_t offset, int whence);

여기서 file_descriptor는 오프셋을 변경하고자 하는 파일의 파일 디스크립터를 나타내며, offset은 이동할 바이트 수를 나타냅니다. whence는 오프셋 이동의 기준을 나타내며, 주로 SEEK_SET, SEEK_CUR, SEEK_END 등이 사용됩니다.

  • SEEK_SET: 파일의 시작을 기준으로 오프셋을 설정합니다.
  • SEEK_CUR: 현재 파일 위치를 기준으로 오프셋을 설정합니다.
  • SEEK_END: 파일의 끝을 기준으로 오프셋을 설정합니다.

예를 들어, 파일의 특정 위치로 이동하고자 할 때는 다음과 같이 lseek() 함수를 사용할 수 있습니다:

int file_descriptor = open("파일명.txt", O_RDONLY);
off_t new_offset;

// 파일의 10번째 바이트로 이동
new_offset = lseek(file_descriptor, 10, SEEK_SET);

if (new_offset == -1) {
    printf("오프셋 이동 오류\n");
    return 1;
}

// 이후 파일 처리 작업 수행

close(file_descriptor); // 파일 닫기

lseek() 함수를 통해 파일의 오프셋을 이동시켜 원하는 위치로 이동할 수 있습니다. 이를 통해 파일에서 원하는 위치의 데이터를 읽거나 쓸 수 있습니다. 파일을 읽거나 쓸 때 파일 내 특정 위치로 이동할 필요가 있을 때 lseek() 함수를 활용하면 효율적으로 처리할 수 있습니다.

remove() 함수

remove() 함수는 파일을 삭제할 때 사용됩니다. 지정된 파일을 삭제하고 해당 파일이 존재하면 파일 시스템에서 제거합니다.

remove() 함수의 기본 형식은 다음과 같습니다:

int remove(const char *filename);

여기서 filename은 삭제하려는 파일의 이름을 나타냅니다. 해당 파일이 존재하면 삭제를 시도하고, 삭제에 성공하면 0을 반환하고 실패하면 -1을 반환합니다.

예를 들어, 파일을 삭제하고자 할 때는 다음과 같이 remove() 함수를 사용할 수 있습니다:

if (remove("삭제할_파일.txt") == 0) {
    printf("파일이 성공적으로 삭제되었습니다.\n");
} else {
    printf("파일 삭제에 실패했습니다.\n");
}

remove() 함수는 파일을 삭제하는 데 사용되며, 삭제 시도 후에 해당 파일이 성공적으로 삭제되었는지 여부를 반환값을 통해 확인할 수 있습니다. 파일을 삭제할 때는 삭제 권한이 있는지와 파일이 정상적으로 삭제되었는지를 항상 확인해야 합니다.

오류 처리

파일 접근 시 발생하는 오류 코드들은 다양한 상황을 나타냅니다. C 프로그래밍에서 파일 접근 오류 코드를 다루는 몇 가지 일반적인 코드를 살펴보겠습니다.

  1. ENOENT – 파일 또는 디렉토리를 찾을 수 없음: 파일이나 디렉토리를 찾을 수 없는 경우 발생합니다.
  2. EACCES – 액세스 거부: 파일에 액세스할 수 있는 권한이 없는 경우 발생합니다.
  3. EISDIR – 디렉토리에 대한 작업을 파일로 수행: 파일 대신 디렉토리에 대한 작업을 수행하려고 할 때 발생합니다.
  4. EIO – 입출력 오류: 입출력 오류가 발생했을 때 발생하는 일반적인 오류 코드입니다.
  5. ENOMEM – 메모리 부족: 시스템 메모리 부족으로 파일을 열거나 작업할 수 없을 때 발생합니다.
  6. ENOSPC – 공간 부족: 디스크 공간이 부족하여 파일을 생성하거나 작업할 수 없을 때 발생합니다.

이러한 오류 코드들은 파일 작업 중에 발생하는 다양한 문제를 식별하는 데 도움이 됩니다. 이러한 코드들을 사용하여 파일 작업 중 발생할 수 있는 예외 상황을 처리하고, 적절한 오류 메시지를 표시하여 사용자에게 알려줄 수 있습니다.

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    int file_descriptor;

    // 파일을 읽기 모드로 열기
    file_descriptor = open("존재하지_않는_파일.txt", O_RDONLY);

    if (file_descriptor == -1) {
        if (errno == ENOENT) {
            printf("파일을 찾을 수 없습니다.\n");
        } else if (errno == EACCES) {
            printf("파일에 액세스할 권한이 없습니다.\n");
        } else if (errno == EIO) {
            printf("입출력 오류가 발생했습니다.\n");
        } else {
            printf("다른 오류가 발생했습니다.\n");
        }
        return 1;
    }

    close(file_descriptor);

    return 0;
}

이 예제는 파일을 읽기 모드로 열 때 발생할 수 있는 오류 코드를 확인하는 방법을 보여줍니다. open() 함수가 -1을 반환하면 errno 변수를 사용하여 발생한 오류 코드를 확인하고, 해당하는 오류 메시지를 출력합니다. 파일을 열거나 다룰 때 발생할 수 있는 다양한 오류를 확인하고 적절히 처리함으로써 안정성을 높일 수 있습니다.

마무리

이번 글에서는 C 에서 저수준 파일 입출력 방법과 함수들을 알아보았습니다. 이제 빠르게 파일 접근 및 핸들링을 할 수 있습니다.

Leave a Comment