본문으로 이동
  • 현재 모든 문서는 작성 중입니다. 보시는 데 불편함이 있더라도 양해 부탁드립니다.

운영체제 기본/Linux의 파일시스템/ext4의 파일 읽기: 두 판 사이의 차이

~ldmsys
ext4 메타데이터 참조 관계와 /home/ldmsys 디렉터리 블록 구조 그림 추가
읽기 완료의 기본 흐름에 집중하도록 후처리 예시 축소
 
134번째 줄: 134번째 줄:
== 읽기 완료와 mmap의 연결 ==
== 읽기 완료와 mmap의 연결 ==


장치 읽기가 끝나면 bio의 완료 함수인 <code>mpage_end_io()</code>가 실행된다. 필요한 후처리가 없다면 <code>__read_end_io()</code>가 곧바로 <code>folio_end_read()</code>를 호출해 성공 여부를 반영하고 대기자를 깨운다. 암호화나 무결성 검증이 필요한 파일에서는 해당 후처리를 거친 뒤 완료된다. 따라서 요청을 제출한 순간과 캐시의 데이터를 사용할 수 있는 순간은 같지 않다.<ref name="ext4-readpage"/>
장치 읽기가 끝나면 bio의 완료 함수인 <code>mpage_end_io()</code>가 실행된다. 필요한 후처리가 없다면 <code>__read_end_io()</code>가 곧바로 <code>folio_end_read()</code>를 호출해 성공 여부를 반영하고 대기자를 깨운다. 따라서 요청을 제출한 순간과 캐시의 데이터를 사용할 수 있는 순간은 같지 않다.<ref name="ext4-readpage"/>


준비된 데이터는 <code>filemap_read()</code>의 <code>copy_folio_to_iter()</code>를 통해 읽기 대상 버퍼로 복사된다. 공통 계층은 읽은 만큼 위치를 진행시키고 바이트 수를 반환한다. EOF에서는 요청량보다 적게 읽거나 0을 반환할 수 있으며, 장치 오류가 있으면 정상 데이터처럼 취급하지 않는다.<ref name="ext4-filemap"/>
준비된 데이터는 <code>filemap_read()</code>의 <code>copy_folio_to_iter()</code>를 통해 읽기 대상 버퍼로 복사된다. 공통 계층은 읽은 만큼 위치를 진행시키고 바이트 수를 반환한다. EOF에서는 요청량보다 적게 읽거나 0을 반환할 수 있으며, 장치 오류가 있으면 정상 데이터처럼 취급하지 않는다.<ref name="ext4-filemap"/>

2026년 10월 31일 (토) 08:56 기준 최신판

파일시스템 구현 목차 · 이전 VFS와 파일 객체 · 관련 procfs의 파일 읽기

Linux v6.12의 ext4 버퍼드 읽기는 공통 페이지 캐시를 사용한다. 파일 데이터를 장치에서 가져와야 하면 ext4의 블록 매핑 경로로 연결된다. 먼저 디스크의 메타데이터와 디렉터리 구조를 살펴보고, 파일을 연 뒤의 버퍼드 읽기 경로를 따라간다. DAX 파일과 O_DIRECT 읽기는 다른 경로로 분기하므로 여기서 설명하는 페이지 캐시 경로와 구별해야 한다.

이름과 inode, 논리 블록과 물리 블록의 구분은 파일과 입출력에서 이어진다.

ext4의 메타데이터와 데이터 위치

ext4는 저장 공간을 블록 그룹으로 나눈다. superblock은 블록 크기, 그룹당 inode 수, 지원 기능처럼 파일시스템 전체를 해석하는 정보를 담는다. 각 그룹의 group descriptor는 해당 그룹의 블록 비트맵, inode 비트맵, inode 테이블이 어디에 있는지 기록한다. 비트맵은 할당 상태를 표시하고, inode 테이블은 파일과 디렉터리의 inode 레코드를 저장한다.[1][2]

[ superblock: block size, inodes per group, features ]
                         |
                         v
             [ group descriptor table ]
                         |
                [ descriptor for g ]
                 /       |        \
                v        v         v
       [ block bitmap ]  |   [ inode table ]
                         v          |
                 [ inode bitmap ]   | block mapping
                                    v
                         [ directory / file blocks ]

