强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Unix 设计哲学教程 / 第 9 章:内核设计

第 9 章:内核设计

“The kernel is the core of the operating system. Everything else is a user.”

Unix 内核是整个操作系统的基石。它管理硬件资源,为用户空间程序提供统一的抽象接口。本章深入探讨 Unix 内核的设计哲学——从系统调用到进程管理,从虚拟文件系统到模块化架构。


9.1 内核架构概述

宏内核 vs 微内核

操作系统内核架构对比

宏内核(Monolithic Kernel)          微内核(Microkernel)
┌─────────────────────────┐    ┌─────────────────────────┐
│  用户空间               │    │  用户空间               │
│  ┌──┐ ┌──┐ ┌──┐ ┌──┐  │    │  ┌──┐ ┌──┐ ┌──┐ ┌──┐  │
│  │文件│ │网络│ │驱动│ │进程│  │    │  │文件│ │网络│ │驱动│ │进程│  │
│  └──┘ └──┘ └──┘ └──┘  │    │  └──┘ └──┘ └──┘ └──┘  │
├─────────────────────────┤    ├─────────────────────────┤
│  内核空间(全部在一起)  │    │  内核空间(极小)       │
│  ┌─────────────────────┐│    │  ┌─────────────────────┐│
│  │VFS│网络│驱动│调度│IPC││    │  │  IPC│调度│内存管理  ││
│  └─────────────────────┘│    │  └─────────────────────┘│
└─────────────────────────┘    └─────────────────────────┘
  Linux, FreeBSD                 Mach, QNX, seL4
维度宏内核微内核
性能高(直接函数调用)较低(IPC 开销)
稳定性一个驱动崩溃可能影响整个内核驱动崩溃不影响内核
代码量大(数百万行)小(数万行)
扩展性模块化加载服务级别扩展
安全性攻击面大攻击面小

Linux 的折中方案

Linux 采用宏内核 + 可加载模块的混合架构:

┌─────────────────────────────────────┐
│           用户空间                    │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐       │
│  │bash│ │vim │ │nginx│ │app │       │
│  └────┘ └────┘ └────┘ └────┘       │
├─────────────────────────────────────┤
│           系统调用接口               │
├─────────────────────────────────────┤
│           内核空间                    │
│  ┌─────────────────────────────────┐│
│  │ VFS │ 进程调度 │ 网络栈 │ 内存管理││
│  ├─────────────────────────────────┤│
│  │ 设备驱动 │ 文件系统 │ 安全模块   ││
│  │ (可动态加载的模块)               ││
│  ├─────────────────────────────────┤│
│  │           硬件抽象层              ││
│  └─────────────────────────────────┘│
├─────────────────────────────────────┤
│           硬件                       │
│  ┌────┐ ┌────┐ ┌────┐ ┌────┐       │
│  │ CPU│ │内存│ │磁盘│ │网卡│       │
│  └────┘ └────┘ └────┘ └────┘       │
└─────────────────────────────────────┘
# 查看内核模块
lsmod | head -20

# 加载/卸载内核模块
sudo modprobe vfat          # 加载 VFAT 文件系统模块
sudo rmmod vfat             # 卸载模块

# 查看模块信息
modinfo ext4

# 查看内核配置
zcat /proc/config.gz | grep CONFIG_MODULES  # 部分发行版
cat /boot/config-$(uname -r) | grep CONFIG_MODULES

9.2 系统调用(System Call)

系统调用接口

系统调用是用户空间程序与内核交互的唯一正规途径。

用户空间 → 内核空间的转换过程

用户程序: read(fd, buf, count)
C 库 (glibc): 封装系统调用
系统调用号加载到寄存器
触发软中断/特殊指令 (syscall/sysenter)
CPU 切换到内核模式
内核: sys_read() 执行实际操作
返回用户空间

核心系统调用

/* 进程管理 */
pid_t fork(void);              // 创建子进程
int execve(const char *path, char *const argv[], char *const envp[]);
pid_t waitpid(pid_t pid, int *status, int options);
void _exit(int status);
pid_t getpid(void);
pid_t getppid(void);

/* 文件 I/O */
int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
off_t lseek(int fd, off_t offset, int whence);
int close(int fd);
int dup(int oldfd);
int dup2(int oldfd, int newfd);

/* 文件系统 */
int stat(const char *pathname, struct stat *statbuf);
int mkdir(const char *pathname, mode_t mode);
int unlink(const char *pathname);
int rename(const char *oldpath, const char *newpath);
int chmod(const char *pathname, mode_t mode);
int chown(const char *pathname, uid_t owner, gid_t group);

