운영체제 기본/Linux의 파일시스템/페이지 캐시

파일시스템 구현 목차

기준은 Linux v6.12의 일반적인 버퍼드 읽기와 파일 기반 읽기 전용 매핑이다. 파일 읽기와 파일 메모리 매핑은 사용자에게 데이터를 전달하는 방식이 다르지만, 파일 내용을 준비하는 과정에서는 같은 캐시를 이용할 수 있다. O_DIRECT와 DAX처럼 이 경로를 우회하는 경우는 여기서 다루지 않는다.[1]

파일 내용을 RAM에 남기는 이유

저장장치의 파일을 읽을 때마다 같은 블록에 I/O를 요청하면 이미 읽었던 내용에도 다시 장치 접근 비용을 지불한다. 페이지 캐시는 파일 내용의 일부를 RAM에 보관하여 다음 접근에서 재사용하게 한다. 파일 전체가 항상 올라와 있는 것은 아니며, 필요한 부분을 준비하고 메모리가 부족하면 다시 확보할 수 있는 캐시를 회수한다. 따라서 파일이 존재한다는 사실과 그 파일의 내용이 지금 RAM에 있다는 사실은 별개다.

이 캐시는 특정 read() 호출에만 딸린 임시 버퍼가 아니다. 먼저 읽은 프로세스가 채운 내용을 나중에 다른 프로세스가 읽거나 mmap으로 접근할 때도 재사용할 수 있다. 파일시스템은 파일의 논리적 위치에 해당하는 내용을 제공하고, 공통 메모리 관리 코드는 그 내용을 캐시에 유지하고 찾는 역할을 맡는다.[2]

캐시의 기준은 프로세스 주소가 아니라 파일 안의 위치다

일반 파일의 inode->i_mapping은 struct address_space를 가리킨다. 이름에 address가 들어가지만 이것은 프로세스의 가상 주소 공간이 아니라, 여기서는 파일의 backing과 페이지 캐시를 관리하는 객체다. host는 inode를, i_pages는 인덱스로 캐시 항목을 찾는 XArray 자료구조를, a_ops는 내용을 준비하는 파일시스템 연산표를 연결한다. 열린 파일에서도 file->f_mapping으로 이 객체에 접근한다.[3]

캐시를 찾는 기준은 개념적으로 mapping과 파일 내부의 페이지 인덱스의 조합이다. 바이트 오프셋을 기본 페이지 크기로 나눈 값이 인덱스가 되고, 나머지는 그 페이지 안의 위치가 된다. 같은 파일 오프셋을 서로 다른 가상 주소에 mmap하더라도 이 캐시 식별 기준은 바뀌지 않는다. 반대로 다른 파일의 같은 오프셋은 mapping이 다르므로 같은 항목이 아니다.

v6.12 코드에서는 캐시 관리 단위로 folio가 자주 등장한다. folio는 하나 이상의 기본 페이지를 함께 관리하는 단위다. 인덱스의 단위는 여전히 기본 페이지이므로, folio 하나가 항상 인덱스 하나에만 대응한다고 생각하면 안 된다. 큰 folio는 여러 연속 인덱스를 포함할 수 있으며, 요청한 위치가 그 범위 안에 있는지 확인한다.[2]

read는 복사하고 mmap은 페이지 테이블로 연결한다

buffered read 경로의 filemap_read()는 먼저 요청 범위의 캐시 내용을 준비하고, copy_folio_to_iter()로 필요한 바이트를 사용자 버퍼에 복사한다. 사용자 버퍼는 파일 캐시와 별개의 메모리다. 이 복사가 있기 때문에 프로그램은 일반적인 read() 뒤에 자기 버퍼의 내용을 사용한다.[2]