이 그림은 참조 관계이며 디스크에 놓이는 고정 순서가 아니다. flex_bg가 활성화되면 여러 그룹의 비트맵과 inode 테이블을 함께 배치할 수 있다. 따라서 해당 그룹의 inode 테이블이 반드시 그 그룹 안의 정해진 오프셋에 있다고 가정하면 안 된다. 위치는 group descriptor의 주소 필드로 찾는다. [1][2]

inode 번호도 블록 주소와 다르다. inode 번호가 n이면 그룹 번호는 (n - 1) / s_inodes_per_group의 정수 몫이고, 그룹 내 inode 슬롯 번호는 그 나머지다. 해당 그룹의 inode 테이블 시작 위치와 inode 레코드 크기인 s_inode_size를 이용하면 디스크의 레코드를 찾을 수 있다. 이름으로 파일을 찾을 때마다 비트맵을 순회하는 구조가 아니다.[3]

/home/ldmsys에서 이름으로 inode를 찾는다

디렉터리도 inode와 내용을 가진 파일이다. 다만 그 내용은 사용자 문서의 바이트가 아니라 이름과 inode 번호를 연결하는 디렉터리 엔트리다. 다음은 /home/ldmsys/a.txt를 찾는 관계다. 모두 같은 ext4 파일시스템 안에 있고 심볼릭 링크가 없는 경우를 가정한다. 실제 조회는 VFS의 이름 캐시를 활용할 수 있으며, 아래 그림이 매번 모든 블록을 장치에서 읽는다는 뜻은 아니다.

/                  directory: "home"   -> inode of /home
/home              directory: "ldmsys" -> inode D
                                           |
                                           v
                                [ inode D: /home/ldmsys ]
                                           |
                                  directory block mapping
                                           |
                                           v
                           [ directory data block ]
                             "."     -> D
                             ".."    -> inode of /home
                             "a.txt" -> F ----------------+
                                                        |
                                                        v
                                              [ inode F: a.txt ]
                                              size, mode, owner
                                                        |
                                              i_block: extents
                                              or extent tree
                                                        |
                                              logical -> physical
                                                        |
                                                        v
                                              [ file data blocks ]

여기서 D와 F는 설명용 inode 번호다. 디렉터리 엔트리는 a.txt의 파일 내용이나 데이터 블록 주소를 직접 담지 않는다. 엔트리의 inode 번호로 파일 inode를 찾고, 그 inode의 블록 매핑을 따라 파일 내용을 찾는다. extent를 사용하는 일반적인 외부 블록 저장 형태를 그렸으며, 작은 데이터를 inode 내부에 보관하는 inline data는 다른 형태다.[4][5]

filetype 기능을 사용하는 보통의 디렉터리 엔트리는 ext4_dir_entry_2 형식이다. 고정 헤더 뒤에 실제 이름 바이트가 오며, 다음 엔트리의 위치는 rec_len으로 계산한다.[4][2]

One directory entry (offsets in bytes)

+0           +4        +6        +7         +8
|            |         |         |          |
v            v         v         v          v
+------------+---------+---------+----------+-----------+---------+
| inode      | rec_len | name_len| file_type| name      | padding |
| 4 bytes    | 2 bytes | 1 byte  | 1 byte   |           | / slack |
+------------+---------+---------+----------+-----------+---------+
|<------------------------- rec_len ---------------------------->|
                                                                next

"a.txt": inode=F, rec_len=16, name_len=5, file_type=1

[ header: 8 bytes ][ a . t x t ][ padding: 3 bytes ] -> next entry

Directory leaf block:
[ entry ][ entry ][ entry ... ][ final entry + slack ][ tail* ]
  +------> +------>              rec_len

그림의 tail*은 메타데이터 체크섬을 사용할 때 리프 블록 끝에 붙는 체크섬 꼬리 레코드다. name_len은 파일 이름의 바이트 수다. 문자 수나 전체 레코드 크기가 아니며, 이름을 널 문자로 끝내야 하는 형식도 아니다. rec_len은 헤더, 이름, 정렬 패딩과 추가 여유 공간을 포함하는 레코드 길이로 4바이트 단위다. 따라서 같은 이름이라도 rec_len은 더 클 수 있고, 위의 16은 가능한 짧은 예일 뿐이다. 엔트리는 블록 경계를 넘지 않는다. file_type = 1은 일반 파일, 2는 디렉터리를 나타낸다.[4]

