Skip to content

6. 源码分析:一个系统调用的完整链路 ​

Linux 内核源码很大,但不需要全读完。本节教你怎么抓住一条主线:从一个 read() 系统调用出发,穿过系统调用入口、VFS、page cache、文件系统,最后回到用户空间。

6.1 Linux 源码怎么读 ​

源码结构 ​

linux/
├── arch/          # 架构相关代码
├── kernel/        # 进程调度、系统调用、中断等核心
├── mm/            # 内存管理
├── fs/            # 文件系统
├── net/           # 网络
├── block/         # 块层
├── drivers/       # 设备驱动
├── include/       # 头文件
├── init/          # 初始化
└── tools/         # 工具

AI Infra 工程师读源码时,重点看:

  • kernel/sched/:调度器(含 ext.c 中的 sched_ext 框架);
  • io_uring/:异步 I/O 核心实现;
  • mm/:内存管理;
  • fs/:文件系统;
  • kernel/cgroup/:cgroup 实现。

阅读方法 ​

  1. 先用 lxr / elixir / GitHub 搜索找到入口函数;
  2. 跟着函数调用链走,不要一开始就看所有细节;
  3. 用 grep 找关键数据结构;
  4. 看注释和 commit message;
  5. 遇到看不懂的宏和条件编译先跳过,抓主线。

6.2 read() 系统调用的完整链路 ​

用户态入口:glibc ​

你的程序调用 read(fd, buf, count),glibc 会把它包装成系统调用:

c
// 简化示意
mov rax, 0        // __NR_read
mov rdi, fd
mov rsi, buf
mov rdx, count
syscall

内核态入口 ​

x86-64 上,syscall 指令会跳转到 entry_SYSCALL_64:

asm
// arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
    swapgs
    mov    %rsp, PER_CPU_VAR(rsp_scratch)
    mov    PER_CPU_VAR(cpu_current_top_of_stack), %rsp
    ...
    call   do_syscall_64

do_syscall_64 在 arch/x86/entry/common.c 中,根据 rax 里的系统调用号找到处理函数:

c
// arch/x86/entry/common.c
void do_syscall_64(struct pt_regs *regs)
{
    regs->ax = sys_call_table[regs->ax](regs);
}

sys_call_table 定义在 arch/x86/entry/syscalls/syscall_64.c。

sys_read ​

系统调用表指向 ksys_read:

c
// fs/read_write.c
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
    struct fd f = fdget(fd);
    ...
    ret = vfs_read(f.file, buf, count, &pos);
    ...
}

VFS 层 ​

vfs_read 调用具体文件系统的 read 方法:

c
// fs/read_write.c
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    ...
    ret = file->f_op->read(file, buf, count, pos);
    // 或者 aio_read / read_iter
}

文件系统层:以 ext4 为例 ​

ext4 的 read_iter 会走到 generic_file_read_iter:

c
// mm/filemap.c
ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
    ...
    if (iocb->ki_flags & IOCB_DIRECT) {
        // Direct I/O,绕过 page cache
        return generic_file_direct_read(iocb, iter, ...);
    }
    // Buffered I/O,先查 page cache
    return generic_file_buffered_read(iocb, iter, ...);
}

Page Cache ​

如果走 buffered I/O,内核会调用 pagecache_get_page 查找缓存页:

c
// mm/filemap.c
struct page *pagecache_get_page(struct address_space *mapping, pgoff_t index,
                                fgp_t fgp_mask, gfp_t gfp_mask)
{
    ...
    page = find_get_entry(mapping, index);
    if (radix_tree_exceptional_entry(page))
        ...
}

命中 page cache 就直接把数据拷贝到用户空间(copy_page_to_iter)。没命中就发起 I/O:

c
// mm/filemap.c
static int filemap_fault(...)
{
    // 分配物理页,向块层发请求,把文件内容读入 page cache
}

块层 ​

块层把文件系统的请求转换成对块设备的请求。submit_bio 是关键入口:

c
// block/blk-core.c
blk_qc_t submit_bio(struct bio *bio)
{
    ...
    return generic_make_request(bio);
}

bio 经过 I/O 调度器,最终发到设备驱动。

返回用户空间 ​

数据读入 page cache 后,内核把数据从内核空间拷贝到用户空间的 buf。这里有一次内存拷贝。

最后,ksys_read 返回读取的字节数,控制权回到 glibc,再返回给用户程序。

