File Systems Implementation

🔊 이화여자대학교 반효경 교수님의 KOCW 2014년 1학기 운영체제 강의를 들으며 정리한 노트입니다.
      캡쳐한 이미지 중 따로 출처 명시를 하지 않은 이미지 또한 반효경 교수님 강의 자료에 있음을 밝힙니다.

Allocation of File Data in Disk

디스크에다가 파일의 데이터를 저장하는 방법은 크게 3가지 방법으로 나눠볼 수 있다.

  • Contiguous Allocation

    연속 할당을 하는 방법

  • Linked Allocation

    링크를 둔 연결 할당 방법

  • Indexed Allocation

    인덱스를 두는 할당 방법

파일이라는 것은 크기가 균일하지 않고, 각각 여러 다른 크기를 가지고 있다.

image

디스크에다가 파일을 저장할 때는 보통, 동일한 크기의 섹터 단위로 나누어서 저장을 하고 있다.

파일 시스템이라던지, 디스크 외부에서 볼 때는 각각의 동일한 크기의 저장 단위를 ‘논리적인 블럭’ 이라고 부른다. 그리고, 실제로 디스크 내부에서는 각각의 섹터 단위로 데이터를 저장하고 있다.

즉, 임의의 크기의 파일을 동일 크기의 블럭 단위로 나누어서 저장을 하고 있음. (메모리 관리에서 페이징 기법하고 조금 유사)

Contiguous Allocation

연속 할당 방법, 하나의 파일이 디스크 상에 연속해서 저장이 되는 방법

image

예를 들어서,

  • 블럭 2개로 구성되는 파일은 (위 그림 0, 1번과 같이) 1번째 블럭 하고, 2번째 블럭이 인접한 블럭 번호를 가지게 저장이 되고,
  • 블럭의 크기가 6개라면, 19번부터 들어간다고 가정했을 때 24번 블럭까지 연속해서 들어가도록 하는 것이 ‘연속 할당’ 이다.

전 챕터에서 디렉토리라는 파일은 그 디렉토리 밑에 있는 파일들의 메타데이터를 내용으로 한다고 말했었다.

그래서, 위 그림을 보면 어떤 디렉토리가 있는데,

  • 그 디렉토리에 파일이 5개가 있으며,
  • 그리고 디렉토리에는 해당 파일의 메타데이터를 담고 있기 때문에, 파일의 이름이라던지 그 파일의 위치 정보start 등을 디렉토리가 가지고 있다.

  • count라는 파일은.. 연속 할당으로 저장을 한다고 가정하면,
    start0번 위치부터 length길이가 2 이므로, 0번과 1번 두 개의 블럭에 걸쳐서 저장이 되는 것이다.
  • 단점

    • external fragmentation

      위 그림을 보면, 각각의 파일들의 길이가 균일하지 않기 때문에, 중간 중간에 내용이 들어있지 않은 free블럭들(색칠 안되어 있는 애들)이 있게 된다. 따라서, 비어 있는 공간임에도 불구하고, 활용될 수 없는 경우가 생긴다. 그러므로, 외부 조각이 생길 수가 있다.

    • File grow가 어려움

      파일은 크기가 중간 중간에 바뀔 수가 있다.(수정을 하면)
      “File grow가 어렵다”는 얘기는 파일의 크기가 커질 수 있는 것에 제약이 있다는 말이다. 연속 할당이라는 규칙 하에 뒤에 빈 블럭의 수가 필요한 것보다 적은 경우가 생길 수 있기 때문.

      • file 생성시 얼마나 큰 hole을 배당할 것인가?

        이 단점을 극복하기 위해 예를 들어, 지금은 길이가 3이지만, 길이가 5까지 커질걸 대비해서 2개를 미리 할당해놓는다고 해도, 미리 할당된 크기 만큼만 파일이 더 커질 수 있지 더 이상 커지기는 어려움.

      • grow 가능 vs 낭비 (internal fragmentation)

        그리고, 미리 할당을 해놓으면, 당장 사용되지는 않는 공간이지만, 파일이 커질 경우를 대비해서 할당해놓는 공간이기 때문에, ‘내부 조각(internal fragmentation)’ 이 발생하게 된다. (공간이 낭비 되는 것임)

        • 외부 조각
          • 아무도 사용하지 않는 것이기 때문에 누군가에게 할당될 수 있는 공간을 의미
        • 내부 조각
          • 누군가한테 할당이 됐는데, 아직 사용이 되지 않는 공간
  • 장점

    • Fast I/O

      빠른 I/O가 가능하다.
      특히, 하드디스크 같은 매체는 대부분의 접근 시간이 디스크 헤드가 바깥쪽 트랙에서 안쪽 트랙으로 이동하는 시간이다. 디스크인 경우는 실제로 데이터를 읽거나 쓰는 크기는 별로 상관이 없다.

      • 한번의 seek/rotation으로 많은 바이트 transfer

        그래서, 이 방법은 어떤 파일을 통째로 읽고 싶다고 하면, 한 번 seek를 해가지고(디스크 헤드가 한 번 이동을 해가지고), 많은 양의 데이터를 한꺼번에 받아올 수가 있다.

        즉, 그림과 같이 길이가 6인 파일이 19번부터 있다고 하면, 19번 위치까지만 seek를 하면, 그 다음부터 나머지의 데이터는 더 이상 seek가 필요없이 읽어올 수 있다. (6개의 블럭들이 같은 트랙 상에 존재한다는 가정 하에… 대부분의 경우에 트랙 안에 여러개의 섹터들이 들어갈 수 있기 때문에 대부분의 경우에는 한번 seek해서 많은 데이터를 읽고 쓰는게 가능, 빠른 I/O를 위해서 효과적)

      • Realtime file 용으로, 또는 이미 run 중이던 process의 swapping 용

        그래서, 이러한 연속 할당은 파일 시스템 용도말고, 또다른 용도로 디스크를 사용하는 방법인 프로세스의 swap area용도로 씀. swap area는 파일을 저장하는게 아니라 프로세스의 주소 공간 중에 일부를 물리적인 메모리에서 쫓아내고, 나중에 필요할 때 올려놓고… 이런 용도로 사용함.

        파일 시스템이라는 것은 영속적인 공간… 전원이 나가더라도 내용이 유지되어야 하는데, swap area는 그런게 아니다.(프로세스가 끝나면 의미가 없는 정보이기 때문에… 말하자면, 임시로 저장해놓고, 프로세스의 주소 공간… 굉장히 많은 대용량의 크기를 빠르게 디스크로 쫓아냈다가 또 필요하면 빨리 메모리로 올려야되는 것)
        그렇기 때문에, 이러한 swapping 용도는 공간 효율성보다는 속도 효율성이 더 중요한 데이터에 속한다. 디스크를 많이 차지하고 있더라도 어차피 금방 지워질 데이터이기 때문에, 빠른 I/O를 위해서 연속 할당을 해주는 것이 효과적이다.

        또, 파일 중에서는 Realtime 용으로 사용하는 파일이 있을 수 있다. Realtime 이라는 것은 데드라인이 있고, 빠른 I/O가 필요

    • Direct access(=random access) 가능

      직접 접근이 가능

      image

      예를 들면, mail이라는 데이터 파일은 길이가 6인데, 이 파일에서 “앞에서부터 5번째 블럭을 보고 싶다” 그러면, 앞에 4개 블럭을 다 접근을 해야지만 5번째 블럭을 볼 수 있는게 아니다.

      연속 할당인 경우에는 mail이라는 파일이 19번 블럭에서부터 연속적으로 6개가 저장이 되어있는 것이기 때문에, “앞에서부터 5번째 블럭이 보고 싶다” 그러면, 19번에다가 숫자 4를 더해주면, 중간 위치에 있는 블럭의 위치를 미리 알 수 있으므로, 바로 5번째 위치인 23번 블럭을 접근할 수 있다.

      Linked Allocation에서는 하드디스크임에도 불구하고, 직접 접근이 불가능하다.

Linked Allocation

Linked Allocation은 파일의 데이터를 디스크에다가 연속적으로 배치하지 않고, 빈 위치면 아무데나 들어갈 수 있게 배치를 하는 것이다.

image

예를 들어, jeep이라는 파일이 있다고 가정하면,

  • 이 파일의 첫번째 블럭start은 (디스크의 논리적 블럭 중에서) 9번 블럭에 있고, 9번의 끝에 가면 두번째 블럭이 어디에 있다는 것을 적어놓는다.
  • 그림에 따르면 두번째 블럭은 16번에 있다.
  • 그러면, 16번에 가면 이 파일의 두번째 블럭 내용이 있고, 더 길다면 세번째 블럭이 어디에 있는지를 16번에다가 적어놓는다.
  • 세번째 블럭은 1번에 있다는 것을 알 수 있다.
  • 네번째 블럭은 10번,
  • 다섯번째 블럭은 25번에 있고,
  • 그 다음에 “더이상 이 파일의 내용이 없다”면, 끝났다는 표시-1를 해둔다.

