Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널][가상파일시스템] 파일 객체: read() 함수 연산 세부 동작 분석 13. Virtual Filesystem

파일 객체: read() 함수 연산 세부 동작 분석

유저 공간에서 read() 함수를 호출할 때 커널에서는 가상 파일시스템 관련 코드가 실행하는지 알아봅시다.

다음 그림과 같이 유저 공간에서 read() 함수를 호출하면 파일시스템별로 관리하는 파일객체에서 파일 오퍼레이션을 수행하는 방식입니다.
 

위 그림을 보면 유저 공간에서 read() 함수를 호출하면 각 파일시스템별 파일 오퍼레이션에 따라 다른 함수를 실행한다는 사실을 알 수 있습니다.

유저 공간에서 read() 함수를 호출하면 시스템 콜을 발생시키고 커널 공간에서 read() 함수에 해당하는 시스템 콜 핸들러인 sys_read() 함수를 실행합니다.

이제부터 sys_read() 함수부터 각 파일시스템 내 파일 종류별로 설정한 vfs_read() 함수까지 실행 흐름을 알아보겠습니다. 

먼저 sys_read() 함수를 분석하겠습니다. sys_read() 함수를 살펴보기 전에 함수 선언부를 소개합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count);

sys_read() 함수로 전달되는 인자의 의미는 다음 테이블에서 확인할 수 있습니다.
인자 속성
unsigned int fd 파일을 생성하거나 오픈했을 때 획득한 파일 디스크립터(정수형)
const char __user *buf 파일에서 읽어들일 내용을 저장하기 위한 버퍼 주소
size_t count 읽어들일 파일 내용 사이즈

sys_read() 함수를 호출해서 지정한 버퍼로 파일 내용을 제대로 읽으면 읽은 파일 내용 크기를 반환합니다. 이외의 경우 마이너스로 된 에러 매크로를 반환합니다.


다음은 라즈베리파이에서 추출한 ftrace 로그로 시스템 콜 핸들러 sys_read() 함수를 호출할 때 메시지입니다.
1 lxterminal-794 [000] .... 173.055558: sys_enter: NR 3 (15, 1051211, 1def, 1000, 1051000, 76f625bc)
2 lxterminal-794 [000] .... 173.055567: ext4_file_read_iter+0x10/0x54 <-__vfs_read+0xe4/0x134
3 lxterminal-794 [000] .... 173.055576: <stack trace>
4  => SyS_read+0x4c/0xa0
5 => __sys_trace_return+0x0/0x10
6 lxterminal-794   [000] ....   173.055580: sys_exit: NR 3 = 35

위 메시지는 1번째와 6번째 메시지에서 시스템 콜 동작을 출력하고, 2~5번째 메시지에서는 ext4_file_read_iter() 함수를 호출할 때 함수 호출 흐름을 표현합니다.

먼저 시스템 콜 동작을 먼저 보겠습니다.

1번째 줄 메시지를 보겠습니다.
1 lxterminal-794 [000] .... 173.055558: sys_enter: NR 3 (6, 1051211, 1def, 1000, 1051000, 76f625bc)

"NR 3" 이란 정보로 read() 함수 시스템 콜 번호가 3번이란 사실을 알 수 있습니다.
두 번째로 유심히 봐야할 정보는 다음과 같이 sys_read() 함수로 전달된 인자입니다.
(15, 1051211, 1def, 1000, 1051000, 76f625bc)

괄호로 된 인자 중 가장 왼쪽에 있는 15는 open() 함수를 호출할 때 생성했던 파일 디스크립터 번호(정수형)를 의미합니다.

6번째 줄 메시지에서 눈여겨볼 로그는 "NR 3 = 35"입니다.  
6 lxterminal-794   [000] ....   173.055580: sys_exit: NR 3 = 35

읽어들인 파일 내용의 크기를 의미합니다.

ftrace 시스템 콜 디버깅 정보를 요약하면 15이란 파일디스크립터로 35바이트만큼 파일을 읽었다는 사실을 알 수 있습니다.

2~5번째 줄은 ext4_file_read_iter() 함수가 실행할 때 함수 호출 흐름(콜스택)입니다.


sys_read () 함수 동작은 3단계로 분류할 수 있습니다.

1단계: 파일 객체 읽기
프로세스의 파일 디스크립터 테이블에서 파일 디스크립터에 해당하는 파일 객체를 읽습니다.

2단계: 파일 오퍼레이션 실행 
파일별 지정한 read() 함수 포인터 실행합니다.

3단계: 파일 포인터 위치 정보를 갱신합니다.
버퍼에 설정한 버퍼 사이즈만큼 읽기 동작을 수행했으니 파일 포인터 위치를 이동합니다.

반복하면 유저 공간에서 리눅스 저수준 함수로 read() 함수를 호출하면 커널 공간으로 실행 흐름을 이동한 후 read()에 해당하는 시스템 콜 핸들러인 sys_read() 함수를 호출합니다.

