본문 바로가기

정글캠프-WIL/서브아이템

[C언어] - Robust I/O 함수

개념
단순히 "강하다" 가 아니라

에러나 인터럽트(EINTR)에도 안전하게 동작하는 I/O라는 의미
일반 read(), write() 함수의 문제
1. 중간에 인터럽트 발생 (EINTR)
2. 일부만 읽힘 / 일부만 써짐 (partial read/write)
3. 다시 호출해야 함(직접 처리)
즉, 사용자가 직접 예외처리를 계속 해야 함
Robust I/O 함수의 특징
1. 자동 재시도 (retry)
2. 정확한 바이트 수 보장
3. EINTR 처리 포함
즉, 이렇기에 "robust" (견고한 I/O 라고 부름)
Robust I/O 함수는 "내부 버퍼를 직접 관리하느냐"를 기준으로 나눌 수 있음
cf. rio_readn은 표준 라이브러리가 아니라 CSAPP에서 제공하는 유틸 함수
즉, 내부 버퍼 없이 바로 read, write를 안정적으로 감싼 것 -> Unbuffered Robust I/O
내부 버퍼를 두고 읽기 효율과 편의성을 높인 것 -> Buffered Robust I/O

Unbuffered Robust I/O

내부 버퍼를 사용하지 않고, 시스템 콜 read, write를 감싸서 더 안전하게 사용할 수 있도록 만든 함수
대표적인 함수
rio_readn
rio_writen

- 목적

ⓐ. EINTR 같은 인터럽트 상황에 대해 재시도

ⓑ. 일부만 읽히거나 일부만 써지는 문제를 보완

ⓒ. 사용자가 원하는 바이트 수만큼 최대한 정확하게 처리

즉, 성능 최적화보단 안정적인 입출력 보장에 더 초점

Buffered Robust I/O

내부 버퍼를 직접 관리하는 구조체(rio_t)를 사용해서 입력을 더 효율적으로 처리하는 함수
대표적인 함수
rio_readinitb
rio_readnb
rio_readlineb

- 목적

ⓐ. 여러 번의 작은 read() 호출을 줄임

ⓑ. 한 번 크게 읽어 내부 버퍼에 저장한 뒤, 필요한 만큼 잘라서 전달

ⓒ. 텍스트 기반 프로토콜에서 줄 단위 입력 처리가 쉬움

즉, 안정성에 더해 성능과 편의성까지 고려한 입력 방식

rio_readn

rio_readn() 함수란?
파일 디스크립터로부터 "정확히 n바이트를 읽으려고 시도하는" Robust I/O 함수

Why Used?

기본 read()는 생각보다 불완전하다.
ssize_t read(int fd, void *buf, size_t n);
요청한 n바이트를 항상 다 읽어주지 않음
중간에 인터럽트(EINTR) 발생 가능
소켓에서는 특히 "조금씩 끊어서" 들어옴
즉, read(fd, buf, 100); 했는데 실제로는 20바이트만 읽히는 경우도 많음

함수 원형

ssize_t rio_readn(int fd, void *usrbuf, size_t n);
매개변수 설명
fd 소켓 파일 디스크립터
usrbuf 데이터를 저장할  버퍼
n 읽고 싶은 바이트 수
rio_readn이 해결하는 것
- 반복해서 read() 호출
- 누적해서 n바이트 채움
- EINTR 자동 처리
즉, 가능한 한 n바이트를 다 읽을 때까지 계속 시도
#include <unistd.h>   // read()
#include <errno.h>    // errno

ssize_t rio_readn(int fd, void *usrbuf, size_t n)
{
    size_t nleft = n;        // 아직 읽어야 할 바이트 수
    ssize_t nread;
    char *bufp = usrbuf;

    while (nleft > 0) {
        if ((nread = read(fd, bufp, nleft)) < 0) {
            if (errno == EINTR)  // 인터럽트 발생
                nread = 0;       // 다시 시도
            else
                return -1;       // 실제 에러
        } else if (nread == 0) {
            break;               // EOF
        }

        nleft -= nread;          // 남은 바이트 감소
        bufp += nread;           // 버퍼 포인터 이동
    }

    return (n - nleft);          // 실제 읽은 바이트 수
}