즉, Linked Allocation은 어떤 파일의 시작 위치만 디렉토리가 가지고 있고, 그 다음 위치, 그 다음 위치는 실제로 그 위치에 가보면 (만약 파일의 길이가 더 길다면) 그 다음 위치를 기록해놓는 방법이다.

  • 장점

    • External fragmentation 발생 안 함

      디스크에서 비어있는 블럭이면 어디든지 아무 내용이나 들어갈 수 있게 때문에, 외부 조각 발생 안 함

  • 단점

    • No random access

      예를 들어서, 앞에서부터 그림과 같은 “jeep파일에서 네번째 블럭을 보겠다”고 하면, 디렉토리에는 첫번째 블럭 위치만 가지고 있기 때문에, 네번째 블럭을 보려면 첫번째 블럭을 먼저 가봐야한다. 가보면, 두번째 위치가 어딘지 알려주고… 이런식으로 순차적으로 세번째 → 네번째…

      Linked Allocation에서는 중간 위치를 보려면, 앞에 부분을 다 탐색을 해서 내용을 봐야지만 중간 위치를 볼 수가 있다. (건너뛰는게 불가능)

      디스크라는 매체는 직접 접근이 가능한 매체이지만, 여기다가 파일을 관리하는 방법을 Linked Allocation을 쓰게 되면 직접 접근이 안되고, 중간 중간에 내용을 다 접근해야지만 원하는 위치를 갈 수가 있기 때문에 “직접 접근 불가능”.

    • Reliability 문제 (릴라이어빌리티)

      • 한 sector가 고장나 pointer가 유실되면 많은 부분을 잃음

        디스크의 섹터들이 간혹 bad sector가 나거나 할 수 있는데, 예를 들어, 여기서 파일을 구성하는 섹터의 개수가 1천개 된다고 가정 했을 때, 중간에 하나가 bad sector가 나면 그 다음 위치는 모조리 다 접근이 불가능한 상태가 된다.

        그래서, 하나의 섹터가 bad sector가 나버리면, 포인터가 상당히 많이 유실돼서 그 뒷 부분을 완전히 다 놓치는 Reliability와 관련된 문제가 생길 수 있다.

    • Pointer를 위한 공간이 block의 일부가 되어 공간 효율성을 떨어뜨림

      • 512 bytes/sector, 4 bytes/pointer

        보통 디스크에서 하나의 섹터는 512바이트로 구성이 된다.

        그리고, 디스크 바깥쪽 (컴퓨터에서 디스크를 접근할 때) 디스크에 어떤 데이터를 저장하라는 단위가 512바이트의 배수로 구성이 된다.

        근데, 512바이트의 배수를 디스크에다가 저장하라고 요청을 하는데,
        하나의 섹터의 512바이트에서 다음 위치를 가리키는 포인터를 위해서 4바이트가 소요가 된다면, 실제로 데이터를 저장할 수 있는 위치는 512 - 4바이트가 된다.

        보통은 512바이트 단위로 저장을 하라고 요청을 하기 때문에, 그러면, 한 섹터에 들어갈 내용이 다음 위치를 가리키는 포인터 저장때문에, 두 섹터에 저장이 되는 상당히 비효율적인 문제가 생길 수 있다는 것이다.

  • 변형

    • File-allocation table (FAT) 파일 시스템

      Linked Allocation을 약간 변형해가지고, 굉장히 효율적인 구현을 하고 있다.

      • 포인터를 별도의 위치에 보관하여 reliability와 공간효율성 문제 해결

Indexed Allocation

Indexed Allocation에서는 직접 접근이 가능하게 하기 위해서,

image

디렉토리에 파일의 위치 정보를 바로 저장하는게 아니라, 먼저 인덱스를 갖다가 가리키게 해놓는다. 디렉토리가 가지고 있는 블럭이 인덱스 블럭이기 때문에, 인덱스 블럭이라는 것은 실제 이 파일의 내용을 담고 있는게 아니라, 이 파일이 어디어디에 저장되어있다는 위치정보를 블럭 하나에다가 쭉 열거해놓는 방식.

그림의 jeep이라는 파일은,

  • 총 5개 블럭으로 구성이 되고,
  • 첫번째 블럭, 9번
  • 두번째 블럭, 16번…
  • 다섯번째 블럭, 25

인덱스 블럭 안에다가 내용으로 적어놓는 것이다.

만약 “jeep 파일에서 앞에서부터 4번째 블럭을 보고싶다” 면, 인덱스 블럭만 살펴보면 앞에서부터 네번째 블럭은 10번이라는 것을 알고, 네번째 블럭으로 바로 접근 할 수 있다.

즉, 중간 위치를 보기 위해서 앞에서부터 다 따라가야되는게 아니라, 바로 건너뛰어서 직접 접근이 가능하다는게 인덱스 블럭의 장점이면서, 위에 순차 접근에서 생겼던 중간 중간에 홀이 생기는 문제도 해결. 비어있는 위치라면 어디든 활용이 가능.

  • 장점

    • External fragmentation이 발생하지 않음
    • Direct access 가능
  • 단점

    • Small file의 경우 공간 낭비 (실제로 많은 file들이 small)

      아무리 작은 파일이라 하더라도 ‘인덱스를 위한 블럭’ + ‘실제 데이터를 저장하기 위한 블럭’ 총 2개 필요함.

      그래서, 파일이 굉장히 작은 경우 공간 낭비가 있다는 것이다.

    • Too Large file의 경우 하나의 block으로 index를 저장하기에 부족

      굉장히 큰 파일의 경우에, 하나의 인덱스 블럭으로 그 파일의 블럭 위치들을 다 표현을 못한다. 블럭 하나가 512바이트 - 4바이트 포인터라고 하면 들어갈 수 있는 개수가 한정돼있다.

      그래서, 굉장히 큰 파일의 경우에는 인덱스 블럭 하나로 표시를 할 수가 없기 때문에…

      • 해결 방안

        1. linked scheme

          링크드 스킴은 인덱스 블럭에 “실제 이 파일의 위치가 어딘지를 쭉 적다가 끝까지 갔는데, 파일의 크기를 다 커버 못하겠다” 그러면, 마지막에다가 실제 이 파일의 위치가 아니라 또 다른 인덱스 블럭을 가리키게 해놓는 것이다. (미리 약속)

        2. multi-level index

          멀티 레벨 인덱스는 하나의 인덱스 블럭이 직접 파일의 위치를 가리키는게 아니라, 이게 또다른 인덱스를 가리키게 해서 (2단계 페이지 테이블 쓰듯이) 인덱스가 두 번 거쳐야지만 가리키게 된다거나… 하는 이런 식으로 하면 굉장히 큰 파일을 표현할 수가 있다.

          (그렇지만, 인덱스를 위한 공간 낭비가 있다는게 이런 방법의 단점이 될 것이다.)

여기까지, 파일을 디스크에 저장하는 기본적인 방법 3가지를 설명함.
이 방법들은 우리가 이론적으로 이렇게 할당이 가능하다는 것을 설명한 것임

다음 내용은 실제 파일 시스템에서 어떤 할당 방법을 어떻게 쓰는가?
또 어떻게 변형해서 사용하는가? 그 부분을 설명.

UNIX 파일 시스템의 구조

UNIX는 역사가 굉장히 오래됐고, 이 파일 시스템 구조는 가장 기본적인 파일 시스템 구조이다.

UNIX나 리눅스의 파일 시스템이 이런 기본적인 파일 시스템에서 점점 발전해서 Fast File System 이라는게 나오고, 또 ext2, ext3, ext4 등 굉장히 많은 파일 시스템들이 발전을 해왔다. 이와 같은 것들은 이 기본 구조를 좀 더 효율적으로 어떻게 더 잘 저장할 수 있을지 또, 어떻게 더 효율적으로 시간을 줄여서 할지 이런 것들이 발전해왔다.

image

그림을 보면 하나의 논리적인 디스크(파티션)가 있다.
여기다가 우리가 파일 시스템을 설치를 해놓은 것이다.

UNIX의 파일 시스템은 저장되는 구조가 크게 4가지로 구성이 된다.

  • 제일 앞 부분, Boot block이 있고,
  • 두번째, Super block이 있고,
  • 세번째, Inode list라는게 있고,
  • 네번째, Data block이라는게 있다.

