커널 스터디(iamroot 18기) 5주차 내용 정리 #2, 가상 파일 시스템

문연수·2021년 7월 29일
0

iamroot (Linux Internal)

목록 보기
11/24

14. 가상 파일 시스템 (Virtual File System)

리눅스 커널은 파일 시스템과 태스크 사이에 가상적(virtual) 층을 도입하였다. 이 가상의 층은 서로 다른 파일 시스템을 추상화하여 통일화된 인터페이스를 제공한다. 따라서 프로그래머는 현재 리눅스에서 어떤 파일 시스템을 사용하는지 신경 쓰지 않고 open(), read(), write(), close() 와 같은 POSIX API 함수를 호출할 수 있다. 이러한 가상의 층을 가상 파일 시스템(VFS, Virtual File System) 이라 부른다.

15. 가상 파일 시스템 객체

VFS 는 네 개의 객체(Object) 를 정의하며, 사용자 태스크에게 제공할 일관된 인터페이스를 정의하였다.

수퍼 블록(Super block) 객체

각 파일 시스템은 자신이 관리하고 있는 파티션에 고유한 정보를 수퍼 블록에 저장한다. VFS 는 이를 읽어서 관리하기 위해 범용적인 구조체인 수퍼 블록 객체를 정의한다.

아이노드(inode) 객체

특정 파일과 관련된 정보를 담기 위한 구조체이다. VFS 가 아이노드 객체를 생성하고 파일 시스템이 특정 파일에 대한 정보를 요청하면 실제 파일 시스템은 자신이 관리하고 있는 영역에서 파일의 메타 데이터를 읽어 아이노드 객체에 채워 넣는다.

파일(File) 객체

두 개의 태스크가 한 개의 파일을 동시에 접근한다면 각 태스크마다 접근하는 파일의 오프셋은 서로 다르게 유지되어야 할 것이다. 이러한 정보를 위해 각 태스크는 아이노드 객체를 접근하는 동안 메모리에 파일 객체를 생성해둔다.

디엔트리(Dentry) 객체

태스크가 파일에 접근하려면 해당 파일의 아이노드 객체를 자신의 태스크와 연관된 객체인 파일 객체에 연결시켜야 한다. 이러한 연결을 조금 더 빠르게 만들기 위한 일종의 캐시 역할을 수행한다.

16. 태스크 구조와 VFS 객체

태스크가 파일 시스템 마운트를 요청하면 VFS 는 파일 시스템의 마운트 함수를 호출하면서 인자로 빈 수퍼블록 객체를 하나 넘긴다. 파일 시스템을 자체적으로 구현한 내부 함수를 이용하여, 파티션 앞 부분에 기록해두었단 수퍼 블록 구조체를 읽은 뒤 이를 바탕으로 VFS 가 넘긴 수퍼 블록 객체의 내용을 채워서 반환한다.

file_struct 구조체

file_struct 에는 fd_arrays 라는 이름의 변수가 있고, 유닉스 계열의 운영체제에서는 일반적으로 이것을 fd, 또는 file descriptor 라고 부른다. fd_array 의 각 항목은 파일 객체(struct file) 을 가리킨다. 유닉스 계열 운영체제에서는 이 자료구조를 흔히 파일 테이블이라 부른다.

file 객체

파일 객체에는 여러가지 변수가 있으며 이는 아래와 같다:

  • f_dentry - 디엔트리 객체를 가리킨다. 디엔트리 객체는 다시 inode 를 가리킨다. 이때 디엔트리 객체는 캐시의 역할을 한다.
  • f_pos - 현재 파일에서 읽거나 쓴 위치를 나타낸다.
  • f_op - file_operations 라는 자료구조를 가리키는 포인터이다. 파일 유형에 적합한 파일 연산들이 해당 자료구조에 등록된다.

inode 객체