사용 예시

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

ssize_t rio_readn(int fd, void *usrbuf, size_t n); // 선언

int main() {
    int fd = open("test.txt", O_RDONLY);
    char buf[100];

    int n = rio_readn(fd, buf, 100);

    if (n < 0) {
        perror("rio_readn error");
        return 1;
    }

    printf("읽은 바이트 수: %d\n", n);

    close(fd);
    return 0;
}

 

rio_writen

rio_writen() 함수란?
파일 디스크립터에 "정확히 n바이트를 쓰려고 시도하는" Robust I/O 함수

Why Used?

기본 write()도 read()처럼 문제가 있다.
ssize_t write(int fd, const void *buf, size_t n);
한 번에 n바이트를 다 못 쓸 수 있음
일부만 쓰고 끝날 수 있음 (partial write)
EINTR로 중단될 수 있음
즉, 1024바이트를 보내려고 했는데 -> 300 바이트만 전송될 수 있음

함수 원형

ssize_t rio_writen(int fd, void *usrbuf, size_t n);
매개변수 설명
fd 소켓 파일 디스크립터
usrbuf 데이터를 저장할  버퍼
n 읽고 싶은 바이트 수
rio_writen이 해결하는 것
- 반복해서 write() 호출
- 누적해서 n바이트 채움
- EINTR 자동 처리
즉, 가능한 한 n바이트를 다 읽을 때까지 계속 시도
#include <unistd.h>
#include <errno.h>

ssize_t rio_writen(int fd, void *usrbuf, size_t n)
{
    size_t nleft = n;       // 남은 바이트
    ssize_t nwritten;
    char *bufp = usrbuf;

    while (nleft > 0) {
        if ((nwritten = write(fd, bufp, nleft)) <= 0) {
            if (errno == EINTR)   // 인터럽트 발생
                nwritten = 0;     // 다시 시도
            else
                return -1;        // 실제 에러
        }

        nleft -= nwritten;        // 남은 바이트 감소
        bufp += nwritten;         // 포인터 이동
    }

    return n;
}

사용 예시

// 파일에 데이터 쓰기
int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);

char msg[] = "Hello World";

rio_writen(fd, msg, sizeof(msg));

close(fd);

rio_readinitb

rio_readinitb() 함수란?
Buffered Robust I/O를 사용하기 위해 내부 버퍼(rio_t)를 초기화하는 함수
반드시 rio_readnb, rio_readlineb 쓰기 전에 반드시 먼저 호출해야 하는 초기화 함수

Why Used?

typedef struct {
    int rio_fd;        // 파일 디스크립터
    int rio_cnt;       // 버퍼에 남은 바이트 수
    char *rio_bufptr;  // 현재 읽을 위치
    char rio_buf[8192];// 내부 버퍼
} rio_t;
실제로 Buffered RIO는 위와 같은 구조를 사용하므로
그냥 쓰면 안되고 초기 상태를 설정해줘야 함

함수 원형

void rio_readinitb(rio_t *rp, int fd);
매개변수 설명
rp rio_t 버퍼 구조체
fd 사용할 파일 디스크립터
void rio_readinitb(rio_t *rp, int fd)
{
    rp->rio_fd = fd;        // 사용할 파일 디스크립터
    rp->rio_cnt = 0;        // 버퍼에 남은 데이터 없음
    rp->rio_bufptr = rp->rio_buf; // 버퍼 시작 위치
}

사용 예시

rio_t rio;

rio_readinitb(&rio, fd);   // 1. 초기화

rio_readlineb(&rio, buf, MAXLINE); // 2. 사용
rio_readnb(&rio, buf, n);

rio_readnb