/* 进程间通信 */
int pipe(int pipefd[2]);
int socketpair(int domain, int type, int protocol, int sv[2]);
int shmget(key_t key, size_t size, int shmflg);  // 共享内存
int semget(key_t key, int nsems, int semflg);      // 信号量
int msgget(key_t key, int msgflg);                 // 消息队列

/* 信号 */
int kill(pid_t pid, int sig);
sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

/* 内存管理 */
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
int mprotect(void *addr, size_t length, int prot);
void *sbrk(intptr_t increment);

系统调用数量

# 查看系统支持的系统调用数量
ausyscall --dump | wc -l    # 需要安装 auditd
# 或
cat /usr/include/asm-generic/unistd.h | grep __NR_ | wc -l

# Linux x86_64 约有 300+ 个系统调用
# 对比:Windows 有数千个 Win32 API
# Unix 的"少即是美"——用少量系统调用组合实现复杂功能

strace:追踪系统调用

# strace 是调试 Unix 程序的利器

# 基本用法
strace ls /tmp

# 统计系统调用
strace -c ls /tmp
# % time     seconds  usecs/call     calls    errors syscall
# ------ ----------- ----------- --------- --------- --------
#  45.23    0.001234          10       123           read
#  30.12    0.000823           5       164           write
#  ...

# 跟踪特定系统调用
strace -e trace=open,read,write ls /tmp

# 跟踪正在运行的进程
strace -p PID

# 跟踪子进程
strace -f command

# 过滤输出
strace -e trace=file ls /tmp       # 只显示文件相关
strace -e trace=network curl url   # 只显示网络相关
strace -e trace=process bash -c "echo hello"  # 只显示进程相关

# 输出到文件
strace -o trace.log ls /tmp

# 时间戳
strace -t ls /tmp                  # 显示时间
strace -T ls /tmp                  # 显示每个调用的耗时
# strace 实战:调试"为什么程序找不到配置文件"
strace -e trace=open,openat myapp 2>&1 | grep -i conf
# openat(AT_FDCWD, "/etc/myapp/config.ini", O_RDONLY) = -1 ENOENT (No such file or directory)
# openat(AT_FDCWD, "/home/user/.myapp/config.ini", O_RDONLY) = -1 ENOENT
# openat(AT_FDCWD, "./config.ini", O_RDONLY) = -1 ENOENT
# → 程序在三个位置查找配置文件,但都不存在

9.3 进程管理

进程的生命周期

进程状态转换图

  ┌──────────┐
  │  创建    │ fork()
  │ (created)│
  └────┬─────┘
  ┌──────────┐    调度器选择    ┌──────────┐
  │  就绪    │ ──────────────→ │  运行    │
  │ (ready)  │                 │(running) │
  └──────────┘                 └────┬─────┘
       ▲                            │
       │          时间片用完         │
       └────────────────────────────┘
                    ┌────────────────┼────────────────┐
                    │                │                │
                    ▼                ▼                ▼
             ┌──────────┐    ┌──────────┐    ┌──────────┐
             │  等待    │    │  僵尸    │    │  终止    │
             │(waiting) │    │ (zombie) │    │ (dead)   │
             └──────────┘    └──────────┘    └──────────┘
             I/O 完成/信号到达
             ┌──────────┐
             │  就绪    │
             │ (ready)  │
             └──────────┘

fork + exec 模型

/* Unix 进程创建的经典模型:fork + exec */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        /* fork 失败 */
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        /* 子进程 */
        printf("子进程 PID: %d, 父进程 PID: %d\n", getpid(), getppid());
        execlp("ls", "ls", "-la", NULL);
        /* 如果 exec 返回,说明执行失败 */
        perror("exec");
        exit(1);
    } else {
        /* 父进程 */
        int status;
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("子进程以退出码 %d 结束\n", WEXITSTATUS(status));
        }
    }

    return 0;
}

进程查看与管理

# 查看所有进程
ps aux                          # BSD 风格
ps -ef                          # System V 风格

# 进程树
pstree                          # 树形显示
ps auxf                         # 带缩进的森林视图

# 实时进程监控
top                             # 经典工具
htop                            # 增强版(需安装)
atop                            # 高级系统和进程监控

# 查看特定进程
ps -p 1 -o pid,ppid,comm,args   # 查看 PID 1 的详细信息
ls -la /proc/1/exe              # 查看进程的可执行文件
cat /proc/1/cmdline | tr '\0' ' '  # 查看进程的完整命令行
cat /proc/1/environ | tr '\0' '\n' # 查看进程的环境变量

# 进程优先级
nice -n 10 command              # 以较低优先级运行
renice -n 5 -p PID              # 修改运行中进程的优先级
ionice -c 2 -n 4 -p PID         # 修改 I/O 调度优先级