image

  • 유닉스 파일 시스템의 중요 개념

    • Boot block

      • 부팅에 필요한 정보 (bootstrap loader)

        그 중에서, 첫번째 나와있는 ‘부트 블럭’은 UNIX 파일 시스템만 제일 앞에 부트 블럭이 나오는 것이 아니라, 어떤 파일 시스템이던 ‘부트 블럭’이 항상 제일 앞에 나온다. (이것은 약속이다.)

        왜냐하면, 컴퓨터에 어떤 파일 시스템이 설치되어있는지 모르는데, 컴퓨터 전원을 키면 부팅을 해야될 것이다. 그럼 어디에 있는 데이터를 메모리로 올려서 부팅을 할 수 있느냐? 어떤 파일 시스템을 쓰던 간에 항상 ‘0번 블럭’을 메모리에 올리면, 이게 바로 ‘Bootstrap loader(부트스트랩 로더)’ 라고 해서 부팅을 하기 위해서 필요한 기본 정보가 0번 블럭에 항상 저장이 돼있다.

        이것을 올려놓고, 부트 블럭이 시키는대로 하면, 이 파일 시스템에서 운영체제 커널의 위치가 어딘지를 찾아서 그것을 메모리에 올려서 정상적인 부팅이 다 이루어지도록 하는 역할을 하는게 0번인 ‘부트 블럭’ 이다.

    • Super block

      • 파일 시스템에 관한 총체적인 정보를 담고 있다.

        구체적으로 말하자면, 어디가 빈 블럭이고, 어디가 실제로 파일이 저장된 사용 중인 블럭인지 이런 것들을 관리 해야될 것이다.

        또, 다음에 나와있는 Inode list와 관련된 것도 어디까지가 Inode list가 있고, 어디부터 실제 Data block이 있고… 이런 것들을 ‘Super block’이 총체적으로 관리를 해주는 것이다.

    • Inode

      지금까지는, 파일의 메타데이터는 그 파일을 가지고 있는 디렉토리에 가면, 그 파일의 메타데이터가 기록돼있다. 이렇게 배웠다. 그런데, 실제 파일 시스템의 구현에서는 디렉토리가 메타데이터를 다 가지고 있지는 않다.

      특히, 유닉스의 파일 시스템의 경우에는 디렉토리는 지극히 일부만 가지고 있고, 실제 파일의 메타데이터들은 별도의 위치에다가 빼서 보관을 하고 있는데, 그 위치가 바로 ‘Inode list’ 라는 부분이다.

      • 파일 이름을 제외한 파일의 모든 메타데이터를 저장

        image

        그림을 보면, ‘Inode’라는게 ‘Index node’인데, Inode라고 하는 것들이 빨간색으로 표시된 것처럼 하나씩 저장될 수 있는 위치가 있고, 이 빨간색으로 표시된 Inode 하나가 파일 하나 당 (Inode가) 하나씩 할당이 되는 것이다.

        그리고, 이 Inode는 그 파일의 메타데이터를 가지고 있는 구조이다.
        (메타데이터는 파일의 소유주라던지, 접근 권한, 최종 수정된 시각, 위치 정보 이런것들을 가지고 있다.)

        근데, 이 유닉스 파일 시스템에서 파일의 메타데이터를 전부 Inode가 가지고 있는건 아니고, 딱 한 가지! 파일의 이름은 디렉토리가 가지고 있다.

        즉, 디렉토리에 가면 그 디렉토리 밑에 있는 파일의 메타데이터가 저장돼있다고 했는데, 그 메타데이터 중에 파일의 이름은 디렉토리가 직접 가지고 있고, 나머지 메타데이터들은 Inode에 저장이 되있기 때문에, 디렉토리에 가면, 해당 파일에 대한 Inode번호를 가지고 있는 것이다.

        그래서, 만약 “Inode 10번이다” 라고 하면, 이 파일의 나머지 메타데이터들은 ‘Inode list’에서 10번째 가면, 10번 Inode에 그 파일의 메타데이터가 저장이 된다는 것이다.

        “Inode 라는게 있다.” 이게 유닉스 파일 시스템의 가장 핵심적이고, 기본적인 구조이다.

        그러면, 그 파일의 위치 정보는 어떻게 저장하고 있느냐?
        유닉스 파일 시스템은 기본적으로 Indexed Allocation을 변형해서 사용하고 있다.(Indexed Allocation 거의 그대로 쓰긴 함)

        Inode라는 것은 어차피 크기가 고정돼있다. 각 파일 당 Inode크기가 가변적인게 아니고, 미리 할당된 크기들이 구성이 되있는 것이기 때문에, 여기서 표시될 수 있는 위치 정보를 나타내는 포인터 개수도 유한한 것이다. 그렇지만, 가급적 작은 Inode를 가지고, 굉장히 큰 파일을 표현할 수 있어야 한다.

        그래서 UNIX에서는 Indexed Allocation 중에서 direct index가 있고, single indirect, double indirect, triple indirect 이렇게 4가지로 그 파일의 위치 정보를 구성한다.

        • 파일의 크기가 굉장히 작다면, direct index로만 가지고 그 파일의 위치를 표현할 수가 있는 것이다. direct index포인터가 유한한 개수가 있는데, 굉장히 작은 파일이면 이 포인터 몇 개만 가지고, 그 파일data의 위치를 충분히 가리킬 수가 있다.
        • 대단히 큰 파일인 경우에는, indirect를 이용해서 single indirect라는 것은 한 번 따라가면, 그 파일의 내용이 있는게 아니라 인덱스 블럭이 어딘가에 있고, 그 인덱스 블럭에는 포인터가 또 여러개 들어갈 수가 있음… 거기에 실제 파일의 내용data을 가리키는 포인터들이 위치하는 것임.
        • 더 큰 파일을 표현하겠다면, 그 밑에 있는 포인터 하나인 double indirect가 그 용도로 쓰이는 것이다. 한 번 따라가면, 인덱스가 있는게 아니라 거기에서 또 한 번 따라가야지만, 실제 파일의 위치를 가리키는 인덱스들이 있고, 여기서 이제 실제 파일 위치data가 저장이 돼있는 것이다.
        • 이걸로도 부족하다… 파일이 엄청 크다 그러면, triple indirect포인터를 쓰는 것이다. 이것은 3단계 인덱스 구조가 있고, 그것을 다 통과해야지만, 실제 파일의 위치 정보가 있다는 것이다.

        (바로 위 그림 참고하면서 보기)

        이게 왜 효율적이냐?
        대부분의 파일은 크기가 아주 작다. 그리고, 작은 파일들은 한 번의 포인터 접근으로 Inode만 메모리에 올려놓으면 즉, open이 돼있으면 파일의 위치를 바로바로 알 수가 있고…

        그 다음에, 가끔 발생하는 일이긴 하지만, 굉장히 큰 파일이다 그러면 indirect 블럭들을 이용해서 인덱스를 디스크에서 추가적으로 접근해서, 그 파일의 위치를 찾는 것이다.

        이런식으로 해서, 실제로 계산을 해보면 굉장히 큰 파일을 한정된 크기의 Inode로 지원할 수 있다는 것을 확인할 수 있다.

image

그래서 요약을 하자면, 유닉스에서는

  • Boot block
  • Super block
  • Inode list
  • Data block

으로 디스크를 관리하고 있고, 그 중에 파일의 메타데이터는 Inode에다가 별도로 관리를 한다.

FAT File System

FAT 파일 시스템은 마이크로소프트 사가 MS-DOS를 만들었을 때, 처음 만든 파일 시스템이다. 최근에도, 윈도우즈 계열과 일부 모바일 환경에서 FAT 파일 시스템을 일부 사용하는 경우가 있다.

image

FAT 파일 시스템은 위 그림과 같이 Boot block, FAT, Root directory, Data block 이런 구조로 되어있다.

  • 부트 블럭
    • 어떤 파일 시스템이든 마찬가지로, 부팅과 관련된 정보를 담고 있음.
  • FAT
    • FAT 파일 시스템에서는 파일의 메타데이터 중에 일부를 FAT이라는 곳에 보관하고 있다.
    • 메타데이터 중에서 일부라는 것은 지극히 제한적인 위치 정보만 FAT에다가 따로 빼놓고 있고, 나머지 메타데이터는 디렉토리가 가지고 있다.

즉, 원래 파일의 메타데이터는 디렉토리가 가지고 있다고 했는데, FAT 파일 시스템의 경우에는 파일의 이름을 비롯한 접근 권한, 소유주, 파일의 사이즈 등 모든 것을 디렉토리가 다 가지고 있고, 심지어 그 파일의 첫번째 위치가 어딘지도 디렉토리가 가지고 있다.

만약, “첫번째 위치가 217번 블럭이다” 그러면, 217번에 가면 그 파일의 첫번째 내용이 있을 것이다.

위에 Linked Allocation에서는 첫번째 블럭이 끝날 때, “이 파일의 크기가 크다” 그러면, 두번째 블럭의 위치를 저장하고 있다고 했는데, 그럼으로 인해서 생기는 문제가 몇가지 있었다. 중간에 Bad sector가 나면 뒤에거를 다 찾을 수가 없는 것도 문제였고, 약간의 크기가 줄어듦으로 인해서 512바이트 섹터를 다 활용할 수 없는 것도 문제였다.

FAT 파일 시스템은 그래서 어떤식으로 그 문제들을 해결했냐면, 217번 블럭의 다음 블럭이 뭔지를 FAT이라는 별도의 테이블, 배열에다가 담고 있는 것이다. FAT이라는 테이블의 배열의 크기는, 디스크가 관리하는 데이터 블럭의 개수만큼… N개의 데이터 블럭이 있다면, 배열의 크기가 N개가 되는 것이다. 그 배열에는 숫자를 하나 담을 수 있는데, 그 숫자는 그 블럭의 다음 블럭이 어딘지를 담고 있다.

