面试-Linux内核基础.md 36 KB


title: 面试-Linux内核基础 tags: [

面试,
Linux内核,
嵌入式,
内核配置,
内核模块,
系统调用,
进程调度,
内存管理,
设备模型,

] created: 2026-09-17

updated: 2026-09-17

面试-Linux内核基础

本文档涵盖Linux内核基础的综合面试题,覆盖内核配置编译、模块机制、系统调用VFS、进程调度、内存管理、设备模型六大核心领域。


一、内核配置与编译

Q1: Linux内核配置的方式有哪些?各自适用场景是什么?

答案要点

  1. make menuconfig 基于ncurses的终端图形界面
  2. make xconfig 基于Qt的图形界面
  3. make defconfig 使用架构默认配置
  4. make oldconfig 基于旧配置更新

详细解答: 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

追问

  • 追问1:.config文件的作用是什么?如何利用它快速配置内核?
  • 追问2:内核配置选项中[Y][M][N]分别代表什么含义?

Q2: 内核编译的步骤是什么?如何交叉编译?

答案要点

  1. 配置内核生成.config文件
  2. 执行编译命令生成内核镜像
  3. 交叉编译需要指定ARCH和CROSS_COMPILE

详细解答: 内核编译的基本流程:

# 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:交叉编译工具链前缀
  • 可以写入Makefile或通过环境变量设置

追问

  • 追问1:zImage和uImage有什么区别?如何选择?
  • 追问2:如何加快内核编译速度?(ccache、make -j)

Q3: 内核镜像类型有哪些?各自的用途是什么?

答案要点

  1. vmlinux 原始内核ELF镜像
  2. zImage 压缩的自解压镜像
  3. uImage U-Boot专用镜像
  4. Image 未压缩的二进制镜像

详细解答

镜像类型 压缩 大小 用途
vmlinux 调试,包含符号表
zImage gzip ARM常用,自解压
uImage gzip U-Boot专用,含头部
Image arm64常用

U-Boot启动zImage的过程:

  1. U-Boot将zImage加载到内存
  2. 跳转到zImage入口
  3. 解压代码自动解压内核
  4. 跳转到解压后的内核执行

追问

  • 追问1:如何将vmlinux转换为uImage?
  • 追问2:内核压缩率一般是多少?解压时间是否影响启动?

Q4: 什么是内核Kbuild系统?它的作用是什么?

答案要点

  1. Kbuild是内核构建系统
  2. 负责配置、编译、链接内核
  3. 支持模块化编译

详细解答: Kbuild系统的核心组件:

  • Kconfig:配置脚本语言,定义配置选项
  • Makefile:定义编译规则和目标
  • .config:存储用户配置选择

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  # 根据配置决定

追问

  • 追问1:如何编写自己的Kconfig文件?
  • 追问2:CONFIG_前缀的变量是如何生成的?

Q5: 内核启动参数(bootargs)如何传递?常用参数有哪些?

答案要点

  1. U-Boot通过bootargs传递给内核
  2. 内核解析 cmdline 获取参数
  3. 常用参数包括root、console等

详细解答: 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()注册的处理函数

追问

  • 追问1:如何在运行时查看当前的内核启动参数?
  • 追问2:内核命令行参数的解析机制是什么?

二、内核模块

Q6: 内核模块的基本结构是什么?编写一个最简单的模块。

答案要点

  1. 模块需要初始化函数和退出函数
  2. 使用module_init/module_exit宏注册
  3. 必须包含MODULE_LICENSE

详细解答

#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

追问

  • 追问1:__init__exit宏的作用是什么?
  • 追问2:为什么必须声明MODULE_LICENSE?

Q7: insmod和modprobe有什么区别?各适用于什么场景?

答案要点

  1. insmod直接加载模块,不处理依赖
  2. modprobe自动处理模块依赖
  3. modprobe更推荐使用

详细解答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

追问

  • 追问1:如何查看模块的依赖关系?
  • 追问2:如何设置模块黑名单阻止自动加载?

Q8: 内核模块如何与用户空间通信?有哪些方式?

答案要点

  1. 文件操作接口(最常用)
  2. proc文件系统
  3. sysfs文件系统
  4. netlink套接字

详细解答方式一: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);

