4、内核模块编程基础:内核空间与用户空间区别、printk日志级别、常用内核API

说实话,很多刚接触内核开发的朋友,第一个坎儿就是搞不清「内核空间」和「用户空间」到底有啥区别。我当年刚入行时也犯过迷糊——在用户态写个printf跑得飞起,一进内核模块里照搬,结果编译报错,模块加载直接崩掉。嗯,这节课咱们就把这些基础打扎实。

4.1 内核空间 vs 用户空间:两个世界

Linux 把虚拟内存分成了两大区域。用户空间跑应用程序,内核空间跑操作系统核心代码。它们之间是隔离的,不能随便互相访问。

核心区别速览:

  • 地址空间:用户空间 0~3G,内核空间 3G~4G(32位系统典型划分)
  • 权限级别:用户态 Ring 3,内核态 Ring 0
  • 直接访问硬件:用户态不行,内核态可以
  • 崩溃影响:用户态进程崩了不影响系统,内核态崩了直接 panic

我在项目中遇到过好几次这样的情况:调试一个驱动时,不小心在内核态解引用了一个空指针,整个服务器直接黑屏。嗯,从那以后我写内核代码都格外小心指针检查。

为什么会这样?因为内核空间是所有进程共享的,一个 bug 就能拖垮整个系统。用户空间就不一样,进程之间互相隔离,崩了一个大不了重启进程。

Linux 虚拟内存空间划分 内核空间 (3G ~ 4G) Ring 0 · 内核模块 · 驱动 · 系统调用入口 kmalloc / kfree / printk 在此运行 边界 用户空间 (0 ~ 3G) Ring 3 · 应用程序 · 进程隔离 malloc / free / printf 在此运行 进程 A 进程 B 进程 C 用户态通过系统调用(syscall)陷入内核态,完成特权操作后返回

4.2 printk 日志级别:内核版的 printf

内核模块里不能用 printf,得用 printk。它俩长得像,但 printk 多了个日志级别参数。说白了,就是给每条消息贴个标签,告诉系统这条消息有多重要。

宏定义 数值 含义 典型场景
KERN_EMERG 0 紧急事件 系统即将崩溃
KERN_ALERT 1 必须立即处理 严重硬件故障
KERN_CRIT 2 临界条件 驱动加载失败
KERN_ERR 3 错误 设备 I/O 错误
KERN_WARNING 4 警告 配置不推荐
KERN_NOTICE 5 普通但重要 模块加载/卸载
KERN_INFO 6 信息 版本信息打印
KERN_DEBUG 7 调试信息 开发阶段日志

我的习惯:开发调试阶段用 KERN_DEBUG 或 KERN_INFO,正式发布时只保留 KERN_ERR 及以上级别。不然 dmesg 会被刷爆,你想想看,生产环境里满屏调试信息,真正的问题反而被淹没了。

用法很简单:

printk(KERN_INFO "Hello, kernel module loaded!\n");
printk(KERN_ERR "Device %d init failed with error %d\n", dev_id, ret);

查看日志用 dmesg 命令,或者直接看 /var/log/kern.log。我个人习惯在调试时开个终端窗口跑 dmesg -w 实时跟踪,这样模块一加载就能看到输出。

4.3 常用内核 API:内存分配与数据拷贝

内核里写代码,内存管理是基本功。用户态用 malloc/free,内核态用 kmalloc/kfree。还有数据拷贝,用户态和内核态之间不能直接传指针,得用专门的拷贝函数。

4.3.1 kmalloc 与 kfree

kmalloc 分配的是物理连续的内存,这点跟用户态的 malloc 不一样。它有两个参数:要分配的大小,以及 GFP 标志(控制分配行为)。

GFP 标志 含义 使用场景
GFP_KERNEL 普通分配,可能睡眠 进程上下文(最常用)
GFP_ATOMIC 原子分配,不会睡眠 中断上下文、自旋锁内
GFP_DMA 可用于 DMA 的内存 设备驱动 DMA 缓冲区
__GFP_ZERO 分配后清零 需要初始化内存时