읽기 전용 mmap은 파일 내용에 접근할 가상 주소 범위를 마련한다. 아직 페이지 테이블 연결이 없는 주소에 접근하면 fault 처리가 필요하다. filemap_fault()는 해당 파일 위치의 캐시 페이지를 확보하여 상위 fault 처리에 넘기고, 그 처리가 프로세스 페이지 테이블에 연결한다. filemap_map_pages()는 이미 준비된 주변 캐시 페이지들도 함께 매핑하여 뒤이은 fault를 줄일 수 있다. 매핑이 갖춰진 뒤의 접근은 매번 파일 읽기 함수를 호출하는 것이 아니라 페이지 테이블을 통해 진행된다.[2]

                  파일의 address_space
                (mapping, 파일 페이지 인덱스)
                           |
                    페이지 캐시 folio
                     /           \
          filemap_read()        mmap fault 처리
               |               /            \
         바이트를 복사       프로세스 A PTE   프로세스 B PTE
               |               \            /
          사용자 버퍼          같은 캐시 페이지 참조

여러 프로세스가 같은 파일 내용을 매핑할 때 캐시가 프로세스별로 복제되는 것은 아니다. 공유되는 것은 파일 데이터 페이지이고, 가상 주소와 PTE는 프로세스마다 독립적이다. 따라서 다른 프로세스가 이미 파일을 읽어 두었어도 현재 프로세스에 PTE가 없다면 fault는 발생할 수 있다. 페이지 fault와 페이지 캐시 miss는 같은 사건이 아니다.

캐시에 있다는 것과 읽을 준비가 끝났다는 것은 다르다

조회한 folio의 내용이 준비되어 있으면 요청 범위의 데이터를 바로 사용할 수 있다. 하지만 캐시에 항목이 있어도 다른 실행 흐름이 I/O를 진행 중일 수 있다. 커널은 folio의 잠금과 데이터 유효 상태를 확인하고, 필요한 경우 완료를 기다리거나 읽기를 수행한다. 항목을 찾았다는 이유만으로 아직 채워지지 않은 메모리를 사용자에게 전달하지 않는다.[2]

캐시 miss라면 공간을 마련하고 파일시스템에 내용 준비를 요청한다. 순차 접근에서는 readahead로 앞으로 필요할 범위도 미리 준비하여 다음 요청이 기다릴 가능성을 줄인다. 따라서 한 번의 사용자 읽기가 한 번의 장치 요청과 일대일로 대응하지는 않는다. 준비 과정에 오류가 나면 정상 데이터로 취급하지 않으며, read는 이미 복사한 바이트가 있는지에 따라 부분 성공이나 오류를 반환하고, mmap fault의 읽기 실패는 SIGBUS로 이어질 수 있다.[2]

캐시 miss가 반드시 데이터 블록의 장치 읽기를 뜻하지도 않는다. 예를 들어 sparse 파일의 hole은 저장된 데이터 블록을 읽는 대신 해당 범위를 0으로 채워 제공할 수 있다. 이것을 알아내는 데 필요한 메타데이터 접근과 파일 데이터 블록 읽기는 구분해야 한다.[4]

실제 내용을 채우는 책임은 파일시스템에 있다

공통 페이지 캐시 코드는 ext4의 저장 형식을 직접 해석하지 않는다. mapping->a_ops->read_folio와 readahead 같은 연산을 통해 파일시스템에 요청한다. ext4의 연산표는 이를 ext4_read_folio()와 ext4_readahead()에 연결하며, 일반적인 블록 저장 파일에서는 ext4_mpage_readpages()로 이어진다. 파일 오프셋을 저장장치의 블록과 연결하는 구체적인 과정은 ext4의 블록 매핑에서 다룬다.[5]

페이지 캐시에 변경 내용이 있다는 사실만으로 저장장치에 영속화되었다고 할 수는 없으며, 쓰기 완료와 fsync()의 구분은 운영체제 기본/파일과 입출력을 참고한다.

참고 문헌

  1. ↑ Linux 6.12 documentation, Page Cache.
  2. ↑ 2.0 2.1 2.2 2.3 2.4 2.5 Linux v6.12, mm/filemap.c, filemap_get_pages(), filemap_read(), filemap_fault(), filemap_map_pages().
  3. ↑ Linux v6.12, include/linux/fs.h, struct address_space, struct address_space_operations, struct inode, struct file.
  4. ↑ Linux v6.12, fs/ext4/readpage.c, ext4_mpage_readpages(), hole 처리와 folio_zero_segment().
  5. ↑ Linux v6.12, fs/ext4/inode.c, ext4_aops, ext4_read_folio(), ext4_readahead().