즉, 배열에 있는 숫자는 해당 블럭이 어떤 파일에 속한 블럭이라거나 또는, 비어있는 블럭이라거나 이런건 상관없다. 그냥 무슨 의미인지는 모르겠지만, 그 블럭의 다음 블럭을 번호로 저장해놓는 것이다.

그림의 파일 경우에는 첫번째 블럭이 217번이었다.

  • 217번에 가면, 그 블럭의 내용이 있고,
  • 두번째 블럭은 FAT에서 217번 엔트리를 가면 618이라고 써져있으니 “이 파일의 두번째 블럭은 618번에 있다” 는 것을 아는 것이다.
  • 세번째 블럭은 어디에 있는지 찾아야 되겠다면, 또 FAT의 618번 엔트리를 가보는 것이다. 그랬더니 339라고 써져있다. 그러면 “이 파일의 세번째 블럭은 339번이다” 는 것을 안다.
  • 네번째 블럭이 있는지 339번 엔트리에 또 가봤더니, 이번에는 “이 파일이 끝났다”는 약속된 번호가 적혀있다. 그러면 이 파일은 339번으로 끝나고, “더 이상의 내용은 없구나” 라는걸 알 수 있다는 것이다.

이것은 Linked Allocation을 활용한건데, 다음 위치 다음 위치를 찾기 위해서 실제 ‘데이터 블럭’을 접근해야되는게 아니라, 바로 FAT만 확인해보면 그 파일의 다음 위치가 어디에 있는지를 알 수가 있다는 것이다.

그래서, 이 FAT 파일 시스템의 또다른 장점은 ‘직접 접근’이 가능하다는 것이다.
왜 가능하냐?
“이 파일의 네번째 블럭을 보겠다” 그러면, 이 FAT이라는 것은 어차피 작은 테이블이다. 그래서, 이미 메모리에 올라가 있는 상황이고, 그 상황에서 이 파일의 네번째 블럭을 보겠다고 하면,

  • 217번 엔트리에 가면 두번째가 618
  • 618에 가면 세번째가 339
  • 339에 가서 만약 네번째가 있다면 숫자 얼마(NNN)

이렇게 FAT 테이블을 메모리에 올려놓고, 쭉 따라가는 것이기 때문에 이것은 곧바로 그 파일의 네번째 위치가 어딘지를 파악할 수가 있고, 그러면 실제 데이터 블럭…디스크에서 두번째, 세번째 블럭을 봐야지만 네번째 블럭을 알 수 있는게 아니라, 바로 알 수가 있다는 것이다.

그래서, FAT이라는 파일 시스템은 Linked Allocation의 단점을 모조리 다 극복을 하고 있다.

  • 랜덤 엑세스 되고,
  • Reliability 문제 해결하고

좀 더 구체적으로 어떻게 해결하느냐?
포인터 하나가 유실되더라도(Bad sector가 나더라도) FAT에 내용이 있기 때문에, 데이터 블럭의 내용하고, FAT의 내용하고는 완전 분리가 돼있다. 그리고, FAT은 대단히 중요한 정보일 것이다. 데이터의 위치를 담고 있기 때문에 그래서, FAT은 한 카피만 두는게 아니라 중요한 정보라서 보통은, 디스크에다가 두 카피 이상을 저장을 하고 있다. 그러니까 Reliability 문제가 더 개선이 될 수 있는 것이다.

  • 512바이트도 충분히 활용 가능함

그래서, 이 ‘FAT 파일 시스템’은 Linked Allocation을 변형했지만, 단점을 모두 극복하는 방법이라는 것이다.

여기서 ‘UNIX의 파일 시스템’, ‘FAT 파일 시스템’ 이렇게 두 가지만 예제로 말하고 있는데, 실제로 사용되는 파일 시스템들이 대단히 많다. 이러한 기본적인 방법을 상당히 개선해가지고 사용을 하고 있다. 그래서, 필요하면 현재 널리 사용되는 파일 시스템의 구조가 어떤지도 살펴보는 것이 도움이 될 것이다.

Free-Space Management

image

위 그림과 같이 중간 중간 비어있는 블럭은 어떻게 관리하느냐? 에 대해서 설명

