본문 바로가기
Kernel

VFS: read_iter() & write_iter()

by hyeyoo 2022. 1. 24.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.

글에 사진이 없으면 심심한 법

원래는 페이지 캐시를 정리하려고 했는데

정리하다보니 read/write 매커니즘을 정리할 수밖에 없었다. 이 글에선 read_iter()가 어떻게 동작한는지 간단하게 알아본다.

관련 글

이 글은 가상 파일시스템에 관해 다루므로 VFS가 처음이라면 아래 글을 읽어보자.

 

[Linux Kernel] 가상 파일시스템이란 (VFS, Virtual Filesystem Switch)

Virtual Filesystem이란 리눅스를 사용하면 다양한 형식으로 포맷된 디스크를 사용할 수 있다. 보통 리눅스어세는 ext2, ext3, ext4를 사용하지만 윈도우에서 사용하는 NTFS나 FAT같은 디스크도 사용할 수

hyeyoo.com

자료구조

파일 입출력에서 알아볼 자료구조는 크게 kiocb와 iov_iter이다. 내가 파일시스템쪽엔 딱히 지식이 없어서 완벽하게는 설명하지 못하지만 대략적으로라도 설명해보자.

struct kiocb

struct kiocb {
        struct file             *ki_filp;

        /* The 'ki_filp' pointer is shared in a union for aio */
        randomized_struct_fields_start

        loff_t                  ki_pos;
        void (*ki_complete)(struct kiocb *iocb, long ret);
        void                    *private;
        int                     ki_flags;
        u16                     ki_hint;
        u16                     ki_ioprio; /* See linux/ioprio.h */
        struct wait_page_queue  *ki_waitq; /* for async buffered IO */
        randomized_struct_fields_end
};

우선 kiocb는 현재 열린 파일의 정보 (ki_filp)와 해당 파일에서의 offset (ki_pos), IO가 끝났을 때 실행할 ki_complete함수의 포인터를 저장한다. kiocb는 동기와 비동기 IO에 모두 사용할 수 있는데, ki_complete == NULL이면 동기로 간주한다.

struct iovec

struct iovec
{
        void *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
        __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};

iovec은 입출력에 사용할 버퍼의 정보(버퍼의 주소와 길이)를 담고 있으며, iovec이라는 이름만큼 원소 하나보다는 주로 여러 개의 버퍼를 배열로 담아서 사용한다.

iov_iter

iov_iter는 iovec (최근엔 iovec 뿐만 아니라 다양한 vector/array 자료구조 들을 표현하는 자료구조로 바뀐 것으로 보인다.)의 정보를 나타낸다. 이게 READ/WRITE를 하기 위한 것인지, iovec의 길이와 iovec 버퍼 상의 오프셋 등등의 정보를 저장한다.

struct iov_iter {
        u8 iter_type;
        bool nofault;
        bool data_source; 
        size_t iov_offset;
        size_t count;
        union {
                const struct iovec *iov;
                const struct kvec *kvec;
                const struct bio_vec *bvec;
                struct xarray *xarray; 
                struct pipe_inode_info *pipe;
        };
        union {
                unsigned long nr_segs; 
                struct {
                        unsigned int head;
                        unsigned int start_head;
                };
                loff_t xarray_start;   
        };
};

무엇이 바뀌었나

자료들을 찾아보면, 원래는 read()를 처리할 때 read() 시스템 호출의 정의처럼 fd, buf, len 정도만 받아서 단일 버퍼에 대한 입출력을 처리했었다. file_operations에 read_iter, write_iter를 도입해서 단일 버퍼가 아니라 iov_iter를 인자로 입출력을 처리할 수 있게 되면서 분산된 버퍼에 대해 system call로 인한 context switching 비용을 줄일 수 있게 되었다. 어떻게 해서 비동기쪽 코드로부터 read_iter, write_iter가 도입되었는지에 대한 상세한 히스토리와 동기는 잘 모르겠다.

파일시스템쪽 구현을 보면 흥미로운데, _iter() 버전이 도입된 이후로는 대부분 file_operations에서 read()/write()보단 {read,write}_iter()를 구현하는 추세이다.

read() 시스템 호출 코드 분석

read() in fs/read_write.c

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
        return ksys_read(fd, buf, count);
}

read()는 ksys_read()에 대한 래퍼 함수이다.

ksys_read()

ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
        struct fd f = fdget_pos(fd);
        ssize_t ret = -EBADF;

        if (f.file) {
                loff_t pos, *ppos = file_ppos(f.file);
                if (ppos) {
                        pos = *ppos;
                        ppos = &pos;
                }
                ret = vfs_read(f.file, buf, count, ppos);
                if (ret >= 0 && ppos)
                        f.file->f_pos = pos; 
                fdput_pos(f);
        }
        return ret; 
}

ksys_read는 다시 vfs_read()를 호출하고 오프셋을 갱신한다.

vfs_read()

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
        ssize_t ret;

        if (!(file->f_mode & FMODE_READ))
                return -EBADF;
        if (!(file->f_mode & FMODE_CAN_READ))
                return -EINVAL;
        if (unlikely(!access_ok(buf, count)))
                return -EFAULT;

        ret = rw_verify_area(READ, file, pos, count);
        if (ret)
                return ret;
        if (count > MAX_RW_COUNT)
                count =  MAX_RW_COUNT;

        if (file->f_op->read)
                ret = file->f_op->read(file, buf, count, pos);
        else if (file->f_op->read_iter)
                ret = new_sync_read(file, buf, count, pos);
        else
                ret = -EINVAL;
        if (ret > 0) {
                fsnotify_access(file);
                add_rchar(current, ret);
        }
        inc_syscr(current);
        return ret;
}

