VFS 3 - 파일시스템 제어 흐름 분석

Jin Hur·2021년 8월 5일
0

[Linux] File system

목록 보기
15/22
post-thumbnail

reference: https://pages.cs.wisc.edu/~remzi/OSTEP/, 시스템 프로그래밍, 운영체제 수업(최종무 교수님)
"리눅스 커널 내부구조" / 백승재, 최종무

sys_open() with f_op


source: http://egloos.zum.com/rousalome/v/9993077

파일 이름을 인자로 sys_open()이 호출되면 파일시스템은 요청된 파일에 대한 inode를 찾는다. 그리고 task_struct와 VFS의 객체를 연결한다.
아래 그림은 sys_open()이 커널에서 처리되는 제어 흐름이다.


source: https://heotory.tistory.com/8

먼저 sys_open() 함수는 filp_open()이라는 커널 내부 함수를 호출한다. filp_open() 함수는 인자로 전달된 파일 이름과 디렉터리 구조를 이용해 그 파일에 대응하는 아이노드 객체를 찾아내 리턴한다. 이때 파일 구조의 f_op 변수도 초기화되는데, f_op에는 각 파일의 유형에 따라 적합한 파일 연산으로 등록된다.

filp_open() => file 객체 초기화 => file->f_op->open() 호출

f_op는 struct file_operations라는 자료구조이다. 여기에는 lseek, read, write, open 등과 같은 파일 관련 함수를 나타내는 변수들로 구성된다. 이 각 변수에는 실제 연산을 수행하는 함수의 시작 주소가 등록된다.


source: https://heotory.tistory.com/8

리눅스에는 각 파일의 유형에 따라 서로 다른 파일 연산들이 구현되어 있으며, 파일이 오픈될 때 각 파일의 유형에 맞는 연산이 f_op에 등록된다. ext2 파일시스템에서는 로컬에서 파일을 읽어드리고, NFS에서는 네트워크를 통한 분산 파일시스템에서 파일을 읽을 것이다. 따라서 파일의 연산은 각 파일의 유형에 따라 서로 다른 함수로 구현되어 있다. 또한 블록 장치 파일이나 문자 장치 파일의 경우에도 파일 연산이 따로 구현되어 있다.

(블록 장치 파일) sys_open() => file->f_op->open() => blkdev_open()
(문자 장치 파일) sys_open() => file->f_op->open() => chrdev_open()

정리하자면, 사용자는 open()이라는 하나의 함수를 사용하지만 파일의 유형과 파일시스템 종류에 따라 서로 다른 함수가 호출되는 것이다.

파일 유형에 맞는 파일 연산을 f_op 변수에 등록, file->f_op->open() 함수를 호출

  • 만일 요청한 파일이 ext2에 속한 정규(regular) 파일이라면, ext2가 제공하는 open 함수 호출(사실은 NULL, 실제로 디스크 상에 존재하는 대부분의 파일 시스템은 open이 NULL).
  • NFS의 경우 nfs_file_opne() 함수가 호출되어 NFS 파일 서버와 클라이언트 간에 RPC를 위한 통로 생성.
  • 요청한 파일이 파이프인 경우(매번 언급하였듯 리눅스에서 모든 것은 파일로 다룸) fifi_open()이 호출되어 파이프를 위한 공간 할당.

    결국 특정 파일에 고유한 open() 함수 호출

특정 파일에 고유한 open 함수까지 호출하고 나면 filp_open() 함수는 리턴. 이후 sys_open() 함수는 태스크에서 현재 사용하지 않는 파일 디스크립터(fd_array)의 한 항을 할당하고, 이 항을 생성된 파일 객체를 가리키도록 설정한다. => task_struct와 VFS 객체 연결!

sys_read() with i_op


source: http://egloos.zum.com/rousalome/v/10003475

sys_read() 함수는 인자로 전달된 fd를 이용해 파일 객체를 찾고, 이 구조의 f_op에 등록된 read 함수를 호출한다. 특정 파일 유형에 고유한 read 함수를 호출하는 것이다. 아래 그림은 장치 파일이나 pipe가 아닌 (정규) 파일을 읽는 경우에 대한 분석 그림이다.


source: https://heotory.tistory.com/8

그림에 specific file layer와 specific FS layer가 구분되어 있다. 일반적인 파일을 읽는 경우 먼저 다른 장치 파일 등과 구별되어 generic_file_read_iter() 함수가 호출된다.

generic_file_read_iter() 함수는 먼저 요청한 데이터가 페이지 캐시(커널 내부 캐시)에 있는지 찾는다. 있다면 캐시에서 바로 데이터를 제공하면 되는 것이고, 없다면 디스크에서 데이터를 읽어 와야 한다. 이 디스크에서 데이터를 읽어오는 과정은 각 파일시스템마다 다르다.

