본문 바로가기

파일의 개념과 속성

파일이 디스크 블록을 추상화하는 방식을 이해하고 유형·메타데이터·연산·접근 방식·잠금의 의미를 정리합니다.

디스크에 데이터를 저장할 때 우리는 파일이라는 개념을 당연하게 사용합니다.

하지만 디스크 자체는 파일이 무엇인지 모릅니다.

디스크는 단지 번호가 매겨진 블록의 배열일 뿐입니다.

파일(File)은 운영체제가 만들어낸 추상화입니다.

이 추상화 없이는 모든 프로그램이 디스크의 물리적 주소를 직접 다뤄야 하고, 그렇게 되면 실수로 다른 프로그램의 데이터를 덮어쓰는 일이 일상이 됩니다.


파일이란 무엇인가

파일은 이름이 붙은 데이터의 연속입니다.

사용자 관점에서 파일은 디스크에 저장된 논리적 단위이며, 프로그램 코드, 이미지, 텍스트, 데이터베이스 등 무엇이든 담을 수 있습니다.

운영체제 관점에서 파일은 관련된 디스크 블록들의 모음과 그 메타데이터(이름, 크기, 위치, 권한 등)를 포함하는 추상화입니다.

사용자가 보고서.txt를 열어줘라고 하면, OS는 파일 이름을 디스크 블록 위치로 변환하여 데이터를 읽어옵니다.

이 변환 과정이 파일 시스템의 핵심 역할입니다.

파일 시스템 추상화는 세 가지 문제를 동시에 해결합니다.

첫째, 사용자에게 친숙한 이름을 부여합니다.

둘째, 여러 프로세스가 파일을 공유할 수 있는 연산과 잠금 수단을 제공합니다. 애플리케이션의 여러 연산을 자동으로 하나의 트랜잭션으로 만들지는 않습니다.

셋째, 권한을 통해 다른 사용자의 데이터를 보호합니다.


파일 유형

일반 파일의 내용 형식은 보통 애플리케이션과 도구가 해석합니다. 커널의 일반 파일·디렉터리 같은 유형과 PDF·PNG 같은 내용 형식은 다릅니다.

내용 형식을 추정하는 대표적인 단서는 확장자와 매직 넘버입니다.

확장자 기반: Windows가 주로 사용하는 방식입니다.

.txt, .pdf, .exe 같은 확장자가 파일 유형을 나타냅니다.

사용자가 확장자를 바꾸면 연결 프로그램만 바뀔 뿐, 파일 내용은 변하지 않습니다.

매직 넘버 기반: Unix 계열이 주로 사용하는 방식입니다.

파일의 처음 몇 바이트를 읽어 유형을 판별합니다.

PDF는 %PDF로, PNG는 \x89PNG로, ELF 실행 파일은 \x7fELF로 시작합니다.

확장자가 없어도 알려진 서명을 바탕으로 형식을 추정할 수 있지만, 짧거나 손상·위조된 입력의 전체 유효성까지 보장하지는 않습니다.

magic_number_check.c
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc < 2) return 1;

    FILE *fp = fopen(argv[1], "rb");
    if (!fp) { perror("fopen"); return 1; }

    unsigned char header[8];
    size_t n = fread(header, 1, sizeof(header), fp);
    fclose(fp);

    if (n >= 4 && memcmp(header, "\x7f" "ELF", 4) == 0)
        printf("ELF 실행 파일\n");
    else if (n >= 4 && memcmp(header, "%PDF", 4) == 0)
        printf("PDF 문서\n");
    else if (n >= 8 && memcmp(header, "\x89PNG\r\n\x1a\n", 8) == 0)
        printf("PNG 이미지\n");
    else if (n > 0)
        printf("알 수 없는 형식 (첫 바이트: 0x%02x)\n", header[0]);
    else
        printf("읽은 바이트 없음 (빈 파일 또는 읽기 오류)\n");

    return 0;
}

Linux의 file은 매직 데이터베이스와 다른 검사로 내용 형식을 추정합니다. 데이터베이스 설치 경로는 배포판에 따라 다릅니다. 위 C 코드는 세 가지 서명만 검사하며 파일 형식 검증기가 아닙니다.


파일 속성

파일에는 데이터 외에 다양한 메타데이터(Metadata)가 함께 저장됩니다.

속성의미와 예시
이름디렉터리 엔트리의 이름 · report.txt
유형일반 파일·디렉터리 등; PDF·PNG 내용 형식은 별도 추정
크기바이트 단위 · 1,048,576바이트 = 1MiB
위치 정보inode 등 메타데이터가 데이터 블록 매핑을 관리
권한접근 허용 범위 · rw-r--r--
시각내용 수정 mtime, 접근 atime, 상태 변경 ctime; 생성 시각과 구별
소유자사용자·그룹 · user:group
링크 수같은 파일을 가리키는 하드 링크 수
file_metadata.py
import os
import stat
import time

path = "/etc/passwd"
st = os.lstat(path)

print(f"크기:     {st.st_size} bytes")
print(f"inode:   {st.st_ino}")
print(f"링크 수: {st.st_nlink}")
print(f"소유자:  UID={st.st_uid}, GID={st.st_gid}")
print(f"권한:    {oct(stat.S_IMODE(st.st_mode))}")  # 0o644
print(f"수정:    {time.ctime(st.st_mtime)}")
print(f"접근:    {time.ctime(st.st_atime)}")

# 파일 유형 확인
if stat.S_ISREG(st.st_mode):
    print("일반 파일")
elif stat.S_ISDIR(st.st_mode):
    print("디렉토리")
elif stat.S_ISLNK(st.st_mode):
    print("심볼릭 링크")

위 코드는 Unix 경로의 메타데이터 조회 예시입니다. lstat은 링크 자체를, stat은 링크 대상을 조회합니다. 주석의 권한 값은 환경에 따라 달라지며 여기서 실행한 결과가 아닙니다.

ls -la를 실행하면 이 속성들이 한 줄에 요약됩니다.

-rw-r--r-- 1 user group 1024 Jan 15 10:30 report.txt에서 권한, 링크 수, 소유자, 그룹, 크기, 수정 시각, 이름까지 담겨 있습니다.

타임스탬프의 실무적 의미

mtime(수정 시각)은 파일 내용이 변경된 시각입니다.

make는 mtime을 비교하여 재컴파일 대상을 결정합니다.

atime(접근 시각)은 파일이 읽힌 시각입니다.

매 읽기마다 atime을 갱신하면 디스크 쓰기가 발생하므로, 성능을 위해 noatime 또는 relatime 마운트 옵션을 사용합니다.

ctime(상태 변경 시각)은 권한·소유자뿐 아니라 내용 쓰기 등으로 inode 상태가 바뀔 때도 갱신됩니다. 생성 시각이 아니며 일반적인 파일 시각 설정 API로 임의 값을 직접 지정하지 않습니다.


파일 연산

OS는 파일에 대해 다음 연산을 시스템 콜로 제공합니다.

  • 생성(Create): 새 파일을 만들고 디렉터리에 등록합니다.
  • 열기(Open): 파일 디스크립터를 할당하고, 커널 테이블에 엔트리를 만듭니다.
  • 읽기(Read): 현재 파일 포인터 위치에서 데이터를 읽습니다.
  • 쓰기(Write): 현재 파일 포인터 위치에 데이터를 씁니다.
  • 탐색(Seek): 파일 포인터를 원하는 위치로 이동합니다.
  • 삭제(Delete): 파일 이름을 제거합니다. 마지막 링크와 열린 참조 등이 사라져야 저장 공간을 회수할 수 있습니다.
  • 절단(Truncate): 파일 길이를 변경합니다. 내용 일부를 잘라내거나 확장하며 크기·시각 같은 속성도 바뀔 수 있습니다.
  • 닫기(Close): 파일 디스크립터를 해제합니다. close 성공만으로 저장장치까지 동기화되었다고 보장하지 않습니다.
file_operations.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    /* 파일 생성 및 열기 (O_CREAT | O_WRONLY | O_TRUNC) */
    int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd < 0) { perror("open"); return 1; }

    /* 쓰기 */
    const char *msg = "Hello, File System!\n";
    write(fd, msg, strlen(msg));

    /* lseek으로 처음으로 돌아가기 */
    lseek(fd, 0, SEEK_SET);
    /* 쓰기 전용이므로 읽기는 다시 열어야 함 */
    close(fd);

    /* 읽기 */
    fd = open("test.txt", O_RDONLY);
    if (fd < 0) { perror("open"); return 1; }

    char buf[100];
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    if (n < 0) { perror("read"); close(fd); return 1; }
    buf[n] = '\0';
    printf("Read: %s", buf);

    /* 파일 끝에서의 lseek: 파일 크기 확인 */
    off_t size = lseek(fd, 0, SEEK_END);
    printf("파일 크기: %ld bytes\n", (long)size);

    close(fd);

    /* 절단: 크기를 10바이트로 줄이기 */
    truncate("test.txt", 10);

    /* 삭제 (unlink) */
    /* unlink("test.txt"); */
    return 0;
}

앞의 C 예제는 연산 순서를 보여 줍니다. 짧은 쓰기·중단, lseek·truncate·close 오류의 완전한 처리는 생략되어 있으며 성공한 경로를 전제로 설명합니다.

원 성공 경로의 문자열 Hello, File System!\n은 마지막 개행을 포함해 20바이트입니다. 문자열 리터럴의 종료 NUL은 strlen이 세지 않으므로 파일 데이터에 쓰지 않습니다. 읽기용으로 다시 열어 처음부터 읽으면 같은 20바이트를 얻고, 끝으로 이동한 lseek 결과도 20입니다. 이후 truncate(..., 10)은 처음 10바이트 Hello, Fil을 남깁니다. 탐색은 열린 파일의 현재 위치를, 절단은 파일 길이를 바꾸므로 같은 변화로 읽지 않습니다.

독립 확인: 새 ASCII 데이터가 log:ABCD\n이고, 해당 데이터 전체 쓰기와 읽기·크기 조회·5바이트 절단이 모두 성공했다고 가정하세요. 개행을 포함한 최초 크기, 절단 후 크기와 남는 내용을 구하세요. 뒤에 있던 개행이나 문자열 종료 NUL이 새로 추가되나요?

풀이 확인

처음 크기는 log:의 4바이트, ABCD의 4바이트, 개행 1바이트를 합한 9바이트입니다. 절단 후 크기는 5이고 처음 5바이트 log:A만 남습니다. 기존 개행은 잘려 없어지며, 절단은 텍스트 행을 완성하는 연산이 아니므로 개행이나 종료 NUL을 추가하지 않습니다.

unlink는 이름을 지우고, 마지막 열린 참조가 사라져야 공간 회수가 가능합니다

이름과 열린 fd가 같은 파일을 유지한다. 남은 열린 참조도 제거한다. 각 단계의 값과 전환 이유를 비교합니다.

파일 연산 · 이름과 수명

unlink는 이름을 지우고, 마지막 열린 참조가 사라져야 공간 회수가 가능합니다

일반 파일 report.txt에 하드 링크 이름이 하나, 열린 fd가 하나 있고 다른 매핑·참조가 없는 예입니다. 이름을 제거하는 시점과 열린 파일의 수명이 끝나는 시점을 분리해서 봅니다.