对比:io_uring 路径不走 do_syscall_64 分发,而是把 io_uring_enter 作为唯一系统调用入口,之后在内核中直接消费提交队列里的 SQE,完成时写入 CQE。读源码时可以从 io_uring/io_uring.c 的 io_submit_sqes() 入手。

6.3 CFS 核心数据结构 ​

sched_entity ​

每个可调度实体(进程/线程)都有一个 sched_entity:

c
// include/linux/sched.h
struct sched_entity {
    struct load_weight      load;
    struct rb_node          run_node;
    u64                     vruntime;
    u64                     exec_start;
    u64                     sum_exec_runtime;
    ...
};

cfs_rq ​

每个 CPU 的 CFS 运行队列:

c
// kernel/sched/sched.h
struct cfs_rq {
    struct load_weight load;
    unsigned long runnable_weight;
    struct rb_root_cached tasks_timeline;
    ...
};

tasks_timeline 就是按 vruntime 排序的红黑树。

调度时机 ​

schedule() 函数是调度器的总入口。它在以下时机被调用:

  • 进程主动放弃 CPU(sched_yield、sleep);
  • 时间片用完;
  • 从中断/系统调用返回时检查 TIF_NEED_RESCHED。

6.4 cgroup v2 的核心实现 ​

cgroup 文件系统 ​

cgroup v2 统一挂载到 /sys/fs/cgroup:

bash
mount -t cgroup2 none /sys/fs/cgroup

内核中,cgroup 是一个伪文件系统,类型为 cgroup2_fs_type。

cgroup 子系统 ​

每个控制器(cpu、memory、pids 等)注册到 cgroup:

c
// kernel/cgroup/cgroup.c
struct cgroup_subsys {
    const char *name;
    struct cgroup_subsys_state *(*css_alloc)(...);
    ...
};

CPU 控制器在 kernel/sched/core.c 附近与调度器交互;memory 控制器在 mm/memcontrol.c。

资源限制如何生效 ​

以 CPU quota 为例:

  1. 用户写入 cpu.max;
  2. cgroup 文件系统调用 cpu 控制器的 write_u64;
  3. 更新 cfs_bandwidth 结构;
  4. CFS 调度器在运行时检查该 cgroup 的 quota 是否用完;
  5. 用完则 throttle,让出 CPU。

6.5 sched_ext:把调度器导出成回调 ​

sched_ext 的巧妙之处在于,它没有把调度逻辑搬进 BPF,而是定义了一组调度回调(kernel/sched/ext.c):

c
// include/linux/sched/ext.h(简化)
struct sched_ext_ops {
    s32 (*select_cpu)(struct task_struct *p, s32 prev_cpu, u64 wake_flags);
    void (*enqueue)(struct task_struct *p, u64 enq_flags);
    void (*dispatch)(s32 cpu, struct task_struct *prev);
    void (*runnable)(struct task_struct *p, u64 enq_flags);
    ...
};

BPF 程序实现这些回调,框架负责与 CFS 运行队列、负载均衡、cgroup 的交互。这个“框架在内核、策略在 BPF”的分层,是理解现代内核可扩展性设计的一个好例子。2026 年这个框架继续演进:7.0 加入 deadline server,7.1/7.2 铺入 sub-scheduler 基础设施,7.3 完成 per-cgroup sub-scheduler(kernel/sched/ext.c 的相关代码值得对照版本阅读)。

6.6 阅读内核源码的建议 ​

  1. 抓主线:不要陷入每个宏和分支;
  2. 用工具:LXR、elixir.bootlin.com、GitHub 搜索;
  3. 看版本:不同内核版本差异大,确认你读的是对应版本;
  4. 配合文档:Kernel Documentation 比源码更容易入门;
  5. 动手实验:改一个内核参数或 bpftrace 脚本,验证你的理解。

6.7 本节小结 ​

  • read() 系统调用的链路:glibc → syscall → do_syscall_64 → vfs_read → ext4 → page cache → 块层 → 设备驱动;
  • CFS 核心:sched_entity、cfs_rq、红黑树、vruntime;
  • cgroup v2:统一层级、控制器注册、cfs_bandwidth 控制 CPU quota;
  • sched_ext:调度逻辑以 BPF 回调形式导出,“框架在内核、策略在 BPF”;
  • 读内核源码要抓主线,配合文档和工具,不要陷入细节。

下一节进入工程实践:用 Python 写一个 Linux 机制模拟器。

Released under CC-BY-SA-4.0 License.