追问

  • 追问1:ioctl和sysfs各适用于什么场景?如何选择?
  • 追问2:如何实现高效的内核与用户空间数据传输?

Q9: 内核模块的参数传递机制是怎样的?

答案要点

  1. 使用module_param宏定义参数
  2. 支持在加载时传递参数
  3. 参数类型有多种

详细解答

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 所有用户可读写

追问

  • 追问1:模块参数在运行时如何修改?
  • 追问2:如何验证模块参数的有效性?

Q10: 内核模块的加载和卸载过程中发生了什么?

答案要点

  1. 加载过程:分配内存、初始化、注册接口
  2. 卸载过程:释放资源、注销接口、释放内存
  3. 引用计数防止正在使用的模块被卸载

详细解答模块加载流程

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

追问

  • 追问1:模块能否卸载正在使用的设备?如何处理这种情况?
  • 追问2:内核如何防止模块循环依赖?

三、系统调用与VFS

Q11: 系统调用的完整流程是怎样的?请从用户空间到内核空间详细描述。

答案要点

  1. 用户程序调用库函数(如open)
  2. 库函数通过SWI/SVC指令陷入内核
  3. 内核根据系统调用号查找处理函数
  4. 执行对应的内核函数并返回

详细解答: 以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 返回值

追问

  • 追问1:ARM64和ARM32的系统调用机制有什么区别?
  • 追问2:如何查看系统调用表?系统调用号是如何定义的?

Q12: VFS的作用是什么?它的核心数据结构有哪些?

答案要点

  1. VFS提供统一的文件系统抽象层
  2. 核心结构体包括super_block、inode、dentry、file
  3. 通过函数指针支持不同文件系统

详细解答: 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;             // 私有数据
    // ...
};

追问

  • 追问1:dentry缓存的作用是什么?它是如何工作的?
  • 追问2:inode缓存和页面缓存有什么区别?

Q13: 文件系统的挂载过程是怎样的?mount系统调用做了什么?

答案要点

  1. 解析挂载参数和设备
  2. 查找文件系统类型
  3. 调用文件系统的mount回调
  4. 建立挂载点与文件系统的关联

详细解答: 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 (文件系统)

追问

  • 追问1:如何查看当前系统的所有挂载点?
  • 追问2:bind mount和普通mount有什么区别?

Q14: 设备文件(/dev)是如何与驱动程序关联的?

答案要点

  1. 设备号(major/minor)标识设备
  2. 字符设备和块设备使用不同注册方式
  3. udev/mdev动态创建设备节点

详细解答: 设备号的组成:

设备号 = 主设备号(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

追问

  • 追问1:udev和mdev有什么区别?嵌入式系统为什么常用mdev?
  • 追问2:如何手动创建设备节点?在什么情况下需要手动创建?

Q15: copy_to_user和copy_from_user的作用和使用方法是什么?

答案要点

  1. copy_to_user:内核空间复制到用户空间
  2. copy_from_user:用户空间复制到内核空间
  3. 必须检查返回值

详细解答

#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跨越用户/内核边界
  • 必须检查返回值(0成功,非0失败)
  • 可能被信号中断,需要处理-ERESTARTSYS

追问

  • 追问1:为什么不直接使用指针访问用户空间内存?
  • 追问2:如何优化大数据量的内核/用户空间数据传输?

四、进程调度

Q16: Linux进程调度器的发展历程是怎样的?O(1)和CFS有什么区别?

答案要点

  1. O(1)调度器是早期的调度器
  2. CFS是当前的默认调度器
  3. CFS基于虚拟运行时间,更公平

详细解答调度器发展历程

  • Linux 2.4:简单的调度算法,O(n)复杂度
  • Linux 2.6:O(1)调度器,固定时间复杂度
  • Linux 2.6.23+:CFS(完全公平调度器)

O(1)调度器特点

- 使用140个优先级队列(0-139)
- 实时进程(0-99)+ 普通进程(100-139)
- 时间片计算:prio * 10ms
- 优点:固定时间复杂度
- 缺点:交互式进程响应不理想

CFS调度器特点

- 基于虚拟运行时间(vruntime)
- 使用红黑树管理进程
- vruntime = 实际运行时间 × (NICE_0_LOAD / 权重)
- 选择vruntime最小的进程运行
- 优点:公平性好,响应及时

追问

  • 追问1:CFS的红黑树是如何维护的?为什么选择红黑树?
  • 追问2:nice值对进程调度有什么影响?如何查看和修改nice值?

Q17: 实时调度策略SCHED_FIFO和SCHED_RR有什么区别?

答案要点

  1. SCHED_FIFO:先来先服务,不会被同优先级抢占
  2. SCHED_RR:时间片轮转,同优先级轮转执行
  3. 优先级范围1-99,数字越大优先级越高

详细解答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, &param);

