원래는 페이지 캐시를 정리하려고 했는데
정리하다보니 read/write 매커니즘을 정리할 수밖에 없었다. 이 글에선 read_iter()가 어떻게 동작한는지 간단하게 알아본다.
관련 글
이 글은 가상 파일시스템에 관해 다루므로 VFS가 처음이라면 아래 글을 읽어보자.
자료구조
파일 입출력에서 알아볼 자료구조는 크게 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()와 다른게 거의 없어서 생략한다.
참고 문서
'Kernel' 카테고리의 다른 글
Page Cache: write (0) | 2022.03.09 |
---|---|
Page Cache: filemap_read (3) | 2022.02.09 |
Direct Memory Access API (1) | 2021.12.29 |
[Linux Kernel] 리눅스는 얼마나 작아질 수 있을까? (1) | 2021.12.05 |
[Paper] When Poll is Better than Interrupt (0) | 2021.11.06 |
댓글