본 공부에서 다루게 되는 내용 및 리눅스 커널 버전 선정
1. Top View (가상 파일 시스템에서 NVMe 드라이버까지)
2. Source Code Level
3. 관심 있는 코드, 기여할 수 있는 부분 탐색
커널 버전 선정
Longterm 버전 중 선정하도록 한다. 현재 최신 Longterm인 6.12.19로 일단 시작해본다. 충분히 공부한 뒤에는 Mainline을 따라갈 수 있도록 한다. 처음부터 전체를 파악하지 않도록 한다. 다음 걸음을 쉽게 내딛으려면 몸과 마음을 가볍게 해야한다.
현실적으로 공부할 수 있는 시간은 주말 아침, 저녁이다.
Top View
Virtual File System - File Systems - Generic Block Layer - I/O Scheduler - Block Device Drivers
위 흐름을 correction 하고 세분화하는 작업을 한다.
1. Cache
Inode, Dentry Cache와 Data Cache가 존재
Virtual File System - Cache - File Systems - Generic Block Layer - I/O Scheduler - Block Device Drivers
정확히 Cache는 Hierarchy 상에서 필수는 아니라서 아래와 같은 그림이 더 올바르다고 할 수 있다. Cache에 존재하지 않으면 Individual File System에 접근하여 필요 데이터를 Fetch 해온다.
Virtual File System - Cache
|
File Systems
|
Generic Block Layer
|
I/O Scheduler
|
Block Device Drivers
Cache 중 Page Cache에 집중해서 알아보자.
Page Cache 사용목적: Performance Improvement
Requirements: Applications에게 Transparent 해야함. Overall Performance Improvements가 있어야 함.
- Transparent 특성: applications은 본인이 page cache가 아닌 저장 장치를 direct하게 사용하고 있다고 여긴다. page cache로 사용 중인 메모리 공간은 applications에게 "free" or "available"로 report 될 것이다 (공간이 부족해지면 page cache를 reclaim하여 메모리 공간을 확보하면 된다.).
사용 방법: main memory의 남는 공간을 사용한다. 물론 충분히 빠르다면 upper tier secondary storage를 사용할 수도 있겠다 (개인 생각).
과거에는 read/writes가 메인메모리에 비해 느리고 random access에 의한 disk seek이 어마어마한 시간을 필요로 했다. 메모리에 캐시된 데이터가 많을수록 성능은 증가했다.
이 부분에 대해서 숙고할 부분: 현재의 NVMe SSD는 Hard Disk와 Bandwidth, Latency 측면에서 많이 다르다. 현대 데이터센터는 Memory Wall에 직면해 있다. 어플리케이션들은 메모리를 점점 더 많이 사용하고, 하드웨어에 장착 가능한 메모리 용량은 메모리 사용량을 따라가지 못하고 있다. 그래서 DDR4를 장착한 CXL Expander까지 써가면서 Memory Capacity를 늘리려 한다. Page Cache로 메모리를 차지하는 Cost는 변하고 있다.
Reclaim은 얼마나 빨리 이루어지는가? Victim Selection, 탐색 등에 드는 Cost는 리눅스 커널에 반영되어 있는가. NVMe SSD 사용 시 Page Cache는 어떤 모습이어야 하는가? 현재는 어떤 모습이며 각 데이터센터는 어떻게 사용하고 있는가?
2. 세분화
Virtual File System - Cache
|
File Systems (ext4, NFS 따위)
|
Generic Block Layer
|
I/O Scheduler
|
Block Device Drivers
코드 상에서 위 세분화가 어디에 속하는지 보자. ext4 파일 시스템을 사용한다고 생각하자.
VFS
read system call --> ksys_read() --> vfs_read() --> new_sync_read() --> ext4_file_read_iter()
ext4_file_read_iter()은 아래 세 함수 중 하나를 부른다. 각 함수에 대한 설명은 아래 참고.
--> ext4_dax_read_iter()
--> ext4_dio_read_iter()
--> generic_file_read_iter()
fs/read_write.c
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (fd_file(f)) {
loff_t pos, *ppos = file_ppos(fd_file(f));
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_read(fd_file(f), buf, count, ppos);
if (ret >= 0 && ppos)
fd_file(f)->f_pos = pos;
fdput_pos(f);
}
return ret;
}
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;
-- 각 파일 시스템이 read를 지원한다면 read가 불릴 것임.
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;
}
ext4의 경우 아래와 같이 read가 file_operations에 없다.
/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,
.iopoll = iocb_bio_iopoll,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = ext4_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
.fop_flags = FOP_MMAP_SYNC | FOP_BUFFER_RASYNC |
FOP_DIO_PARALLEL_WRITE,
};
static ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
struct kiocb kiocb;
struct iov_iter iter;
ssize_t ret;
init_sync_kiocb(&kiocb, filp);
kiocb.ki_pos = (ppos ? *ppos : 0);
iov_iter_ubuf(&iter, ITER_DEST, buf, len);
-- 각 파일 시스템의 read_iter 함수를 부른다.
ret = filp->f_op->read_iter(&kiocb, &iter);
BUG_ON(ret == -EIOCBQUEUED);
if (ppos)
*ppos = kiocb.ki_pos;
return ret;
}
static ssize_t ext4_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct inode *inode = file_inode(iocb->ki_filp);
if (unlikely(ext4_forced_shutdown(inode->i_sb)))
return -EIO;
if (!iov_iter_count(to))
return 0; /* skip atime */
#ifdef CONFIG_FS_DAX
-- S_DAX를 확인: Direct Access, avoiding the page cache
-- 해당 파일의 데이터가 직접 비휘발성 메모리(NVDIMM 같은 것)에 매핑됨. In-memory key-value store에서 고성능을 위해 사용된다고 함. DAX 사용을 위해서는 하드웨어도 지원해야함.
if (IS_DAX(inode))
return ext4_dax_read_iter(iocb, to);
#endif
-- IOCB: I/O Control Block
-- IOCB_DIRECT: 이것도 page cache 안 쓰는 건데, 어플리케이션 버퍼와 storage 사이에서 데이터가 전송됨. 대용량 데이터를 순차적으로 처리할 때 성능 향상 기대. 어플리케이션 버퍼가 특정 align 및 size 요구사항을 만족해야함.
if (iocb->ki_flags & IOCB_DIRECT)
return ext4_dio_read_iter(iocb, to);
-- 둘 다 지원하지 않으면 아래 함수를 탄다.
return generic_file_read_iter(iocb, to);
}
각 함수에 대해 탐색할 것
먼저 generic_file_read_iter()를 보자.
참고 사이트
- https://elixir.bootlin.com/linux/v6.12.19/source
- https://linux-kernel-labs.github.io
- https://en.wikipedia.org/wiki/Page_cache
- https://manpages.debian.org/bookworm/procps/free.1.en.html
댓글 없음:
댓글 쓰기