Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


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

파일 객체: write 연산 세부 동작 분석

유저 공간에서 write() 함수를 호출할 때 가상 파일시스템에서 어떤 흐름으로 파일 별 write 오퍼레이션을 수행하는지 살펴보겠습니다.

유저 공간에서 리눅스 저수준 함수로 write() 함수를 호출하면 시스템 콜을 발생시켜 커널 공간으로 실행 흐름을 스위칭합니다. 이 후 write()에 해당하는 시스템 콜 핸들러인 sys_write() 함수를 호출합니다.

먼저 sys_write() 함수 선언부와 인자와 반환값을 확인하겠습니다.
[https://elixir.bootlin.com/linux/v4.19.30/source/include/linux/syscalls.h]
asmlinkage long sys_write(unsigned int fd, const char __user *buf,
  size_t count);

먼저 함수에 전달하는 인자를 알아보겠습니다.
인자 속성
unsigned int fd 파일을 생성하거나 오픈했을 때 획득한 파일 디스크립터(정수형)
const char __user *buf 파일에 쓰려고 하는 내용이 저장된 버퍼 주소
size_t count buf에 있는 데이터 중에 실제 파일로 저장하려는 버퍼 사이즈

다음은 라즈베리파이에서 추출한 ftrace 로그로 시스템 콜 핸들러 sys_write() 함수를 호출할 때 메시지입니다.
1 lxterminal-794   [000] ....   172.191995: sys_enter: NR 4 (15, 111040c, 8, 1, 8, 111040c)
2 lxterminal-794   [000] ....   172.192002: ext4_file_write_iter+0x14/0x448 <-__vfs_write+0xe4/0x13c
3 lxterminal-794   [000] ....   172.192018: <stack trace>
4 => SyS_write+0x4c/0xa0
5 => __sys_trace_return+0x0/0x10
6 lxterminal-794   [000] ....   172.192121: sys_exit: NR 4 = 8

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

1번째 줄 메시지를 보겠습니다.
1 lxterminal-794 [000] .... 172.191995: sys_enter: NR 4 (15, 111040c, 8, 1, 8, 111040c)

"NR 4" 이란 정보로 write() 함수 시스템 콜 번호가 4번이란 사실을 알 수 있습니다.
두 번째로 유심히 봐야할 정보는 다음과 같이 sys_write() 함수로 전달된 인자입니다.
(15, 111040c, 8, 1, 8, 111040c)

15번은 open() 함수를 호출할 때 생성했던 파일 디스크립터 번호(정수형)를 의미합니다.

다음 6번째 줄 로그를 살펴 봅시다.  
6 lxterminal-794   [000] ....   172.192121: sys_exit: NR 4 = 8

눈여겨볼 로그는 "NR 4 = 8"입니다. 파일에 쓴 내용의 크기를 의미합니다.

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

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


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

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

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

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

sys_write() 함수 구현부를 보겠습니다.
1 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
2 size_t, count)
3 {
4 struct fd f = fdget_pos(fd);
5 ssize_t ret = -EBADF;
6
7 if (f.file) {
8 loff_t pos = file_pos_read(f.file);
9 ret = vfs_write(f.file, buf, count, &pos);
10 if (ret >= 0)
11 file_pos_write(f.file, pos);
12 fdput_pos(f);
13 }
14
15 return ret;
16 }

1단계인 파일 객체를 읽는 함수부터 점검합시다.

4번째 줄 코드를 보겠습니다.
4 struct fd f = fdget_pos(fd);

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

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로 파일 객체를 저장합니다.

2단계인 파일 객체로 파일별 지정한 write() 함수 포인터를 실행하는 코드를 보겠습니다.
8 loff_t pos = file_pos_read(f.file);
9 ret = vfs_write(f.file, buf, count, &pos);

8번째 줄 코드를 보겠습니다.
file_pos_read() 함수를 호출해서 파일 포인터를 위치를 읽습니다. 

다음 9번째 줄 코드에서 vfs_write() 함수를 실행해서 파일별로 지정한 write() 함수 포인터를 실행합니다.

3단계 흐름입니다.
10 if (ret >= 0)
11 file_pos_write(f.file, pos);
12 fdput_pos(f);

파일 쓰기 실행이 제대로 완료되면 ret는 0보다 크므로 11번째 줄 코드를 실행합니다. file_pos_write() 함수를 실행해서 파일 포인터 위치를 갱신합니다.

