4、内核模块编程基础:内核空间与用户空间区别、printk日志级别、常用内核API
说实话,很多刚接触内核开发的朋友,第一个坎儿就是搞不清「内核空间」和「用户空间」到底有啥区别。我当年刚入行时也犯过迷糊——在用户态写个printf跑得飞起,一进内核模块里照搬,结果编译报错,模块加载直接崩掉。嗯,这节课咱们就把这些基础打扎实。
4.1 内核空间 vs 用户空间:两个世界
Linux 把虚拟内存分成了两大区域。用户空间跑应用程序,内核空间跑操作系统核心代码。它们之间是隔离的,不能随便互相访问。
核心区别速览:
- 地址空间:用户空间 0~3G,内核空间 3G~4G(32位系统典型划分)
- 权限级别:用户态 Ring 3,内核态 Ring 0
- 直接访问硬件:用户态不行,内核态可以
- 崩溃影响:用户态进程崩了不影响系统,内核态崩了直接 panic
我在项目中遇到过好几次这样的情况:调试一个驱动时,不小心在内核态解引用了一个空指针,整个服务器直接黑屏。嗯,从那以后我写内核代码都格外小心指针检查。
为什么会这样?因为内核空间是所有进程共享的,一个 bug 就能拖垮整个系统。用户空间就不一样,进程之间互相隔离,崩了一个大不了重启进程。
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 是你写驱动时最常用的工具。把这些基础打牢,后面写复杂模块时才不会手忙脚乱。