디렉토리 구조
계층형 디렉토리에서 경로를 inode로 해석하는 과정과 하드 링크·심볼릭 링크·마운트·VFS의 관계를 이해합니다.
파일이 수백만 개 있다면 이름만으로 원하는 파일을 찾는 것은 불가능합니다.
디렉토리(Directory)는 파일들을 계층적으로 정리하는 구조입니다.
디렉토리 자체도 파일의 일종으로, 그 안에 포함된 파일들의 이름과 위치 정보를 담고 있습니다.
OS 관점에서 디렉토리는 이름 → inode 번호 매핑의 목록이며, readdir() 시스템 콜로 순회할 수 있습니다.
파일이 많아질수록 디렉토리는 검색, 경로 해석, 링크 관리의 기준 구조가 된다. 사용자는 이름을 보지만 파일 시스템은 각 이름을 inode와 권한, 캐시된 dentry로 연결한다.
- Tree structure
현대 파일 시스템은 하위 디렉토리를 둬 깊은 계층을 구성하고 이름 충돌 범위를 나눈다.
- Path resolution
루트 또는 현재 디렉토리에서 각 이름을 차례로 찾고 권한을 확인하며 inode를 얻는다.
- Links
하드 링크는 같은 inode, 심볼릭 링크는 다른 경로 문자열을 가리켜 해석 단계가 달라진다.
디렉토리 구조의 발전
단일 레벨(Single-level) 디렉토리는 모든 파일이 하나의 디렉토리에 존재합니다.
초기 임베디드 시스템이나 MS-DOS 1.0에서 사용했습니다.
파일 이름이 전체에서 유일해야 하고, 수가 많아지면 관리가 불가능합니다.
검색도 O(n)입니다.
이중 레벨(Two-level) 디렉토리는 사용자별로 별도의 디렉토리를 제공합니다.
다른 사용자와 파일 이름이 충돌하지 않습니다.
하지만 한 사용자 내부에서는 여전히 평면 구조라서 파일이 많으면 같은 문제가 발생합니다.
초기 멀티유저 시스템(TOPS-10 등)이 이 방식이었습니다.
트리 구조(Tree) 디렉토리는 현대 파일 시스템의 표준입니다.
디렉토리 안에 디렉토리를 만들 수 있어 무한히 깊은 계층을 구성할 수 있습니다.
/home/user/documents/reports/2024/january/ 같은 경로가 가능한 이유입니다.
비순환 그래프(Acyclic Graph) 구조는 하나의 파일이 여러 디렉토리에서 참조될 수 있습니다.
삭제 시 참조 카운트를 관리해야 합니다.
이것이 링크로 구현됩니다.
디렉토리 연산
OS가 디렉토리에 제공하는 시스템 콜:
- opendir(): 디렉토리를 열어 핸들을 얻습니다.
- readdir(): 다음 엔트리(파일 이름 + inode 번호)를 읽습니다.
- mkdir(): 새 디렉토리를 생성합니다.
- rmdir(): 빈 디렉토리를 삭제합니다.
- rename(): 파일이나 디렉토리 이름을 변경합니다.
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>
int main(int argc, char *argv[]) {
const char *path = argc > 1 ? argv[1] : ".";
DIR *dir = opendir(path);
if (!dir) { perror("opendir"); return 1; }
struct dirent *entry;
struct stat st;
char fullpath[512];
printf("%-30s %-10s %10s\n", "이름", "유형", "크기");
printf("%-30s %-10s %10s\n", "---", "---", "---");
while ((entry = readdir(dir)) != NULL) {
/* . 과 .. 건너뛰기 */
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0)
continue;
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);
if (stat(fullpath, &st) == 0) {
const char *type = S_ISDIR(st.st_mode) ? "DIR" :
S_ISLNK(st.st_mode) ? "LINK" : "FILE";
printf("%-30s %-10s %10ld\n", entry->d_name, type, st.st_size);
}
}
closedir(dir);
return 0;
}경로명
절대 경로(Absolute Path)는 루트 디렉토리에서부터의 전체 경로입니다.
Unix에서는 /home/user/hello.c, Windows에서는 C:\Users\user\hello.c입니다.
어떤 디렉토리에서 실행하든 동일한 파일을 가리킵니다.
상대 경로(Relative Path)는 현재 작업 디렉토리(Current Working Directory, CWD)를 기준으로 합니다.
.은 현재 디렉토리, ..은 상위 디렉토리를 의미합니다.
cd ../src/는 "상위로 올라갔다가 src로 들어가라"는 뜻입니다.
경로 해석(Path Resolution)은 왼쪽부터 각 구성 요소를 순서대로 찾아갑니다.
/home/user/hello.c를 열면:
- 루트 디렉토리(
/)의 디렉토리 엔트리에서 home을 찾아 inode를 가져옵니다. - home의 inode에서 데이터 블록을 읽고 user를 찾아 inode를 가져옵니다.
- user의 inode에서 hello.c를 찾아 최종 inode를 가져옵니다.
구성 요소마다 디스크 읽기가 필요하므로, 깊은 경로는 느릴 수 있습니다.
OS는 DNLC(Directory Name Lookup Cache)로 최근 조회한 경로를 캐싱합니다.
경로 해석은 문자열을 한 번에 파일로 바꾸는 과정이 아니라, 각 경로 조각에서 디렉토리 엔트리와 inode를 반복해서 확인하는 과정입니다.
그래서 깊은 경로, 권한 실패, 캐시 여부를 함께 봐야 합니다.
디렉토리는 경로 문자열을 파일 위치로 바꾸는 인덱스이며, 구조에 따라 탐색 비용이 달라집니다.
- 경로 분해
경로 절대 경로와 상대 경로를 컴포넌트 단위로 나눠 따라갑니다.
- 이름 매핑
Entry 각 디렉토리 엔트리는 이름을 inode나 파일 식별자로 연결합니다.
- 반복 탐색
Cache 자주 쓰는 경로는 dentry cache로 탐색 비용을 줄입니다.
- 접근 확인
Permission 중간 디렉토리의 실행 권한이 없으면 파일까지 도달할 수 없습니다.
하드 링크와 심볼릭 링크
하드 링크(Hard Link)는 동일한 파일 데이터(inode)를 가리키는 또 다른 이름입니다.
원본과 하드 링크는 완전히 동등합니다.
원본을 삭제해도 하드 링크를 통해 데이터에 접근할 수 있습니다.
모든 하드 링크가 삭제되어야(그리고 열고 있는 프로세스도 없어야) 실제 데이터가 해제됩니다.
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
int main() {
/* 파일 생성 */
FILE *fp = fopen("original.txt", "w");
fprintf(fp, "shared data\n");
fclose(fp);
/* 하드 링크 생성 */
link("original.txt", "hardlink.txt");
/* inode 번호 확인 - 동일해야 함 */
struct stat st1, st2;
stat("original.txt", &st1);
stat("hardlink.txt", &st2);
printf("original inode: %lu (링크 수: %lu)\n",
st1.st_ino, st1.st_nlink); /* 링크 수: 2 */
printf("hardlink inode: %lu (링크 수: %lu)\n",
st2.st_ino, st2.st_nlink); /* 동일한 inode */
/* 원본 삭제 후에도 hardlink로 접근 가능 */
unlink("original.txt");
stat("hardlink.txt", &st2);
printf("삭제 후 링크 수: %lu\n", st2.st_nlink); /* 1 */
unlink("hardlink.txt");
return 0;
}하드 링크의 제약: 디렉토리에는 하드 링크를 만들 수 없습니다(순환 방지).
다른 파일 시스템의 파일에도 하드 링크를 만들 수 없습니다(inode는 파일 시스템 내에서만 유일).
심볼릭 링크(Symbolic Link, Soft Link)는 다른 파일의 경로 문자열을 가리키는 특수 파일입니다.
Windows의 바로가기와 유사합니다.
import os
# 심볼릭 링크 생성
os.symlink("/home/user/docs/report.txt", "report_link")
# 링크 대상 확인
target = os.readlink("report_link")
print(f"링크 대상: {target}")
# 심볼릭 링크와 원본의 inode는 다름
print(f"링크 inode: {os.lstat('report_link').st_ino}")
print(f"원본 inode: {os.stat('/home/user/docs/report.txt').st_ino}")
# 원본 삭제 시 심볼릭 링크는 깨짐 (dangling link)
# os.remove("/home/user/docs/report.txt")
# open("report_link") → FileNotFoundError심볼릭 링크의 장점: 디렉토리에도 생성 가능, 다른 파일 시스템도 가리킬 수 있음, 순환 감지를 OS가 처리합니다(ELOOP 에러, 보통 40회 한도).
지금까지의 디렉토리, 경로명, 링크 개념을 한 번에 연결하면 이름 공간이 어떤 단계로 확장되는지 보입니다.
다음 다이어그램은 마운트 설명으로 넘어가기 전에 디렉토리·링크·마운트의 역할을 나누어 정리합니다.
디렉토리는 이름을 inode로 찾고, 링크는 같은 파일에 새 이름이나 경로를 붙이며, 마운트는 특정 디렉토리 아래를 다른 파일 시스템으로 넘깁니다.
- 이름 목록
Directory name -> inode 디렉토리 엔트리는 파일 이름을 실제 파일 식별자로 바꿉니다.
- 같은 inode
Hard link new name -> same inode 원본 이름이 지워져도 다른 하드 링크가 남으면 데이터는 유지됩니다.
- 경로 문자열
Symbolic link file -> target path 대상 경로를 다시 해석하므로 원본이 사라지면 링크가 깨집니다.
- 트리 접합
Mount /mnt/usb -> other FS 한 디렉토리 아래에서 다른 저장장치의 루트가 시작됩니다.
- 무엇을 공유하나
하드 링크는 inode를 공유하고, 심볼릭 링크는 경로 문자열만 저장합니다.
- 어디서 끊기나
심볼릭 링크는 대상 경로가 없으면 실패하고, 마운트는 경계 아래 구현이 바뀝니다.
- 검수 순서
문제가 생기면 이름, inode, 대상 경로, 마운트 지점을 차례로 확인합니다.
마운트
Unix 시스템에서는 모든 파일 시스템이 하나의 디렉토리 트리에 합쳐집니다.
USB를 꽂으면 /mnt/usb에, 네트워크 드라이브를 연결하면 /mnt/nas에 연결됩니다.
이것이 마운트(Mount)입니다.
# USB 디스크 마운트
mount /dev/sdb1 /mnt/usb
# 네트워크 파일 시스템 마운트
mount -t nfs 192.168.1.100:/share /mnt/nas
# tmpfs (메모리 기반 파일 시스템) 마운트
mount -t tmpfs -o size=1G tmpfs /tmp/ramdisk
# 현재 마운트 목록
df -hT # -T는 파일 시스템 유형도 표시
# 안전한 언마운트 (열린 파일이 있으면 실패)
umount /mnt/usb
# 어떤 프로세스가 마운트 포인트를 사용 중인지 확인
fuser -mv /mnt/usb마운트 포인트(Mount Point)에 접근하면 OS가 자동으로 해당 파일 시스템으로 요청을 전달합니다.
사용자 입장에서는 로컬 디스크든 USB든 네트워크 드라이브든 동일한 경로 체계로 접근할 수 있습니다.
/etc/fstab에 마운트 정보를 등록하면 부팅 시 자동으로 마운트됩니다.
각 행은 장치, 마운트 포인트, 파일 시스템 유형, 옵션, dump 주기, fsck 순서를 명시합니다.
VFS (Virtual File System)
Linux 커널은 VFS(Virtual File System) 레이어를 두어 다양한 파일 시스템(ext4, XFS, Btrfs, NFS, FAT 등)을 통일된 인터페이스로 접근할 수 있게 합니다.
open(), read(), write() 같은 시스템 콜은 VFS를 거쳐 실제 파일 시스템의 구현 함수를 호출합니다.
새로운 파일 시스템을 추가할 때도 VFS 인터페이스만 구현하면 됩니다.
Windows는 드라이브 문자(C:, D:)로 파일 시스템을 구분하는 다른 접근을 취하지만, 내부적으로 유사한 계층 구조를 갖습니다.
마운트와 VFS는 경로 문자열이 중간에서 다른 파일 시스템의 구현으로 넘어가는 지점을 설명합니다.
아래 다이어그램은 /mnt/usb/report.txt 같은 경로가 루트 조회에서 마운트 감지, 실제 파일 시스템 호출로 이어지는 흐름을 정리합니다.
사용자는 하나의 경로를 열지만, VFS는 경로 조각을 따라가다가 마운트 포인트를 만나면 요청을 다른 파일 시스템 구현으로 넘깁니다.
- 경로 걷기
VFS가 루트에서 mnt, usb 이름을 차례로 찾습니다.
- 마운트 확인
마운트 테이블에서 /mnt/usb가 다른 파일 시스템의 시작점인지 확인합니다.
- 구현 전환
ext4, FAT, NFS 같은 실제 파일 시스템의 lookup 함수로 요청을 보냅니다.
- inode 반환
report.txt의 inode와 데이터 접근 방법이 VFS를 통해 돌아옵니다.
- 붙이기
mount 장치나 네트워크 파일 시스템을 기존 디렉토리 아래에 연결합니다.
- 확인하기
fstab / df 자동 마운트 규칙과 현재 파일 시스템 유형, 용량을 확인합니다.
- 분리하기
umount 열린 파일이 남아 있으면 EBUSY로 실패하므로 사용 중인 프로세스를 먼저 봅니다.
다음 절에서는 파일 시스템이 내부적으로 어떻게 구현되는지, 디스크 블록을 어떻게 관리하는지를 살펴보겠습니다.