이름과 열린 참조를 나눈 같은 파일의 세 상태같은 파일의 세 시간 상태이다. 첫 상태에는 report.txt 이름과 fd3의 열린 참조가 각각 inode를 유지한다. unlink 성공 뒤 이름 간선은 없고 fd3의 열린 참조만 남는다. 마지막 fd close 뒤 이름0·열린참조0이며 다른매핑·참조가없다는조건에서공간회수조건이충족된다. 영역안화살표는수명유지참조를줄여그린개념간선이며실제fd층의열린파일설명은별도로구분한다. 모바일영역사이화살표는unlink와close의성공한시간전환이다. 공간의즉시덮어쓰기나안전삭제를뜻하지않는다.이름 참조열린 참조열린 참조report.txt이름 존재같은 inode파일 데이터fd 3열린 참조 1이름 없음링크 수 0같은 inode파일 데이터fd 3열린 참조 1이름 없음링크 수 0유지 참조 없음회수 조건 만족fd 닫힘열린 참조 01 이름·열린 참조둘 다 존재이름 1 · 열린 참조 12 unlink 성공이름 참조 제거이름 0 · 열린 참조 13 마지막 fd close열린 참조 제거이름 0 · 열린 참조 0
이름과 열린 참조를 나눈 같은 파일의 세 상태같은 파일의 세 시간 상태이다. 첫 상태에는 report.txt 이름과 fd3의 열린 참조가 각각 inode를 유지한다. unlink 성공 뒤 이름 간선은 없고 fd3의 열린 참조만 남는다. 마지막 fd close 뒤 이름0·열린참조0이며 다른매핑·참조가없다는조건에서공간회수조건이충족된다. 영역안화살표는수명유지참조를줄여그린개념간선이며실제fd층의열린파일설명은별도로구분한다. 모바일영역사이화살표는unlink와close의성공한시간전환이다. 공간의즉시덮어쓰기나안전삭제를뜻하지않는다.이름 참조열린 참조열린 참조unlink 성공close 성공report.txt이름 존재같은 inode파일 데이터fd 3열린 참조 1이름 없음링크 수 0같은 inode파일 데이터fd 3열린 참조 1이름 없음링크 수 0유지 참조 없음회수 조건 만족fd 닫힘열린 참조 01 이름·열린 참조둘 다 존재이름 1 · 열린 참조 12 unlink 성공이름 참조 제거이름 0 · 열린 참조 13 마지막 fd close열린 참조 제거이름 0 · 열린 참조 0

영역은 같은 파일의 시간별 상태입니다. 영역 안의 화살표는 수명을 유지하는 참조이며, 이름을 지우는 동작과 열린 참조를 닫는 동작을 구분합니다. fd에서 파일로 가는 간선은 개념을 줄여 그린 것으로, 실제 열린 파일 설명 층은 파일 관리 도식에서 따로 봅니다.

  1. 이름도 열린 참조도 있음

    report.txt → 같은 파일

    하드 링크 수 1 · 열린 fd 3의 참조 1

    이름으로 열거나 기존 fd로 읽을 수 있습니다.

  2. unlink(report.txt) 성공

    이름 제거 · 하드 링크 수 0

    열린 fd 3의 참조는 남습니다. 그 fd로 파일을 계속 읽을 수 있지만, 사라진 이름으로 새로 열 수는 없습니다.

  3. 마지막 fd를 close

    하드 링크 수 0 · 열린 참조 0

    이 예에서는 파일을 유지하던 마지막 참조도 사라져 저장 공간 회수 조건이 충족됩니다.

다른 하드 링크, 열린 fd 또는 매핑이 있으면 수명이 이어질 수 있습니다. close와 공간 회수는 물리적 데이터의 즉시 덮어쓰기나 안전한 삭제를 보장하지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 두 참조부터 마지막 close까지 진행합니다.
관계도의 보충 설명
전체 도표의 표기·관계·조건
구분전체 도표의 내용
관계 설명같은 파일의 세 시간 상태이다. 첫 상태에는 report.txt 이름과 fd 3의 열린 참조가 각각 inode를 유지한다. unlink 성공 뒤 이름 간선은 없고 fd 3의 열린 참조만 남는다. 마지막 fd close 뒤 이름 0·열린 참조 0이며 다른 매핑·참조가 없다는 조건에서 공간 회수 조건이 충족된다. 영역 안 화살표는 수명 유지 참조를 줄여 그린 개념 간선이며 실제 fd 층의 열린 파일 설명은 별도로 구분한다. 모바일 영역 사이 화살표는 unlink와 close의 성공한 시간 전환이다. 공간의 즉시 덮어쓰기나 안전 삭제를 뜻하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
두 참조