// 设置SCHED_RR
sched_setscheduler(pid, SCHED_RR, &param);

追问

  • 追问1:实时进程的优先级范围是多少?普通进程的优先级范围是多少?
  • 追问2:如何防止实时进程饿死普通进程?

Q18: 什么是进程上下文切换?它的开销在哪里?

答案要点

  1. 上下文切换保存和恢复进程状态
  2. 包括CPU寄存器、页表、内核栈等
  3. 切换开销包括CPU时间、缓存失效等

详细解答: 上下文切换流程:

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刷新
调度开销 调度算法计算

追问

  • 追问1:如何减少上下文切换的次数?
  • 追问2:用户态和内核态的上下文切换有什么区别?

Q19: 调度域(sched_domain)和负载均衡的原理是什么?

答案要点

  1. 调度域描述CPU拓扑结构
  2. 负载均衡在调度域间迁移任务
  3. 支持多级均衡策略

详细解答: 调度域层级结构:

System (SMT域)
  └── MC域 (多核)
      └── DIE域 (同一芯片)
          └── NUMA域 (跨节点)

负载均衡流程:

1. 检测负载不平衡
   - 定期检查(tick中断)
   - 或者新任务创建时

2. 选择迁移任务
   - 查找最繁忙的组
   - 选择迁移代价最小的任务

3. 执行迁移
   - 将任务从繁忙CPU移到空闲CPU
   - 更新负载统计

相关文件:

# 查看调度域信息
cat /proc/sys/kernel/sched_domain/cpu0/domain0/name

# 查看负载均衡统计
cat /proc/schedstat

追问

  • 追问1:调度域是如何根据CPU拓扑自动构建的?
  • 追问2:如何调整负载均衡的触发频率?

Q20: CFS调度器的vruntime是如何计算的?权重和nice值的关系是什么?

答案要点

  1. vruntime = 实际运行时间 × (NICE_0_LOAD / 权重)
  2. nice值越低权重越大,vruntime增长越慢
  3. vruntime最小的进程最先调度

详细解答: 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获得更高优先级

追问

  • 追问1:新创建的进程的vruntime是如何初始化的?
  • 追问2:睡眠唤醒后的进程vruntime如何调整?

五、内存管理

Q21: Linux虚拟内存的页表结构是怎样的?4级页表和5级页表有什么区别?

答案要点

  1. 页表负责虚拟地址到物理地址的映射
  2. 4级页表用于48位虚拟地址
  3. 5级页表用于57位虚拟地址

详细解答: 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

追问

  • 追问1:为什么选择4KB作为页大小?有哪些其他选择?
  • 追问2:大页(HugePage)的原理和优势是什么?

Q22: kmalloc、vmalloc、kzalloc有什么区别?如何选择?

答案要点

  1. kmalloc分配物理连续内存
  2. vmalloc分配虚拟连续内存
  3. kzalloc分配并清零内存

详细解答

函数 物理连续 虚拟连续 适用场景 常用标志
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可用内存

追问

  • 追问1:kmalloc的最大分配大小是多少?如何处理大内存分配需求?
  • 追问2:为什么DMA必须使用物理连续内存?

Q23: 内核中的slab分配器是什么?它解决了什么问题?

答案要点

  1. slab是内核的小对象内存分配器
  2. 解决频繁分配/释放的性能问题
  3. 支持对象缓存和预分配

详细解答: 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

追问

  • 追问1:slab、slub、slob三种分配器有什么区别?嵌入式系统常用哪种?
  • 追问2:如何监控slab内存的使用情况?

Q24: 内核的内存回收机制是怎样的?什么是OOM killer?

答案要点

  1. 内存不足时触发回收
  2. 回收页面缓存、slab等
  3. OOM killer选择进程杀死

详细解答: 内存回收流程:

内存不足
  → 触发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               # 虚拟内存统计

追问

  • 追问1:如何避免嵌入式系统触发OOM?
  • 追问2:zRAM的作用是什么?它如何帮助内存管理?

Q25: 什么是DMA映射?它的类型有哪些?

答案要点

  1. DMA映射用于CPU和设备间的数据传输
  2. 一致性映射(Coherent DMA)
  3. 流式映射(Streaming DMA)

详细解答一致性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 双向

追问

  • 追问1:一致性映射和流式映射各适用于什么场景?
  • 追问2:DMA映射为什么需要考虑cache一致性问题?

六、设备模型

Q26: Linux设备模型的总线-设备-驱动框架是怎样的?

答案要点

  1. 总线连接设备和驱动
  2. 设备描述硬件信息
  3. 驱动实现设备操作
  4. 匹配后绑定设备和驱动

详细解答: 设备模型架构:

┌─────────────────────────────────────────┐
│                  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()

追问

  • 追问1:match函数是如何匹配设备和驱动的?有哪些匹配方式?
  • 追问2:设备和驱动的注册顺序对匹配有什么影响?

Q27: 设备树(Device Tree)的作用是什么?如何解析设备树节点?

答案要点

  1. 设备树描述硬件信息
  2. 避免内核中的硬件描述代码
  3. 使用of_系列函数解析

详细解答: 设备树描述示例:

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);
}

追问

  • 追问1:设备树overlay的作用是什么?如何使用?
  • 追问2:如何在设备树中描述GPIO、时钟、电源等资源?

Q28: platform设备的注册和探测流程是怎样的?

答案要点

  1. 平台设备用于片上外设
  2. 通过platform_device注册设备
  3. 通过platform_driver注册驱动
  4. 匹配后调用probe函数

详细解答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 */ }
};

追问

  • 追问1:platform_device和platform_driver的注册顺序对驱动加载有什么影响?
  • 追问2:如何查看系统中所有已注册的platform设备?

Q29: 字符设备和块设备有什么区别?各自如何注册?

答案要点

  1. 字符设备按字节访问
  2. 块设备按块访问,支持缓冲
  3. 注册方式类似但操作接口不同

详细解答字符设备注册:

#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");
// 添加请求队列和处理函数

追问

  • 追问1:块设备的请求队列(request_queue)是什么?
  • 追问2:如何实现一个简单的块设备驱动?

Q30: 内核中的锁机制有哪些?自旋锁和信号量有什么区别?

答案要点

  1. 自旋锁用于短时间锁定
  2. 信号量用于可能睡眠的场景
  3. 读写锁用于读多写少场景
  4. RCU用于读多写少的无锁场景

详细解答

锁类型 可睡眠 复杂度 适用场景
自旋锁 短临界区、中断上下文
信号量 长临界区、进程上下文
读写锁 读多写少
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);

追问

  • 追问1:mutex和semaphore有什么区别?应该优先使用哪个?
  • 追问2:如何避免死锁?有哪些常见的死锁场景?

附录:高频考点速查

领域 考点 出现频率 难度
内核配置 交叉编译流程 ⭐⭐⭐⭐⭐ ⭐⭐⭐
内核配置 内核镜像类型 ⭐⭐⭐ ⭐⭐
内核模块 模块基本结构 ⭐⭐⭐⭐⭐ ⭐⭐
内核模块 insmod/modprobe区别 ⭐⭐⭐⭐⭐ ⭐⭐
系统调用 系统调用流程 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
系统调用 VFS核心结构 ⭐⭐⭐⭐ ⭐⭐⭐
进程调度 CFS调度原理 ⭐⭐⭐⭐ ⭐⭐⭐⭐
进程调度 实时调度策略 ⭐⭐⭐ ⭐⭐⭐
内存管理 kmalloc/vmalloc区别 ⭐⭐⭐⭐⭐ ⭐⭐⭐
内存管理 内存回收机制 ⭐⭐⭐ ⭐⭐⭐⭐
设备模型 总线-设备-驱动 ⭐⭐⭐⭐⭐ ⭐⭐⭐
设备模型 设备树解析 ⭐⭐⭐⭐ ⭐⭐⭐

最后更新: 2026-09-17 关联页面: [[面试-Linux内核核心]]