안동민 개발노트

본문 시작

디렉토리 구조

계층형 디렉토리에서 경로를 inode로 해석하는 과정과 하드 링크·심볼릭 링크·마운트·VFS의 관계를 이해합니다.

파일이 수백만 개 있다면 이름만으로 원하는 파일을 찾는 것은 불가능합니다.

디렉토리(Directory)는 파일들을 계층적으로 정리하는 구조입니다.

디렉토리 자체도 파일의 일종으로, 그 안에 포함된 파일들의 이름과 위치 정보를 담고 있습니다.

OS 관점에서 디렉토리는 이름 → inode 번호 매핑의 목록이며, C 라이브러리의 readdir() 인터페이스로 순회할 수 있습니다.


디렉토리 구조의 발전

단일 레벨(Single-level) 디렉토리는 모든 파일이 하나의 디렉토리에 존재합니다.

초기 임베디드 시스템이나 MS-DOS 1.0에서 사용했습니다.

파일 이름이 전체에서 유일해야 하고, 수가 많아지면 관리가 불가능합니다.

단순 목록 구현의 검색은 O(n)입니다. 평면 이름 공간 자체가 인덱스 사용을 금지하지는 않습니다.

이중 레벨(Two-level) 디렉토리는 사용자별로 별도의 디렉토리를 제공합니다.

다른 사용자와 파일 이름이 충돌하지 않습니다.

하지만 한 사용자 내부에서는 여전히 평면 구조라서 파일이 많으면 같은 문제가 발생합니다.

초기 멀티유저 시스템(TOPS-10 등)이 이 방식이었습니다.

트리 구조(Tree) 디렉토리는 현대 파일 시스템의 표준입니다.

디렉토리 안에 디렉토리를 만들 수 있어 여러 단계의 계층을 구성할 수 있습니다. 경로 길이와 구현상 제한은 남습니다.

/home/user/documents/reports/2024/january/ 같은 경로가 가능한 이유입니다.

비순환 그래프(Acyclic Graph) 구조는 하나의 파일이 여러 디렉토리에서 참조될 수 있습니다.

삭제 시 참조 카운트를 관리해야 합니다.

이것이 링크로 구현됩니다.


디렉토리 연산

디렉토리 작업에 사용하는 시스템 콜과 C 라이브러리 인터페이스:

  • 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;

        int len = snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);
        if (len < 0 || (size_t)len >= sizeof(fullpath)) continue;
        if (lstat(fullpath, &st) == 0) {
            const char *type = S_ISDIR(st.st_mode) ? "DIR" :
                               S_ISLNK(st.st_mode) ? "LINK" : "FILE";
            printf("%-30s %-10s %10lld\n", entry->d_name, type, (long long)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를 가져옵니다.

구성 요소마다 이름·권한 확인이 필요하지만, 캐시가 있으면 각 단계마다 디스크를 읽지는 않습니다. Linux는 dentry·inode 캐시를 활용하며 다른 OS에서는 이름 조회 캐시를 DNLC라고 부르기도 합니다.

각 중간 디렉토리의 검색 권한이 부족하면 최종 파일에 도달하기 전에 실패할 수 있습니다.


하드 링크와 심볼릭 링크

하드 링크(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회 한도).

하드 링크는 inode를, 심볼릭 링크는 경로를 공유합니다

하드 링크는 inode를, 심볼릭 링크는 경로를 공유합니다

이름과 경로의 참조 대상original.txt와hardlink.txt는같은inode를가리킨다. report_link는별도링크파일에대상경로문자열을저장하여경로를다시해석한다.original.txthardlink.txt같은 inode같은 파일 데이터report_link별도 링크 inode저장한 경로/home/user/docs/report.txt대상 경로 해석존재·권한 확인
이름과 경로의 참조 대상original.txt와hardlink.txt는같은inode를가리킨다. report_link는별도링크파일에대상경로문자열을저장하여경로를다시해석한다.original.txthardlink.txt같은 inode같은 파일 데이터report_link별도 링크 inode저장한 경로/home/user/docs/report.txt대상 경로 해석존재·권한 확인

하드 링크에는 원본 이름이라는 우선권이 없습니다. 심볼릭 링크는 대상 이름이 사라지면 경로 해석이 실패할 수 있습니다.

위 예제의 파일 생성·링크 생성·조회는 성공을 전제로 하며 실제 파일을 실행해 만든 결과가 아닙니다. 디렉토리 예제는 정렬하지 않으므로 나열 순서도 고정되지 않습니다.


마운트

Unix 시스템에서는 모든 파일 시스템이 하나의 디렉토리 트리에 합쳐집니다.

예를 들어 USB 파일 시스템을 /mnt/usb에, 네트워크 파일 시스템을 /mnt/nas에 연결할 수 있습니다. 이 경로로 자동 연결되는 것은 아닙니다.

이것이 마운트(Mount)입니다.

다음 명령은 장치·디렉토리·권한을 갖춘 별도 Linux 실습 환경의 예시입니다. 장치 이름과 서버 주소는 사용 환경에 맞게 확인하며 여기서는 실행하지 않습니다.

마운트 실무
# 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은 마운트 설정을 기록하며, 부팅 시 적용 여부는 noauto 같은 옵션과 부팅 관리 정책에 따라 달라집니다.

각 행은 장치, 마운트 포인트, 파일 시스템 유형, 옵션, 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의 report.txt는 그 파일 시스템의 조회 구현을 통해 찾습니다.

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