이름과 열린 fd가 같은 파일을 유지한다.

  • 이름·열린 참조 1: 이름 1
  • 이름·열린 참조 2: 열린 참조 1
  • 이름 또는 fd로 접근 가능
unlink 성공

이름 참조만 제거한다.

  • 이름·열린 참조 1: 이름 0
  • 이름·열린 참조 2: 열린 참조 1
  • 이름·열린 참조 3: fd 3 읽기 가능
  • 사라진 이름으로 새 open 불가
마지막 close

남은 열린 참조도 제거한다.

  • 이름·열린 참조 1: 이름 0
  • 이름·열린 참조 2: 열린 참조 0
  • 이름·열린 참조 3: 다른 참조 없음
  • 공간 회수 조건 만족

unlink는 이름을 지우고, 마지막 열린 참조가 사라져야 공간 회수가 가능합니다

이름과 열린 fd가 같은 파일을 유지한다. 남은 열린 참조도 제거한다. 각 단계의 값과 전환 이유를 비교합니다.

파일 연산 · 이름과 수명

unlink는 이름을 지우고, 마지막 열린 참조가 사라져야 공간 회수가 가능합니다

일반 파일 report.txt에 하드 링크 이름이 하나, 열린 fd가 하나 있고 다른 매핑·참조가 없는 예입니다. 이름을 제거하는 시점과 열린 파일의 수명이 끝나는 시점을 분리해서 봅니다.

이름과 열린 참조를 나눈 같은 파일의 세 상태같은 파일의 세 시간 상태이다. 첫 상태에는 report.txt 이름과 fd3의 열린 참조가 각각 inode를 유지한다. unlink 성공 뒤 이름 간선은 없고 fd3의 열린 참조만 남는다. 마지막 fd close 뒤 이름0·열린참조0이며 다른매핑·참조가없다는조건에서공간회수조건이충족된다. 영역안화살표는수명유지참조를줄여그린개념간선이며실제fd층의열린파일설명은별도로구분한다. 모바일영역사이화살표는unlink와close의성공한시간전환이다. 공간의즉시덮어쓰기나안전삭제를뜻하지않는다.이름 참조열린 참조열린 참조report.txt이름 존재같은 inode파일 데이터fd 3열린 참조 1이름 없음링크 수 0같은 inode파일 데이터fd 3열린 참조 1이름 없음링크 수 0유지 참조 없음회수 조건 만족fd 닫힘열린 참조 01 이름·열린 참조둘 다 존재이름 1 · 열린 참조 12 unlink 성공이름 참조 제거이름 0 · 열린 참조 13 마지막 fd close열린 참조 제거이름 0 · 열린 참조 0
이름과 열린 참조를 나눈 같은 파일의 세 상태같은 파일의 세 시간 상태이다. 첫 상태에는 report.txt 이름과 fd3의 열린 참조가 각각 inode를 유지한다. unlink 성공 뒤 이름 간선은 없고 fd3의 열린 참조만 남는다. 마지막 fd close 뒤 이름0·열린참조0이며 다른매핑·참조가없다는조건에서공간회수조건이충족된다. 영역안화살표는수명유지참조를줄여그린개념간선이며실제fd층의열린파일설명은별도로구분한다. 모바일영역사이화살표는unlink와close의성공한시간전환이다. 공간의즉시덮어쓰기나안전삭제를뜻하지않는다.이름 참조열린 참조열린 참조unlink 성공close 성공report.txt이름 존재같은 inode파일 데이터fd 3열린 참조 1이름 없음링크 수 0같은 inode파일 데이터fd 3열린 참조 1이름 없음링크 수 0유지 참조 없음회수 조건 만족fd 닫힘열린 참조 01 이름·열린 참조둘 다 존재이름 1 · 열린 참조 12 unlink 성공이름 참조 제거이름 0 · 열린 참조 13 마지막 fd close열린 참조 제거이름 0 · 열린 참조 0

