Lionel Blog

The road is under your feet, the heart looks to the distance

고성능 I/O를 위한 유닉스 시스템 호출 가이드

유닉스 계열 운영 체제(리눅스, BSD 등)에서 고성능 I/O 작업은 대용량 데이터 전송, 네트워크 서버, 데이터 스트리밍 등에서 필수적입니다. 이번 포스팅에서는 readv(), writev(), splice(), sendfile(), funopen(), sendmsg(), io_uring, 그리고 shm_open 같은 함수들의 기능과 사용 예시를 정리하며, 리눅스와 BSD에서의 차이점을 살펴봅니다.

1. readv() / writev(): Scatter-Gather I/O

  • 기능: 여러 버퍼를 단일 시스템 호출로 읽거나 쓰는 scatter-gather I/O.
  • 특징:
    • 리눅스와 BSD 모두 지원.
    • 메모리 복사(memcpy)가 필요하며, zero-copy는 아님.
    • BSD에서 파이프 데이터 전송에 효율적.
  • 사용 예시:
    #include <sys/uio.h>
    #include <fcntl.h>
    #include <stdio.h>
    
    int main() {
        int fd = open("input.txt", O_RDONLY);
        char buf1[100], buf2[100];
        struct iovec iov[2] = {
            { .iov_base = buf1, .iov_len = 100 },
            { .iov_base = buf2, .iov_len = 100 }
        };
        ssize_t n = readv(fd, iov, 2);
        printf("Read %zd bytes\n", n);
        close(fd);
        return 0;
    }

2. splice(): Zero-Copy 파이프 전송

  • 기능: 파이프나 유닉스 소켓 간 데이터를 사용자 공간 복사 없이 전송.
  • 특징:
    • 리눅스 전용, BSD 미지원.
    • 대용량 데이터 전송에 최적.
  • 사용 예시:
    #include <fcntl.h>
    #include <stdio.h>
    
    int main() {
        int fd_in = open("input.txt", O_RDONLY);
        int fd_out = open("output.txt", O_WRONLY | O_CREAT, 0644);
        ssize_t n = splice(fd_in, NULL, fd_out, NULL, 1024, SPLICE_F_MOVE);
        printf("Spliced %zd bytes\n", n);
        close(fd_in);
        close(fd_out);
        return 0;
    }

3. sendfile(): 파일에서 소켓으로 Zero-Copy 전송

  • 기능: 파일 데이터를 소켓으로 직접 전송.
  • 특징:
    • 리눅스와 BSD 모두 지원.
    • 파일→소켓 전용, 웹 서버에서 주로 사용.
  • 사용 예시:
    #include <sys/sendfile.h>
    #include <fcntl.h>
    #include <sys/socket.h>
    
    int main() {
        int file_fd = open("file.txt", O_RDONLY);
        int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
        // 소켓 연결 설정 생략
        ssize_t n = sendfile(sock_fd, file_fd, NULL, 1024);
        printf("Sent %zd bytes\n", n);
        close(file_fd);
        close(sock_fd);
        return 0;
    }

4. funopen(): 사용자 정의 스트림

  • 기능: 사용자 정의 읽기/쓰기 함수로 커스텀 I/O 스트림 생성.
  • 특징:
    • BSD 전용, 리눅스에서는 fopencookie() 사용.
    • 비표준 I/O 작업에 유연.
  • 사용 예시:
    #include <stdio.h>
    #include <stdlib.h>
    
    int my_read(void *cookie, char *buf, int n) {
        // 사용자 정의 읽기 로직
        return n;
    }
    
    int main() {
        FILE *fp = funopen(NULL, my_read, NULL, NULL, NULL);
        fclose(fp);
        return 0;
    }

5. sendmsg(): 소켓 메시지 전송

  • 기능: 소켓을 통해 복잡한 메시지(다중 버퍼, 제어 정보) 전송.
  • 특징:
    • 리눅스, BSD 지원.
    • 소켓 전용, 파이프에는 사용 불가.
  • 사용 예시:
    #include <sys/socket.h>
    #include <string.h>
    
    int main() {
        int sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0);
        struct msghdr msg = {0};
        char buf[] = "Hello";
        struct iovec iov = { .iov_base = buf, .iov_len = strlen(buf) };
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        sendmsg(sock_fd, &msg, 0);
        close(sock_fd);
        return 0;
    }

6. io_uring: 비동기 I/O 프레임워크

  • 기능: 비동기 I/O 작업을 위한 고성능 프레임워크.
  • 특징:
    • 리눅스 5.1 이상 전용.
    • splice()와 비슷하거나 더 나은 성능.
  • 사용 예시:
    #include <liburing.h>
    #include <stdio.h>
    
    int main() {
        struct io_uring ring;
        io_uring_queue_init(8, &ring, 0);
        // 비동기 I/O 작업 추가
        io_uring_queue_exit(&ring);
        return 0;
    }

7. shm_open: 공유 메모리

  • 기능: 프로세스 간 공유 메모리 객체 생성.
  • 특징:
    • 리눅스, BSD 지원, 높은 포터블성.
    • 동기화 필요.
  • 사용 예시:
    #include <sys/mman.h>
    #include <fcntl.h>
    #include <stdio.h>
    
    int main() {
        int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
        ftruncate(fd, 1024);
        void *ptr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        sprintf(ptr, "Shared data");
        printf("Wrote: %s\n", (char *)ptr);
        munmap(ptr, 1024);
        shm_unlink("/about");
        close(fd);
        return 0;
    }

BSD vs 리눅스

  • BSD: splice() 미지원으로 readv()/writev()가 파이프 전송에 효율적. 공유 메모리(shm_open)는 더 빠른 대안.
  • 리눅스: splice()io_uring이 대용량 데이터 전송에 최적. sendfile()은 파일→소켓 전송에 강력.

결론

고성능 I/O 작업은 사용 사례와 운영 체제에 따라 적합한 함수를 선택해야 합니다. 리눅스에서는 splice()io_uring이 뛰어난 성능을 제공하며, BSD에서는 readv()/writev()shm_open이 좋은 대안입니다. 각 함수의 특성과 예제를 참고해 최적의 솔루션을 선택하세요!

참고 자료: