File & Directory (파일 & 디렉토리)

Dong-Hyeon Park·2025년 3월 25일

Operating System

목록 보기
20/20
post-thumbnail

본 글의 내용은 Operating Systems: Three Easy Pieces의 File & Directory 챕터를 정리한 것입니다.

☑️ 개요

  • 운영 체제의 발전은 CPU 가상화인 프로세스, 메모리 가상화인 주소 공간을 통해 발전해왔다.

  • 이것에 영구 저장장치라는 중요한 요소가 추가된다. HDD나 SSD와 같은 장치가 데이터를 영구적으로 저장한다.

  • 전원이 끊기면 내용이 휘발되는 메모리와 달리 영구 저장 장치는 데이터를 유지한다.

☑️ 파일 및 디렉토리

  • 저장 공간 추상화의 첫번째 요소는 파일이다.

  • 파일은 단순히 바이트의 선형적인 배열로, 각 바이트를 읽고 쓸 수 있다.

  • 또한 숫자로 된 low-level의 이름을 가지는데, 그것이 inode number(i-number)이다.

  • 대부분의 OS는 파일의 구조에 대해서 잘 모르며, 단순히 데이터를 디스크에 저장하고, 로드하는 것을 한다.

  • 저장 공간 추상화의 두번째 요소는 디렉토리로, 파일과 동일하게 inode number를 가지지만, 유저 친화적인 이름과 low-level 이름의 쌍을 보관한다.

  • 예시로 파일 foo 가 있고, i-number가 10이라면 (foo, 10) 이 저장되는 것이다.

  • 디렉토리의 각 항목은 다른 디렉토리가 될 수도 있다. 디렉토리가 다른 디렉토리에 포함되면서 디렉토리 트리(혹은 디렉토리 계층)가 구성된다.

  • 디렉토리는 루트 디렉토리(UNIX에서는 / )를 시작으로 일종의 구분 기호를 통해 하위 디렉토리의 이름이 지정된다.

  • 위 그림의 /foo/bar.txt/bar/foo/bar.txt 처럼 다른 위치에 있는 한 같은 이름을 가질 수 있다.

  • 파일의 이름은 보통 마침표로 구분되는데, 마침표의 앞 부분은 이름, 뒷 부분은 파일의 유형이다. 다만 이것은 관례에 불과하며, 뒷 부분이 파일의 실제 유형을 강제할 수 없다.

  • 결론적으로 모든 파일에 이름을 지정 가능한 파일 시스템의 일관성이 훌륭함을 알 수 있다. 파일 시스템이 디스크, USB, CD-ROM, 기타 장치 등의 파일에 접근할 수 있는 통합된 방법을 제공한다.

☑️ 파일 생성

  • 파일 생성은 open() 에 여러 파라미터를 전달하는 것으로 가능하다.

  • 위 예시에는 O_CREAT , O_WRONLY 등의 플래그가 전달된다.

    • O_CREAT : 파일이 존재하지 않으면 생성

    • O_WRONLY : 파일 쓰기만 가능

    • O_TRUNC : 파일이 이미 존재하면 기존 컨텐츠 제거

    • S_IRUSR|S_IWUSR : 권한 지정 (파일을 읽고 쓰도록)

  • open() 의 반환 값은 파일 디스크립터(file descriptor)가 되는데, 이것은 프로세스 별로 private한 정수 값일 뿐이다. UNIX 시스템에서 파일에 접근하는 데 사용된다. 파일을 읽고 쓸 수 있는 권한이 있다면 이것으로 파일을 수정할 수 있다.

  • 즉, 파일 디스크립터는 어떤 작업을 수행하게 해줄 핸들이라고 할 수 있다.

  • 이런 파일 디스크립터는 운영 체제에 의해 관리되며, 프로세스에 의해 open file table의 인덱스로 사용된다. 즉, 파일 정보를 추적하는 데 사용되는 파일에 대한 포인터라고 할 수 있다.

☑️ 파일 읽기 및 쓰기

  • 위 코드에서는 echo 의 출력을 foo 파일로 리다이렉션하여, 파일에 hello 라는 단어가 기록된다.

  • 이후 cat 으로 파일의 내용을 확인하는데, 어떤 방식이 활용될까?

  • 리눅스에서는 strace 같은 추적 도구로 그 과정을 확인할 수 있다.

  • 첫째로 파일이 읽기 전용(O_RDONLY)으로 열리고, 64비트 오프셋(O_LARGEFILE)이 사용된다.

  • 그리고 open 함수에 의해 파일 디스크립터 3이 반환된다. (프로세스는 보통 표준 입력, 출력, 오류 전용 파일이 이미 열려있어 0, 1, 2가 선점되어 있다)

  • read 로 읽기가 시작될 때, 파라미터로 파일 디스크립터, 읽을 문자열, 버퍼 크기가 전달된다. 반환 값은 읽은 길이이다.

  • 이제 write 로 표준 출력에 목표 문자열을 기록한다. 이후 read 의 반환 값이 0이므로 close 로 마무리 된다.