영역은 같은 파일의 시간별 상태입니다. 영역 안의 화살표는 수명을 유지하는 참조이며, 이름을 지우는 동작과 열린 참조를 닫는 동작을 구분합니다. fd에서 파일로 가는 간선은 개념을 줄여 그린 것으로, 실제 열린 파일 설명 층은 파일 관리 도식에서 따로 봅니다.

  1. 이름도 열린 참조도 있음

    report.txt → 같은 파일

    하드 링크 수 1 · 열린 fd 3의 참조 1

    이름으로 열거나 기존 fd로 읽을 수 있습니다.

  2. unlink(report.txt) 성공

    이름 제거 · 하드 링크 수 0

    열린 fd 3의 참조는 남습니다. 그 fd로 파일을 계속 읽을 수 있지만, 사라진 이름으로 새로 열 수는 없습니다.

  3. 마지막 fd를 close

    하드 링크 수 0 · 열린 참조 0

    이 예에서는 파일을 유지하던 마지막 참조도 사라져 저장 공간 회수 조건이 충족됩니다.

다른 하드 링크, 열린 fd 또는 매핑이 있으면 수명이 이어질 수 있습니다. close와 공간 회수는 물리적 데이터의 즉시 덮어쓰기나 안전한 삭제를 보장하지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 두 참조부터 마지막 close까지 진행합니다.

관계도의 보충 설명

전체 도표의 표기·관계·조건
구분전체 도표의 내용
관계 설명같은 파일의 세 시간 상태이다. 첫 상태에는 report.txt 이름과 fd 3의 열린 참조가 각각 inode를 유지한다. unlink 성공 뒤 이름 간선은 없고 fd 3의 열린 참조만 남는다. 마지막 fd close 뒤 이름 0·열린 참조 0이며 다른 매핑·참조가 없다는 조건에서 공간 회수 조건이 충족된다. 영역 안 화살표는 수명 유지 참조를 줄여 그린 개념 간선이며 실제 fd 층의 열린 파일 설명은 별도로 구분한다. 모바일 영역 사이 화살표는 unlink와 close의 성공한 시간 전환이다. 공간의 즉시 덮어쓰기나 안전 삭제를 뜻하지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
두 참조

이름과 열린 fd가 같은 파일을 유지한다.

  • 이름·열린 참조 1: 이름 1
  • 이름·열린 참조 2: 열린 참조 1
  • 이름 또는 fd로 접근 가능
unlink 성공

이름 참조만 제거한다.

  • 이름·열린 참조 1: 이름 0
  • 이름·열린 참조 2: 열린 참조 1
  • 이름·열린 참조 3: fd 3 읽기 가능
  • 사라진 이름으로 새 open 불가
마지막 close

남은 열린 참조도 제거한다.

  • 이름·열린 참조 1: 이름 0
  • 이름·열린 참조 2: 열린 참조 0
  • 이름·열린 참조 3: 다른 참조 없음
  • 공간 회수 조건 만족

open() 시스템 콜은 파일 디스크립터(File Descriptor)를 반환합니다.

이후의 모든 연산은 이 정수 값을 통해 파일을 참조합니다.

파일 디스크립터에 대해서는 10-4절에서 자세히 다룹니다.

독립 확인: 같은 일반 파일에 이름 두 개가 각각 하드 링크로 연결되고, fd 3으로 한 번 열려 있습니다. 다른 fd·매핑·참조는 없습니다. 첫 이름을 unlink하고 fd 3을 close한 뒤, 마지막 이름을 unlink합니다. 각 단계 뒤 이름 수와 열린 참조 수, 기존 파일을 유지하는 근거를 설명하세요.

풀이 확인

첫 unlink 뒤 이름 수는 1, 열린 참조 수는 1입니다. close 뒤 이름 수는 여전히 1이고 열린 참조 수는 0이므로 남은 이름이 파일을 유지합니다. 마지막 unlink 뒤 이름 수 0·열린 참조 수 0이 되어 이 조건에서는 공간 회수 조건을 만족합니다. fd를 먼저 닫았다는 사실만으로 남은 이름까지 제거되지는 않습니다.

열린 파일 카운트와 자원 누수

OS는 시스템 전체에서 열 수 있는 파일 수를 제한합니다.

