컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
os · 08

파일 시스템

OS파일저장장치

파일 시스템은 데이터를 파일 단위로 저장하고 디렉토리로 계층 구조를 만들어 관리하는 OS의 핵심 기능입니다. inode 기반(Linux ext4)과 FAT 기반(Windows FAT32/exFAT)이 대표적인 두 가지 방식입니다.

핵심 개념

inode (Index Node)

파일의 메타데이터(크기, 권한, 타임스탬프, 소유자)와 데이터 블록 포인터를 저장하는 자료구조입니다. 파일명은 inode에 없고 디렉토리 엔트리에 있습니다. ls -i 명령으로 inode 번호를 확인할 수 있습니다.

핵심 개념

디렉토리 구조

inode 기반 파일 시스템(ext4 등)에서 디렉토리는 파일명과 inode 번호의 쌍을 저장하는 특수 파일입니다. 경로 탐색 시 OS는 루트(/)부터 각 디렉토리의 inode를 거쳐 목표 파일의 inode를 찾습니다.

핵심 개념

FAT (File Allocation Table)

디스크 블록(클러스터)의 연결 관계를 테이블로 관리합니다. 각 항목이 다음 블록 번호를 가리키는 연결 리스트 방식입니다. 단순하지만 랜덤 접근이 느리고 FAT가 손상되면 파일의 블록 연결 정보를 잃을 수 있습니다.

핵심 개념

하드 링크 vs 심볼릭 링크

하드 링크는 같은 inode를 가리키는 또 다른 디렉토리 엔트리입니다. 한 이름을 지워도 다른 이름(링크)이 남아 있으면 데이터가 유지됩니다. 심볼릭 링크(소프트 링크)는 경로를 저장하는 별도 파일로 원본 삭제 시 깨집니다.

핵심 개념

저널링 (Journaling)

파일 시스템 변경 전 로그(저널)에 먼저 기록합니다. 시스템 충돌 시 저널을 재실행해 일관성을 복구합니다. ext4와 NTFS는 저널링을 쓰고, APFS는 저널 대신 메타데이터를 쓰기 시점 복사(CoW) 방식으로 갱신해 일관성을 유지합니다.

핵심 개념

VFS (Virtual File System)

다양한 파일 시스템(ext4, FAT, NTFS, tmpfs)을 동일한 인터페이스로 추상화하는 Linux 커널 계층입니다. open(), read(), write() 같은 시스템 콜 인터페이스로 여러 파일 시스템에 접근할 수 있는 이유입니다(지원하는 기능은 파일 시스템마다 다릅니다).

블록 포인터 구조 — 전통적 Unix inode(ext2·ext3)는 직접 포인터(12개), 1단계 간접 포인터, 2단계 간접 포인터, 3단계 간접 포인터로 구성됩니다. 블록 크기 4KB 기준으로 이론상 수 TB 크기의 파일까지 가리킬 수 있습니다(숫자로 확인하기의 계산 참고). ext4는 이 방식 대신 연속된 블록 범위를 묶어 표현하는 extent를 쓸 수 있어 대용량 파일에서 더 효율적입니다.
파일 삭제의 실제 동작 — rm 명령은 데이터를 지우지 않습니다. 디렉토리 엔트리를 제거하고 inode의 링크 카운트를 1 감소시킵니다. 카운트가 0이 되고 파일을 열고 있는 프로세스가 없으면 블록이 해제됩니다. 일반적으로 이 때문에 복구 도구가 동작할 수 있습니다(저장장치와 설정에 따라 다릅니다).
ext4 vs NTFS vs APFS — ext4는 많은 Linux 배포판의 기본 파일 시스템입니다. NTFS는 Windows 기본으로 권한 관리와 암호화를 지원합니다. APFS는 macOS/iOS 기본으로 SSD 최적화, 스냅샷, 쓰기 시점 복사(CoW)를 지원합니다.
파일 디스크립터 (fd) — open() 시스템 콜이 반환하는 정수입니다. 프로세스의 파일 디스크립터 테이블 인덱스이며, 그 항목은 열린 파일 정보를 거쳐 inode를 가리킵니다. 관례상 0(stdin), 1(stdout), 2(stderr)입니다.

파일 시스템이 하는 일

디스크는 번호가 매겨진 블록의 모음일 뿐이라서, 이름이 있는 파일과 디렉토리 계층으로 보이게 하려면 어떤 블록이 어떤 파일의 것인지 기록하는 구조가 필요합니다. 파일 시스템은 이 기록과 빈 공간 관리, 접근 권한, 충돌 뒤 복구를 맡습니다. 리눅스는 여러 종류의 파일 시스템을 VFS라는 공통 계층 아래에서 같은 인터페이스로 다룹니다.

숫자로 확인하기

inode 블록 포인터가 가리키는 크기(계산) — 블록이 4KB이고 포인터가 4바이트이면 한 블록에 포인터가 1,024개 들어갑니다. 직접 포인터 12개는 48KB, 1단계 간접은 1,024 × 4KB = 4MB, 2단계 간접은 1,024² × 4KB = 4GB, 3단계 간접은 1,024³ × 4KB = 4TB를 가리킬 수 있습니다.
하드 링크 수 — 파일 하나에 하드 링크를 2개 더 만들면 inode의 링크 수는 3이 됩니다. 이름을 하나씩 지우면 3 → 2 → 1 → 0이고 0이 되고 더 이상 열려 있지 않을 때 블록이 해제됩니다.
작은 파일과 블록 단위(계산) — 블록이 4KB이고 인라인 데이터 같은 기능이 없다면 100바이트짜리 파일 10,000개의 데이터는 합쳐서 약 1MB이지만 블록으로는 10,000 × 4KB = 약 40MB를 차지합니다.
FAT32의 파일 크기 한계 — 디렉토리 항목의 파일 크기 필드가 32비트라서 파일 하나는 최대 2³² − 1바이트, 곧 약 4GiB 미만입니다.

자주 하는 오해

파일 이름은 inode에 저장된다 — 이름은 디렉토리 항목에 있고 inode에는 크기, 권한, 시각, 블록 위치 같은 메타데이터가 있습니다. 그래서 같은 inode에 이름이 여러 개일 수 있습니다.
rm으로 지우면 데이터가 바로 사라진다 — 이름을 지우고 링크 수를 줄일 뿐입니다. 링크 수가 0이어도 어떤 프로세스가 파일을 열고 있으면 닫을 때까지 해제되지 않습니다(unlink(2) 매뉴얼).
저널링이면 데이터가 절대 손상되지 않는다 — ext4의 기본 모드는 메타데이터를 저널링하고, 파일 내용의 일관성은 별도 설정에 달려 있습니다. 저널링은 파일 시스템 구조를 빠르게 복구하는 장치입니다.
심볼릭 링크는 하드 링크와 같다 — 심볼릭 링크는 경로 문자열을 담은 별도 파일이라 대상이 지워지면 끊어지고, 하드 링크는 같은 inode를 가리키는 또 다른 이름입니다.

면접에서 이렇게 물어요

면접 질문

inode란 무엇인가요?

파일의 메타데이터와 데이터 블록 위치를 담은 구조체입니다. 파일 이름은 담지 않고 디렉토리 항목이 이름과 inode 번호를 연결합니다.

면접 질문

/home/a.txt를 열 때 어떤 일이 일어나나요?

루트 디렉토리의 inode에서 시작해 home 디렉토리의 항목을 찾고, 그 디렉토리에서 a.txt의 inode 번호를 찾습니다. 그 inode의 권한을 확인한 뒤 데이터 블록을 읽습니다.

면접 질문

하드 링크와 심볼릭 링크는 어떻게 다른가요?

하드 링크는 같은 inode에 이름을 하나 더 붙이는 것이라 같은 파일 시스템 안에서만 가능하고 어느 이름을 지워도 데이터는 남습니다. 심볼릭 링크는 경로를 담은 별도 파일이라 다른 파일 시스템도 가리킬 수 있고 대상이 지워지면 끊어집니다.

면접 질문

저널링은 왜 필요한가요?

파일 하나를 바꾸는 데 여러 블록을 고쳐야 하는데 중간에 전원이 꺼지면 구조가 어긋납니다. 변경 내용을 먼저 로그에 기록해 두면 재부팅 때 로그를 보고 완료하거나 취소해 일관성을 복구할 수 있습니다.

실제 시스템에서는

확인 명령 — ls -i로 inode 번호를, stat으로 inode의 메타데이터를, df -i로 inode 사용량을 볼 수 있습니다. ext2·ext3·ext4처럼 inode 수가 만들 때 정해지는 파일 시스템에서는 파일이 적어도 inode가 바닥나면 새 파일을 만들 수 없습니다.
ext4 구조 — 리눅스 커널 문서는 ext4가 블록 포인터 대신 extent로 파일의 블록 위치를 표현할 수 있고, 전통적인 ext2·ext3의 직접·간접 블록 맵도 설명합니다.
Java의 링크 — Files.createLink와 Files.createSymbolicLink로 하드 링크와 심볼릭 링크를 만들 수 있습니다(파일 시스템과 OS가 지원하고 권한이 있을 때).
데이터베이스의 로그 — PostgreSQL의 WAL은 저널링과 비슷하게, 데이터 파일을 고치기 전에 변경을 먼저 로그에 기록합니다.

함께 보면 좋은 토픽

시스템 콜 — open, read, write가 파일 시스템으로 가는 길을 봅니다.
디스크 스케줄링 — 파일 시스템 아래에서 디스크 요청을 정렬하는 방식을 봅니다.
프로세스 — 프로세스의 파일 디스크립터 테이블을 봅니다.
가상 메모리 — 파일을 메모리에 매핑하는 방식과 연결해 봅니다.
트랜잭션 — 로그를 먼저 쓰는 방식이 트랜잭션에서 쓰이는 모습을 봅니다.

참고 자료

C — open() / read() / write() 시스템 콜

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>      // strlen
#include <sys/stat.h>    // struct stat, fstat

int main() {
    /* open(): 경로 탐색 → inode 확인 → 파일 디스크립터 반환 */
    int fd = open("/home/user/hello.txt", O_RDWR | O_CREAT, 0644);
    if (fd < 0) { perror("open"); return 1; }
    // fd: 0=stdin, 1=stdout, 2=stderr 이후 가장 작은 정수 (보통 3)

    /* write(): fd → 파일 테이블 → inode → 데이터 블록 쓰기 */
    const char *msg = "Hello, File System!\n";
    ssize_t n = write(fd, msg, strlen(msg));
    if (n < 0) { perror("write"); }

    /* lseek(): 파일 오프셋 이동 (inode의 파일 위치 포인터 변경) */
    lseek(fd, 0, SEEK_SET);   // 처음으로 돌아가기

    /* read(): 커널이 inode → 블록 포인터 → 디스크 읽기 → 버퍼 복사 */
    char buf[64];
    n = read(fd, buf, sizeof(buf) - 1);
    if (n > 0) { buf[n] = '\0'; printf("%s", buf); }

    /* stat(): inode 메타데이터 조회 (크기, 권한, 타임스탬프) */
    struct stat st;
    fstat(fd, &st);
    printf("inode: %lu, size: %ld, blocks: %ld\n",
           st.st_ino, st.st_size, st.st_blocks);

    close(fd);   // fd 해제 (inode 링크 카운트는 유지)
    return 0;
}