이제 sys_read() 함수 상세 코드 분석을 시작하겠습니다.
1 SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
2 {
3 struct fd f = fdget_pos(fd);
4 ssize_t ret = -EBADF;
5
6 if (f.file) {
7 loff_t pos = file_pos_read(f.file);
8 ret = vfs_read(f.file, buf, count, &pos);
9 if (ret >= 0)
10 file_pos_write(f.file, pos);
11 fdput_pos(f);
12 }
13 return ret;
14 }

1단계 실행 흐름입니다. 

프로세스 태스크 디스크립터에 저장된 파일 객체를 읽는 동작입니다. 다음 3번째 줄 코드에서 해당 동작을 수행합니다.
3 struct fd f = fdget_pos(fd);

fdget_pos() 함수 호출로 정수형 파일 디스크립터인 fd에 해당하는 파일 객체(struct file)가 포함된 struct fd 구조체를 읽습니다.

6~8번째 줄 코드를 볼 차례입니다.
6 if (f.file) {
7 loff_t pos = file_pos_read(f.file);
8 ret = vfs_read(f.file, buf, count, &pos);

6번째 줄에서는 파일 객체를 저장한 f.file 멤버가 유효한지 점검을 합니다. 파일 객체를 제대로 읽지 못하면 파일을 읽을 수 없기 때문입니다.


struct fd 구조체를 보면 다음과 같습니다. 
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/file.h]
1 struct fd {
2 struct file *file;
3 unsigned int flags;
4 };

첫 번째 멤버인 file로 파일 객체를 저장합니다.


7번째 줄 코드는 f.file 이란 파일 객체 인자로 파일 포인터 위치를 pos 변수로 읽습니다.
8번째 줄 코드에서 vfs_read() 함수를 호출합니다.

9~10번째 줄 코드는 파일 쓰기 동작이 완료된 후 파일 포인터 위치를 변경하는 동작입니다.
9 if (ret >= 0)
10 file_pos_write(f.file, pos);

이번에는 vfs_read() 함수를 볼 차례입니다.
1 ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
2 {
3 ssize_t ret;
4
5 if (!(file->f_mode & FMODE_READ))
6 return -EBADF;
7 if (!(file->f_mode & FMODE_CAN_READ))
8 return -EINVAL;
9 if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
10 return -EFAULT;
11
12 ret = rw_verify_area(READ, file, pos, count);
13 if (!ret) {
14 if (count > MAX_RW_COUNT)
15 count =  MAX_RW_COUNT;
16 ret = __vfs_read(file, buf, count, pos);
...
17 }
18
19 return ret;
20 }

파일 객체인 struct file 구조체 멤버인 f_mode가 FMODE_READ 혹은 FMODE_CAN_READ가 아닐 경우 예외 처리를 수행하는 코드입니다.
5 if (!(file->f_mode & FMODE_READ))
6 return -EBADF;
7 if (!(file->f_mode & FMODE_CAN_READ))
8 return -EINVAL;

파일 객체 멤버인 f_mode로 파일 속성 모드를 점검해서 파일 읽기를 실행할 조건을 점검합니다.

다음 13~16번째 줄 코드를 보면 14번째 줄에서 __vfs_read() 함수를 호출합니다.
13 if (!ret) {
14 if (count > MAX_RW_COUNT)
15 count =  MAX_RW_COUNT;
16 ret = __vfs_read(file, buf, count, pos);

이렇게 vfs_read() 함수는 파일 객체 f_mode 속성을 읽고 예외 처리를 수행한 다음 __vfs_read() 함수를 호출합니다.

다음으로 __vfs_read() 함수 코드를 보겠습니다.
1 ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
2    loff_t *pos)
3 {
4 if (file->f_op->read)
5 return file->f_op->read(file, buf, count, pos);
6 else if (file->f_op->read_iter)
7 return new_sync_read(file, buf, count, pos);
8 else
9 return -EINVAL;
10 }

파일 객체의 핵심 함수 흐름 중 하나입니다. file->f_op->read 멤버가 유효한 함수 주소를 가르킬 경우 실행합니다. struct file 구조체 멤버 중인 f_op에 접근해서 read란 포인터가 가르키는 주소를 호출합니다.

여기까지 read() 함수 오퍼레이션까지 살펴봤습니다. 다음 소절에서는 파일 포인터 위치를 변경하는 lseek() 함수 동작을 가상 파일시스템에서 어떻게 실행하는지 알아봅시다.

"혹시 궁금한 점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답변 올려드리겠습니다!" 

Thanks,
Austin Kim(austindh.kim@gmail.com)


Reference(가상 파일시스템)

가상 파일시스템 소개
파일 객체
파일 객체 함수 오퍼레이션 동작
프로세스는 파일객체 자료구조를 어떻게 관리할까?
슈퍼블록 객체
아이노드 객체
덴트리 객체
가상 파일시스템 디버깅

핑백

덧글

댓글 입력 영역