icon

안동민 개발노트

10장 : 파일 시스템

디렉토리 구조


파일이 수백만 개 있다면 이름만으로 원하는 파일을 찾는 것은 불가능합니다. 디렉토리(Directory)는 파일들을 계층적으로 정리하는 구조입니다. 디렉토리 자체도 파일의 일종으로, 그 안에 포함된 파일들의 이름과 위치 정보를 담고 있습니다. OS 관점에서 디렉토리는 이름 → inode 번호 매핑의 목록이며, readdir() 시스템 콜로 순회할 수 있습니다.


디렉토리 구조의 발전

단일 레벨(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(): 파일이나 디렉토리 이름을 변경합니다.
directory_listing.c
#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를 열면:

  1. 루트 디렉토리(/)의 디렉토리 엔트리에서 home을 찾아 inode를 가져옵니다.
  2. home의 inode에서 데이터 블록을 읽고 user를 찾아 inode를 가져옵니다.
  3. user의 inode에서 hello.c를 찾아 최종 inode를 가져옵니다.

구성 요소마다 디스크 읽기가 필요하므로, 깊은 경로는 느릴 수 있습니다. OS는 DNLC(Directory Name Lookup Cache)로 최근 조회한 경로를 캐싱합니다.


하드 링크와 심볼릭 링크

하드 링크(Hard Link)는 동일한 파일 데이터(inode)를 가리키는 또 다른 이름입니다. 원본과 하드 링크는 완전히 동등합니다. 원본을 삭제해도 하드 링크를 통해 데이터에 접근할 수 있습니다. 모든 하드 링크가 삭제되어야(그리고 열고 있는 프로세스도 없어야) 실제 데이터가 해제됩니다.

hard_link_demo.c
#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의 바로가기와 유사합니다.

symlink_demo.py
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회 한도).


마운트

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:)로 파일 시스템을 구분하는 다른 접근을 취하지만, 내부적으로 유사한 계층 구조를 갖습니다.

다음 절에서는 파일 시스템이 내부적으로 어떻게 구현되는지, 디스크 블록을 어떻게 관리하는지를 살펴보겠습니다.

목차