💡 Open file table

각 프로세스는 파일 디스크립터 배열을 보관하고 있다. 그리고 파일 디스크립터는 open file table의 항목(entry)을 참조한다. 이 항목에는 디스크립터가 참조하는 파일, 오프셋, 읽기/쓰기 가능 여부 등의 세부 정보를 제공한다.

☑️ 순차적이지 않은 읽기 및 쓰기

  • 때로는 파일 내의 특정 오프셋부터 읽거나 써야할 때가 많다. (문서에서 목차를 사용하는 경우 등) 이럴 때 lseek 을 사용할 수 있다.

  • 마지막 인자 whence 는 시작 위치를 어떤 방식으로 설정할 지 결정한다. SEEK_SET 이면 offset 이 시작 위치가 되며, SEEK_CUR 라면 현재 위치에 offset 을 더한다. SEEK_END 면 파일 크기에 offset 이 더해진 위치로 설정된다.

  • readwrite암시적으로 시작 위치를 업데이트 하며, lseek명시적으로 시작 위치를 업데이트 하는 것이다.

  • 오프셋 값은 보통 file 구조체에 기록된다.

  • xv6 커널에서는 이런 파일 구조체들의 배열이 하나의 lock과 함께 open file table로 사용되기도 한다.

  • 이제 read 와 비교해보면, read 는 반복적으로 호출됐을 때 오프셋은 읽은 만큼 늘어나게 된다.

  • 동일한 파일을 두 번 열고 각각 read 를 호출하는 경우는 위와 같다.

  • lseek 을 사용한 경우는 위와 같다.

☑️ 공유 파일 테이블 목록: fork() & dup()

  • 다른 프로세스가 같은 파일을 읽더라도, open file table에는 유니크한 항목이 생긴다.

  • 그러나 부모 프로세스가 fork 를 사용하여 자식을 만든 경우, open file table을 공유하게 된다. 몇개의 프로세스가 파일을 참조하고 있는지도 기록된다.

  • dup 을 사용하면 파일 디스크립터를 복사할 수 있고, 특정 파일 디스크립터가 바라보는 파일을 변경할 수도 있다.

☑️ fsync() 로 즉시 쓰기

  • 프로그램에서 write() 를 호출하는 것은, 사실 미래의 어느 시점에 디스크에 기록해달라고 요청하는 것이다. (성능 상의 이유로 디스크에 기록하는 것을 메모리에 버퍼링 했다가 처리한다)

  • 그래서 데이터가 유실될 수도 있고, DBMS 같은 경우 수시로 디스크에 강제 쓰기를 하는 기능이 필요하다.

  • UNIX에서는 fsync() 를 호출하면 모든 더티 데이터(디스크에 기록 안 된 데이터)를 강제로 디스크에 기록한다.

  • 경우에 따라서 파일을 관리하는 디렉토리에 fsync() 를 호출해야 될 때도 있다.

☑️ 파일명 변경

  • 터미널에서 mv 명령어로 가능한 파일명 변경은, 원자적으로 처리된다.

  • 즉, 변경되기 전 상태와 변경된 후 두가지 경우만 존재할 수 있다.

☑️ 파일 정보 얻기

  • 파일 시스템은 저장하고 있는 각 파일에 대한 정보, 메타데이터를 보관한다.

  • 특정 파일의 메타데이터는 stat 혹은 fstat 시스템 콜로 확인할 수 있다.

  • 메타데이터에는 파일 크기, 로우 레벨 식별자(inode number), 소유권 정보 등이 기록되어 있다.

☑️ 파일 제거

  • UNIX에서 rm 명령어로 가능한 파일 제거는 내부적으로 어떤 시스템 콜이 사용될까?

  • strace 같은 도구로 확인하면 unlink 가 호출되는 것을 확인할 수 있다. 이 시스템 콜이 호출되는 이유를 파악하려면 디렉토리 또한 이해해야 된다.

☑️ 디렉토리 생성

  • 디렉토리는 파일 시스템에게 메타데이터로 취급되며, 직접 관리되기 때문에 디렉토리 하위에 다른 파일/디렉토리를 추가 하는 등의 작업이 아니면 직접적으로 수정할 수 없다.

  • mkdir 시스템 콜로 디렉토리가 생성되는데, 빈 디렉토리라도 내부에 두 항목은 존재한다. 바로 ... 인데, . 는 자기 자신, .. 는 부모 디렉토리를 의미한다.

