Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

102258
1323
114604


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

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

유저 공간에서 lseek() 함수를 호출하면 파일 쓰기 포인터 위치를 조절할 수 있습니다. 
다음 예제 코드를 보면서 lseek() 함수 동작에 대해 살펴보겠습니다. 
1 #include <unistd.h>
2 #include <sys/types.h>
3 #include <fcntl.h>
4
5 #define FILENAME_NAME "/home/pi/sample_text.text"
6
7 int main() 
8 {
9 int fd = 0;
10 ssize_t read_buf_size;
11  off_t new_file_pos;
12
13     fd = open(FILENAME_NAME, O_RDWR);
14 new_file_pos = lseek(fd, (off_t)0, SEEK_END);
15    
16     printf("[+]file size: %s \n", new_file_pos);
17
18 close(fd);
19
20 return 0;
21 }

13번째 줄 코드는 "/home/pi/sample_text.text" 파일을 읽고 해당 파일에 대응하는 정수형 파일 디스크립터를 반환하는 동작입니다.
13     fd = open(FILENAME_NAME, O_RDWR);

다음 14번째 줄 코드를 보겠습니다.
14 new_file_pos = lseek(fd, (off_t)0, SEEK_END);

lseek() 함수를 호출할 때 세 번째 인자로 SEEK_END를 지정합니다. 파일 포인터 위치를 파일 가장 끝인 EOF로 옮기는 동작입니다. 파일의 끝인 EOF 위치는 어떤 의미일까요? 바로 바이트 단위 파일 크기를 의미합니다.

파일 포인터 위치를 설정하기 위해서 어떤 기준점이 필요하며 리눅스에서는 이를 위해 다음과 같이 3가지 종류 설정 매크로를 지원합니다.
매크로 특징
SEEK_SET 파일의 첫 번째 바이트를 시작점
SEEK_CUR 읽기/쓰기 파일 포인터의 현재 위치를 시작점
SEEK_END 파일 끝(end-of-file)을 시작점

파일 포인터를 지정하기 위한 기준 매크로는 다음 해더 파일에서 확인할 수 있습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/uapi/linux/fs.h]
#define SEEK_SET 0 /* seek relative to beginning of file */
#define SEEK_CUR 1 /* seek relative to current file position */
#define SEEK_END 2 /* seek relative to end of file */

이 함수를 실행하면 가상 파일시스템에서 어떻게 파일 포인터를 처리하는지 살펴보겠습니다.

먼저 sys_lseek() 함수 선언부를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/include/linux/syscalls.h]
asmlinkage long sys_lseek(unsigned int fd, off_t offset,
  unsigned int whence);

다음 sys_lseek() 함수에 전달하는 인자를 살펴봅시다.
인자 속성
unsigned int fd 파일 디스크립터로 정수형 인자
off_t offset 설정하려는 파일 포인터 위치
unsigned int whence 파일 포인터 기준 위치
SEEK_SET, SEEK_CUR, SEEK_END 중 하나

sys_lseek() 함수는 전달된 인자에 따라 파일 포인터 위치를 반환합니다.


ftrace 로그에서 lseek() 시스템 콜이 실행할 때 함수 호출 흐름을 소개합니다.

분석할 ftrace 로그는 다음과 같습니다.
1 RPi_VFS-878   [002] ....   173.052379: sys_enter: NR 19 (3, 0, 2, 3f0, 3e8, 7e80b204)
2 RPi_VFS-878   [002] ....   173.052382: ext4_llseek+0x14/0x40c <-SyS_lseek+0xa8/0xd8
3 RPi_VFS-878   [002] ....   173.052393: <stack trace>
4 RPi_VFS-878   [002] ....   173.052396: sys_exit: NR 19 = 20868

먼저 시스템 콜 디버깅 정보부터 확인하겠습니다.
1 RPi_VFS-878   [002] ....   173.052379: sys_enter: NR 19 (3, 0, 2, 3f0, 3e8, 7e80b204)