open()은 경로를 탐색해 inode를 찾고, 프로세스의 파일 디스크립터 테이블에 항목을 추가한 뒤 정수 fd를 반환합니다.
write()는 즉시 디스크에 쓰지 않고 커널 페이지 캐시(Page Cache)에 씁니다. fsync()를 호출해야 디스크에 확실히 반영됩니다.
fstat()로 st_ino(inode 번호), st_size(바이트 단위 크기), st_blocks(512B 단위 블록 수)를 조회할 수 있습니다. st_nlink가 하드 링크 개수입니다.

Java — NIO.2 / Files API로 inode 정보 확인

import java.nio.file.*;
import java.nio.file.attribute.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileSystemDemo {

    public static void main(String[] args) throws Exception {
        Path path = Path.of("/home/user/hello.txt");

        // ── inode 메타데이터 조회 ──────────────────────────────────
        BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
        System.out.println("크기: "     + attr.size());
        System.out.println("생성: "     + attr.creationTime());
        System.out.println("수정: "     + attr.lastModifiedTime());
        System.out.println("디렉토리: " + attr.isDirectory());

        // Linux inode 번호 (UnixPath에서만 가용)
        if (attr instanceof PosixFileAttributes) {
            // unix:ino — 플랫폼 의존 속성, Unix/Linux에서만 가용
            Object inode = Files.getAttribute(path, "unix:ino");
            System.out.println("inode: " + inode);
        }

        // ── FileChannel: 블록 단위 직접 I/O ─────────────────────────
        try (var fc = FileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buf = ByteBuffer.allocate(1024);
            int n = fc.read(buf);       // 커널 → JVM 버퍼로 복사
            buf.flip();
            byte[] bytes = new byte[n];
            buf.get(bytes);
            System.out.println(new String(bytes));
        }

        // ── 하드 링크 생성 (같은 inode를 가리키는 새 경로) ────────
        Path link = Path.of("/home/user/hello_hard.txt");
        Files.createLink(link, path);     // 링크 카운트 +1

        // ── 심볼릭 링크 (경로를 가리키는 별도 inode) ──────────────
        Path sym = Path.of("/home/user/hello_sym.txt");
        Files.createSymbolicLink(sym, path);

        System.out.println("하드링크 대상: " + Files.isSameFile(path, link));
        System.out.println("심볼릭 대상: "  + Files.readSymbolicLink(sym));
    }
}

Java NIO.2의 Files.readAttributes()는 내부적으로 fstat() 시스템 콜을 호출합니다. BasicFileAttributes는 크기·타임스탬프, PosixFileAttributes는 권한·소유자까지 제공합니다.
FileChannel은 전통적인 InputStream보다 블록 단위로 효율적으로 I/O하며, 메모리 맵 파일(MappedByteBuffer)로 사용하면 디스크 블록을 메모리처럼 직접 접근할 수 있습니다.
Files.createLink()는 하드 링크(같은 inode)를 만들어 링크 카운트를 1 증가시킵니다. 원본 파일을 삭제해도 카운트가 0이 되지 않으면 데이터는 유지됩니다.

Q1

inode 방식에서 파일의 실제 데이터 위치를 찾는 방법은?

inode는 파일의 메타데이터와 데이터 블록 위치를 담고 있습니다.
inode는 파일의 권한, 크기 등 메타데이터와 함께 실제 데이터가 저장된 블록들의 포인터를 가지고 있어, 이를 따라가며 데이터를 찾습니다.

Q2

FAT(File Allocation Table) 방식에서 파일의 다음 블록을 찾는 방법은?

테이블 안에서 클러스터들이 사슬처럼 연결되어 있습니다.
FAT 방식은 각 클러스터 항목이 다음 클러스터 번호를 가리키는 연결 리스트 형태로, 파일의 시작 클러스터부터 체인을 따라가며 전체 파일을 찾습니다.

Q3

파일 시스템에서 디렉터리의 역할은?

디렉터리는 '이름표' 역할을 합니다.
디렉터리는 파일 이름을 해당 파일의 inode 번호(또는 FAT 시작 클러스터)와 매핑해, 이름으로 파일을 찾을 수 있게 해줍니다.