☑️ 디렉토리 읽기

  • 터미널 명령어 ls 의 내부 구조를 확인하면 디렉토리 읽기가 얼마나 단순한지 알 수 있다.

  • opendir() , readdir() , closedir() 세 가지 호출을 사용하여 작업을 수행한다.

  • 디렉토리 구조체는 위와 같이 적은 수의 정보를 보관한다.

☑️ 디렉토리 삭제

  • 디렉토리 삭제는 rmdir() 을 통해 가능한데, 하위의 다른 많은 것들도 제거할 수 있기 때문에 디렉토리가 비어있어야만 제대로 동작한다.

☑️ 하드 링크

  • 하드 링크를 통해 파일 제거에 unlink() 가 수행되는 이유에 대해 이해할 수 있다.

  • link()하드 링크를 만들 수 있는데, 이렇게 만들어진 파일은 같은 inode를 참조하게 된다.

  • unlink() 를 수행하는 이유를 여기서 찾을 수 있다. 파일이 생성될 때 파일의 크기, 디스크의 블록 위치 등을 추적하는 inode 구조체가 생성되고, 파일에 인간 친화적인 이름을 붙인 뒤 디렉토리에 파일 링크가 추가된다.

  • 이것을 확인하기 위해 하드 링크를 만들고, unlink() 를 사용해보자. 여러 개의 하드 링크 중 하나가 unlink() 를 호출해도 여전히 다른 하드 링크들은 실제 파일에 접근할 수 있다.

  • 파일 시스템은 inode 구조체에 참조 횟수를 기록하기 때문에, 참조 수가 0이 될 때만 실제 데이터 블록도 해제한다. (실제로 삭제한다)

☑️ 심볼릭 링크 (소프트 링크)

  • 하드 링크는 디렉토리나 다른 디스크 파티션을 대상으로 만들 수 없다.

  • 이를 위해 심볼릭 링크를 사용하며, 아래와 같은 명령어로 생성 가능하다. 생성된 링크를 참조하면 원본 파일의 내용이 출력된다.

  • 그러나 하드 링크와 근본적으로는 다른데, 심볼릭 링크는 그 자체로 다른 유형의 파일이다. 파일 시스템이 파일, 디렉토리와 별개로 인지하는 유형인 것이다. stat 명령어로 다음과 같은 출력을 볼 수 있다.

  • 심볼릭 링크는 파일의 경로를 데이터로 저장하기 때문에, 경로가 길면 심볼릭 링크의 크기는 커진다.

  • 심볼릭 링크는 dangling reference가 발생할 수 있는데, 이것은 심볼릭 링크가 가리키는 파일이 제거됐을 때 심볼릭 링크가 더 이상 존재하지 않는 경로명을 가리키는 것을 뜻한다.

☑️ 권한 비트 및 접근 제어

  • 파일 시스템은 CPU/메모리 가상화와 유사하게, 사용자에게 리소스를 공유할 수 있게 만들어준다.

  • 그러나 파일이 비공개가 아니라는 점에서 CPU/메모리 가상화와는 다르다고 할 수 있다.

  • 파일 공유의 메커니즘의 첫번째 요소는 권한 비트라고 할 수 있는데, UNIX 시스템에서 파일 정보를 확인하면 보통 다음과 같다.

  • -rw-r--r-- 로 나타난 첫번째 정보에 권한 비트가 담겨져 있다. 첫번째 문자를 제외한 9개의 문자가 파일(및 디렉토리)에 누가 접근할 수 있는지 결정한다.

  • 이 비트는 소유자/그룹/모든 사람이 할 수 있는 작업에 대한 권한을 결정하며, 권한에는 읽기/쓰기/실행이 포함된다.

  • 위 비트를 예시로 들면 첫 세 비트 rw- 로 사용자가 읽기/쓰기 권한이 있는 것을 알 수 있고, 다음의 r-- 로 그룹이 읽을 수 있음을 알 수 있으며, 다음 r-- 로 모두가 읽을 수 있음을 알 수 있다.

  • 이 비트는 소유자가 chmod 명령어로 변경 가능하고, 전달 인자로 숫자를 사용한다. (이진수이므로 읽기가 4, 쓰기가 2, 실행이 1이다. 이 값들을 더해서 전달하면 된다)

  • 디렉토리의 실행 비트는 파일과 조금 다른데, 실행 권한이 허가된다면 디렉토리 내에 파일을 생성하는 등의 작업을 할 수 있다.

  • 권한 비트 외에도 특정 시스템에서는 디렉토리 별 접근 제어 목록(ACL)을 사용하기도 한다. 이것을 통해 파일을 읽고 읽을 수 없는 사람에 대해 매우 구체적으로 정의할 수 있다. 이런 권한은 권한을 허가받은 사용자들이 변경할 수 있다.