"NR 19"는 시스템 콜 번호를 의미합니다.
다음 해더 파일 정보와 같이 lseek에 대한 시스템 콜 번호는 19번 입니다.         
#define __NR_lseek (__NR_SYSCALL_BASE+ 19)

ftrace로 시스템 콜을 디버깅할 때 가장 중요한 정보는 시스템 콜 핸들러로 전달되는 인자값입니다.
(3, 0, 2, 3f0, 3e8, 7e80b204)

위 인자값 중 첫 번째인 3은 파일디스크립터, 0은 설정하려는 파일 포인터 위치 그리고 2는 파일 포인터 기준 위치를 의미합니다.

이 정보를 모아 유저 어플리케이션에서 다음과 같이 lseek() 함수를 호출했다고 볼 수 있습니다.
lseek(fd, 0, SEEK_END)
lseek(3, 0, 2)

다음은 sys_lseek() 시스템 콜 핸들러 실행 마무리 후 메시지입니다.
4 RPi_VFS-878   [002] ....   173.052396: sys_exit: NR 19 = 20868

위 메시지에서 20868는 파일 포인터 위치를 의미합니다.

ftrace 시스템 콜 정보를 모아서 유저 어플리케이션에서 다음과 같은 코드를 실행했음을 알 수 있습니다.
20868 = lseek(3, 0, 2);

sys_lseek() 함수 실행 흐름은 크게 3단계로 나눌 수 있습니다.

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

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

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

sys_lseek() 함수 구현부는 다음과 같습니다. 
1 SYSCALL_DEFINE3(lseek, unsigned int, fd, off_t, offset, unsigned int, whence)
2 {
3 off_t retval;
4 struct fd f = fdget_pos(fd);
5 if (!f.file)
6 return -EBADF;
7
8 retval = -EINVAL;
9 if (whence <= SEEK_MAX) {
10 loff_t res = vfs_llseek(f.file, offset, whence);
11 retval = res;
12 if (res != (loff_t)retval)
13 retval = -EOVERFLOW; /* LFS: should only happen on 32 bit platforms */
14 }
15 fdput_pos(f);
16 return retval;
17 }

1단계 동작인 4~6번째 줄 코드를 보겠습니다.
4 struct fd f = fdget_pos(fd);
5 if (!f.file)
6 return -EBADF;

4번째 줄 코드와 같이 fdget_pos() 함수를 호출해서 프로세스에 저장된 파일 객체를 읽습니다.
5번째 줄 코드에서 f.file에 파일 객체가 없으면 6번 줄 코드를 실행해서 -EBADF 정수값을 반환하며 함수 실행을 마칩니다.

EBADF 매크로는 유효하지 않은 파일 디스크립터란 의미입니다. 유저 공간에서 open() 함수를 호출해서 얻은 fd 정수값이 아닌 다른 fd 정수로 read() 함수를 호출했을 경우 6번째 줄 코드를 실행합니다. 매크로를 반환하며 함수 실행을 중단합니다.