다음 12번째 줄 코드를 봅시다.
fdput_pos() 함수를 호출해서 파일 객체인 f를 저장합니다.

여기까지 sys_write() 함수 실행 흐름은 간략히 알아봤습니다. 이제부터 각 단계별 실행 코드를 조금 더 자세히 살펴봅시다.

이번에는 1단계 코드 흐름을 조금 더 자세히 짚어 보겠습니다.
1 SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
2 size_t, count)
3 {
4 struct fd f = fdget_pos(fd);

fdget_pos() 함수를 호출해서 프로세스의 파일 디스크립터 테이블에 접근하는 실행 흐름은 다음과 같습니다.

프로세스 태스크 디스크립터에서 파일 디스크립터 테이블을 어떻게 로딩할까요?
프로세스에서 파일을 관리하는 current->files에 접근 후 파일 디스크립터를 로딩합니다.

__fget_light() 함수 3번째 줄 코드를 보면 current->files 포인터를 struct files_struct 구조체인 files 포인터에 저장합니다
1 static inline struct file *__fcheck_files(struct files_struct *files, unsigned int fd)
2 {
3 struct fdtable *fdt = rcu_dereference_raw(files->fdt);
4
5 if (fd < fdt->max_fds) {
6 fd = array_index_nospec(fd, fdt->max_fds);
7 return rcu_dereference_raw(fdt->fd[fd]);
8 }
9 return NULL;
10 }

7번째 줄 코드를 보면 current->files->fdt->fd[fd]에 접근해서 struct file 구조체인 파일 객체를 반환합니다.

2단계로 write 파일 오퍼레이션 관련 코드를 더 상세히 보겠습니다.
1 ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
2 {
3 ssize_t ret;
4
5 if (!(file->f_mode & FMODE_WRITE))
6 return -EBADF;
7 if (!(file->f_mode & FMODE_CAN_WRITE))
8 return -EINVAL;
..
9 ret = rw_verify_area(WRITE, file, pos, count);
10 if (!ret) {
11 if (count > MAX_RW_COUNT)
12 count =  MAX_RW_COUNT;
13 file_start_write(file);
14 ret = __vfs_write(file, buf, count, pos);
15 if (ret > 0) {
16 fsnotify_modify(file);
17 add_wchar(current, ret);
18 }
19 inc_syscw(current);
20 file_end_write(file);
21 }
22
23 return ret;
24 }

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

다음 14번째 줄에서 __vfs_write() 함수를 호출합니다.
10 if (!ret) {
11 if (count > MAX_RW_COUNT)
12 count =  MAX_RW_COUNT;
13 file_start_write(file);
14 ret = __vfs_write(file, buf, count, pos);

이번에는 _vfs_write() 함수 코드를 보겠습니다.
1 ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
2     loff_t *pos)
3 {
4 if (file->f_op->write)
5 return file->f_op->write(file, p, count, pos);
6 else if (file->f_op->write_iter)
7 return new_sync_write(file, p, count, pos);
8 else
9 return -EINVAL;
10}

파일 객체의 핵심 함수 흐름 중 하나입니다. 

file->f_op->write 멤버가 유효한 함수 주소를 가르킬 경우 실행합니다.
struct file 구조체 멤버 중인 f_op에 접근해서 write란 포인터가 가르키는 주소를 호출합니다.

이를 함수 포인터라고 하며 가상 파일 시스템의 핵심인 함수 포인터로 함수를 호출하는 패턴입니다.

3단계로 파일 포인터를 저장하는 코드를 보겠습니다.
1 static inline void file_pos_write(struct file *file, loff_t pos)
2 {
3 file->f_pos = pos;
4 }

파일 객체인 struct file 구조에서 파일 포인터 정보는 f_pos멤버가 관리합니다.
이 멤버에 파일 포인터 주소를 저장하는 것입니다.


파일 포인터 위치는 제어하는 동작은lseek() 함수 분석에서 더 자세히 다룹니다.

이번 소절에서는 파일을 쓸 때 가상 파일시스템 함수에서 어떤 흐름으로 파일별 함수를 호출하는지 알아봤습니다. 다음 소절에서는 read() 파일 함수 오퍼레이션 동작에 대해 알아봅시다.

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

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


Reference(가상 파일시스템)

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

핑백

덧글

댓글 입력 영역