아이노드 객체는 파일당 하나씩 주어지며 아래와 같은 정보를 가진다:

  • i_dev - inode 가 실제 존재하고 있는 파일 시스템의 위치를 나타낸다.
  • i_rdev - 파일이 장치 파일인 경우 관련되어 있는 디바이스 주번호를 나타낸다.
  • i_ino - inode 의 고유한 번호를 나타낸다.
  • i_mode - inode 가 관리하는 파일의 속성과 접근제어 정보를 유지한다.
  • i_nlink - inode 를 가리키는 파일의 수
  • i_size - inode 객체에 해당하는 파일 크기
  • i_op - inode_operations 객체를 가리키는 포인터이다. 사용자가 파일 시스템의 메타 데이터와 관련된 연산을 요청하면 커널이 적절한 파일 시스템 고유(filesystem specific) 한 함수를 사용하여 서비스를 제공하는데 그때 사용되는 객체이다.

17. 파일 시스템 흐름 분석

파일 이름을 인자로 sys_open() 이 호출되면 파일 시스템은 요청된 파일에 대한 inode 를 찾는다. 그 과정은 아래와 같다:

sys_open()

  1. sys_open()filp_open() 커널 내부 구조 함수를 호출한다.
  2. filp_open() 함수는 인자로 전달된 파일 이름과 디렉터리 구조를 통해 대응하는 아이노드 객체를 찾아 반환한다.
  3. filp_open() 은 파일 유형에 따라 적합한 파일 연산을 f_op 에 등록한다.
  4. f_op 에 등록된 open() 함수를 호출한다.
  5. 특정 파일의 고유한 open() 함수의 호출이 끝나면 filp_open() 함수는 반환된다.
  6. sys_open() 함수는 현재 태스크가 사용하지 않는 파일 디스크립터(fd_array) 의 한 항을 할당한다. 이 항은 생성된 파일 객체를 가리킨다.

sys_read()

  1. 인자로 전달된 fd 를 이용해 파일 객체를 찾는다.
  2. f_op 에 등록된 read() 함수를 호출한다.
  3. 여러가지 파일이 존재하지만 일반적인 파일(e.g. 텍스트 파일)을 읽게 되면 generic_file_read_iter() 가 호출된다. 호출되는 함수는 파일 유형마다 달라질 수 있다.
  4. 요청한 데이터가 페이지 캐시에 있는지 확인한다.
    => 있다면? 캐시에 있는 데이터를 바로 사용자에게 전달한다.
  5. 없다면? 파일이 속한 파일 시스템에 맞는 디스크 연산 함수를 호출하기 위해 inode 객체에 i_op (struct inode_operations)에 등록된 적절한 함수를 호출한다.

18. 새로운 파일 시스템의 등록

새로운 파일 시스템의 등록은 register_filesystem() 함수 호출을 통해 가능하다. 위 함수는 struct filesystem_type 을 인자로 받고 해당 구조체는 아래의 멤버 변수를 가진다:

  • name - 파일 시스템의 이름
  • fs_flags - 파일 시스템의 속성
  • mount - 수퍼 블록을 읽어 파티션을 마운트하기 위한 포인터
  • etc - 기타 등등

커널 내의 존재하는 모든 file_system_type 구조들은 리스트로 연결되며, 이 리스트의 시작은 file_systems 라는 커널 내 전역 변수가 가리킨다.

특정 파일 시스템에 대한 마운트가 요청되면, 커널은 file_systems 에서 요청된 파일 시스템을 찾는다.

get_sb 에 기록된 함수를 호출하여 파일 시스템의 수퍼블록 정보를 얻어 온 후 VFS 에 수퍼 블록 객체를 저장해둔다.

수퍼 블록을 읽음으로 앞서 살펴본 inode_operations, file_operations 와 같은 자세한 정보를 가져올 수 있게 된다.

19. 인터럽트 (Interrupt)

인터럽트(interrupt) 란 주변장치(peripheral device) 와 커널이 통신하는 방식 중 하나로, 주변 장치와 CPU 가 자신에게 발생한 사건을 리눅스 커널에게 알리는 매커니즘이다.

