title: 面试-Linux内核基础 tags: [
面试,
Linux内核,
嵌入式,
内核配置,
内核模块,
系统调用,
进程调度,
内存管理,
设备模型,
] created: 2026-09-17
本文档涵盖Linux内核基础的综合面试题,覆盖内核配置编译、模块机制、系统调用VFS、进程调度、内存管理、设备模型六大核心领域。
答案要点:
详细解答: Linux内核提供多种配置方式,各有特点:
make menuconfig:最常用,基于ncurses库的终端界面,适合SSH远程操作,支持搜索功能(按/键)make xconfig:基于Qt的图形界面,更直观,但需要图形环境make gconfig:基于GTK的图形界面make defconfig:使用当前架构的默认配置,快速生成.config文件make oldconfig:基于现有.config文件提示新选项,适合内核升级make savedefconfig:将配置压缩为最小差异,便于版本控制配置流程:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 配置完成后
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
追问:
[Y]、[M]、[N]分别代表什么含义?答案要点:
详细解答: 内核编译的基本流程:
# 1. 清理之前的编译产物
make distclean
# 2. 配置内核
make ARCH=arm menuconfig
# 3. 编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage
# 4. 编译模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
# 5. 安装模块到指定目录
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_install INSTALL_MOD_PATH=/path/to/rootfs
交叉编译关键变量:
ARCH:目标架构(arm、arm64、x86等)CROSS_COMPILE:交叉编译工具链前缀追问:
答案要点:
详细解答:
| 镜像类型 | 压缩 | 大小 | 用途 |
|---|---|---|---|
| vmlinux | 无 | 大 | 调试,包含符号表 |
| zImage | gzip | 小 | ARM常用,自解压 |
| uImage | gzip | 小 | U-Boot专用,含头部 |
| Image | 无 | 大 | arm64常用 |
U-Boot启动zImage的过程:
追问:
答案要点:
详细解答: Kbuild系统的核心组件:
Kbuild工作流程:
Kconfig → menuconfig → .config
.config + Kconfig → auto.conf
Makefile + auto.conf → 编译规则
关键Makefile变量:
obj-y += foo.o # 编译进内核
obj-m += bar.o # 编译为模块
obj-$(CONFIG_BAZ) += baz.o # 根据配置决定
追问:
答案要点:
详细解答: U-Boot设置bootargs示例:
setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootfstype=ext4
saveenv
boot
常用内核启动参数:
| 参数 | 说明 | 示例 |
|---|---|---|
| console | 控制台设备 | console=ttyS0,115200 |
| root | 根文件系统设备 | root=/dev/mmcblk0p2 |
| rootfstype | 根文件系统类型 | rootfstype=ext4 |
| rw/ro | 读写/只读挂载 | rw |
| init | 初始进程 | init=/sbin/init |
| loglevel | 日志级别 | loglevel=7 |
内核解析流程:
bootloader → cmd_line指针 → __setup()注册的处理函数
追问:
答案要点:
详细解答:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
// 模块初始化函数
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel!\n");
return 0; // 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_AUTHOR("Author");
MODULE_DESCRIPTION("A simple module");
编译Makefile:
obj-m += hello.o
KDIR := /path/to/kernel
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
追问:
__init和__exit宏的作用是什么?答案要点:
详细解答: insmod:
insmod /lib/modules/$(uname -r)/kernel/drivers/usb/core/usbcore.ko
# 必须指定完整路径
# 不会自动加载依赖模块
modprobe:
modprobe usbcore
# 在标准路径下搜索
# 自动加载依赖模块
# 支持黑名单(/etc/modprobe.d/)
依赖关系存储在/lib/modules/$(uname -r)/modules.dep文件中,由depmod命令生成。
模块加载流程对比:
insmod: 直接init_module系统调用
modprobe: 解析依赖 → 多次init_module
追问:
答案要点:
详细解答: 方式一:file_operations结构体
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.ioctl = my_ioctl,
.release = my_release,
};
// 注册字符设备
register_chrdev(240, "mydev", &fops);
方式二:proc文件系统
static int hello_proc_show(struct seq_file *m, void *v)
{
seq_printf(m, "Hello from proc\n");
return 0;
}
// 创建 /proc/hello
proc_create("hello", 0, NULL, &hello_proc_ops);
方式三:sysfs文件系统
static ssize_t my_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", my_value);
}
static DEVICE_ATTR(my_attr, 0644, my_show, my_store);
追问:
答案要点:
详细解答:
static int my_value = 10;
static char *my_string = "hello";
static int my_array[5] = {0, 1, 2, 3, 4};
module_param(my_value, int, 0644);
module_param(my_string, charp, 0644);
module_param_array(my_array, int, NULL, 0644);
加载时传递参数:
# 方式一
insmod mymodule.ko my_value=20 my_string="world"
# 方式二(通过/etc/modules-load.d/或modprobe配置)
# /etc/modprobe.d/mymodule.conf
options mymodule my_value=20 my_string="world"
参数权限说明:
| 权限值 | 说明 |
|---|---|
| 0444 | 只读 |
| 0644 | root可写,其他只读 |
| 0666 | 所有用户可读写 |
追问:
答案要点:
详细解答: 模块加载流程:
insmod → 系统调用init_module
→ copy_from_user(模块代码)
→ 验证模块签名
→ 分配module结构体
→ 解析重定位
→ 调用module_init函数
→ 模块正式加入内核
模块卸载流程:
rmmod → 系统调用delete_module
→ 检查模块引用计数
→ 调用module_exit函数
→ 注销所有接口
→ 释放内存
→ 从模块列表移除
引用计数管理:
// 增加引用
try_module_get(THIS_MODULE);
// 减少引用
module_put(THIS_MODULE);
// 查看引用计数
cat /sys/module/<module_name>/refcnt
追问:
答案要点:
详细解答: 以open系统调用为例:
用户空间:
// 应用程序调用
fd = open("/dev/mydev", O_RDWR);
// glibc封装的open函数
// 1. 设置系统调用号(如__NR_openat)
// 2. 设置参数到寄存器
// 3. 执行SVC #0指令
内核空间:
SVC指令 → 异常向量表 → el0_svc
→ 保存寄存器到pt_regs
→ 根据系统调用号索引sys_call_table
→ 执行对应的sys_xxx函数
→ 恢复寄存器,返回用户空间
ARM64架构的系统调用约定:
| 寄存器 | 用途 |
|---|---|
| x8 | 系统调用号 |
| x0-x5 | 参数 |
| x0 | 返回值 |
追问:
答案要点:
详细解答: VFS的四个核心对象:
super_block(超级块):
struct super_block {
struct super_operations *s_op; // 操作函数
struct dentry *s_root; // 根目录
unsigned long s_flags; // 挂载标志
// ...
};
inode(索引节点):
struct inode {
struct inode_operations *i_op; // 操作函数
struct file_operations *fops; // 文件操作
umode_t i_mode; // 文件类型和权限
// ...
};
dentry(目录项):
struct dentry {
struct dentry_operations *d_op;
struct inode *d_inode; // 关联的inode
struct dentry *d_parent; // 父目录
char *d_name; // 目录名
// ...
};
file(打开的文件):
struct file {
struct file_operations *f_op; // 操作函数
loff_t f_pos; // 当前位置
void *private_data; // 私有数据
// ...
};
追问:
答案要点:
详细解答: mount系统调用流程:
用户调用: mount("/dev/sda1", "/mnt", "ext4", 0, NULL)
→ 内核解析参数
→ 查找文件系统类型(find_filesystem)
→ 调用get_tree_bdev()
→ 分配super_block
→ 调用ext4_fill_super()
→ 读取磁盘上的超级块
→ 初始化内存中的super_block
→ 创建根目录dentry
→ 将挂载点与super_block关联
→ 更新挂载树
关键数据结构关系:
mount → vfsmount
→ dentry (挂载点)
→ super_block (文件系统)
追问:
答案要点:
详细解答: 设备号的组成:
设备号 = 主设备号(major) << MINORBITS | 次设备号(minor)
字符设备注册流程:
// 1. 申请设备号
alloc_chrdev_region(&devno, 0, 1, "mydev");
// 2. 初始化cdev
cdev_init(&my_cdev, &fops);
// 3. 添加到系统
cdev_add(&my_cdev, devno, 1);
// 4. 创建设备节点(用户空间)
// 内核通知 → udev/mdev创建/dev/mydev
设备号查看:
cat /proc/devices # 查看已分配的主设备号
ls -l /dev/mydev # 查看设备的major:minor
追问:
答案要点:
详细解答:
#include <linux/uaccess.h>
// 内核读取用户空间数据
static ssize_t my_read(struct file *file, char __user *buf,
size_t count, loff_t *pos)
{
char kbuf[100];
int len = sprintf(kbuf, "Hello from kernel");
// 复制到用户空间
if (copy_to_user(buf, kbuf, len))
return -EFAULT;
return len;
}
// 内核写入用户空间数据
static ssize_t my_write(struct file *file, const char __user *buf,
size_t count, loff_t *pos)
{
char kbuf[100];
// 从用户空间复制
if (copy_from_user(kbuf, buf, count))
return -EFAULT;
// 处理数据
return count;
}
注意事项:
memcpy跨越用户/内核边界-ERESTARTSYS追问:
答案要点:
详细解答: 调度器发展历程:
O(1)调度器特点:
- 使用140个优先级队列(0-139)
- 实时进程(0-99)+ 普通进程(100-139)
- 时间片计算:prio * 10ms
- 优点:固定时间复杂度
- 缺点:交互式进程响应不理想
CFS调度器特点:
- 基于虚拟运行时间(vruntime)
- 使用红黑树管理进程
- vruntime = 实际运行时间 × (NICE_0_LOAD / 权重)
- 选择vruntime最小的进程运行
- 优点:公平性好,响应及时
追问:
答案要点:
详细解答: SCHED_FIFO:
- 按照FIFO原则调度
- 高优先级可以抢占低优先级
- 同优先级不会被抢占
- 一直运行直到主动放弃、阻塞或被更高优先级抢占
SCHED_RR:
- 在SCHED_FIFO基础上增加时间片
- 同优先级进程轮转执行
- 时间片用完后放到队列尾部
- 默认时间片通常为100ms
设置实时调度策略:
struct sched_param param;
param.sched_priority = 80; // 优先级1-99
// 设置SCHED_FIFO
sched_setscheduler(pid, SCHED_FIFO, ¶m);
// 设置SCHED_RR
sched_setscheduler(pid, SCHED_RR, ¶m);
追问:
答案要点:
详细解答: 上下文切换流程:
1. 保存当前进程上下文
- 保存通用寄存器到task_struct.thread
- 保存SP、PC到thread_struct
- 保存FPU/SIMD寄存器(如果使用)
2. 切换地址空间
- 更新TTBR0寄存器(ARM64)
- 刷新TLB
3. 恢复目标进程上下文
- 恢复通用寄存器
- 恢复SP、PC
- 恢复FPU/SIMD寄存器
开销分析:
| 开销类型 | 说明 |
|---|---|
| CPU时间 | 保存/恢复寄存器,约1-10微秒 |
| 缓存失效 | L1/L2缓存被污染 |
| TLB失效 | 页表切换导致TLB刷新 |
| 调度开销 | 调度算法计算 |
追问:
答案要点:
详细解答: 调度域层级结构:
System (SMT域)
└── MC域 (多核)
└── DIE域 (同一芯片)
└── NUMA域 (跨节点)
负载均衡流程:
1. 检测负载不平衡
- 定期检查(tick中断)
- 或者新任务创建时
2. 选择迁移任务
- 查找最繁忙的组
- 选择迁移代价最小的任务
3. 执行迁移
- 将任务从繁忙CPU移到空闲CPU
- 更新负载统计
相关文件:
# 查看调度域信息
cat /proc/sys/kernel/sched_domain/cpu0/domain0/name
# 查看负载均衡统计
cat /proc/schedstat
追问:
答案要点:
详细解答: vruntime计算公式:
vruntime += delta_exec × (NICE_0_LOAD / weight)
其中:
- delta_exec:实际运行时间
- NICE_0_LOAD = 1024(基准权重)
- weight:进程权重
nice值与权重映射表:
| Nice值 | 权重 | 说明 |
|---|---|---|
| -20 | 88761 | 最高优先级 |
| -10 | 71755 | |
| 0 | 1024 | 基准 |
| 10 | 135 | |
| 19 | 15 | 最低优先级 |
示例:
进程A:nice=0,权重=1024,运行100ms
vruntime_A = 100 × (1024/1024) = 100
进程B:nice=-5,权重=3121,运行100ms
vruntime_B = 100 × (1024/3121) = 32.8
// 进程B获得更高优先级
追问:
答案要点:
详细解答: ARM64的4级页表结构(4KB页):
虚拟地址 [47:0] 48位
┌─────────┬─────────┬─────────┬─────────┬─────────┐
│ PGD[9] │ PUD[9] │ PMD[9] │ PTE[9] │Offset[12]│
└─────────┴─────────┴─────────┴─────────┴─────────┘
第4级 第3级 第2级 第1级 页内偏移
页表遍历流程:
TTBR → PGD → PUD → PMD → PTE → 物理页帧
5级页表(57位虚拟地址):
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ P4D[9] │ PGD[9] │ PUD[9] │ PMD[9] │ PTE[9] │Offset[12]│
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
页表大小对比:
| 页表级别 | 虚拟地址范围 | 最大内存 |
|---|---|---|
| 4级 | 256TB | 256TB |
| 5级 | 128PB | 128PB |
追问:
答案要点:
详细解答:
| 函数 | 物理连续 | 虚拟连续 | 适用场景 | 常用标志 |
|---|---|---|---|---|
| kmalloc | 是 | 是 | 小内存、DMA | GFP_KERNEL |
| vmalloc | 否 | 是 | 大块内存 | GFP_KERNEL |
| kzalloc | 是 | 是 | 需要清零 | GFP_KERNEL |
使用示例:
// kmalloc - 小内存分配
char *buf = kmalloc(1024, GFP_KERNEL);
if (!buf) return -ENOMEM;
// ... 使用内存 ...
kfree(buf);
// vmalloc - 大块内存
void *vbuf = vmalloc(1024 * 1024);
if (!vbuf) return -ENOMEM;
// ... 使用内存 ...
vfree(vbuf);
// kzalloc - 分配并清零
struct my_struct *ptr = kzalloc(sizeof(*ptr), GFP_KERNEL);
// ptr已清零
GFP标志说明:
| 标志 | 说明 |
|---|---|
| GFP_KERNEL | 可能睡眠,用于进程上下文 |
| GFP_ATOMIC | 不可睡眠,用于中断上下文 |
| GFP_DMA | 分配DMA可用内存 |
追问:
答案要点:
详细解答: slab分配器的设计目标:
- 减少频繁分配/释放的开销
- 消除内存碎片
- 利用硬件缓存对齐
slab分配器架构:
kmalloc
└── SLAB分配器
├── 通用缓存(kmalloc-64, kmalloc-128等)
└── 专用缓存(task_struct, inode等)
使用示例:
// 创建专用缓存
struct kmem_cache *my_cache;
my_cache = kmem_cache_create("my_struct",
sizeof(struct my_struct),
0, // 对齐
SLAB_HWCACHE_ALIGN,
NULL); // 构造函数
// 分配对象
struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 释放对象
kmem_cache_free(my_cache, obj);
// 销毁缓存
kmem_cache_destroy(my_cache);
查看slab信息:
cat /proc/slabinfo
slabtop
追问:
答案要点:
详细解答: 内存回收流程:
内存不足
→ 触发kswapd(异步回收)
→ 或直接回收(同步)
→ 尝试释放页面缓存
→ 尝试释放slab
→ 如果还不够,触发OOM
OOM killer工作原理:
1. 计算每个进程的oom_score
- 基于内存使用量、进程优先级等
2. 选择oom_score最高的进程
- /proc/<pid>/oom_score_adj可调整
3. 发送SIGKILL信号杀死进程
调整OOM保护:
// 设置进程不可被OOM杀死
echo -1000 > /proc/<pid>/oom_score_adj
// 或在代码中
oom_score_adj = -1000;
内存信息查看:
cat /proc/meminfo # 内存使用统计
vmstat # 虚拟内存统计
追问:
答案要点:
详细解答: 一致性DMA映射:
// 分配一致性DMA内存
dma_addr_t dma_handle;
void *cpu_addr;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// CPU和设备看到的数据始终一致
// 不需要手动同步
// 使用完毕
dma_free_coherent(dev, size, cpu_addr, dma_handle);
流式DMA映射:
// 映射已有缓冲区
dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
// 设备访问完后
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);
// 需要手动同步
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_TO_DEVICE);
dma_sync_single_for_device(dev, dma_handle, size, DMA_TO_DEVICE);
DMA方向说明:
| 方向 | 说明 |
|---|---|
| DMA_TO_DEVICE | 内存→设备 |
| DMA_FROM_DEVICE | 设备→内存 |
| DMA_BIDIRECTIONAL | 双向 |
追问:
答案要点:
详细解答: 设备模型架构:
┌─────────────────────────────────────────┐
│ Bus │
│ ┌──────────┐ ┌──────────┐ │
│ │ Device │ ←匹配→ │ Driver │ │
│ │ (设备信息)│ │ (操作函数)│ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────┘
核心数据结构:
struct bus_type {
const char *name;
int (*match)(struct device *dev, struct device_driver *drv);
// ...
};
struct device {
struct device *parent;
struct bus_type *bus;
struct device_driver *driver;
void *platform_data;
// ...
};
struct device_driver {
const char *name;
struct bus_type *bus;
int (*probe)(struct device *dev);
// ...
};
匹配流程:
设备注册 → 设备加入总线设备列表
驱动注册 → 驱动加入总线驱动列表
→ 遍历设备列表,调用match函数
→ 匹配成功调用probe()
追问:
答案要点:
详细解答: 设备树描述示例:
my_device@40000000 {
compatible = "vendor,mydevice";
reg = <0x40000000 0x1000>;
interrupts = <GIC_SPI 16 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk 0>;
status = "okay";
};
解析设备树的API:
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
static int my_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct resource res;
int irq;
// 解析reg属性
of_address_to_resource(np, 0, &res);
// res.start = 0x40000000
// 解析interrupts属性
irq = irq_of_parse_and_map(np, 0);
// irq = 中断号
// 解析compatible属性
const char *compatible;
of_property_read_string(np, "compatible", &compatible);
// 解析整数属性
u32 val;
of_property_read_u32(np, "property-name", &val);
}
追问:
答案要点:
详细解答: platform_device定义:
static struct platform_device my_device = {
.name = "my_platform_device",
.id = -1,
.dev = {
.platform_data = &my_data,
},
.resource = my_resources,
.num_resources = ARRAY_SIZE(my_resources),
};
// 注册平台设备
platform_device_register(&my_device);
platform_driver定义:
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_platform_device", // 或使用of_match_table
.of_match_table = my_of_match,
},
};
module_platform_driver(my_driver);
设备树匹配表:
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,mydevice" },
{ /* sentinel */ }
};
追问:
答案要点:
详细解答: 字符设备注册:
#include <linux/cdev.h>
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 0; }
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *pos) { return 0; }
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
};
static dev_t devno;
static struct cdev my_cdev;
// 注册
alloc_chrdev_region(&devno, 0, 1, "mychar");
cdev_init(&my_cdev, &fops);
cdev_add(&my_cdev, devno, 1);
块设备注册:
#include <linux/blkdev.h>
static struct block_device_operations my_bops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
};
// 注册
register_blkdev(240, "myblock");
// 添加请求队列和处理函数
追问:
答案要点:
详细解答:
| 锁类型 | 可睡眠 | 复杂度 | 适用场景 |
|---|---|---|---|
| 自旋锁 | 否 | 低 | 短临界区、中断上下文 |
| 信号量 | 是 | 低 | 长临界区、进程上下文 |
| 读写锁 | 否 | 中 | 读多写少 |
| RCU | 否 | 高 | 读极多写极少 |
自旋锁使用:
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
// 临界区
spin_unlock(&lock);
// 中断安全版本
spin_lock_irqsave(&lock, flags);
// 临界区
spin_unlock_irqrestore(&lock, flags);
信号量使用:
struct semaphore sem;
sema_init(&sem, 1); // 初始值1
down(&sem); // 获取
// 临界区(可以睡眠)
up(&sem); // 释放
读写锁使用:
rwlock_t rwlock;
rwlock_init(&rwlock);
read_lock(&rwlock);
// 读临界区
read_unlock(&rwlock);
write_lock(&rwlock);
// 写临界区
write_unlock(&rwlock);
追问:
| 领域 | 考点 | 出现频率 | 难度 |
|---|---|---|---|
| 内核配置 | 交叉编译流程 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 内核配置 | 内核镜像类型 | ⭐⭐⭐ | ⭐⭐ |
| 内核模块 | 模块基本结构 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 内核模块 | insmod/modprobe区别 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 系统调用 | 系统调用流程 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 系统调用 | VFS核心结构 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 进程调度 | CFS调度原理 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 进程调度 | 实时调度策略 | ⭐⭐⭐ | ⭐⭐⭐ |
| 内存管理 | kmalloc/vmalloc区别 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 内存管理 | 内存回收机制 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 设备模型 | 总线-设备-驱动 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 设备模型 | 设备树解析 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
最后更新: 2026-09-17 关联页面: [[面试-Linux内核核心]]