운영체제 기본/Linux의 파일 읽기
응용 프로그램은 read(fd, buf, count)에 정수 fd를 넘긴다. 커널은 이 숫자만 보고 어떻게 파일을 찾고, 파일과 파이프에 서로 다른 읽기 동작을 적용할까? 이 장에서는 Linux v6.12의 소스에서 그 두 연결 지점만 따라간다.
Elixir 링크는 같은 버전의 파일을 가리킨다. 소스 내용은 Torvalds의 Linux 저장소 v6.12 태그와 대조했으며, Elixir에 접근하기 어려운 경우 사용할 원문 링크도 함께 적었다.
읽기 전에
파일과 입출력에서 설명한 fd와 open file description의 차이를 알고 있으면 편하다. 여기서는 일반적인 read() 경로를 살펴본다. io_uring, mmap()을 통한 접근 등 모든 입출력 경로를 설명하는 것은 아니다.
코드를 읽을 때는 먼저 다음 두 질문에 집중한다.
- 정수 fd를 어느 테이블에서 찾는가?
- 찾은 객체에서 어떤 읽기 함수를 선택하는가?
fd에서 struct file까지
fd = 3
|
+--> current->files (files_struct)
|
+--> fdt (fdtable)
|
+--> fd[3] --> struct file
current는 현재 실행 중인 태스크를 나타낸다. current->files로 접근하는 files_struct에 파일 디스크립터 테이블이 연결되어 있고, fdtable.fd는 struct file 포인터의 배열이다. 같은 fd 값도 어느 테이블에서 찾는지에 따라 다른 열린 파일을 가리킬 수 있다.
이 관계를 설명용 의사 코드로 줄이면 다음과 같다. 실제 소스의 연속된 인용이 아니며, 그대로 커널에 넣어 사용할 코드도 아니다.
files = current->files; table = files->fdt; opened_file = table->fd[fd];
실제 ksys_read()는 fdget_pos()를 통해 객체를 얻는다. 그 아래의 fdget(), __fget_light()와 조회 함수들은 참조와 동시 접근을 처리한다. 위 그림에서는 이 부분을 생략했다. 처음 읽을 때에는 필드의 관계를 먼저 확인하고, 직접 배열을 읽어도 안전한지에 관한 문제는 별도로 구분하면 된다.
볼 곳:
- include/linux/fdtable.h의
fdtable,files_struct,files_lookup_fd_raw()(원문). - fs/file.c의
__fget_light(),fdget_pos()(원문).
struct file에서 읽기 연산까지
struct file의 f_op는 연산 함수들을 모은 file_operations를 가리킨다. 파일마다 응용 프로그램의 호출 형태를 바꾸는 대신, 커널 안에서 해당 대상에 맞는 연산을 선택하는 구조다.
struct file
|
+--> f_op (file_operations)
|
+--> read
|
+--> read_iter
ksys_read()는 얻은 객체를 vfs_read()에 넘긴다. vfs_read()는 접근 모드, 사용자 버퍼 등에 관한 검사를 거쳐 읽기 구현을 선택한다. 분기만 뽑은 설명용 의사 코드는 다음과 같다.
if (opened_file->f_op->read)
opened_file->f_op->read(...);
else if (opened_file->f_op->read_iter)
new_sync_read(...);
new_sync_read()는 read_iter 호출에 필요한 요청과 버퍼 정보를 구성한다. 두 함수 포인터를 차례로 모두 호출하는 것이 아니라, 사용할 수 있는 구현에 따라 경로가 갈린다는 점을 확인하면 된다.
볼 곳:
- include/linux/fs.h의
struct file,struct file_operations(원문). - fs/read_write.c의
ksys_read(),vfs_read(),new_sync_read()(원문).
여기까지 읽고 확인할 것
응용 프로그램에는 fd와 읽기 인터페이스가 보인다. 커널 내부에서는 테이블 조회로 객체를 찾고, 연산 테이블로 구현을 선택한다. 추상화에서 설명한 인터페이스와 구현의 분리가 실제로 드러나는 지점이다.
다만 함수 포인터를 호출하는 것만으로 읽기가 끝나는 것은 아니다. 선택된 구현 아래에서 파일시스템, 캐시, 장치 드라이버 등의 처리가 이어질 수 있다. 이번에는 그 아래까지 한꺼번에 내려가기보다, fd와 struct file, file_operations가 각각 무엇을 나타내는지 구분하는 데 집중하면 된다.