第2章:Linux内核基础与驱动模型
说实话,很多刚接触嵌入式Linux驱动开发的朋友,一上来就急着写代码。我当年也是这样,结果折腾半天连编译都过不了。后来我才明白,内核基础不牢,后面全是坑。这一章,咱们就把地基打扎实。
2.1 Linux内核源码结构
先聊聊内核源码的目录结构。你下载一份内核源码,解压后看到一堆文件夹,别慌。我习惯先看顶层目录,心里有个谱。
几个关键目录:
- arch/:架构相关代码。比如arm、x86、mips等。咱们做嵌入式,重点关注arch/arm或arch/arm64。
- drivers/:驱动代码的大本营。里面按设备类型分,比如char、net、i2c、spi等。我们的字符设备驱动,通常放在drivers/char/下。
- fs/:文件系统实现。VFS(虚拟文件系统)的核心在这里。
- include/:头文件。驱动开发常用的
、 都在这里。 - kernel/:内核核心功能,比如调度、进程管理。
- mm/:内存管理。
- Documentation/:文档。嗯,虽然有时候文档更新不及时,但总比没有强。
我个人有个习惯:拿到一个新内核,先看Documentation/目录下的文件列表,能快速了解这个版本有哪些新特性。
2.2 内核编译与模块编译
编译内核,说白了就是配置+编译两步。但这里面的门道不少。
2.2.1 内核编译
标准流程:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
第一步生成默认配置,第二步按需调整,第三步开编。注意-j4表示4个线程并行编译,根据你电脑CPU核数来设。
我曾经踩过的坑:交叉编译工具链版本必须与内核版本匹配。有一次我用gcc 9.x编译一个老内核,结果链接阶段各种报错。后来换成gcc 7.x才搞定。所以,先确认工具链版本,别盲目用最新的。
2.2.2 模块编译
驱动开发中,我们更多是编译模块,而不是整个内核。模块编译的Makefile写法有固定套路:
obj-m := my_driver.o
KERNELDIR := /path/to/kernel/source
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
然后执行:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
生成.ko文件,这就是你的驱动模块。用insmod加载,rmmod卸载。
小技巧:调试阶段,我习惯在Makefile里加一行CFLAGS_my_driver.o += -DDEBUG,这样代码里的printk(KERN_DEBUG ...)就能输出调试信息。上线前再删掉。
2.3 字符设备驱动基础框架
字符设备驱动,是Linux驱动里最基础、最典型的一类。说白了,就是按字节流读写数据的设备,比如串口、LED、按键等。
一个最简单的字符设备驱动框架,包含这几部分:
- 模块入口/出口函数:module_init和module_exit。
- 设备号申请/释放:register_chrdev_region或alloc_chrdev_region。
- cdev结构体初始化与添加:cdev_init和cdev_add。
- file_operations结构体实现:这是核心,后面细讲。
来看一个骨架代码:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
static int major = 0;
static struct cdev my_cdev;
static int __init my_driver_init(void)
{
dev_t devno;
int ret;
// 动态分配设备号
ret = alloc_chrdev_region(&devno, 0, 1, "my_device");
if (ret < 0) {
printk(KERN_ERR "alloc_chrdev_region failed\n");
return ret;
}
major = MAJOR(devno);
// 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 添加cdev到内核
ret = cdev_add(&my_cdev, devno, 1);
if (ret) {
printk(KERN_ERR "cdev_add failed\n");
unregister_chrdev_region(devno, 1);
return ret;
}
printk(KERN_INFO "my_driver loaded, major=%d\n", major);
return 0;
}
static void __exit my_driver_exit(void)
{
dev_t devno = MKDEV(major, 0);
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
printk(KERN_INFO "my_driver unloaded\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
你可能会问:为什么用alloc_chrdev_region而不是register_chrdev_region?我个人建议新手用动态分配,省得跟别人冲突。等产品定型了,再考虑固定设备号。
2.4 file_operations结构体
这个结构体,是字符设备驱动的灵魂。它把用户空间的系统调用(open、read、write等)和内核空间的驱动函数关联起来。
定义在
| 成员 | 对应系统调用 | 说明 |
|---|---|---|
| .open | open() | 打开设备,通常做初始化 |
| .release | close() | 关闭设备,做清理工作 |
| .read | read() | 从设备读取数据 |
| .write | write() | 向设备写入数据 |
| .unlocked_ioctl | ioctl() | 设备控制命令 |
| .llseek | lseek() | 修改文件读写位置 |
来看一个实际例子:
static int my_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "device closed\n");
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
// 注意:这里要用copy_to_user,不能直接memcpy
// 因为用户空间和内核空间地址不能直接互访
printk(KERN_INFO "read called, count=%zu\n", count);
return 0;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
{
// 同理,用copy_from_user
printk(KERN_INFO "write called, count=%zu\n", count);
return count;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
重点提醒:read和write函数里,数据拷贝必须用copy_to_user/copy_from_user。我见过有人直接memcpy,结果内核崩溃。为什么?因为用户空间的指针可能是虚拟地址,内核不能直接访问。这是新手最容易犯的错误之一。
另外,.owner = THIS_MODULE这行不能省。它告诉内核,这个结构体属于哪个模块。当模块被卸载时,如果还有进程在使用这个设备,内核会阻止卸载。
嗯,说到这我想起一个案例。有次我写一个LED驱动,open函数里做了GPIO申请,release里忘了释放。结果反复打开关闭设备几次后,GPIO资源就耗尽了。所以,open和release一定要成对考虑,申请的资源必须释放。
小结
这一章的内容,说白了就是驱动开发的"Hello World"。内核源码结构让你知道文件放哪,编译流程让你能跑起来,字符设备框架和file_operations让你知道怎么写。下一章,我们会在这个基础上,加入实际硬件操作,让设备真正动起来。
记住:驱动开发,七分在框架,三分在逻辑。框架搭对了,后面就顺了。