how read() work

read() 기본 개념

C
ssize_t n = read(int fd, void *buf, size_t count);
  • 의미: fd에서 최대 count 바이트까지 읽어서 buf에 넣고, 실제로 읽은 바이트 수를 리턴
  • 중요한 포인트:
    • “최대(count)”지, “무조건(count)”이 아님
    • n은 0 < n ≤ count이면 전부 정상 동작임
fd 타입예시언제 read()가 깨어나는지 / 얼마나 읽는지 (블록 모드 기준)
일반 파일 (regular)open("a.txt", O_RDONLY)디스크에 있는 만큼 count까지 최대한 채워서 줌. EOF 근처 아니면 거의 항상 full read처럼 보임.
터미널 TTY (canonical)인터랙티브 쉘, 키보드 입력한 줄이 완성될 때까지(개행/EOF)는 안 깨고, 줄 하나만 읽어서 리턴. 요청한 count보다 줄이 짧으면 그만큼만.
터미널 TTY (non-canonical)stty raw 등최소 바이트 수(VMIN) 이상 들어오면 바로 리턴. 줄 개념 없음.
파이프 / FIFOpipe(), mkfifo()커널 파이프 버퍼에 현재 있는 만큼 읽고, 1바이트 이상 읽었으면 거기까지만 리턴해도 됨. 더 필요하면 유저가 또 read() 호출해야 함.
TCP/UDP 소켓socket(AF_INET, SOCK_STREAM)커널 수신 버퍼에 있는 만큼 읽고, SO_RCVLOWAT 이상 읽었으면 바로 리턴 가능. 기본 low-water는 1바이트.

공통:

“내가 원하는 만큼 채워질 때까지 자동으로 반복해주는 read는 없다.”

“정확히 N바이트”가 필요하면, 애플리케이션 쪽에서 루프를 돌려야 한다.

1. 일반 파일에서의 read()

일반 파일(디스크)은 가장 “단순하게 보이는” 케이스.

  • 커널 문서: “read()는 fd에서 최대 count 바이트까지 읽는다”
  • 시퀀셜 파일에서:
    • 디스크 → 페이지 캐시 → 유저 버퍼로 복사
    • 중간에 끊길 이유가 거의 없어서, 대부분 count 바이트 꽉 채워서 리턴
  • 예외:
    • EOF 근처: 남은 데이터가 count보다 적으면 그만큼만 리턴
    • 시그널/에러 등으로 중간에 끊길 수는 있음 (네트워크 파일시스템 등).

그래서 우리가 체감상:

“파일에 대해 read는 거의 항상 내가 요청한 만큼 꽉 채워준다”

라고 느끼는 건, 그렇게 구현하는 게 자연스러워서 그럴 뿐,

API 차원에서 강제되는 건 아님.

2. 터미널(tty) – canonical 모드

stdin이 터미널이고, 기본 설정(요즘 쉘 기본)에서는 canonical 모드(line mode, cooked mode).

특징

  • 사용자가 입력한 내용이 tty line discipline(N_TTY) 버퍼에 먼저 쌓임
  • 엔터(개행, \n) 혹은 특수 EOL/EOF 문자가 들어오기 전까지는
    • 프로그램의 read()는 절대 깨어나지 않음
  • 개행이 들어온 순간:
    • 그 줄(line) 전체를 최대 4096바이트까지 line discipline 버퍼에 확정 저장하고,
    • read() 호출한 프로세스에 “한 줄치 데이터를 넘겨줌”

Jonathan Lam 블로그 설명도 딱 이 구조 얘기함:

canonical 모드 tty에서 read는 **줄 끝(개행)**이 올 때까지 완료되지 않고,

한 번의 read에서

한 줄을 초과해서 보내지 않는다

C
char buf[0x600];
ssize_t n = read(0, buf, sizeof(buf));  // fd=0: 터미널
  • 터미널에 hi⏎ 쳤다면:
    • buf에는 "hi\n" (3바이트)만 들어오고,
    • n == 3 으로 리턴
  • count가 0x600이어도, 줄 길이가 3바이트면 딱 3바이트만 가져다줌.

즉:

  • “엔터 전까지는 아예 안 깨어난다”
  • 깨어나면, 그 줄(line)만큼만 리턴하고 끝.

3. 터미널(tty) – non-canonical 모드 (raw 모드)

non-canonical (raw) 모드에서는 줄(line) 개념이 사라짐.

termios에서:

  • ICANON 플래그 OFF → non-canonical 모드
  • VMIN, VTIME 두 파라미터가 언제 read를 깨울지 결정

일반적인 패턴:

  • VMIN = 1, VTIME = 0:
    • 입력 버퍼에 최소 1바이트만 도착해도 바로 read() 깨어남
    • read(fd, buf, 0x600) 이라고 해도, 딱 1바이트만 읽고 리턴해도 됨
  • VMIN = N:
    • 최소 N바이트 도착할 때까지 블록
    • 도착하면 N~count 사이의 아무 값으로 리턴 가능 (커널 구현에 따라)

N_TTY 문서에서도 canonical/non-canonical 모드별 read 버퍼 정책을 정확히 설명해줌.

4. 파이프 / FIFO

파이프도 “바이트 스트림”이고, send/write 쪽에서 끊어 보낸 경계는 의미가 없음.