vfs_read()도 복잡하지는 않다. 현재 읽기 모드에서 읽기가 가능한지, 버퍼의 주소가 올바른지, 파일에 접근할 권한이 있는지 확인한 후 filesystem마다 다른 함수들을 저장하는 file_operations로부터 read()나 new_sync_read()를 호출한다. 파일시스템은 file_operations의 read()와 read_iter() 둘 중 하나만 구현하면 read() 시스템호출을 처리할 수 있다. read_iter()가 구현된 경우에는 new_sync_read()에서 이를 호출한다.

new_sync_read()

static ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
        struct iovec iov = { .iov_base = buf, .iov_len = len };
        struct kiocb kiocb;
        struct iov_iter iter;
        ssize_t ret; 

        init_sync_kiocb(&kiocb, filp);
        kiocb.ki_pos = (ppos ? *ppos : 0);
        iov_iter_init(&iter, READ, &iov, 1, len);

        ret = call_read_iter(filp, &kiocb, &iter);
        BUG_ON(ret == -EIOCBQUEUED);
        if (ppos)
                *ppos = kiocb.ki_pos;
        return ret; 
}

new_sync_read는 kiocb와 iov_iter를 초기화한 후 call_read_iter()로 read_iter()를 호출한다 read_iter()는 파일시스템에 따라서 mm/filemap.c의 generic_file_read_iter()를 그대로 사용하거나 파일시스템별로 각자 구현할 수도 있다. 내가 지금 분석중인 리눅스 환경에서는 btrfs를 쓰고있어서 btrfs_file_read_iter()가 별도로 구현되어있다. 여기선 btrfs 기준으로 설명해본다.

file_operations in fs/btrfs/file.c

const struct file_operations btrfs_file_operations = {
        .llseek         = btrfs_file_llseek,
        .read_iter      = btrfs_file_read_iter,
        .splice_read    = generic_file_splice_read,
        .write_iter     = btrfs_file_write_iter,
        .splice_write   = iter_file_splice_write,
        .mmap           = btrfs_file_mmap,
        .open           = btrfs_file_open,
        .release        = btrfs_release_file,
        .fsync          = btrfs_sync_file,
        .fallocate      = btrfs_fallocate,
        .unlocked_ioctl = btrfs_ioctl,
#ifdef CONFIG_COMPAT
        .compat_ioctl   = btrfs_compat_ioctl,
#endif
        .remap_file_range = btrfs_remap_file_range,
};

btrfs_file_read_iter()

static ssize_t btrfs_file_read_iter(struct kiocb *iocb, struct iov_iter *to) 
{
        ssize_t ret = 0; 

        if (iocb->ki_flags & IOCB_DIRECT) {
                ret = btrfs_direct_read(iocb, to); 
                if (ret < 0 || !iov_iter_count(to) ||
                    iocb->ki_pos >= i_size_read(file_inode(iocb->ki_filp)))
                        return ret; 
        }

        return filemap_read(iocb, to, ret);
}

btrfs_file_read_iter()도 코드가 매우 직관적이다. kiocb의 ki_flags에 IOCB_DIRECT가 명시된 경우 페이지 캐시를 거치지 않고 디스크에서 바로 읽어온다. 이것을 direct IO라고 하며, 어플리케이션에 파일의 내용을 관리하는 별도의 캐시가 있다면 direct IO를 사용하는게 적합하다. 그렇지 않은 경우에는 filemap_read()를 호출해 페이지 캐시에서 파일의 내용을 복사한다.

마무리

filemap은 페이지 캐시의 구조를 알아야하므로 별도의 글로 정리한다. write도 분석해볼까 했는데 read()와 다른게 거의 없어서 생략한다.

참고 문서

 

Asynchronous block loop I/O [LWN.net]

Did you know...?LWN.net is a subscriber-supported publication; we rely on subscribers to keep the entire operation going. Please help out by buying a subscription and keeping LWN on the net. By Jonathan Corbet January 30, 2013 The kernel's block loop drive

lwn.net

 

 

[Linux/Kernel] Kernel 4.6.3 new read() implementation

- Kernel 4.6.3 read() syscall internal implementation - 이번에 Kernel 버전이 4.0으로 바뀌면서 read syscall의 커널 내부 구현이 상당수 바뀌었다. 기존에 존재하던 do_sync_read함수가 없어지고 아예 new_sy..

revdev.tistory.com

 

The iov_iter interface [LWN.net]

Please consider subscribing to LWNSubscriptions are the lifeblood of LWN.net. If you appreciate this content and would like to see more of it, your subscription will help to ensure that LWN continues to thrive. Please visit this page to join up and keep LW

lwn.net

 

'Kernel' 카테고리의 다른 글

Page Cache: write  (0) 2022.03.09
Page Cache: filemap_read  (3) 2022.02.09
Direct Memory Access API  (0) 2021.12.29
[Linux Kernel] 리눅스는 얼마나 작아질 수 있을까?  (0) 2021.12.05
[Paper] When Poll is Better than Interrupt  (0) 2021.11.06

댓글