01 — What is a File
한 줄 답: 파일은 이름이 붙은 바이트 시퀀스이며, UNIX는 이 단순한 추상화 위에 모든 입출력 인터페이스(파이프·소켓·디바이스)를 통일했다.
Why — 왜 이런 추상화가 필요했나
1960년대까지의 컴퓨터는 장치마다 다른 명령어로 데이터를 읽고 썼다.
- 자기테이프:
MOUNT TAPE; READ BLOCK; UNMOUNT - 천공카드:
READ CARD; ADVANCE - 라인프린터:
PRINT LINE; SKIP PAGE - 디스크: 트랙·섹터 주소 직접 지정
문제는 셋이다.
| 문제 | 결과 |
|---|---|
| 프로그램이 장치 종류를 알아야 한다 | 디스크용 코드를 테이프에 못 쓴다 |
| 프로그램이 장치 위치를 알아야 한다 | 같은 장치에서도 트랙이 바뀌면 다시 짜야 한다 |
| 프로그램끼리 데이터를 못 주고받는다 | 장치 간 형식 변환 코드가 필수 |
1969년, Ken Thompson과 Dennis Ritchie가 UNIX를 만들면서 하나의 추상화로 이 모두를 통합했다 — 파일이다.
“On UNIX, everything is a file.” — 모든 입출력은 바이트 스트림에 대한 read/write로 환원된다.
How — 어떻게 동작하나
1) 4가지 시스템 콜이면 충분하다 (POSIX)
int fd = open("/path/to/file", O_RDONLY); // 1. 열기 → 정수 핸들(fd)
ssize_t n = read(fd, buffer, 4096); // 2. 읽기
ssize_t m = write(fd, buffer, 4096); // 3. 쓰기
close(fd); // 4. 닫기이 네 함수는 다음 모두에 동일하게 동작한다:
| 대상 | 경로 예 | 정체 |
|---|---|---|
| 일반 파일 | /etc/passwd | 디스크 위 바이트 |
| 디렉토리 | /home | 파일들의 색인(디렉토리도 파일이다) |
| 디바이스 | /dev/null, /dev/sda | 커널 디바이스 드라이버 |
| 파이프 | mkfifo /tmp/p | 메모리 버퍼 |
| 소켓 | (socket()로 생성) | 네트워크 endpoint |
/proc, /sys | /proc/cpuinfo | 커널 상태를 파일처럼 노출 |
2) 파일 디스크립터(fd)는 정수다
open()은 정수를 반환한다. 이 정수는 프로세스의 파일 디스크립터 테이블에 대한 인덱스다.
프로세스 PID 1234
┌─────────────────────────────┐
│ fd 0 → stdin (terminal) │
│ fd 1 → stdout (terminal) │
│ fd 2 → stderr (terminal) │
│ fd 3 → /etc/passwd │ ← 방금 open()한 파일
│ fd 4 → socket(tcp:443) │
└─────────────────────────────┘stdin/stdout/stderr가 파일과 똑같이 다뤄진다는 것이 UNIX의 천재성이다.
이 덕에 program > file.txt처럼 리다이렉트가 가능하다 — fd 1을 다른 파일로 바꿔치기만 하면 끝.
3) 파일은 이름이 아니다
이것이 처음 들으면 혼란스러운 사실:
파일의 본체는 inode(메타데이터+데이터 블록 포인터)이고, 이름은 디렉토리 안의 entry일 뿐이다.
$ ls -li /tmp/a.txt
1234567 -rw-r--r-- 2 user staff 13 May 10 a.txt1234567= inode 번호 (실제 파일)2= 하드링크 수 — 같은 inode를 가리키는 이름이 2개 있다는 뜻
$ ln /tmp/a.txt /tmp/b.txt # 하드링크: 같은 inode를 가리키는 새 이름
$ ls -li /tmp/{a,b}.txt
1234567 ... a.txt
1234567 ... b.txt ← 같은 inode!→ a.txt를 지워도 b.txt는 살아 있다. 진짜 파일은 마지막 이름까지 사라질 때 비로소 지워진다.
What — 구체 사양
POSIX 파일 모델 (요약)
| 개념 | 설명 |
|---|---|
| 파일 = 바이트 시퀀스 | 0바이트 ~ 거대한 크기까지, 내부 구조에 대한 가정 없음 |
| 무지향성 | OS는 파일이 텍스트인지 이미지인지 모른다. 그 해석은 응용 프로그램의 책임 |
| 랜덤 액세스 | lseek(fd, offset, SEEK_SET)로 임의 위치로 점프 가능 |
| 메타데이터 분리 | 권한·소유자·시간은 inode에, 이름은 디렉토리에 |
| 파일 = file descriptor | 한 번 열면 정수 하나로 추상화됨 |
핵심 시스템 콜 표 (Linux/macOS)
| 시스템 콜 | 하는 일 | 비고 |
|---|---|---|
open(path, flags) | fd 반환 | O_RDONLY/O_WRONLY/O_CREAT/O_APPEND 등 |
read(fd, buf, n) | n바이트까지 읽기 | EOF 시 0 반환 |
write(fd, buf, n) | n바이트 쓰기 | 단축 쓰기(short write) 가능 |
lseek(fd, off, w) | 파일 오프셋 이동 | SEEK_SET/CUR/END |
stat(path, &st) | 메타데이터 조회 | inode, 크기, 시간, 권한 |
fstat(fd, &st) | fd로 메타데이터 | 경로 없이 조회 |
unlink(path) | 이름 제거 | 마지막 링크면 데이터도 회수 |
dup(fd) / dup2(old, new) | fd 복제 | 리다이렉트 구현에 사용 |
mmap(fd, ...) | 파일을 메모리에 매핑 | 큰 파일을 효율적으로 다룰 때 |
”everything is a file”의 진짜 모습
# 디바이스도 파일
$ cat /dev/urandom | head -c 16 | xxd
00000000: 7a 1c 9b ... (랜덤 바이트)
# 커널 상태도 파일
$ cat /proc/cpuinfo
processor : 0
model name : Apple M2 ...
# 소켓도 fd → epoll/select가 파일과 소켓을 함께 다룬다
$ ls -l /proc/$$/fd/
0 -> /dev/pts/0
1 -> /dev/pts/0
3 -> socket:[12345] ← 소켓도 fd로 보인다What-if — 잘못 이해하면
1) “파일은 텍스트 또는 바이너리다”라고 믿으면
→ 실은 모두 바이너리다. 다음 챕터(02-encoding-binary.md)에서 자세히.
“텍스트 모드”는 윈도우의 \r\n ↔ \n 자동 변환 같은 응용 레이어 약속일 뿐이다.
2) Windows에서 UNIX 가정을 그대로 쓰면
| UNIX 가정 | Windows 현실 |
|---|---|
열린 파일을 삭제할 수 있다 (unlink) | 기본적으로 안 된다 — 핸들이 닫혀야 삭제됨 |
경로 구분자 / | \ (다만 API 대부분 / 허용) |
| 파일 이름 case-sensitive | 기본 case-insensitive (NTFS는 옵션) |
| 권한 = rwx + user/group/other | ACL (훨씬 복잡) |
| fork() 후 fd 상속 | 별도 명시 필요 |
3) “파일 = 이름”이라고 믿으면
→ 하드링크/심볼릭링크/inode를 이해 못 한다. 대표 사고: 백업 도중 파일을 옮겼더니 백업 도구가 같은 파일을 두 번 백업하거나 한 번도 안 함.
4) mmap을 남용하면
큰 파일을 메모리에 매핑하면 빠르지만, 페이지 폴트가 디스크 I/O를 일으켜 응답시간이 튄다. 실시간 시스템에서는 명시적 read가 더 안전.
5) fd 누수
open()만 하고 close()를 잊으면 fd 테이블이 가득 찬다 (EMFILE: too many open files).
Node.js, Java 등 GC 언어에서 자주 발생 — using/finally/with 구문으로 강제 해제 권장.
Insight — 흥미로운 이야기
”에디(Eddie)와 Plan 9 — UNIX가 가지 못한 길”
UNIX의 everything is a file은 강력했지만 완벽하진 않았다. 1980년대 Bell Labs는 후속작 Plan 9에서 이 철학을 극단까지 밀어붙였다 — GUI 윈도우, 마우스, 네트워크 연결까지 전부 파일로 노출했다.
/dev/mouse ← 마우스 좌표를 read하면 (x,y) 반환
/net/tcp/clone ← write하면 새 TCP 연결 생성
/proc/1/mem ← 다른 프로세스 메모리를 파일처럼 read/writePlan 9는 상업적으로 실패했지만, 그 사상은 살아남았다 —
- Linux의
/proc,/sys,/dev - 컨테이너의
/proc/self/ns/*(namespace fd) - 9P 프로토콜 (WSL2에서 Windows ↔ Linux 파일공유에 쓰임)
“왜 fd는 정수인가”
C언어 시대, 가장 빠른 인덱싱 방법이었다. 50년 뒤 Rust도, Go도, 커널 인터페이스에서는 결국 정수 fd를 쓴다. 추상화는 한 번 굳으면 100년을 간다.
”0번 fd가 stdin인 이유”
UNIX 초기 PDP-11 시절, 하드코딩된 약속이었다. 0=입력, 1=출력, 2=에러 — 이 세 정수는 너무 깊이 박혀서 어떤 OS도 못 바꾼다. 컨테이너 런타임도, 파이프도, 셸도 이 약속 위에서 동작한다.
요약 + 다이어그램
파일은 바이트 시퀀스고, fd는 그 시퀀스에 접근하는 정수 핸들이다. UNIX는 이 둘만으로 디바이스·파이프·소켓·커널상태까지 모두 통합했다. “파일 = 이름”이라는 직관은 틀렸다 — 본체는 inode이고, 이름은 그것을 가리키는 디렉토리 항목이다.
다음 문서:
02-encoding-binary.md— 파일이 바이트라면, 그 바이트를 글자로 어떻게 해석하는가?