핵심:

  • 파이프 버퍼에 데이터가 없으면:
    • 블록 모드에서 read()는 잠든다.
  • 누가 write() 해서 버퍼에 데이터가 쌓이면:
    • 최소 1바이트 이상 읽어서 read()를 깨울 수 있다.
  • 이때 read(fd, buf, 0x600) 이라 해도:
    • 버퍼에 50바이트밖에 없으면, 그 50바이트만 읽고 50으로 리턴해도 정상.

즉, 파이프도:

“최대 count까지 읽는다” 뿐이고,

“count가 꽉 찰 때까지 기다려준다” 같은 건 없다.

그래서 네트워크/파이프 프로토콜 짤 때 항상:

C
ssize_t read_full(int fd, void *buf, size_t len) {
    size_t off = 0;
    while (off < len) {
        ssize_t n = read(fd, (char*)buf + off, len - off);
        if (n <= 0) return n; // 에러 or EOF
        off += n;
    }
    return off;
}

이런 “read_exact” 패턴을 쓰는 거고, 이건 커뮤니티에서도 표준 패턴으로 계속 얘기되는 부분.

5. 소켓(TCP) – SO_RCVLOWAT, MSG_WAITALL

소켓은 여기까지 온 내용의 총집합 느낌.

5-1. 기본 동작 (SO_RCVLOWAT = 1)

socket(7)에 이렇게 되어 있음:

  • SO_RCVLOWAT: “수신 버퍼에 최소 몇 바이트 이상 있어야

    사용자에게 데이터를 넘길지 결정하는 값”

  • 기본값은 1바이트

즉, TCP 소켓에서 블록 모드 read / recv는 대략 이런 느낌:

  1. 수신 버퍼에 데이터가 하나도 없으면 → 잠든다.
  2. 적어도 1바이트라도 들어오면:
    • “이제 깨어나도 됨”
    • 그 시점에 버퍼에 있는 만큼을 복사해서 유저에게 넘긴다.
    • 이 값은 1 ~ count 사이 아무거나 (상황/버퍼 상태에 따라).

그래서:

C
ssize_t n = read(sock_fd, buf, 0x600);
  • 상대가 send()를 여러 번에 쪼개서 보냈고,
  • p.send(payload); sleep(2); p.send(pay1); p.send(pay2);
  • 서버는 딱 한 번 read(0, buf, 0x600); 호출한다면:

커널 관점:

  • 첫 번째 payload 도착 → 최소 1바이트 이상 → 그냥 payload만 buf에 복사하고 리턴해도 OK
  • 나중에 오는 pay1, pay2는 수신 버퍼에 계속 쌓였다가,

    다음 read() 호출 때 전달됨.

즉:

“buf에는 payload만 들어가고, 나머지는 커널 버퍼에 남는다”

→

이게 진짜 TCP + read()의 정상 동작

이건 SO_RCVLOWAT / partial read 설명하는 여러 글에서 전부 같은 얘기가 나옴.

fd 타입별 read() 동작 요약

fd 타입내부 버퍼/정책언제 read()가 리턴하는지특징 요약
일반 파일 (regular)페이지 캐시디스크/캐시에서 읽을 수 있는 만큼, 최대 count까지EOF 근처 빼고는 거의 항상 full read처럼 보임
TTY (canonical, cooked 모드)N_TTY line discipline, 라인 버퍼(최대 4096바이트)사용자가 엔터(개행) 입력해서 한 줄이 완성되면 그 줄만큼 읽어서 리턴read() 한 번에 한 줄만; count가 더 커도 줄 길이만큼만
TTY (non-canonical, raw 모드)N_TTY + termios의 VMIN/VTIME입력이 VMIN 바이트 이상 도착하면 바로 리턴줄 개념 없음, 키 입력 단위/블록 단위로 쪼개져 들어옴
파이프 / FIFO커널 파이프 버퍼버퍼에 1바이트 이상 있으면 그 시점에 있는 만큼 읽고 리턴 가능write 경계는 안 보장; 여러 번 read해서 직접 N바이트 맞춰야
TCP/UDP 소켓커널 TCP/UDP 수신 버퍼 + SO_RCVLOWAT (기본 1바이트)수신 버퍼에 SO_RCVLOWAT 이상 도착하면, 그 시점에 있는 만큼 읽고 리턴여러 번 send한 게 여러 번 read로 쪼개져 들어올 수 있음

좀더 헷갈리는 부분을 없애주자면

사실 대부분의 상황에서 원격 공격이니깐

TCP/UDP 소켓 이 상황인데

여기서 보면

C
p.send(payload)
p.send(pay1)
p.send(pay2)

이렇게 보내고

원격에서

C
read(0, buf, 0x600);

이렇게 받을때

  1. 세 개 전부 한 번에 커널 recv 버퍼에 쌓여 있으면 →

    read() 한 번에 payload + pay1 + pay2가 전부 buf에 들어갈 수 있고,

  2. 네가 말한 것처럼 타이밍/버퍼 상황에 따라
    • 첫 read()에 payload만 들어가거나
    • payload + pay1까지만 들어가고
    • 나머지가 **다음 read()**로 넘어갈 수도 있음.

바이트 스트림 + partial read 특성 때문에

“최대 0x600 안에 들어갈 크기”라도 언제나 한 방에 다 들어온다는 보장은 없다 → 이게 포인트.

즉, 그럼 내가 확실히 control 할려면 0x600을 가득채워 payload를 만들어 확실하게 pay1과 pay2는 다음 read에서 받게 하던가

sleep()으로 시간을 줘서 payload가 확실하게 read(0, buf, 0x600); 로 받기를 기다리는 방법이 있다.