# 发送信号
kill PID                        # 发送 SIGTERM(优雅终止)
kill -9 PID                     # 发送 SIGKILL(强制终止)
kill -HUP PID                   # 发送 SIGHUP(通常用于重新加载配置)
killall name                    # 按名称发送信号
pkill pattern                   # 按模式发送信号

进程关系

进程关系
├── 父子关系 —— fork 创建的进程继承父进程的属性
├── 进程组 —— 相关进程的集合(如管道中的命令)
├── 会话 —— 一个登录会话中的所有进程组
├── 控制终端 —— 会话关联的终端
└── 守护进程 —— 脱离控制终端,在后台运行
# 查看进程关系
ps -eo pid,ppid,pgid,sid,tty,comm

# 创建守护进程
# nohup 使进程忽略 SIGHUP
nohup command &

# 使用 systemd 管理守护进程
systemctl start nginx
systemctl status nginx
systemctl stop nginx

# 进程的生命周期管理
# 1. 前台进程: 直接在终端运行
# 2. 后台进程: command &
# 3. 脱离终端: nohup, disown
# 4. 守护进程: systemd, supervisord

9.4 虚拟文件系统(VFS)

VFS 的作用

虚拟文件系统(Virtual File System)是内核中的一个抽象层,它使得不同的文件系统实现可以使用统一的接口。

VFS 在文件系统栈中的位置

  用户程序: open("/home/user/file.txt", O_RDONLY)
  ┌─────────────────────────────────┐
  │     VFS (虚拟文件系统层)         │
  │  统一接口: open/read/write/close │
  ├─────────────────────────────────┤
  │     具体文件系统实现             │
  │  ┌──────┐ ┌──────┐ ┌──────┐    │
  │  │ ext4 │ │ xfs  │ │ btrfs│    │
  │  └──────┘ └──────┘ └──────┘    │
  │  ┌──────┐ ┌──────┐ ┌──────┐    │
  │  │ nfs  │ │ tmpfs│ │ proc │    │
  │  └──────┘ └──────┘ └──────┘    │
  ├─────────────────────────────────┤
  │     块设备层                     │
  │  ┌──────┐ ┌──────┐ ┌──────┐    │
  │  │ sda  │ │ nvme │ │ loop │    │
  │  └──────┘ └──────┘ └──────┘    │
  └─────────────────────────────────┘

VFS 的核心对象

/* VFS 的四个核心对象 */

struct super_block;    /* 超级块 —— 文件系统元数据 */
struct inode;          /* 索引节点 —— 文件元数据(权限、大小、时间戳) */
struct dentry;         /* 目录项 —— 文件名到 inode 的映射 */
struct file;           /* 文件对象 —— 打开文件的实例(读写位置等) */

/* 关系:
 * super_block 1:N inode(一个文件系统有多个文件)
 * inode 1:N dentry(一个文件可以有多个名字,即硬链接)
 * inode 1:N file(一个文件可以被多次打开)
 */

Linux 支持的文件系统

# 查看内核支持的文件系统
cat /proc/filesystems
# nodev   sysfs
# nodev   tmpfs
# nodev   bdev
# nodev   proc
# nodev   devtmpfs
#         ext3
#         ext4
#         xfs
#         btrfs
# ...

# 查看当前挂载的文件系统
df -Th
# Filesystem     Type   Size  Used Avail Use% Mounted on
# /dev/sda1      ext4   100G   45G   50G  48% /
# tmpfs          tmpfs  7.8G     0  7.8G   0% /dev/shm
# /dev/sdb1      xfs    500G  200G  300G  40% /data
文件系统特点适用场景
ext4稳定可靠,广泛支持通用 Linux 系统
XFS高性能,大文件优化数据库、大文件存储
Btrfs快照、压缩、校验NAS、备份
ZFS企业级特性,数据完整性服务器、存储系统
tmpfs内存文件系统/tmp, /dev/shm
procfs虚拟,进程信息/proc
sysfs虚拟,设备信息/sys
overlayfs联合文件系统Docker 容器
FUSE用户空间文件系统sshfs, ntfs-3g

9.5 内存管理

虚拟内存

虚拟内存的工作原理

每个进程看到的地址空间(虚拟地址)
┌──────────────────┐ 高地址
│     内核空间      │ (对用户不可见)
├──────────────────┤
│     栈 (Stack)    │ ↓ 向下增长
│         ↓        │
│                  │
│    空闲区域      │
│                  │
│         ↑        │
│     堆 (Heap)    │ ↑ 向上增长
├──────────────────┤
│   BSS (未初始化)  │
├──────────────────┤
│   数据 (已初始化) │
├──────────────────┤
│   代码 (Text)     │
└──────────────────┘ 低地址

