how read() work
read() 기본 개념
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) 이상 들어오면 바로 리턴. 줄 개념 없음. |
| 파이프 / FIFO | pipe(), 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에서
한 줄을 초과해서 보내지 않는다
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가 꽉 찰 때까지 기다려준다” 같은 건 없다.
그래서 네트워크/파이프 프로토콜 짤 때 항상:
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바이트라도 들어오면:
- “이제 깨어나도 됨”
- 그 시점에 버퍼에 있는 만큼을 복사해서 유저에게 넘긴다.
- 이 값은 1 ~ count 사이 아무거나 (상황/버퍼 상태에 따라).
그래서:
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 소켓 이 상황인데
여기서 보면
p.send(payload)
p.send(pay1)
p.send(pay2)이렇게 보내고
원격에서
read(0, buf, 0x600);이렇게 받을때
- 세 개 전부 한 번에 커널 recv 버퍼에 쌓여 있으면 →
read() 한 번에 payload + pay1 + pay2가 전부 buf에 들어갈 수 있고,
- 네가 말한 것처럼 타이밍/버퍼 상황에 따라
- 첫 read()에 payload만 들어가거나
- payload + pay1까지만 들어가고
- 나머지가 **다음 read()**로 넘어갈 수도 있음.
바이트 스트림 + partial read 특성 때문에
“최대 0x600 안에 들어갈 크기”라도 언제나 한 방에 다 들어온다는 보장은 없다 → 이게 포인트.
즉, 그럼 내가 확실히 control 할려면 0x600을 가득채워 payload를 만들어 확실하게 pay1과 pay2는 다음 read에서 받게 하던가
sleep()으로 시간을 줘서 payload가 확실하게 read(0, buf, 0x600); 로 받기를 기다리는 방법이 있다.