결국 각 파일시스템은 서로 다른 디스크 연산 함수 사용하기에 generic_file_read_iter() 함수는 이것을 파악하여 현재 읽기 요청중인 파일이 속한 파일시스템에 맞는 디스크 연산 함수를 호출한다.

디스크 연산 함수를 호출할 수 있도록 지원하기 위해 사용되는 자료구조가 바로 아이노드 객체의 i_op가 기리키고 있는 inode_operations 자료구조이다. inode와 관련된 연산인 create, lookup, link, mkdir, mknod, readpage 등의 연산을 나타내는 변수들로 구성되어 있고, 각 변수에는 각 연산을 수행하는 함수의 시작 주소가 등록된다.

마치 file_operations에 각 파일에 고유한 연산이 등록되었듯, inode_operations에는 각 파일시스템에 고유한 연산들이 등록되어 있다.
ex) 요청한 파일이 ext2 파일시스템에 속한 것이라면 ext2_readpage() 함수가 호출된다. 이 함수는 디스크에 저장되어 있는 ext2의 inode 구조를 이용하여 데이터를 디스크에서 읽을 것이다.

결국 f_op와 i_op는 각각 특정 파일에 대한 고유한 연산, 특정 파일시스템에 고유한 연산으로 제어를 전달하는 진입점(entry point) 역할을 담담한다.

나만의 새로운 파일을 만들고, 이 파일을 접근할 때 호출하야 하는 함수를 직접 지정할 수도 있다. 가장 간단한 방법이 바로 디바이스 드라이버이다. 리눅스에 새로운 장치를 위한 장치 파일을 연결하려면 이를 위한 새로운 file_operations 구조를 작성하여 커널에 등록해야 하는데, 이것이 바로 디바이스 드라이버에서 가능하다.

만약 새로운 파일시스템을 리눅스와 연결시키려면 file_operations 구조뿐 아니라 inode_operations 구조도 작성하여 커널에 등록해야 한다. 그리고 이들을 커널에 등록하기 위한 커널 내부 함수가 register_filesystem()이다.
이 함수는 file_system_type이라는 자료구조를 인자로 받는다. 이 자료구조에는 파일 시스템 이름을 나타내는 name(ex. ext2, msdos 등), 속성을 위한 fs_flag(파일시스템이 실제 물리적 장치가 필요한지, 읽기 전용인지 등의 정보), 수퍼 블록을 읽어 파티션을 마운트하는 함수의 포인터를 담기위한 mount 등의 필드가 있다.

register_filesystem()을 이용해 커널에 등록된 파일시스템은 하나의 file_system_type 자료구조를 갖게 되며, 커널에 존재하는 모든 file_system_type 구조들은 리스트로 연결된다. 그리고 이 리스트의 시작은 file_systems라는 커널 내 전역 변수가 가리킨다.

특정 파일시스템에 대한 마운트가 요청되면, 커널은 file_systems에서 시작되는 리스트를 검색하여 요청된 파일시스템의 file_system_type 자료구조를 찾는다. 그리고 get_sb(struct file_system_type에 존재하는 필드, 함수 포인터 저장)에 기록된 함수를 호출하여 파일시스템의 수퍼 블록 정보를 얻어와 VFS의 수퍼 블록 객체에 저장해 놓는다. 수퍼 블록을 읽으면 해당 파일시스템의 자세한 정보를 얻을 수 있다. 이에 따라 파일시스템이 제공하는 inode_operations나 file_operations 같은 구조를 접근할 수 있게 된다.

결국 파일시스템을 구현하는 과정은 수퍼 블록과 관련된 수퍼 블록 연산, inode_operations 구조체와 관련된 연산, file_operations 구조체와 관련된 연산 등을 작성하는 것이다.

VFS 내부구조

source: https://heotory.tistory.com/8

태스크는 시스템 콜이라는 인터페이스를 통해 VFS와 통신한다. 태스크가 원하는 내용은 커널 내부의 캐시인 캐시 페이지에 존재할 수도, 존재 않을수도 있다. 존재한다면 캐시에서 바로 제공해줄 수 있으며, 그렇지 않다면 실제 I/O가 일어난다(물론 태스크는 open 시 옵션을 주어 캐시를 거치지 않을 수도 있다.). 이 때 디엔트리 객체와 아이노드 객체를 위한 캐시를 일반 데이터 블록을 위한 캐시 공간과 구분하여 관리함으로써 성능 향상을 꾀한다.
VFS는 앞서 본 것과 같이 태스크와 VFS내에 존재하는 객체의 연결 관계를 이용하여 적절한 inode_operations 구조체와 file_operations 구조체 함수를 호출한다. 따라서 마운트되어 사용중인 파일시스템은 가상화되어 사용자에게 일관된 인터페이스(read(), write() 등)로 보이게 된다.

파일시스템이 관리하고 있는 공간은 커널내의 일반 블록 계층(Generic Block Layer)의 gendisk와 연관이 있다. 실제 디스크 I/O 요청을 보내야 한다.

0개의 댓글