通过页表映射到物理内存
虚拟地址 → 页表 → 物理地址
# 查看进程的内存映射
cat /proc/1/maps
# 或
pmap PID

# 查看系统内存使用
free -h
#               total        used        free      shared  buff/cache   available
# Mem:           15Gi       8.2Gi       2.1Gi       512Mi       5.0Gi       6.5Gi
# Swap:          8.0Gi       1.0Gi       7.0Gi

# 查看虚拟内存统计
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
#  1  0 1048576 2150400 512000 5120000  0    0     5    10  500 1000  5  2 92  1

# 查看 OOM Killer 日志
dmesg | grep -i "out of memory"
journalctl -k | grep -i "oom"

9.6 设备驱动

字符设备与块设备

/* 设备驱动的基本结构 */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

static dev_t dev_num;
static struct cdev my_cdev;

/* 文件操作接口 —— 与用户空间相同! */
static int my_open(struct inode *inode, struct file *filp) {
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *pos) {
    /* 实现读取逻辑 */
    return count;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *pos) {
    /* 实现写入逻辑 */
    return count;
}

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open  = my_open,
    .read  = my_read,
    .write = my_write,
};

static int __init my_init(void) {
    alloc_chrdev_region(&dev_num, 0, 1, "mydevice");
    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, dev_num, 1);
    return 0;
}

static void __exit my_exit(void) {
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
# 查看设备信息
ls -la /dev/ | head -20
cat /proc/devices            # 已注册的字符和块设备
lspci                        # PCI 设备列表
lsusb                        # USB 设备列表
lshw                         # 硬件信息(需 root)

9.7 内核的可扩展性

可加载内核模块(LKM)

# 内核模块允许在运行时扩展内核功能
# 无需重新编译整个内核

# 查看已加载的模块
lsmod
# Module                  Size  Used by
# nvidia              35438592  0
# ext4                 1048576  1
# ...

# 加载模块
sudo modprobe nvidia

# 卸载模块
sudo rmmod nvidia

# 查看模块信息
modinfo nvidia

# 模块参数
cat /sys/module/ext4/parameters/

# 写一个简单的内核模块
cat > hello.c << 'EOF'
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void) {
    printk(KERN_INFO "Hello, Kernel!\n");
    return 0;
}

static void __exit hello_exit(void) {
    printk(KERN_INFO "Goodbye, Kernel!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Hello World Kernel Module");
EOF

# Makefile
cat > Makefile << 'EOF'
obj-m += hello.o
all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
EOF

make
sudo insmod hello.ko
dmesg | tail -5
sudo rmmod hello

9.8 内核参数调优

sysctl 接口

# 查看所有内核参数
sysctl -a | wc -l  # 通常有数千个参数

# 查看特定参数
sysctl net.ipv4.ip_forward
sysctl vm.swappiness
sysctl kernel.pid_max

# 临时修改
sudo sysctl -w net.ipv4.ip_forward=1

# 持久化修改
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

# 或使用 /etc/sysctl.d/ 目录
cat > /etc/sysctl.d/99-custom.conf << 'EOF'
# 网络优化
net.core.somaxconn = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fastopen = 3
net.ipv4.ip_local_port_range = 1024 65535

# 内存优化
vm.swappiness = 10
vm.overcommit_memory = 1
vm.dirty_ratio = 20

# 文件系统
fs.file-max = 2097152
fs.inotify.max_user_watches = 524288

# 安全
kernel.randomize_va_space = 2
net.ipv4.conf.all.rp_filter = 1
EOF

sudo sysctl --system

常用内核参数

参数含义推荐值(服务器)
vm.swappiness交换倾向10
vm.dirty_ratio脏页比率20
net.core.somaxconnTCP 监听队列长度65535
net.ipv4.tcp_tw_reuseTIME_WAIT 重用1
net.ipv4.tcp_fin_timeoutFIN 超时时间15
fs.file-max最大文件描述符数2097152
kernel.pid_max最大 PID4194304

注意事项

  1. 不要在生产环境随意修改内核参数:先在测试环境验证,了解参数的副作用。
  2. strace 有性能开销:不要在生产环境中对高频调用的进程使用 strace,除非必要。
  3. 内核模块是内核代码:有 bug 的内核模块可以让整个系统崩溃。编写和测试要非常谨慎。
  4. fork 不复制内存:现代 Linux 使用 Copy-on-Write(COW),fork 后父子进程共享内存页,只在写入时才复制。
  5. OOM Killer:当系统内存不足时,Linux 内核会主动杀死进程。可以通过 oom_score_adj 调整进程被杀死的优先级。

扩展阅读