큰 디렉터리는 이름의 해시로 탐색 범위를 좁히는 htree 인덱스를 사용할 수 있다. 이 경우 모든 블록을 위와 같은 단순 엔트리 배열로 해석하지 않으며, 인덱스를 통해 도착한 리프 블록에서 실제 이름과 inode 번호를 확인한다. 어느 방식이든 이름 조회가 파일 inode로 연결된 뒤에는 이 문서의 읽기 경로를 따라 파일 내용에 접근한다.[4]

VFS에서 ext4의 읽기 함수로

ext4는 블록 장치에 파일의 내용과 메타데이터를 저장하는 파일시스템이다. inode는 파일의 크기, 종류와 데이터 위치를 관리하는 파일별 객체다. 경로를 찾아 파일을 여는 과정이 끝나면, VFS의 열린 파일 객체인 struct file이 해당 inode와 파일 연산을 가리킨다. 파일 디스크립터가 이 객체로 이어지는 과정은 VFS와 파일 객체에서 다룬다.

ext4 일반 파일의 ext4_file_operations에는 다음 읽기 함수가 등록되어 있다.[6]

.read_iter = ext4_file_read_iter,

VFS가 이 함수를 호출해도 ext4가 곧바로 디스크를 읽는 것은 아니다. ext4_file_read_iter()는 DAX와 직접 입출력 분기를 제외한 읽기를 generic_file_read_iter()에 맡긴다. 이 함수는 공통 버퍼드 읽기 함수인 filemap_read()로 이어진다. 파일시스템별 진입점과 실제 데이터 입출력 사이에 공통 페이지 캐시 계층이 있는 구조다.[6][7]

캐시에 있으면 재사용하고 없으면 채운다

페이지 캐시는 파일 내용의 일부를 메모리에 보관한다. filemap_read()는 file->f_mapping으로 접근하는 캐시에서 읽을 범위를 확보한다. 요청한 내용이 이미 준비되어 있으면 해당 데이터를 사용자 버퍼로 복사하며, 그 데이터를 얻기 위한 장치 읽기는 필요하지 않다. 캐시를 관리하는 메모리 단위인 folio의 상태와 검색 방식은 페이지 캐시에서 설명한다.

캐시에 없다고 해서 매번 read_folio만 호출되는 것은 아니다. filemap_get_pages()는 먼저 동기 선행 읽기를 시도할 수 있다. 선행 읽기인 readahead는 요청 범위와 이어질 것으로 예상되는 범위를 미리 읽는 동작이다. 이미 캐시에 있지만 읽기 작업이 진행 중이라면 완료를 기다릴 수도 있다. 필요한 데이터가 여전히 준비되지 않은 경우에는 단일 folio 읽기 경로를 사용한다. 캐시 적중 중에도 선행 읽기 표시를 만나면 뒤쪽 범위에 대한 추가 읽기를 시작할 수 있다.[7]

공통 계층은 inode의 캐시 매핑에 연결된 address_space_operations, 즉 a_ops를 통해 ext4에 데이터 공급을 요청한다. ext4는 read_folio에 ext4_read_folio()를, readahead에 ext4_readahead()를 등록한다. 일반적인 외부 데이터 블록의 읽기에서는 두 함수 모두 ext4_mpage_readpages()로 이어진다. 작은 파일의 내용을 inode 안에 담는 inline data는 별도 처리되므로 이 블록 읽기 흐름을 그대로 적용하지 않는다.[8]

파일의 위치를 장치의 위치로 바꾼다

파일의 0번째 블록과 장치의 0번째 블록은 다른 위치다. ext4는 파일 안의 논리 블록 번호를 장치상의 물리 블록 번호로 변환해야 한다. extent는 연속된 논리 블록 구간과 그에 대응하는 연속된 물리 블록 구간을 묶어 표현하는 매핑이다. 블록마다 주소를 하나씩 기록하는 대신 구간 단위로 위치를 나타낸다. extent를 사용하는 파일에서는 이 정보가 extent 트리로 관리된다.[9]