☑️ 파일 시스템 생성 및 마운트

  • 파일 시스템을 만들기위해 대부분의 파일 시스템은 생성 도구(보통 mkfs )를 제공한다.

  • 이것에 디스크 파티션(예: /dev/sdal )과 파일 시스템 유형(ext3 등)을 전달하면 해당 파티션에 루트 디렉토리로 시작하는 빈 파일 시스템을 생성한다.

  • 이렇게 생성된 파일 시스템은 정형화된 파일 시스템 트리에서 접근 가능하도록 수정되어야 하고, 이게 마운트를 통해 이루어진다.

  • 마운트란, 간단하게 존재하는 디렉토리를 마운트 대상 지점으로 삼고, 그곳에 새로운 파일 시스템을 붙여넣는 것이다. (예: 파티션 dev/sda1 에 생성한 파일 시스템을 디렉토리 home/users 에 붙여넣는다)

  • 마운트의 장점은 여러 가지 파일 시스템을 하나의 파일 시스템 트리에 통합할 수 있다는 것이며, ext3 (표준 파일 시스템), proc (프로세스에 접근하는 파일 시스템), tmpfs (임시 파일 전용 시스템) 등 다양한 파일 시스템이 한 컴퓨터의 파일 시스템 트리에 통합된다.

💡 파일 시스템 용어

파일 - 생성/읽기/쓰기/제거가 가능한 바이트 배열. 각자 고유 번호를 가지며, 보통 inode number가 그것이다.

디렉토리 - 튜플 모음. 튜플에는 인간 친화적인 이름과 고유 값이 쌍으로 저장된다. 이 데이터 쌍들은 파일 혹은 디렉토리에 대응하며, 디렉토리는 특수하게 자기 자신과 부모를 가리키는 . , .. 튜플도 저장한다.

디렉토리 (계층) 트리 - 모든 파일 및 디렉토리는 루트부터 시작하여 큰 트리로 구성됨

파일 디스크립터 - 시스템 콜을 통해 OS에게 파일 접근 권한을 부여받아 파일 읽기 및 쓰기에 사용할 수 있는 존재. open file table의 항목(entry)을 참조하는 프로세스 별 비공개 객체이다. open file table의 항목은 현재 읽는 오프셋, 대상 파일 등에 대한 정보를 저장한다.

오프셋 변경 - read() , write() 는 오프셋을 자연스럽게 업데이트하고, 명시적으로 lseek() 을 사용해 변경할 수도 있다.

디스크에 기록 - fsync() 를 호출하면 디스크에 데이터를 강제로 갱신할 수 있다. 그러나 성능과 관련이 크기때문에 신중하게 사용해야 한다.

하드 링크, 심볼릭 링크 - 동일한 파일에 접근할 수 있는 여러 개의 링크를 만들 수 있는데, 마지막 하드 링크가 제거 되면 실제로 파일이 제거되며, 심볼릭 링크는 참조하는 파일이 사라지면 dangling reference가 발생한다.

권한 비트 - 파일에는 접근 권한을 설정할 수 있으며, 이것은 권한 비트로 설정 가능하다. 특정 시스템에서는 보다 정교한 접근 제어 목록(ACL)을 사용한다.

✅ 요약

  • 다른 가상화처럼 파일 시스템은 데이터의 공유를 돕는다.

  • 파일은 단순 바이트의 선형 배열로, 인간 친화적인 이름과 low-level name(inode number)를 갖는다.

  • 파일 접근에 파일 디스크립터가 사용되며, 이것은 프로세스의 open file table의 항목(entry)을 참조한다.

  • open file table의 항목에는 참조하는 파일, 오프셋, 접근 권한 등의 정보가 저장된다.

  • 파일 읽기 함수는 암시적으로 offset을 바꾸지만, lseek() 과 같이 명시적으로도 변경할 수 있다.

  • fork() 로 생성한 자식 프로세스는 부모와 동일한 open file table을 사용한다.

  • 데이터가 변경될 때 디스크에 바로 기록되는 것은 아니다. 그러나 fsync() 로 강제 갱신할 수 있다.

  • 파일 제거는 내부적으로 unlink 시스템 콜이 호출되며, 실제 파일을 참조하는 링크가 0개가 되는 순간 실제로 디스크에서 제거된다.

  • 하드 링크심볼릭 링크로 파일을 가리킬 수 있으며, 하드 링크는 동일한 inode를 참조하며, 심볼릭 링크는 파일의 경로만을 참조한다.

  • 권한 비트로 파일 접근 권한을 설정할 수 있다. 특정 시스템에서는 접근 제어 목록(ACL)으로 정교한 접근 권한을 설정한다.

  • 디스크 파티션을 파일 시스템 트리에 연결하는 것을 마운트라고 한다.

profile
Android 4 Life

0개의 댓글