인터럽트는 원인에 따라 아래와 같이 두 개로 나뉜다:

  1. 외부 인터럽트: 현재 수행 중인 태스크와는 무관한 주변 장치에서 발생한 비동기의 하드웨어적 사건을 의미한다.
  2. 트랩: 현재 수행 중인 태스크와 관련있는, 동기적으로 발생하는 소프트웨어적 사건이다.

인터럽트가 발생하면 운영체제의 적절한 인터럽트 핸들러가 수행된다. 이러한 인터럽트 핸들러는 미리 정해진 특정 번지에 기록(보통 여기에 분기 명령어(branch instruction) 를 기록) 해둔다. 인터럽트가 발생하면 CPU 는 특정 번지에 등록된 명령어를 수행하는데 여기에 분기 명령어를 작성하여 적절한 인터럽트 핸들러가 호출될 수 있게 한다. 이를 보통 IDT (Interrupt Descriptor Table), IVT (Interrupt Vector Table) 이라 부른다.

리눅스는 외부 인터럽트와 트랩을 동일하게 처리한다. 이러한 인터럽트를 처리하기 위한 함수를 구현한 뒤, 각 함수의 시작 주소를 리눅스의 IDTidt_table 이라는 이름의 배열에 등록하면, 다양한 CPU 에서도 커널 내부 구조 수정 없이 인터럽트 처리가 가능하다.

  • 0~31 (32개) : CPU 트랩 핸들러로 사용.
  • 나머지: 외부 인터럽트로 사용

20. 외부 인터럽트

외부 인터럽트를 발생시킬 수 있는 하드웨어는 PIC (Programmable Interrupt Controller) 라는 칩의 각 핀에 연결되어 있고, PICCPU 에 하나의 핀으로 연결된다.

외부 인터럽트를 발생시킬 수 있는 라인은 한정적이기 때문에 독점해서 사용해선 안되며, 장치를 관리하는 디바이스 드라이버들은 인터럽트라는 귀중한 자원을 동적으로 할당하고 해제하기도 한다.

리눅스 커널은 128 을 제외한 (이는 시스템 호출을 위해 예약) 32 ~ 255 까지의 idt_table 의 각 원소에 모두 동일한 do_IRQ 인터럽트 핸들러를 등록한다. do_IRQ 함수는 외부 인터럽트 번호를 통해 irq_desc 테이블을 인덱싱 하여 외부 인터럽트 번호와 연관된 irq_desc_t 자료구조를 찾는다.

21. 문맥 교환 (Context Switching)

인터럽트는 언제 발생되는지 알 수 없는 비동기적 사건이므로 인터럽트 핸들러를 호출하기 전에 현재 태스크의 정보(문맥 저장, context save)를 저장하고, 인터럽트 처리가 끝나면 태스크의 정보를 복구(문맥 복구, context load)한다. 이를 문맥 교환 혹은 문맨 전환(context switching) 이라 한다.

22. 트랩

리눅스에서 관리하는 인터럽트의 한 종류인 트랩은 아래와 같이 구분된다:

  1. fault - 리눅스 커널은 fault 를 일으킨 명령어 주소를 eip 에 넣어두었다가 해당 핸들러가 종료되면 다시 eip 에 저장되어 있는 주소부터 다시 실행한다.
  2. trap - 리눅스 커널은 trap 을 일으킨 명령어의 다음 주소를 eip 에 넣어 두었다가 그 다음부터 다시 수행한다.
  3. abort - 이는 심각한 에러에 해당하므로 eip 에 값을 저장하지 않고 현재 태스크를 바로 종료시킨다.

출처

[책] 리눅스 커널: 내부구조 (백승제, 최종무 저)
[이미지] https://www.usenix.org/legacy/publications/library/proceedings/usenix01/full_papers/kroeger/kroeger_html/node8.html

profile
2000.11.30

0개의 댓글