ext4_mpage_readpages()는 이미 얻은 매핑을 재사용하거나 ext4_map_blocks(NULL, inode, &map, 0)을 호출해 읽을 블록의 위치를 조회한다. 이 호출은 새 데이터 블록을 할당하는 요청이 아니다. 매핑 조회 자체도 메모리에 보관된 정보를 활용하므로, 읽을 때마다 반드시 디스크의 extent 트리를 처음부터 읽는 것은 아니다.[8]

read()
  → VFS → ext4_file_read_iter()
  → generic_file_read_iter() → filemap_read()
      ├─ 준비된 캐시 데이터 → 사용자 버퍼로 복사
      └─ 데이터 준비 필요
          → a_ops의 readahead 또는 read_folio
          → ext4_readahead() 또는 ext4_read_folio()
          → ext4_mpage_readpages()
          → ext4_map_blocks()로 위치 조회 또는 기존 매핑 재사용
          → bio 구성 → submit_bio() → 블록 계층 → 저장 장치

bio는 읽을 장치 위치와 데이터를 받을 메모리 범위를 묶는 블록 입출력 요청이다. ext4는 가능한 연속 구간을 묶어 submit_bio()로 제출한다. 반면 hole은 파일의 논리 범위에 실제 데이터 블록이 없는 구간이므로 0으로 채운다. 전부 hole인 folio는 데이터 블록을 읽지 않고 완료될 수 있다. 블록 배치가 이 경로에서 묶기 어려운 경우에는 block_read_full_folio()로 내려가는 보조 경로도 있다.[10]

읽기 완료와 mmap의 연결

장치 읽기가 끝나면 bio의 완료 함수인 mpage_end_io()가 실행된다. 필요한 후처리가 없다면 __read_end_io()가 곧바로 folio_end_read()를 호출해 성공 여부를 반영하고 대기자를 깨운다. 따라서 요청을 제출한 순간과 캐시의 데이터를 사용할 수 있는 순간은 같지 않다.[10]

준비된 데이터는 filemap_read()의 copy_folio_to_iter()를 통해 읽기 대상 버퍼로 복사된다. 공통 계층은 읽은 만큼 위치를 진행시키고 바이트 수를 반환한다. EOF에서는 요청량보다 적게 읽거나 0을 반환할 수 있으며, 장치 오류가 있으면 정상 데이터처럼 취급하지 않는다.[7]

일반 ext4 파일을 mmap()으로 읽을 때도 같은 파일의 페이지 캐시를 이용한다. 다만 사용자 버퍼로 복사하는 대신 페이지 폴트를 처리하며 파일 데이터를 프로세스의 주소 공간에 매핑한다. ext4는 매핑 연산에 ext4_file_mmap()을 등록하며, 비 DAX 경로에서 ext4_file_vm_ops를 설치한다. 이 연산표의 .fault = filemap_fault가 공통 캐시 폴트 처리로 연결된다. read()와 mmap()의 진입점은 달라도 파일 내용의 캐시는 공유한다.[6]

참고 문헌

  1. ↑ 1.0 1.1 Linux v6.12 ext4 문서, High Level Design 및 Block Group Layout.
  2. ↑ 2.0 2.1 2.2 Linux v6.12 fs/ext4/ext4.h의 ext4_group_desc, ext4_dir_entry_2.
  3. ↑ Linux v6.12 ext4 문서, Index Nodes.
  4. ↑ 4.0 4.1 4.2 4.3 Linux v6.12 ext4 문서, Directory Entries.
  5. ↑ Linux v6.12 ext4 문서, The Contents of inode.i_block.
  6. ↑ 6.0 6.1 6.2 Linux v6.12 fs/ext4/file.c의 ext4_file_operations, ext4_file_read_iter, ext4_file_mmap, ext4_file_vm_ops.
  7. ↑ 7.0 7.1 7.2 Linux v6.12 mm/filemap.c의 generic_file_read_iter, filemap_read, filemap_get_pages, filemap_fault.
  8. ↑ 8.0 8.1 Linux v6.12 fs/ext4/inode.c의 ext4_read_folio, ext4_readahead, address_space_operations 정의, ext4_map_blocks.
  9. ↑ Linux v6.12 ext4 문서, The Contents of inode.i_block의 Extent Tree.
  10. ↑ 10.0 10.1 Linux v6.12 fs/ext4/readpage.c의 ext4_mpage_readpages, mpage_end_io, __read_end_io.