운영체제의 가상화(Virtualization) 파트를 지나, 이제 영속성(Persistence)의 세계로 들어왔다. 영속성의 핵심은 전원이 꺼져도 데이터가 날아가지 않게 하는 것이다. 이를 위해 운영체제는 파일 시스템(File System)이라는 인터페이스를 제공한다.
1. 파일과 디렉터리: 기본 개념 (Files and Directories)
파일 시스템은 데이터를 저장하기 위해 두 가지 핵심 추상화를 제공한다.
- 파일 (File): 운영체제 입장에서 파일은 그저 바이트의 선형 배열(Linear Array of Bytes)일 뿐이다. 파일의 내용(이미지인지, 텍스트인지)은 OS가 알 바 아니다. 각 파일은 inode number(아이노드 번호)라는 저수준의 이름으로 식별된다.
- 디렉터리 (Directory): 디렉터리도 파일이다. 다만 그 내용이 (사용자가 읽을 수 있는 이름, inode number)의 매핑 정보 리스트라는 점이 특별하다. 디렉터리 구조는 트리(Tree) 형태를 띤다.
2. 파일 생성과 읽기/쓰기 (open, read, write)
파일을 다루는 가장 기본적인 작업이다.
파일 생성 (open)
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
- 플래그(Flags):
- O_CREAT: 파일이 없으면 생성한다.
- O_WRONLY: 쓰기 전용으로 연다.
- O_TRUNC: 파일이 이미 있으면 내용을 싹 비우고(0바이트로 만듦) 연다.
- 반환값 (File Descriptor): 성공 시 정수(fd)를 반환한다. 이 fd는 프로세스마다 개인적으로 관리하는 배열의 인덱스다.
읽기와 쓰기 (read, write)
// 파일을 읽는 명령어 cat의 동작 원리
while ((bytes_read = read(fd_in, buffer, sizeof(buffer))) > 0) {
write(fd_out, buffer, bytes_read);
}
- read()와 write()는 현재 파일의 오프셋(Offset) 위치에서 작업을 수행하고, 작업한 바이트 수만큼 오프셋을 이동시킨다.
3. 임의 접근: lseek (Reading and Writing, but Not Sequentially)
파일을 항상 순차적으로만 읽을 필요는 없다. 데이터베이스 같은 프로그램은 파일의 중간 내용을 빈번하게 읽어야 한다.
off_t lseek(int fd, off_t offset, int whence);
- 동작: 디스크 헤드를 물리적으로 이동시키는 게 아니라, 커널 메모리 내의 오프셋 변수 값만 수정한다.
- whence 옵션:
- SEEK_SET: 파일의 시작점 기준 (절대 위치)
- SEEK_CUR: 현재 오프셋 기준 (상대 위치)
- SEEK_END: 파일의 끝 기준 (주로 파일 크기를 알거나 뒤에 덧붙일 때 사용)
4. [핵심] 오픈 파일 테이블과 공유 (Shared File Entries)
이 챕터에서 가장 중요하고 헷갈리기 쉬운 부분이다. 운영체제는 열린 파일을 어떻게 관리할까? 세 가지 자료구조가 관여한다.
- 프로세스별 파일 디스크립터 테이블 (Per-process Descriptor Table): 각 프로세스가 가진 fd 배열.
- 시스템 전체 오픈 파일 테이블 (System-wide Open File Table): 모든 프로세스가 공유하는 테이블. 여기에 현재 오프셋(Current Offset), 접근 모드, 참조 횟수 등이 저장된다.
- inode (메타데이터): 실제 파일의 정보.
케이스 A: fork()를 했을 때 (공유 O)
부모 프로세스가 파일을 열고(fd=3), fork()를 호출해 자식을 낳으면?
- 자식도 똑같은 fd=3을 가진다.
- 중요: 둘 다 같은 오픈 파일 테이블 엔트리를 가리킨다.
- 결과: 부모가 읽어서 오프셋을 이동시키면, 자식의 오프셋도 같이 이동한다.
케이스 B: open()을 따로 두 번 했을 때 (공유 X)
같은 파일을 프로세스 A가 열고, 프로세스 B가 따로 열면?
- 각각 다른 오픈 파일 테이블 엔트리가 생성된다.
- 결과: 서로 오프셋이 독립적이다. A가 읽어도 B의 오프셋은 변하지 않는다.
5. 영속성 보장: fsync() (Writing Immediately)
write()를 호출했다고 해서 데이터가 즉시 디스크에 기록되는 것은 아니다. 성능을 위해 OS는 데이터를 메모리(Buffer Cache)에 잠시 담아둔다. 하지만 이 상태에서 전원이 꺼지면? 데이터는 날아간다.
데이터베이스(DBMS)처럼 데이터 유실이 치명적인 프로그램은 fsync(fd)를 사용해야 한다.
write(fd, buffer, size); // 메모리 버퍼에만 씀
fsync(fd); // 디스크에 강제로 내려 씀 (Dirty Page Flush)
- 주의: 파일의 내용뿐만 아니라, 파일을 포함하는 디렉터리 정보까지 안전하게 쓰려면 디렉터리에 대해서도 fsync()를 해줘야 완벽하다.
6. 파일 이름 변경: rename()
파일 이름을 바꿀 때 mv 명령어를 쓰는데, 내부적으로는 rename() 시스템 콜을 사용한다.
rename("old_name", "new_name");
- 원자성 (Atomicity): rename의 가장 큰 특징은 원자적(Atomic)이라는 것이다. 이름이 바뀌는 도중에 시스템이 셧다운 되어도, 파일은 old_name으로 남아있거나 new_name으로 바뀌거나 둘 중 하나다. 어중간한 상태(파일 소실)는 없다.
- 이 특징을 이용해 에디터들은 임시 파일에 저장을 완료한 후, 원본 파일로 rename 하는 방식으로 안전한 저장을 구현한다.
7. 파일 정보 얻기: stat() (Metadata)
파일 시스템은 파일의 내용(Content) 외에도 파일에 대한 정보(Metadata)를 inode에 저장한다. 이를 확인하는 함수가 stat()이다.
struct stat {
dev_t st_dev; // 디바이스 ID
ino_t st_ino; // Inode 번호 (중요!)
mode_t st_mode; // 권한 및 파일 타입
nlink_t st_nlink; // 하드 링크 수 (Reference Count)
uid_t st_uid; // 소유자 ID
off_t st_size; // 파일 크기 (바이트)
time_t st_mtime; // 마지막 수정 시간
...
};
- 파일의 이름은 inode에 없다. 이름은 디렉터리가 관리한다.
8. 파일 삭제: unlink() (Removing Files)
왜 파일을 지우는 함수 이름이 remove나 delete가 아니라 unlink일까?
파일 삭제의 메커니즘은 참조 횟수(Reference Count)에 기반한다.
- 파일(inode)은 link count라는 숫자를 가진다. (이 파일을 가리키는 이름이 몇 개인가?)
- unlink("foo")를 호출하면:
- "foo"라는 이름을 디렉터리에서 지운다.
- 해당 inode의 link count를 1 감소시킨다.
- 만약 link count가 0이 되면, 그때 비로소 inode와 데이터 블록을 해제(Free)하여 파일이 진짜 삭제된다.
9. 디렉터리 다루기 (mkdir, readdir, rmdir)
디렉터리는 일반 파일과 달리 직접 write 할 수 없다. (그랬다간 파일 시스템 구조가 망가진다.)
디렉터리 읽기
디렉터리는 단순히 read()로 읽는 대신 전용 라이브러리를 쓴다.
DIR *dp = opendir("."); // 디렉터리 열기
struct dirent *d;
while ((d = readdir(dp)) != NULL) { // 엔트리 하나씩 읽기
printf("inode: %d, name: %s\n", d->d_ino, d->d_name);
}
closedir(dp);
- ls 명령어가 내부적으로 이렇게 동작한다.
10. 링크: 하드 링크 vs 심볼릭 링크
두 가지 링크 방식의 차이를 이해하는 것은 매우 중요하다.
(1) 하드 링크 (Hard Link)
ln file target # link() 시스템 콜
- 동작: 디렉터리에 (target, 기존 inode 번호) 쌍을 추가한다.
- 특징:
- 원본 파일과 똑같은 inode 번호를 가진다.
- 즉, 별도의 파일이 아니라 같은 파일에 이름만 하나 더 생긴 것이다.
- 원본을 지워도(unlink), 링크가 남아있으면(ref count > 0) 파일은 사라지지 않는다.
- 제약: 디렉터리에는 하드 링크를 걸 수 없다(순환 참조 방지). 다른 파티션(파일 시스템) 간에는 걸 수 없다(inode 번호는 파티션 내에서만 유일하므로).
(2) 심볼릭 링크 (Symbolic Link / Soft Link)
ln -s file target # symlink() 시스템 콜
- 동작: 완전히 새로운 파일(새로운 inode)을 만든다. 이 파일의 내용(Content)은 원본 파일의 경로 문자열이다.
- 특징:
- 원본과 다른 inode 번호를 가진다.
- 원본 파일을 지우면, 심볼릭 링크는 가리킬 곳을 잃어버린 상태(Dangling Reference)가 되어 사용할 수 없다.
- 디렉터리나 다른 파티션에 대해서도 자유롭게 걸 수 있다.
11. 권한과 접근 제어 (Permission Bits and ACLs)
- Permission Bits: 유닉스 파일 시스템은 rwxr-xr-x와 같은 9비트 정보를 통해 소유자(User), 그룹(Group), 기타(Other)에 대한 읽기/쓰기/실행 권한을 관리한다. (chmod로 변경)
- ACL (Access Control List): 9비트만으로는 "철수한테만 쓰기 권한을 주고 싶다" 같은 정교한 제어가 어렵다. 이를 위해 ACL을 사용하여 사용자별로 구체적인 권한을 설정할 수 있다.
12. 파일 시스템 만들기 및 마운트 (mkfs, mount)
파일 시스템을 사용하려면 먼저 초기화하고 트리에 붙여야 한다.
- mkfs (Make Filesystem): 디스크 파티션에 빈 파일 시스템 구조(루트 inode, 프리 리스트 등)를 쓴다.
- mount: 물리적인 장치(예: /dev/sda1)를 현재 파일 시스템 트리의 특정 위치(예: /home)에 갖다 붙인다.
- 마운트 하기 전까지 /home은 그냥 빈 디렉터리지만, 마운트 후에는 해당 디스크의 루트 디렉터리가 /home 위치에 오버레이 된다.
요약 (Summary)
OSTEP 39장은 파일 시스템의 인터페이스를 폭넓게 다뤘다.
- 파일은 바이트 배열, 디렉터리는 이름 목록이다.
- open을 통해 Open File Table 엔트리를 만들고 Offset을 관리한다.
- fork 시 오프셋이 공유되는 원리를 이해해야 한다.
- fsync로 영속성을, rename으로 원자성을 보장한다.
- unlink는 링크 카운트를 줄이며, 0이 될 때 파일이 삭제된다.
- 하드 링크는 이름 추가, 심볼릭 링크는 바로가기 파일 생성이다.
이 API들은 유닉스 철학의 정수이며, 오늘날 대부분의 시스템(Linux, macOS 등)이 이 표준을 따르고 있다.
'Deep Dive > OS' 카테고리의 다른 글
| [OSTEP] 스터디 16주차 Fast File System (FFS) 와 로그 구조화 파일 시스템 (LFS) (0) | 2025.12.23 |
|---|---|
| [OSTEP] 스터디 15주차 Part.2 파일 시스템 구현 (0) | 2025.12.09 |
| [OSTEP] 스터디 15주차 - 영속성 Redundant Array of Inexpensive Disk (RAID) (0) | 2025.12.09 |
| [OSTEP] 스터디 14주차 - 영속성 1 Part.2 (0) | 2025.12.02 |
| [OSTEP] 스터디 14주차 - 영속성 1 Part.1 (0) | 2025.12.02 |