我曾经踩过的坑:在中断处理函数里用了 GFP_KERNEL,结果系统直接死锁。因为中断上下文不能睡眠,而 GFP_KERNEL 在内存紧张时会触发页面回收导致睡眠。记住:中断上下文只能用 GFP_ATOMIC。

代码示例:

#include <linux/slab.h>

struct my_device *dev;
dev = kmalloc(sizeof(*dev), GFP_KERNEL);
if (!dev) {
    printk(KERN_ERR "Failed to allocate memory\n");
    return -ENOMEM;
}
// ... 使用 dev ...
kfree(dev);

4.3.2 copy_from_user 与 copy_to_user

用户空间传过来的指针,内核不能直接解引用。为什么?因为用户空间的地址可能还没映射,或者被换出了。直接访问会导致内核崩溃。

正确的做法是用 copy_from_user 把数据从用户空间拷贝到内核空间,或者用 copy_to_user 把数据从内核空间拷贝回用户空间。

#include <linux/uaccess.h>

static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct my_data kdata;
    
    // 从用户空间拷贝数据到内核空间
    if (copy_from_user(&kdata, (void __user *)arg, sizeof(kdata))) {
        return -EFAULT;
    }
    
    // 处理数据...
    
    // 把结果拷贝回用户空间
    if (copy_to_user((void __user *)arg, &kdata, sizeof(kdata))) {
        return -EFAULT;
    }
    
    return 0;
}

关键点:

  • copy_from_user 和 copy_to_user 会检查地址合法性,失败返回非零值
  • 目标地址参数用 __user 宏标记,提醒这是用户空间地址
  • 这两个函数可能会睡眠,不能在中断上下文使用
  • 返回值是「未拷贝成功的字节数」,0 表示全部成功

你想想看,如果没有这些检查,恶意程序传个非法地址过来,内核直接访问就崩了。所以内核开发者设计这些 API 时,安全是放在第一位的。

4.4 综合示例:一个完整的内核模块

把上面这些知识点串起来,写一个完整的内核模块。它演示了 printk 日志级别、kmalloc/kfree 内存管理,以及 copy_from_user/copy_to_user 数据拷贝。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/fs.h>

#define DEVICE_NAME "demo_dev"
#define BUF_SIZE 256

static char *kbuf;

static long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    char tmp[BUF_SIZE];
    
    // 从用户空间拷贝命令数据
    if (copy_from_user(tmp, (void __user *)arg, BUF_SIZE)) {
        printk(KERN_ERR "copy_from_user failed\n");
        return -EFAULT;
    }
    
    printk(KERN_INFO "Received from user: %s\n", tmp);
    
    // 模拟处理:把数据拷贝到内核缓冲区
    memcpy(kbuf, tmp, BUF_SIZE);
    
    // 把结果拷贝回用户空间
    if (copy_to_user((void __user *)arg, kbuf, BUF_SIZE)) {
        printk(KERN_ERR "copy_to_user failed\n");
        return -EFAULT;
    }
    
    return 0;
}

static int __init demo_init(void)
{
    printk(KERN_NOTICE "Demo module loading\n");
    
    kbuf = kmalloc(BUF_SIZE, GFP_KERNEL);
    if (!kbuf) {
        printk(KERN_CRIT "kmalloc failed\n");
        return -ENOMEM;
    }
    
    printk(KERN_INFO "Demo module loaded successfully\n");
    return 0;
}

static void __exit demo_exit(void)
{
    kfree(kbuf);
    printk(KERN_NOTICE "Demo module unloaded\n");
}

module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Demo module for kernel API practice");

调试小技巧:编译加载后,用 dmesg | tail -20 查看内核日志。如果模块加载失败,KERN_CRIT 级别的错误会第一时间显示出来。我曾经靠这个快速定位过内存分配失败的问题——当时系统内存碎片太多,kmalloc 分配连续内存失败,换成 __GFP_ZERO 组合标志才搞定。

好了,这一节的内容就这些。内核空间和用户空间的隔离是 Linux 安全模型的基石,printk 是你调试时的眼睛,kmalloc/kfree 和 copy_from_user/copy_to_user 是你写驱动时最常用的工具。把这些基础打牢,后面写复杂模块时才不会手忙脚乱。