# 6 内核特殊约束 ## 6.1 完整概念讲解 内核代码运行在 CPU 的最高特权级(Ring 0),没有用户态的运行时库和内存保护机制。这意味着大量用户态编程习惯在内核中不适用。 ### 为什么内核没有 libc - 内核是自包含的独立环境,启动时不依赖任何用户态库 - libc 中的函数(printf、malloc、pthread 等)底层依赖系统调用,而系统调用本身是用户态进入内核态的接口 - 内核中使用这些函数会导致循环依赖和死锁 ### 替代方案对照表 | 用户态函数 | 内核替代 | 头文件 | | ---------- | ------------------------- | -------------------------------------- | | `printf()` | `printk()` | `` | | `malloc()` | `kmalloc()` / `vmalloc()` | `` / `` | | `free()` | `kfree()` / `vfree()` | 同上 | | `strcpy()` | `strscpy()` | `` | | `strlen()` | `strlen()` | `` | | `memcpy()` | `memcpy()` | `` | | `memset()` | `memset()` | `` | | `assert()` | `BUG_ON()` / `WARN_ON()` | `` | ## 6.2 核心API/语法 ### 内核内存分配 ```c #include // 分配物理连续内存(适用于 DMA) void *kmalloc(size_t size, gfp_t flags); void *kzalloc(size_t size, gfp_t flags); // 分配并清零 void kfree(const void *ptr); // GFP flags GFP_KERNEL // 可睡眠,优先级高(最常见的标志) GFP_ATOMIC // 不可睡眠,用于中断上下文 GFP_NOWAIT // 不等待,分配失败立即返回 NULL ``` ```c #include // 分配虚拟连续、物理不连续的内存(大块内存) void *vmalloc(unsigned long size); void vfree(const void *addr); ``` | 分配函数 | 物理连续 | 可睡眠 | 适用场景 | | --------- | -------- | ---------------- | ---------------------- | | `kmalloc` | 是 | 是(GFP_KERNEL) | 小块内存、DMA 缓冲区 | | `kzalloc` | 是 | 是 | 同 kmalloc,分配后清零 | | `vmalloc` | 否 | 是 | 大块内存(如模块加载) | ### copy_from_user / copy_to_user ```c #include // 用户空间 → 内核空间 unsigned long copy_from_user(void *dst, const void __user *src, unsigned long size); // 内核空间 → 用户空间 unsigned long copy_to_user(void __user *dst, const void *src, unsigned long size); // 返回值:0 成功,非0 表示未拷贝的字节数 // 内核内部会检查地址合法性,非法地址会触发 oops ``` ### goto 错误处理 内核编码规范鼓励用 `goto` 进行错误处理,优点: - 减少嵌套层次 - 统一错误清理路径 - 避免重复释放代码 ```c int my_function(void) { int ret; struct resource *res; res = kmalloc(sizeof(*res), GFP_KERNEL); if (!res) return -ENOMEM; ret = some_init(res); if (ret) goto err_free; ret = another_init(res); if (ret) goto err_first; return 0; err_first: some_cleanup(res); err_free: kfree(res); return ret; } ``` ## 6.3 代码示例(完整可编译,附gcc命令) ### 内核模块:演示内核特殊约束 ```c // kernel_constraints.c #include #include #include #include #include #include #include static int __init constraints_init(void) { /* 1. kmalloc 示例:物理连续内存 */ char *kbuf; kbuf = kmalloc(128, GFP_KERNEL); if (!kbuf) { printk(KERN_ERR "kmalloc failed\n"); return -ENOMEM; } strscpy(kbuf, "Hello from kmalloc", 128); printk(KERN_INFO "kmalloc buffer: %s\n", kbuf); kfree(kbuf); /* 2. kzalloc 示例:分配并清零 */ int *arr; arr = kzalloc(sizeof(int) * 10, GFP_KERNEL); if (!arr) return -ENOMEM; printk(KERN_INFO "kzalloc arr[0]=%d (should be 0)\n", arr[0]); kfree(arr); /* 3. vmalloc 示例:虚拟连续大块内存 */ char *vbuf; vbuf = vmalloc(1024 * 1024); /* 1MB */ if (!vbuf) return -ENOMEM; memset(vbuf, 'A', 1024); printk(KERN_INFO "vmalloc buffer first byte: %c\n", vbuf[0]); vfree(vbuf); /* 4. 内核中没有 printf,用 printk */ printk(KERN_INFO "Kernel constraints demo loaded\n"); return 0; } static void __exit constraints_exit(void) { printk(KERN_INFO "Kernel constraints demo unloaded\n"); } module_init(constraints_init); module_exit(constraints_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Example"); MODULE_DESCRIPTION("Demo kernel constraints"); ``` ### Makefile ```makefile obj-m := kernel_constraints.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean ``` ### 编译与运行 ```bash make sudo insmod kernel_constraints.ko dmesg | tail # [xxxxx] kmalloc buffer: Hello from kmalloc # [xxxxx] kzalloc arr[0]=0 (should be 0) # [xxxxx] vmalloc buffer first byte: A # [xxxxx] Kernel constraints demo loaded sudo rmmod kernel_constraints make clean ``` ## 6.4 注意事项与易错点 1. **禁止浮点数运算**:内核不支持 FPU,浮点操作会导致 "not allowed in kernel" 异常。所有计算必须用整数(可用定点数模拟浮点) 2. **内核栈很小**:x86_64 默认 8KB/16KB,不能定义大的局部数组(如 `char buf[1024*1024]`),会直接栈溢出导致内核崩溃 3. **kmalloc 大小限制**:`kmalloc` 最大分配通常为 4MB(取决于 SLAB 配置),大块内存应使用 `vmalloc` 4. **copy_from_user / copy_to_user 不可省略**:用户空间指针可能非法,直接解引用可能导致内核 oops 或安全漏洞 5. **goto 错误处理是内核规范**:不是坏习惯,是 Linux 内核编码风格推荐的做法 6. **volatile 在内核中慎用**:内核有专门的内存屏障 API(`READ_ONCE`、`WRITE_ONCE`、`smp_rmb`),大多数情况下不需要手动加 `volatile` 7. **内核中不能使用 C++**:Linux 内核纯 C 语言,不支持 C++ 特性 ## 6.5 面试要点(3-5个Q&A) **Q1:为什么内核不能用 `malloc` 和 `free`?** A:内核运行在 Ring 0 特权级,不链接用户态 libc。`malloc/free` 依赖 brk/mmap 等系统调用,而系统调用本身是用户态进入内核态的接口,内核中不能调用自身。内核使用 `kmalloc/kfree`(物理连续)和 `vmalloc/vfree`(虚拟连续)替代。 **Q2:`kmalloc` 和 `vmalloc` 的区别是什么?什么时候用哪个?** A:`kmalloc` 分配物理连续内存,速度快但可能因内存碎片失败,适合小块内存和 DMA 缓冲区。`vmalloc` 分配虚拟连续、物理不连续的内存,可以分配大块但有 TLB 抖动开销,适合大块内存(如模块加载)。 **Q3:为什么内核中要用 `copy_from_user` / `copy_to_user` 而不是直接 `memcpy`?** A:用户空间指针可能指向非法地址(NULL、越界、内核地址),直接解引用会导致内核 oops。`copy_from_user/copy_to_user` 内部会检查地址合法性,非法地址时安全失败并返回错误码。 **Q4:为什么内核中 `goto` 错误处理是推荐的做法?** A:内核代码中资源分配失败时需要逐层释放已分配的资源。`goto` 可以将错误处理集中到函数末尾,减少嵌套层次,避免重复释放代码。这是 Linux 内核编码规范明确推荐的模式。 **Q5:内核栈空间有多大?超了会怎样?** A:x86_64 默认 8KB 或 16KB(取决于配置)。栈溢出会覆盖相邻内存页,导致不可预测的内核崩溃(oops 或 panic)。解决方案:使用 `kmalloc` 分配大缓冲区,避免深层递归,减少局部变量大小。