비어있는 블럭을 관리하는 방법도 몇 가지가 있다.

  • Bit map or bit vector

    비트맵을 두는 방법(혹은 비트 벡터 방법)

    image

    각각의 블럭 별로 번호가 있으면, 그거를 UNIX같은 경우라면 Super block부분에다가 비트를 둬서, 첫번째 블럭이 사용중이냐, 비어있느냐를 1과 0으로 표시한다.

    비트맵의 크기는 데이터 블럭 안에 있는 블럭의 개수 만큼으로 구성돼있다.

    비트맵이 표시하는 바는

    • 만약, 값이 0이면, 비어있는 블럭을 나타내고,
    • 1이면, 이미 파일에 할당된 블럭을 나타낸다.

    파일 시스템이 어떤 파일이 새로 만들어지거나, 파일의 크기가 커지거나 하면, 비어있는 블럭 중에 하나를 할당을 해야될 것이고, 파일이 삭제되거나 그러면, 1로 표시돼있던 비트맵을 0으로 바꿔줘야할 것이다. 그런건 파일 시스템이 관리를 하고 있는 것임.

    • Bit map은 부가적인 공간을 필요로 함

      단점이라면, 단점일수도 있지만 그렇게 많은 공간이 필요하지는 않을 것이다. 블럭 하나당 1bit가 필요하기 때문에…

    • 연속적인 n개의 free block을 찾는데 효과적

      연속적인 빈 블럭을 찾는데 효과적.

      파일의 크기가 보통 블럭 하나로 구성되지는 않고, 블럭 10개로 구성된다고 하면, 가능하면 연속 할당을 쓰지는 않지만, 연속적인 빈 공간에 할당을 해주면 좋겠다는 것이다. 디스크 헤드가 이동할 필요가 없이 많은 양을 한꺼번에 읽어올 수가 있기 때문에…

      그래서, 비트맵이라는 것은 쭉 비트맵을 스캔하게 되면, 연속적으로 0인 곳이 어딘지 찾기가 쉽기 때문에, 효과적.

  • Linked list

    링크드 리스트로 관리하는 방법

    • 모든 free block들을 링크로 연결 (free list)

      비어있는 블럭들을 모두 연결해놓는 것

      image

      회색들이 다 비어있는 블럭.

      어차피 비어있는 블럭이기 때문에, 포인터를 가지고 다음에 비어있는 위치가 어딘지 저장할 수 있다.

      ‘Linked list’ 방법은 비어있는 블럭의 첫번째 위치만 우리가 포인터로 가지고 있고, 그 다음 비어있는 위치, 그 다음 위치는 실제 그 빈 블럭에 가면 다음 번 빈 블럭의 위치를 포인트하고 있는 이런 식으로 관리를 한다.

    • 연속적인 가용공간을 찾는 것은 쉽지 않다

      실제로 빈 위치들을 다 디스크 헤드가 seek를 해가지고, 어딘지 어딘지를 다 따라가봐서 연속적인거를 찾겠다고 하면 상당히 비효율적. 이론적으로 이런 방법도 있다는 정도… 실제로 쓰기가 쉽지 않을 것이다.

    • 공간의 낭비가 없다

    이 방법은 위에서 배웠던 Linked Allocation을 비슷하게 바꾼 것

  • Grouping

    Indexed Allocation 같은 걸 빈 블럭을 관리하는데도 활용할 수가 있다.

    그룹핑을 하는 방법이 그 예

    image

    • linked list 방법의 변형
    • 첫번째 free block이 n개의 pointer를 가짐
      • n-1 pointer는 free data block을 가리킴
      • 마지막 pointer가 가리키는 block은 또 다시 n pointer를 가짐

    어차피 비어있으니까 우리가 아무렇게나 쓸 수가 있다.
    처음의 비어있는 위치가 인덱스 역할을 해서,

    • 첫번째 빈 위치에 가면, 비어있는 블럭들의 포인터들이 쭉 저장이 되있고, 비어있는 것들을 가리키고 있고,
    • 마지막 블럭에 가서 “더 비어있는 블럭들이 많이 있다” 그러면 또, 인덱스가 저장이 되있고,
    • 그래서 인덱스의 앞에서부터 n-1개는 빈 블럭을 가리키고, 마지막 포인터는 또 다른 인덱스를 가리키고

    이런식으로 해서 인덱스 형식으로 그룹핑을 해가지고, 빈 블럭의 위치를 가리키게 할 수가 있는 것.

    비어있는 블럭을 한꺼번에 찾기에는 Linked list보다는 효율적이지만, 그래도 이것이 연속적인 빈 블럭을 찾기에 그렇게 썩 효과적이지는 않을 것이다.

  • Counting

    그래서, 연속적인 빈 블럭을 찾기에 효과적인 방법으로 이 ‘카운팅’ 을 하는 방법을 사용할 수가 있다.

    • 프로그램들이 종종 여러 개의 연속적인 block을 할당하고 반납한다는 성질에 착안
    • (first free block, # of contiguous free blocks)을 유지

    이 방법은 연속적인 빈 블럭을 표시하기 위해서,
    빈 블럭의 첫번째 위치하고, 거기서부터 몇 개가 빈 블럭인지를 쌍으로 관리를 한다.

    Grouping같이 포인터가 빈 블럭을 가리키기만 하는게 아니라, 빈 블럭의 위치를 가리키고, 거기서부터 몇 개가 비어있다. 이런식으로 관리.

    이런식으로 관리를 하게 되면, 예를 들어, 우리가 “연속적으로 5개 빈 블럭을 찾고 싶다”고 하면, 프리 블럭의 개수를 가리키는 필드가 5 이상이 되는 것을 찾으면 될 것이다.

Directory Implementation

디렉토리를 어떻게 구현하는지에 대한 설명

‘디렉토리’라는 것은 그 디렉토리 밑에 있는 파일의 메타데이터를 관리하는 특별한 파일.

그러면, 디렉토리 파일의 내용을 어떻게 저장할 것이냐?
그것을 저장하는 방법들이 여기에 소개 되어있음.

  • Linear list

    ‘리니어 리스트’
    단순하게 디렉토리 파일을 (하단 좌측 이미지 같이) 파일의 이름하고, 그 파일의 다른 메타데이터들(파일의 접근 권한, 사이즈, 소유주 이런것들)을 쭉 순차적으로 저장하는 것.

    그리고, 이 메타데이터는 크기가 가변적인게 아니라, 크기를 고정시켜놓는다. 파일 이름은 몇 바이트를 쓰고, 그 다음 나머지 메타데이터.. 접근권한은 예를 들면, 9비트를 쓰고, 파일의 사이즈는 몇 바이트를 쓰고 이런식으로… 딱 크기를 고정을 시켜서 이런식으로 관리를 한다는 것이다.

    그러면, 디렉토리에 대한 연산이 주어져있을 때 “그 디렉토리 밑에 어떤 파일이 있는지 찾아라” 이런 연산을 주게 되면, 파일 이름의 필드가 어떤 단위로 구성되있는지를 알기 때문에, 파일의 이름이 시작되는 위치, 그 다음에 몇 바이트가 지나면 또 다른 파일의 이름이 시작되는 위치… 이런식으로 되있기 때문에, 파일의 이름이 어떤게 있는지 찾으라고 하면 순차적으로 쭉 찾아보면 됨.

    • <file name, file의 metadata>의 list

    • 구현이 간단

    • 디렉토리 내에 파일이 있는지 찾기 위해서는 linear search 필요
      (time-consuming)

      구현은 간단하지만, 어떤 연산에 대해서 시간이 많이 필요하다.

      예를 들면, 특정 파일이 있는지를 찾으라고 할 때, 다 검색을 해봐야 되니까… 상당히 비효율적이다.

  • Hash Table

    그래서 사용할 수 있는 다른 디렉토리 구현 방법…‘해시 테이블’ 방식

    해시 테이블 방식은 파일의 이름을 그냥 저장하는게 아니라, 해시 함수를 적용한다.

    해시 함수라는 것은 어떤 input값이 주어지더라도 해시 함수를 적용하면, 그 해시 함수의 결과 값이 특정 범위 안의 숫자로 한정됨.

    파일의 이름이라는 것도 우리가 해시 함수를 적용할 수 있다.
    예를 들어서, 해시 함수 F를 적용했을 때, 그 파일의 이름에 대한 해시 결과 값이 1부터 n사이의 값이 나오도록 할 수가 있다. 이 경우에, 파일의 해시 함수 결과 값에 해당하는 엔트리에다가 그 파일의 메타데이터를 저장하는 것이다.

    예를 들어서, 파일 이름이 cccc가 있었는데, 이거에 대해서 해시 함수를 적용했더니 “값이 3이 나왔다” 그러면, 3번 엔트리에다가 그 cccc파일의 이름과 메타데이터를 저장해놓는다는 것이다.

    이런식으로 하게 되면, “어떤 파일이 어떤 디렉토리 밑에 있는데 있는지, 없는지 찾아라” 그러면, 순차적으로 탐색해야되는게 아니라 그 파일 이름을 해시 함수를 적용하고, 그 다음에 그 적용된 해시 함수의 결과값에 해당하는 엔트리만 찾아보면 된다. 만약에, 그 파일이 어떤 디렉토리 밑에 있다면, 그 엔트리에 있어야 되는거고 없다면, 그런 파일은 없는거니까… 효율적으로 사용할 수 있다.

    • linear list + hashing

    • Hash table은 file name을 이 파일의 linear list의 위치로 바꾸어줌

    • search time을 없앰

    • Collision 발생 가능

      물론, 해시를 쓰면 Collision(컬리젼)이 발생할 수가 있다.
      서로 다른 파일의 이름에 대해서 결과값이 같은 엔트리로 매핑되는… 해시 함수에서 흔히 발생하는 현상이다.

image

디렉토리에다가 파일의 메타데이터를 직접 보관할 수도 있지만,

  • File의 metadata의 보관 위치

    • 디렉토리 내에 직접 보관

    • 디렉토리에는 포인터를 두고 다른 곳에 보관

      유닉스나 또는 FAT의 구현에서 봤듯이 메타데이터를 전부 디렉토리가 가지고 있는게 아니라 일부는 직접 가지고 있고, 일부는 파일 시스템에서 다른 곳에 별도로 보관을 하고 있었다.

      • inode, FAT 등

        유닉스 파일 시스템의 경우에는 inode라는 곳에 대부분의 메타데이터를 가지고 있었고, FAT 파일 시스템에서는 FAT이라는 곳에 파일의 다음 위치 정보 같은 것들을 가지고 있었다.

  • Long file name의 지원

    긴 파일 이름을 지원하는 방식에 대해서 설명

    디렉토리가 파일의 메타데이터를 저장할 때 파일 이름 길이에 해당하는게 어떤 파일은 이름이 길고, 어떤거는 짧고 이러면 위치가 들쭉날쭉이 될 수가 있다. 그렇게 구현하지는 않고 항상, 엔트리 크기는 고정시켜서 “우리가 뭔가를 검색하겠다” 그러면, 어딘지 막 찾고 이런게 아니라 딱딱 그 위치만 찾아보면 되도록 당연히 이렇게 구현을 해야되겠다.

    근데, 대부분의 메타데이터들은 길이가 한정돼있다. 파일의 접근 권한이라는건 예를 들면, 9비트면 되고, 파일의 최종 수정 시각이라면, 시간을 표시하는 몇 바이트면 되는데… 문제는 파일 이름이다.

    파일 이름을 우리가 특정 바이트 수로 제한할 수도 있지만, 그렇게 하는 것도 또 비효율적이기 때문에 굉장히 긴 이름의 파일을 지원해줘야 될텐데, 엔트리의 크기는 고정돼있고…그래서 이 긴 이름의 파일을 어떻게 지원할 것인지… 설명

    • <file name, file의 metadata>의 list에서 각 entry는 일반적으로 고정 크기
    • file name이 고정 크기의 entry 길이보다 길어지는 경우 entry의 마지막 부분에 이름의 뒷부분이 위치한 곳의 포인터를 두는 방법
    • 이름의 나머지 부분은 동일한 directory file의 일부에 존재

image

(위 그림 우측) 파일 이름에 해당하는 필드를 무작정 길게 하는게 아니라 어느 정도 길이로 한정을 해놓고, 그 다음에 실제로 파일 이름이 대단히 길다면, 앞 부분은 쭉 저장을 하다가 길어서 엔트리 수를 벗어나면, 포인터를 둬서 디렉토리 파일에서 맨 끝에서부터 파일 이름이 거꾸로 저장되도록 하는 것이다.

예를 들어서, 파일 이름이 aaabb다 근데, 저장할 수 있는 것은 3글자까지만 저장이 되고(4글자까지 저장이 되는데, 한 글자는 포인터 위치 정보로 활용),
그러면, a
a a 하고서, 제일 밑에 가면 b b 하고 파일 이름이 끝났다는 것임.

bb 같이 짧은 파일 이름인 경우에는, 그냥 필드 한 줄에 저장하고 끝나는 것이고,

ccaa라면 긴 이름의 파일이라서 c c a 까지만 저장하고, 다 저장이 안되면, 그 다음 위치를 포인터가 가리키고 있고, 아직 미처 다 기록하지 못한 파일 이름 부분은 아래 위치에다가 적고, 또 파일 이름이 끝났다는 표시로 이렇게\0 적어주면 된다는 것이다.

VFS and NFS

image

파일 시스템이 실제로 종류가 대단히 많다.
UNIX에서도 Fast file system이 있고, ext4가 있고, NTFS라는게 있고, 위에서 봤던 FAT 파일 시스템이 있고… 여러 종류의 파일 시스템이 있는데, 사용자가 파일 시스템을 접근할 때는 운영체제한테 파일 시스템 접근을 위한 시스템 콜을 해야된다.

그런데, 파일 시스템 종류별로 서로 다른 시스템 콜 인터페이스를 써야된다면, 사용자가 굉장히 혼란스러울 것이다.

그래서, 보통은 어떤 파일 시스템이 실제로 사용되든 상관없이 개별 파일 시스템 윗 계층에 VFS라는 인터페이스를 하나 두고 있다.

  • Virtual File System (VFS)

    그래서, 사용자가 파일 시스템을 접근할 때는 개별 파일 시스템 종류하고 상관없이 VFS 인터페이스를 사용하는 것이다.

    • 서로 다른 다양한 file system에 대해 동일한 시스템 콜 인터페이스 (API)를 통해 접근할 수 있게 해주는 OS의 layer

      다양한 파일 시스템들이 있지만, 사용자 입장에서는 동일한 API(동일한 시스템 콜 인터페이스)를 통해서 파일 시스템들을 접근할 수 있게 해주는 ‘VFS’ 계층.

  • Network File System (NFS)

    파일 시스템이 자기(로컬) 스토리지에 저장이 될 수도 있지만, 원격에 저장되있는 파일 시스템을 내가 접근할 수도 있다. 그게, NFS 같은 인터페이스를 써서 접근을 해야되는 경우이다.

    image

    위 그림을 보면, 그림에 컴퓨터가 총 2대 있다. (client, server)
    2대의 컴퓨터가 network로 연결되어있는 상황이다.

    client가 어떤 파일 시스템이던 상관없이 VFS 인터페이스를 통해서 접근을 하는데, 그 파일 시스템 중에는 자기 로컬 컴퓨터에 있는 파일 시스템도 접근이 가능하고, 또 원격의 다른 컴퓨터에 있는 파일 시스템을 접근할 수 있는 인터페이스도 지원이 된다는 것이다.

    그게 바로 예를 들자면, NFS라는 것이다. (NFS 외에도 여러가지가 있다.)

    그림을 보면, server 컴퓨터에는 파일 유닉스 파일 시스템이 사용되고 있고, server의 로컬 사용자가 server파일 시스템에 접근하려면 마찬가지로, VFS 인터페이스를 통해서 시스템 콜을 해서 접근을 하면 될 것이다.

    근데 지금은, client에 있는 사용자가 server 파일 시스템에 접근하기 위해서, VFS 인터페이스를 써서 마치 자기 컴퓨터에 있는 파일 시스템 처럼 접근 요청을 하는 것이다.

    VFS를 통해서 딱 따라와봤더니 “내 컴퓨터에 그 파일 시스템이 없다” 그러면, 이 ‘NFS 파일 시스템’이 발동을 해가지고, RPC(원격 접근하는 프로토콜)에 의해서 네트워크를 통해서 서버 쪽을 접근하는 것이다. 그럼 서버 쪽에서도 받아주는 RPC를 받아주는 인터페이스가 있고, 또 ‘NFS 서버’에 해당하는 모듈이 있어서 얘가 마치 자기 사용자가 요청하는 것처럼 VFS 인터페이스를 통해서 파일 시스템 접근 요청을 하는 것.

    그러면, 구체적인 파일 시스템인 유닉스 파일 시스템에 접근해서 내용을 끄집어 내가지고, 전달을 해주면 server 사용자가 아니라, client쪽에다가 전달을 해주는 것이다. 그럼, client에서 ‘NFS 클라이언트’ 모듈이 내용을 받아가지고, 사용자한테 전달을 하게 되는 것이다.

    • 분산 시스템에서는 네트워크를 통해 파일이 공유될 수 있음

      그래서, 이러한 분산 시스템에서 네트워크를 통해서 파일을 접근을 할 수 있게 해주는 방법이고,

      NFS를 지원하려면 서버 쪽에 NFS 모듈도 있어야되고, 클라이언트 쪽에도 NFS 모듈이 있어서 같은 약속을 가지고 접근을 할 수 있게 해주면 된다는 것이다.

    • NFS는 분산 환경에서의 대표적인 파일 공유 방법임

Page Cache and Buffer Cache

페이지 캐시와 버퍼 캐시에 대한 설명

가상 메모리 시스템 관점에서는 ‘페이지 캐시’ 라고 부르고
파일 시스템 관점에서 쓰는 것은 ‘버퍼 캐시’ 라고 부름.

  • Page Cache

    페이지 캐시는 전 챕터 ‘가상메모리 관리’ 챕터에서 설명했던 부분이다.

    • Virtual memory의 paging system에서 사용하는 page frame을 caching의 관점에서 설명하는 용어

      우리가 페이징 시스템에서 사용하는 그 페이지들을 특히, 물리적인 메모리에 있는 페이지 프레임들을 우리가 ‘페이지 캐시’라고 부르는 것이다.

      왜냐하면, swap area, backing store보다 물리적인 메모리의 페이지 프레임은 빠르다.

      그래서, 우리가 캐싱의 관점에서 얘기하자면, 페이징 시스템에서의 페이지 프레임들을 ‘페이지 캐시’라고 부르는 것이다.

      페이지 캐시는 운영체제한테 주어지는 정보가 지극히 제한적.
      (캐시 히트가 나면)즉, 이미 메모리에 존재하는 데이터에 대해서는 하드웨어적인 주소 변환만 하기 때문에, 정확한 접근 시간이라던지 이런 정보를 알 수가 없어서 클락 알고리즘 같은 것을 사용함.

    • Memory-Mapped I/O를 쓰는 경우 file의 I/O에서도 page cache 사용

  • Memory-Mapped I/O

    파일 입출력을 하는 방법 중에서 버퍼 캐시를 이용하는.. 즉, Read나 Write 시스템 콜을 이용해서 파일을 접근하는 방법이 있고,

    ‘Memory-Mapped I/O’ 를 이용해서 파일을 접근하는 방법이 있다.

    ‘Memory-Mapped I/O’ 라는 것은 파일을 접근할 때 (원래는 파일을 접근할 때 그 파일을 open한 다음에 Read시스템 콜이나 Write 콜을 통해서 파일을 접근했다. 그런데, 그런 방법을 안쓰고),

    • File의 일부를 virtual memory에 mapping시킴

      파일의 일정 부분을 그 프로세스의 메모리 영역에다가 매핑을 시켜놓고 쓰는 것이다.

    • 매핑시킨 영역에 대한 메모리 접근 연산은 파일의 입출력을 수행하게 함

      매핑을 해놓고 나면 그 다음부터는, Read나 Write 시스템 콜을 하는게 아니라 메모리에다가 읽고 쓰는…

      마치 우리가, Integer A 에서 메모리에다가 변수를 잡아가지고, 데이터를 읽고 쓰는 것처럼 하는데 실제로는, 그게 파일에다가 데이터를 읽고 쓰는 효과가 나게 하는… 그런 방법이 바로 ‘Memory-Mapped I/O’ 라는 것이다.

  • Buffer Cache

    페이지 캐시에 비해서 ‘버퍼 캐시’는 (전 챕터 ‘파일 시스템’ 챕터에서 설명함) 파일의 데이터를 사용자가 요청했을 때 디스크에서 읽어가지고, 사용자한테만 전달해주고 끝나는게 아니라 운영체제가 읽어온 내용을 자기의 영역 중 일부에다가 저장을 해놓고, 똑같은 파일 데이터를 다른 친구가 또는 같은 친구가 나중에 요청을 하게되면, 디스크까지 가는게 아니라 버퍼 캐시에서 바로 읽어다 주는… 그런게 바로 버퍼 캐시임.

    • 파일 시스템을 통한 I/O 연산은 메모리의 특정 영역인 buffer cache 사용

    • File 사용의 locality 활용

      • 한번 읽어온 block에 대한 후속 요청시 buffer cache에서 즉시 전달
    • 모든 프로세스가 공용으로 사용

    • Replacement algorithm 필요 (LRU, LFU 등)

      페이지 캐시에 반해서 버퍼 캐시는 이미 파일 데이터가 메모리에 올라와있든, 디스크에 있든 간에 어차피 파일을 접근할 때는 시스템 콜을 해야되기 때문에, CPU 제어권이 운영체제한테 넘어오고 그러면, 그 파일에 대한 요청이 언제 일어났는지를 어떤 경우에도 (캐시 히트가 나든, 미스가 나든) 알 수가 있고 그래서, 그 정보를 이용해서 LRU 알고리즘 같은 것을 사용할 수 있다.

  • Unified Buffer Cache

    • 최근의 OS에서는 기존의 buffer cache가 page cache에 통합됨

      최근에는 페이지 캐시하고, 버퍼 캐시를 합쳐서 같이 관리를 하는 운영체제가 많다. 리눅스의 경우도 그렇게 관리를 하고 있음.

      이런 구조를 ‘유니파이드 버퍼 캐시’ 라고 부름.

      “유니파이드 버퍼 캐시로 합쳐져 있다”는 것은 “버퍼 캐시도 페이지 단위로 관리를 한다” 이렇게 알아두면 됨. 그리고, 운영체제에서 페이지 프레임들.. 물리적인 메모리를 관리하는 루틴에 페이지 캐시랑 버퍼 캐시를 같이 관리를 한다는 얘기다.

      합쳐졌다고 해서, 관리할 수 있는 방법이 다르다는 의미는 아님.

image

위 그림 좌측 : 물리적인 메모리
커널 영역이 있고, 사용자 메모리 영역이 있었다.

그래서, 사용자 영역은 페이지 단위로 필요한 데이터가 올라오고, 내려가고 이런식으로 관리가 됐었고,

원래는 커널 메모리 영역에 버퍼 캐시가 존재했었다. 그래서, “어떤 파일의 내용을 읽어와라” 그러면, 버퍼 캐시에 먼저 가져온 다음에 사용자한테 전달해주고, 이런식으로 관리가 됐다.

페이지는 보통 4킬로바이트 단위이고, 블럭 하나는 512바이트로 구성이 된다.

근데, 최근에는 페이지 캐시하고 버퍼 캐시가 합쳐졌기 때문에, 버퍼 캐시에서도 4킬로바이트 즉, 페이지 크기로 블럭들을 관리를 하고 있다. 그것이 ‘유니파이드 버퍼 캐시’의 특징.

그 다음에 가상 메모리 기법에서 쓰는 스왑 영역은 빠르게 데이터를 내려놓고, 올리고 해야되기 때문에, 여러 개의 블럭을 모아서 4킬로바이트 단위로 올려놓거나, 내려놓거나 이런식으로 하는게 기본이고,

더 크게 4킬로바이트 페이지 하나가 아니라, 여러 개의 페이지를 한꺼번에 올리고, 내리고 이런식으로도 한다. (속도 효율성을 위해, 그렇게 해주는 것임)

image

먼저, 기존의 파일의 데이터를 읽고 쓰고 하는 방법은 2가지 인터페이스가 있다.

  • 하나, 파일을 오픈 한 다음에 read(), write() 시스템 콜을 하는 것 (위 그림 우측 보기)
    • 그러면, 운영체제가 시스템 콜이니까 해당하는 파일의 내용이 버퍼 캐시에 있는지를 봐서, 있으면 바로 전달해주고, 없으면 디스크 파일 시스템에서 읽어와서 전달해줌
    • 이게 read, write 시스템 콜을 이용하는 경우에 I/O를 하는 경로
    • read, write 시스템 콜이 들어오면, 운영체제가 그 파일 시스템에 있는 내용을 자신의 버퍼 캐시로 읽어오고, 그런 다음에 사용자 프로그램한테 전달을 한다. 그러면, 사용자 프로그램은 자신의 주소 영역 중에 있는 페이지에다가 버퍼 캐시에 있는 내용을 카피해서 사용을 한다.
  • 또 다른 파일 입출력 방법, memory-mapped I/O(메모리 맵드 파일) (위 그림 좌측 보기)
    • 이것도 처음에는 시스템 콜을 한다. 운영체제한테, “나는 지금 memory-mapped I/O를 쓰겠다” 라는 mmap() 라는 시스템 콜을 해준다.
    • 그러면, 자신의 주소 공간 중에 일부를 파일에다가 매핑을 하는 것, 매핑을 해놓더라도 어차피 디스크에 있는 파일을 읽어오는 것은 똑같다, 디스크에 있는 파일을 읽어와야되는건데… 읽어가지고 먼저, 버퍼 캐시에 읽어오는 것까지는 똑같다.
    • 버퍼 캐시에 읽어오고, 그런 다음에 그 (읽어온) 내용을 페이지 캐시에다가 카피를 한다. 읽어온 내용을 페이지 캐시에다가 카피를 해서 주면, 그 내용이 파일에 map된 내용이 되는 것이다.
    • 그래서, 이 때부터는 사용자 프로그램이 자신의 페이지 캐시에다가 (mmap된 영역에다가) 데이터를 메모리에 읽고 쓰듯이 요청을 하게 되면, 그게 read / write가 되는 것이다.
      (운영체제의 간섭없이 내 메모리 영역에다가 데이터를 읽거나 쓰는… 그냥 메모리 접근하는 방식을 통해서 파일 입출력을 하게 되는 것이다)
      (그렇다 하더라도, 만약에 map만 해놨지 내용을 메모리로 안 읽어왔다면 페이지 캐시를 접근하려고 할 때, 페이지 폴트가 난다. (기존의 가상메모리 관리하는 것 하고 똑같다는 얘기) 그러면, 페이지 폴트 핸들러가 호출돼서 즉, 운영체제로 CPU가 넘어와서 디스크에 있는 파일의 내용을 읽어놓고, 페이지 캐시에다가 카피를 해서 주고 쓰는 것)

이 두 방법이 진행하는 경로는 똑같은데 차이점이 뭐냐하면,

read / write 시스템 콜을 쓸 때는, 그 내용이 버퍼 캐시에 있든 없든 간에 항상 운영체제한테 요청을 해서 받아와야되는 것이고,

mmap을 쓰게 되면(memory-mapped file을 쓰게 되면), 일단 페이지 캐시에 올라온 내용은 운영체제의 도움을 받지 않고, 사용자 프로세스가 직접 (자기의 주소 공간에 매핑이 되어있는거니까) 그냥 자기 메모리 영역을 접근하듯이.. 메모리 접근 하는 방식을 통해서 I/O를 하게 되는 것이다.

이러한 인터페이스의 차이가 있고…
그래서, mmap을 특별히 쓰는 이유가 무엇이냐?
이미 메모리에 올라온 내용에 대해서는 커널의 도움을 받지 않고(운영체제를 호출하지 않고), 자신이 직접 자신의 메모리를 접근하듯이 읽고 쓸 수 있다. 이게 mmap을 사용하는 장점이라는 것이다.

기존의 버퍼 캐시에 환경에서는 read / write 시스템 콜을 사용하든, mmap을 쓰든 어떤걸 쓰더라도 일단, 파일 입출력을 할 때는 버퍼 캐시를 통과를 해야된다. 버퍼 캐시라는건 항상 파일 입출력을 위해서 미리 운영체제가 할당해놓은 영역이고, 그 루틴은 항상 통과해야되기 때문에…

그래서, 양쪽 모두 버퍼 캐시에 있는 내용을 자신의 페이지 캐시에다가 한번 복제를 해야되는 오버 헤드가 있다.

image

반면에, Unified Buffer Cache…
최근에는, 기존의 버퍼 캐시처럼 운영체제가 공간을 따로 나누어놓지 않고, 필요에 따라서 페이지 캐시에서 공간을 할당해서 쓴다고 했다.

그런 경우에는, 경로가 더 단순해진다. (위 그림 보기)

  • 일단 read / write 시스템 콜을 하는 경우에 어떻게 되느냐? (그림 우측)
    • 이 경우에는 무조건 운영체제한테 시스템 콜을 해야됨.
      즉, 그 해당하는 내용이 디스크 파일 시스템에 있든 또는, 버퍼 캐시 즉.. 페이지 캐시에 올라와있든 상관없이 운영체제한테 CPU 제어권이 넘어가고,
    • 그럼 운영체제는 이미 메모리에 올라와있는 내용은, 사용자 프로그램의 주소 영역에 그냥 카피를 해주면 되겠고,
    • 그리고, 메모리에 올라와있지 않은 내용은 디스크의 파일 시스템에서 읽어와가지고, 그거를 사용자 프로그램한테 카피를 해서 전달을 하게 됨.
  • 반면에 memory-mapped I/O인 경우에는 어떠냐 (그림 좌측)
    • 처음에 운영체제한테 자신의 주소 영역 중에 일부를 파일에다가 매핑을 하는 단계를 거치고 나면, 그때부터는 해당 사용자 프로그램의 주소 영역에 페이지 캐시가 매핑이 되는 것임.
    • 그래서, 위에 ‘기존의 파일의 데이터를 읽고 쓰고 하는 방법(Unified Buffer Cache를 이용하지 않는 File I/O)’ 에서 처럼 버퍼 캐시에 있는 내용을 한 번 더 카피해서 올라가는게 아니고, 그냥 페이지 캐시 자체가 해당 사용자 프로세스의 논리적인 주소 영역에 매핑이 된 (그 페이지가 매핑이 되었기 때문에), 이 프로그램은 그냥 페이지 캐시에다가 직접 읽고, 쓰고 할 수가 있게 된다는 것이다.
    • 버퍼 캐시라는게 별도로 존재하는게 아니라, 페이지 캐시랑 버퍼 캐시는 그냥 share돼서 쓰는거기 때문에, 그래서 이런식의 memory-mapped I/O에서 좀 더 단순한 경로를 거치게 되는 것이다.

프로그램의 실행

image

위 그림 설명

  • 프로그램이 파일 시스템에 실행 파일 형태로 저장이 되있다가 실행파일을 실행 시키면 프로세스가 된다.
  • 프로세스가 되면, 그 프로세스만의 독자적인 주소공간 Virtual Memory라는게 만들어진다.
    • code, data, stack… 이런 것들로 구성이 되는 프로세스의 주소공간이 만들어짐
  • 그리고, 주소 변환을 해주는 하드웨어에 의해서 주소 공간의 내용 중에서 당장 필요한 부분은 물리적인 메모리에 올라가있게 되고,
  • 물리적인 메모리의 공간이 한정돼있기 때문에, 쫓겨나는 것들은 디스크의 swap area로 넘어간다.
  • 그러나, 주소 공간의 code부분은 메모리에 올라간 다음에 쫓겨날 때, swap area로 내려가지 않는다.
    • code 부분은 read only
    • 이미 파일 시스템에 실행 파일 형태로 코드가 저장이 되있는 것이므로, code의 일부가 메모리에 올라갔다 쫓겨날 때는, swap 영역에 써줄 필요가 없다는 것이다, 이미 파일 시스템에 있기 때문에…

그래서, 위에서 설명했던

image

memory-mapped I/O 라는게

image

우리 사용자 프로그램이 내가 프로그램을 작성해서 프로그램 도중에 “파일의 내용을 읽어와라” 라고 할 때도 memory-mapped I/O를 쓸 수 있고 그렇지만,

실제로는 실행파일을 실행시킬 때 loader라는 소프트웨어가 프로그램의 메모리에 올려놓을 때, memory-mapped file을 쓰는 대표적인 방법이 바로 ‘실행 파일’에 해당하는 ‘code’ 부분이라는 것이다.

이 code부분은 별도의 swap area 영역을 가지고 있지 않고, 파일 시스템에 파일 형태로 존재하는 내용이 그대로 프로세스의 주소 영역에 매핑이 된 형태라는 것이다.

그래서, code내용은 주소 공간에 매핑이 되있기 때문에, 만약 프로그램이 특정 코드를 접근하는데 그 내용이 “메모리에 안 올라와있다” 그러면, swap 영역에서 올리는게 아니라, 파일 시스템의 해당 ‘실행 파일’에 있는 것을 올려야되는 것이다.

이게 파일을 갖다가 프로그램의 주소 영역에 매핑해서 쓰는 가장 대표적인 예라고 할 수 있다.

실행 파일도 파일 시스템에 저장이 되있지만

image

데이터 파일도 저장이 되있을 것이다.

그래서, 프로그램이 실행되다가 자신의 주소 공간.. 메모리 접근만 하는게 아니라, 실행이 되다보면 어떤 파일의 내용을 읽어오라는 read 시스템 콜을 할 수도 있고 또, 데이터 파일을 접근하기 위해서 memory-mapped I/O를 써서 접근을 할 수도 있고… 이렇게 되는 것이다.

image

예를 들어, B라는 프로그램이 실행이 되다가 “이 데이터 파일을 쓰고 싶다” 쓰고 싶은데, read / write 시스템 콜이 아니라 “memory-mapped I/O 형태로 쓰고 싶다.”

그럴 때는, B프로그램이 운영체제한테 “나 이 데이터 파일의 일부를 내 주소 공간 일부에다가 매핑을 해주십시오” 라고 시스템 콜을 한다. mmap() 화살표

그러면, 운영체제가 “알았다” 그래 가지고,

image

이 데이터 파일의 일부를 B프로그램의 주소 공간 일부에다 매핑을 해놓는다.

그러면, B프로그램이 실행되면서 이 메모리 위치(위 그림에 마우스 포인터로 가리켜져 있는 위치)를 접근했을 때, 만약에 “그 내용이 메모리에 안올라와있다” 그러면, 페이지 폴트가 날 것이다.

페이지 폴트가 나면, CPU가 OS한테 넘어가서, 페이지 폴트 난 페이지를 물리적인 메모리에 올려놓을거고…

그러면 그 이후부터는 어떻게 되느냐하면, 이 가상 페이지(위 그림에 마우스 포인터로 가리켜져 있는 것)가 물리적인 메모리의 이 페이지(Physical memory 검은색 블럭 표시)로 매핑이 되는 것이다.

그래서 지금부터는, 프로세스 B가 이 데이터 파일의 이 부분(위 그림에 마우스 포인터로 가리켜져 있는 부분)을 접근할 때는 OS의 도움을 받지 않고, 그냥 자신의 주소 공간 중에 일부이기 때문에, 그냥 여기다가(Physical memory 검은색 블럭 표시) 데이터를 읽거나 쓰거나 할 수가 있는 것이다.

그런데, 나중에 이(Physical memory 검은색 블럭 표시) 내용이 메모리에서 쫓겨날 때는, swap area에 쓰는게 아니라, 이(Physical memory 검은색 블럭 표시) 내용은 memory-mapped file이기 때문에, 파일데이터 파일에다가 수정된 내용을 써주고, 이거(Physical memory 검은색 블럭 표시)를 쫓아내야겠다.

또다른 프로그램 A도 마찬가지로 동일한 데이터 파일의 동일한 데이터에 대해서 mmap()을(memory-mapped I/O를) 호출할 수가 있다.

image

그 경우에는 이(Physical memory 검은색 블럭 표시) 내용이 share가 되는 것이다.

데이터 파일의 이(Physical memory 검은색 블럭 표시) 내용이 프로세스 B한테도 여기(Physical memory 검은색 블럭 표시) 매핑이 되고, 또 프로세스 A한테도 A의 주소 공간에서 이 물리적인 메모리의 페이지 프레임(Physical memory 검은색 블럭 표시)으로 매핑이 되고… 이렇게 해서 쓸 수가 있는 것.

image

그 다음에 또, 똑같은 이 데이터 파일에 대해서 우리가 read / write 시스템 콜을 해서 사용할 수도 있다.

그 경우에는, 프로그램 A가 “나 지금 이 파일(데이터 파일)의 특정 내용을 달라” 라고 운영체제한테 시스템 콜을 한다.

그러면, 운영체제는 자신의 버퍼 캐시에 이(데이터 파일의) 내용을 먼저 읽어와야 될 것이다.

그런데, Unified Buffer Cache인 경우에는 이게(Physical memory 검은색 블럭 표시) 페이지 캐시이고, 그리고 프로세스 B의 주소 공간에 매핑이 되있지만, 또한 이것은 버퍼 캐시를 겸하고 있는 것이다.

그래서, 만약에 read 시스템 콜을 했는데, 프로세스 A가 요청한 데이터 파일의 내용이 이 페이지 캐시 즉, 버퍼 캐시에 이미 올라와있다(Physical memory 검은색 블럭 표시) .

그러면, 그(Physical memory 검은색 블럭 표시) 내용을

image

카피해서

image

이 사용자 프로세스(프로세스 A)한테 전달을 해주는 것이다.

암튼, 이 사용자 프로그램이 파일을 접근하는 방식은 인터페이스가 2개가 있다는 것이다.

  • read / write 시스템 콜을 이용하는 방법
    • read / write 시스템 콜을 이용하게 되면, 이 캐시에 있는 내용(Physical memory 검은색 블럭 표시)을 카피해서 프로세스의 주소 공간에 전달을 하는 것(프로세스 A의 주소 공간에 검은 점 표시).
  • memory-mapped I/O를 이용하는 방법
    • memory-mapped I/O를 쓰면, 이 내용(Physical memory 검은색 블럭 표시)을 갖다가 카피해서 프로세스 B에 전달하는게 아니라, 이거(Physical memory 검은색 블럭 표시)를 프로세스 B에 매핑해서 쓰는 것임.
    • 그래서, 프로세스 B의 주소 공간에서 검은색 블럭 내용이 곧, Physical memory 검은색 블럭 표시 내용인 것이다.
    • 이 논리적인 페이지(프로세스 B의 주소 공간에서 검은색 블럭)가 이 프레임(Physical memory 검은색 블럭 표시)에 올라와 있다. 이런식으로 매핑이 된다는 것이다.
    • 장점
      • 그래서, memory-mapped I/O를 쓰면, 일단 메모리에 올라온 파일의 내용은 시스템 콜을 하지 않고, 자신이 CPU를 가지고 있으면서 직접 접근 할 수 있기 때문에 더 빠르다는 장점이 있고,
      • 또, 캐시(Physical memory 검은색 블럭 표시)에 올라온 내용을 자신의 주소 공간으로 한번 카피하는 오버헤드가 없이 (그대로 물리적 메모리에서 프로세스(B)의 주소공간으로 매핑 됐으니까) 프로세스 B의 주소 공간에서 바뀌는 내용이 Physical memory 검은색 블럭 표시로 바로 전달이 되도록 한다.
    • 단점 (약간 주의해야할 점)
      • 이 페이지 캐시 즉, 버퍼 캐시(Physical memory 검은색 블럭 표시)를 갖다 프로세스(B)의 주소 공간에 매핑을 해놓는 것이기 때문에, 다른 프로그램도 mmap()을 써서 여기(Physical memory 검은색 블럭 표시)를 갖다가 share해서 쓰게 되면, 일관성 문제… 예를 들면, 프로세스 B가 바꾼 내용이 다른 프로그램한테 정확하게 반영이 안되어 일관성이 깨지는 문제를 주의해야한다.
      • (read / write 시스템 콜을 쓰는 경우에는 copy해서 전달을 하기 때문에 위와 같은 문제는 없다)
      • (프로세스 A가 먼저 이(Physical memory 검은색 블럭 표시) 내용을 가지고 카피를 해갔으면, A는 복제본을 가지고 있는 것이기 때문에 일관성 문제가 발생하지 않고, 운영체제가 항상 중간에서 중재를 하기 때문)

맨 위로 이동하기

Leave a comment