rio_readnb() 함수란?
내부 버퍼를 사용해서 "n바이트를 읽는" Buffered Robust I/O 함수

Why Used?

rio_readn은 "직접 read 반복"
rio_readnb는 "버퍼에 미리 읽어놓고 가져다 씀"

함수 원형

ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);
매개변수 설명
rp rio_t 버퍼 구조체
usrbuf 데이터를 저장할 버퍼
n 읽고 싶은 바이트 수
/**
  * 1. 한 번 크게 읽고 재사용
  * read 8192 bytes -> 여러 번 나눠서 사용
  * 2. 내부 버퍼를 계속 유지
  * 다음 호출에서도 남은 데이터 활용
  * 3. 성능 최적화
  * 시스템 콜은 비싸다 -> read 횟수를 줄이면 성능이 좋아진다.
  */

ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
{
    size_t nleft = n;
    ssize_t nread;
    char *bufp = usrbuf;

    while (nleft > 0) {
        if (rp->rio_cnt <= 0) {  // 버퍼가 비어있으면
            rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf));

            if (rp->rio_cnt < 0) {
                if (errno != EINTR)
                    return -1;
            }
            else if (rp->rio_cnt == 0) {
                return 0; // EOF
            }
            else {
                rp->rio_bufptr = rp->rio_buf;
            }
        }

        int cnt = rp->rio_cnt < nleft ? rp->rio_cnt : nleft;

        memcpy(bufp, rp->rio_bufptr, cnt);

        rp->rio_bufptr += cnt;
        rp->rio_cnt -= cnt;

        nleft -= cnt;
        bufp += cnt;
    }

    return n;
}

사용 예시

rio_t rio;
char buf[100];

rio_readinitb(&rio, fd);

rio_readnb(&rio, buf, 100); // 8192 byte를 읽어서 그 중 100 byte만 buf에 저장

 

rio_readlineb

rio_readlineb() 함수란?
내부 버퍼를 사용해서 "한 줄(\n)"을 읽는 Buffered Robust I/O 함수

Why Used?

"줄 단위 입력을 안전하고 효율적으로 처리하기 위해 사용"

함수 원형

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen);
매개변수 설명
rp rio_t 버퍼 구조체
usrbuf 데이터를 저장할 버퍼
maxlen 사용자가 제공한버퍼(usrbuf)에 "최대 몇 바이트까지 읽을 수 있는지" 제한하는 값
/**
  * 1. 한 글자씩 읽는다.
  * 내부 버퍼 사용 때문에 (이미 8192byte 읽은 상태) 성능이 괜찮다.
  * 2. \n 만나면 종료
  * 3. 문자열 끝 처리 *bufp = 0;
  */
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
{
    int n, rc;
    char c, *bufp = usrbuf;

    for (n = 1; n < maxlen; n++) {
        if ((rc = rio_readnb(rp, &c, 1)) == 1) {
            *bufp++ = c;
            if (c == '\n')
                break;
        } else if (rc == 0) {
            if (n == 1)
                return 0; // EOF
            else
                break;
        } else {
            return -1; // error
        }
    }

    *bufp = 0;
    return n;
}

사용 예시

/**
  * HTTP 요청일 경우
  * GET / HTTP/1.1\r\n
  * \r\n
  * header 내용들....
  *
  * 위와 같이 요청이 들어올 때, 한줄만 읽고 종료하는 것을 아래처럼 구현
  * 결국 필요한 정보는 method uri version 이기 때문에
  */
rio_t rio;
char buf[1024];

rio_readinitb(&rio, connfd);

while (rio_readlineb(&rio, buf, 1024) > 0) {
    printf("%s", buf);

    if (strcmp(buf, "\r\n") == 0)
        break; // 헤더 끝
}

 

결론

Robust I/O를 배우는 이유는 "네트워크 I/O의 불안정성을 해결하기 위해서"
그 과정에서 HTTP 요청 처리에 매우 잘 맞기 때문에 자연스럽게 사용되는 것