다음은 lseek() 에 대한 파일 오퍼레이션을 실행하는 2단계 동작입니다.
9 if (whence <= SEEK_MAX) {
10 loff_t res = vfs_llseek(f.file, offset, whence);
11 retval = res;

9번째 줄 코드와 같이 파일 포인터 위치가 SEEK_MAX 보다 작으면 10~11번째 줄 코드를 실행합니다. vfs_llseek() 함수 호출로 파일별 파일 포인터 위치를 변경합니다.

이번에는 3단계 동작인 파일 객체를 저장하는 코드를 보겠습니다.
15 fdput_pos(f);
16 return retval;

15번째 줄 코드와 같이 fdput_pos() 함수 호출로 파일 객체를 저장합니다.
이후 파일 포인터 위치가 저장된 retval을 반환합니다.

VFS에서 lseek() 시스템 콜에 대한 파일 오퍼레이션을 수행하는 vfs_llseek() 함수를 볼 차례입니다.
1 loff_t vfs_llseek(struct file *file, loff_t offset, int whence)
2 {
3 loff_t (*fn)(struct file *, loff_t, int);
4
5 fn = no_llseek;
6 if (file->f_mode & FMODE_LSEEK) {
7 if (file->f_op->llseek)
8 fn = file->f_op->llseek;
9 }
10 return fn(file, offset, whence);
}

5번째 줄 코드와 같이 fn이란 함수 포인터를 no_llseek() 함수로 지정합니다.

다음 6~9번째 줄 코드를 분석하겠습니다.
6 if (file->f_mode & FMODE_LSEEK) {
7 if (file->f_op->llseek)
8 fn = file->f_op->llseek;
9 }

file->f_mode 멤버에 FMODE_LSEEK 매크로는 언제 설정할까요? 다음 do_dentry_open() 함수 첫 번째 코드와 같이 파일을 오픈할 때 저장합니다.
1 static int do_dentry_open(struct file *f,
2   struct inode *inode,
3   int (*open)(struct inode *, struct file *),
4   const struct cred *cred)
5 {
6 static const struct file_operations empty_fops = {};
7 int error;
8
9 f->f_mode = OPEN_FMODE(f->f_flags) | FMODE_LSEEK |
10 FMODE_PREAD | FMODE_PWRITE;


파일 객체 멤버 중 하나인 f_mode가 FMODE_LSEEK일 때 7~8번째 줄 코드를 실행합니다.

7~8번째 줄 코드를 보면, file->f_op->llseek 멤버에 함수 포인터가 지정돼 있으면
file->f_op->llseek 함수 포인터를 fn 포인터 변수에 저장합니다.

마지막으로 10번째 줄 코드를 볼 차례입니다.
10 return fn(file, offset, whence);

fn 포인터를 실행합니다. 파일 오퍼레이션으로 llseek()이 지정돼 있으면 해당 함수로 분기합니다. 만약 파일 오퍼레이션으로 llseek() 지정을 안했으면 no_llseek() 함수를 실행합니다.

ext4와 proc 파일시스템에서 파일 객체 함수 오퍼레이션을 점검합시다. ext4 혹은 proc 파일시스템에서 관리하는 파일을 열어서 lseek() 함수를 실행하면 어떤 함수를 실행할까요?

ext4 파일시스템인 경우 파일 오퍼레이션을 관리하는 struct file_operations 구조체에 llseek이란 멤버에 ext4_llseek() 함수를 지정합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/ext4/file.c]
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.unlocked_ioctl = ext4_ioctl,

마찬가지로 proc 파일시스템에서는 struct file_operations 구조체에 llseek이란 멤버에 proc_reg_llseek() 함수를 지정합니다.
[https://elixir.bootlin.com/linux/v4.14.70/source/fs/proc/inode.c]
static const struct file_operations proc_reg_file_ops = {
.llseek = proc_reg_llseek,
.read = proc_reg_read,
.write = proc_reg_write,

ext4 파일시스템과 같이 데이터를 하드디스크와 같은 저장매체에 저장하는 파일시스템와 시스템 정보를 출력만 해주는 proc 파일 시스템에서 파일 포인터 위치를 처리하는 함수를 따로 지정합니다.

만약 llseek 멤버에 함수를 지정하지 않는 경우를 살펴봅시다.
1  (static struct file_operations) debugfs_full_proxy_file_operations = (
2    (struct module *) owner = 0x0  
3    (loff_t (*)()) llseek = 0x0  

debufs에서 관리하는 debugfs_full_proxy_file_operations 함수 오퍼레이션의 경우 llseek 멤버가 0x0으로 지정돼 있습니다.

이 경우 vfs_llseek() 함수에서 fn 함수 포인터에 기본으로 지정한 no_llseek() 함수를 호출합니다.

다음 소절에서 유저 공간에서 fsync 시스템 콜 함수를 실행했을 때 어떤 동작을 수행하는지 점검합시다.


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

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


Reference(가상 파일시스템)

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



핑백

덧글

댓글 입력 영역