Linux에서 ulimit -n으로 현재 셸에서 상속될 프로세스당 soft 한도를 확인합니다. 값은 설정에 따라 다르므로 1,024로 고정되지 않습니다.

파일을 열고 닫지 않으면 파일 디스크립터 누수(fd leak)가 발생하여, 결국 open()이 실패합니다.

서버 프로그램에서 흔한 버그입니다.

fd_leak_check.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    /* 의도적으로 close를 빠뜨린 코드 */
    for (int i = 0; i < 2000; i++) {
        int fd = open("/dev/null", O_RDONLY);
        if (fd < 0) {
            printf("fd 소진! 반복 %d에서 open 실패\n", i);
            perror("open");
            return 1;
        }
        /* close(fd); ← 이 줄이 없으면 fd 누수 */
    }
    printf("2000번 열기 성공 (실제로는 닫아야 함)\n");
    return 0;
}

접근 방법

파일 데이터에 접근하는 방식은 크게 두 가지입니다.

순차 접근(Sequential Access): 처음부터 끝까지 순서대로 읽거나 씁니다.

테이프 시대의 접근 방식이며, 로그 파일이나 스트리밍 데이터가 이에 해당합니다.

read() 호출 후 파일 포인터가 자동으로 전진합니다.

직접 접근(Direct/Random Access): 파일의 임의 위치로 바로 이동하여 읽거나 씁니다.

디스크의 물리적 특성이 이를 가능하게 합니다.

lseek()로 원하는 위치로 이동한 후 read() 또는 write()를 호출합니다.

데이터베이스가 대표적입니다 — 특정 레코드에 바로 접근해야 합니다.


파일 잠금

여러 프로세스가 동시에 같은 파일을 수정하면 데이터가 손상될 수 있습니다.

파일 잠금(File Locking)은 이를 방지합니다.

권고적 잠금(Advisory Lock)은 프로세스가 자발적으로 잠금을 확인합니다.

잠금을 무시하고 접근하는 것이 기술적으로 가능합니다.

BSD 계열의 flock()은 Linux의 일반 로컬 파일 시스템에서 이 방식으로 동작합니다.

강제적 잠금(Mandatory Lock)은 OS가 강제로 접근을 차단합니다.

잠금된 파일에 대한 읽기·쓰기 자체를 제한하는 개념입니다. Linux의 과거 일반 파일 mandatory locking은 5.15 이후 지원하지 않으며, SMB 등 원격 파일 시스템의 정책은 별도로 확인합니다.

file_locking.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>

int main() {
    int fd = open("shared.dat", O_RDWR | O_CREAT, 0644);
    if (fd < 0) { perror("open"); return 1; }

    /* 배타적 잠금 획득 (같은 잠금 규칙을 따르는 접근 조정) */
    printf("잠금 대기 중...\n");
    if (flock(fd, LOCK_EX) < 0) {
        perror("flock"); close(fd); return 1;
    }
    printf("잠금 획득! 5초간 작업 수행\n");

    /* 임계 영역: 파일 읽기/수정 */
    write(fd, "exclusive data\n", 15);
    sleep(5);

    /* 잠금 해제 */
    flock(fd, LOCK_UN);
    printf("잠금 해제\n");

    close(fd);
    return 0;
}

fcntl() 기반 레코드 잠금은 파일의 특정 영역만 잠글 수 있습니다.

데이터베이스처럼 하나의 파일 안에 여러 레코드가 있을 때, 전체 파일이 아닌 수정할 레코드만 잠그면 병행성이 크게 향상됩니다.

Linux 로컬 파일 시스템의 flock과 전통적인 fcntl 레코드 잠금은 별개입니다. NFS·SMB에서는 전체 파일의 바이트 범위 잠금으로 구현되어 서로 상호작용할 수 있으므로 프로토콜·커널·마운트 옵션을 확인합니다. 원격 파일이라고 항상 별도 분산 잠금이 필요한 것은 아닙니다.

다음 절에서는 파일들을 체계적으로 정리하는 디렉터리 구조를 살펴보겠습니다.