ARM Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

197239
1625
172594


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

파일 객체: 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(가상 파일시스템)

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


# Reference: For more information on 'Linux Kernel';

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 1

디버깅을 통해 배우는 리눅스 커널의 구조와 원리. 2




핑백

덧글

  • 나그네 2020/02/18 12:45 # 삭제 답글

    안녕하세요 좋은 글 감사합니다.
    올려주신 글을 읽다보니 궁금한게 하나 생겼는데

    파일 f1을 서로 다른 프로세스 a, b에서
    fd = open(append)함수로 연 후에
    write 함수로 4kbyte 이상의 내용한 한번의 호출로 동시에 쓰게 되면 신기하게 중간에 섞인다거나 내용이 사라지지 않더라구요

    서로 다른 프로세스에서 f1파일을 열었으니 fd가 가르키는
    시스템 파일 테이블의 파일이 다를텐데

    어떻게 내용이 삭제되거나 섞이지도 않는지 알수 있을까요?

    write 함수가 inode테이블에서 lock 건다는 글을 본건 같은데...

    세부적인 내용은 알수가 없겠더라구요...







  • AustinKim 2020/02/18 13:29 #

    답글이 길어질 것 같아 새로운 포스팅을 올렸습니다.
    링크는 다음과 같습니다.
    http://rousalome.egloos.com/10011465

    감사합니다.

    Thanks,
    Austin Kim
  • 나그네2 2020/04/02 19:48 # 삭제 답글

    안녕하세요.
    VFS를 공부하는데 많은 도움을 받고 있습니다. 감사합니다.

    질문이 하나 있습니다.
    파일 오퍼레이션 함수를 실행하는 코드를 보면
    file->f_op->write 멤버가 유효한 함수 주소를 가리키고 있지 않을때
    file->f_op->write_iter 멤버가 유효한 함수 주소를 가리키고 있다면 write_iter를 수행하게 되어있는데요

    이건 어떤 케이스에서 발생할 수 있는 일인가요?
    왜 write멤버에 함수 주소를 넣어주지 않고 굳이 write_iter라는 친구가 등장한것인지,
    _iter라는 것이 어떤 의미인지 궁금합니다.

  • AustinKim 2020/04/03 09:12 #

    write_iter 필드는 AIO(Asynchnorous I/O)를 인터페이스를 지원하기 위해 추가됐으며, 관련 커밋과 링크는 다음과 같습니다.

    https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=293bc9822fa9b3c9d4b7893bcb241e085580771a
    http://git.emacinc.com/Linux-Kernel/linux-emac/commit/293bc9822fa9b3c9d4b7893bcb241e085580771a

    참고로, ext4 파일 시스템에서 write 필드 대신 write_iter 필드에 ext4_file_write_iter() 함수를 지정하는 커밋 정보는 아래와 같습니다.
    https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=9b884164d59707216840159d45f6be68073fac6e

    AIO는 유저 공간의 POSIX와 연계 되어 저장매체에 데이터를 저장하는 파일 시스템을 위한 가상 파일 시스템의 인터페이스입니다.
    쉽게 설명을 드리면, 저장 매체에 데이터는 비동기적(Asynchnorous)으로 저장됩니다. 이를 지원하는 파일 시스템을 위해 가상 파일 시스템에서 write_iter/read_iter를 지원한다고 볼 수 있습니다.
댓글 입력 영역