你知道键盘敲下去、鼠标动一下,Linux 系统是怎么知道的吗?其实啊,都是驱动程序在中间搭线呢。用 C 语言开发 Linux 驱动,听着好像是高手才敢碰的活儿,其实不然,只要把步骤拆解开,一步一步来,新手也能摸着门道。今天兔子哥就给大家讲讲,用 C 语言开发 Linux 驱动程序的完整步骤,都是实战里总结出来的,一起往下看吧!
一、先把环境搭好,别上来就写代码
开发 Linux 驱动,环境可得准备对了,不然写好的代码都跑不起来,你说气人不气人?
首先,得有个 Linux 系统,兔子哥建议用 Ubuntu 或者 CentOS,这些发行版对驱动开发支持比较好,安装起来也方便。然后呢,要装内核源码和内核头文件,因为驱动是要跟内核打交道的,没有这些,编译都通不过。安装命令大概是这样的(以 Ubuntu 为例):
plaintext
sudo apt-get install linux-headers-$(uname -r)sudo apt-get install linux-source这里的
uname -r能获取当前系统的内核版本,安装对应版本的头文件,这点很重要,版本不对的话,后面编译会报错,我当初就踩过这个坑,折腾了半天才发现是版本不匹配。还得有个编译器,gcc 肯定是要的,再加个 make 工具,编译驱动全靠它们。哦对了,最好再装个调试工具,比如 dmesg,后面看驱动输出信息用得上。
二、写驱动代码,从最简单的 “Hello World” 开始
别一上来就想写复杂的驱动,先整个最基础的,让驱动能加载、能卸载,打印点信息就行,就像学 C 语言先写 Hello World 一样。
用 C 语言写驱动,结构跟普通 C 程序不太一样,它得包含内核的头文件,比如
#include 和#include 。然后呢,得有两个核心函数:一个是驱动加载时执行的(用module_init指定),一个是驱动卸载时执行的(用module_exit指定)。举个例子,代码大概长这样:
c运行
#include #include // 驱动加载时调用static int __init hello_drv_init(void) {printk("Hello, Linux Driver! 我加载啦~\n");return 0;}// 驱动卸载时调用static void __exit hello_drv_exit(void) {printk("Bye bye, 我卸载啦~\n");}// 注册这两个函数module_init(hello_drv_init);module_exit(hello_drv_exit);// 许可证信息,必须要有,不然加载会警告MODULE_LICENSE("GPL");这里用
printk而不是printf,因为驱动运行在内核态,printf是用户态的函数,用不了,这点可得记牢了。三、写个 Makefile,告诉编译器怎么编译
驱动代码写完了,怎么编译呢?这时候就需要 Makefile 了,它就像个说明书,告诉编译器 “该用哪个内核源码编译,输出文件名是啥”。
一个简单的 Makefile 大概是这样的:
plaintext
obj-m += hello_drv.o # 要编译的驱动文件名,这里是hello_drv.c# 指定内核源码路径,用当前系统的内核KERNELDIR := /lib/modules/$(shell uname -r)/build# 当前目录PWD := $(shell pwd)# 默认编译命令default:make -C $(KERNELDIR) M=$(PWD) modules# 清理编译生成的文件clean:make -C $(KERNELDIR) M=$(PWD) clean写的时候注意,
make前面的缩进必须是 Tab 键,用空格的话会报错,这点特别坑,兔子哥当初就因为这个卡了好久,你说冤不冤。四、编译驱动,生成.ko 文件
环境、代码、Makefile 都准备好了,接下来就是编译了。在终端里进入代码所在的目录,敲个
make命令,回车等着就行。如果一切顺利,会生成一个
.ko文件,这就是编译好的驱动模块。要是报错了,别慌,看看错误信息,大多是内核头文件没装对,或者 Makefile 格式错了。比如提示 “no rule to make target”,大概率是 KERNELDIR 路径不对,检查一下是不是跟uname -r的版本对上了。五、加载和卸载驱动,看看效果
驱动编译好了,怎么用呢?得加载到内核里才行。用
insmod命令加载,比如:plaintext
sudo insmod hello_drv.ko加载成功的话,用
dmesg命令就能看到我们在hello_drv_init里打印的 “Hello, Linux Driver! 我加载啦~”。想卸载的话,用
rmmod命令:plaintext
sudo rmmod hello_drv再用
dmesg看看,是不是出现了 “Bye bye, 我卸载啦~”?这就说明驱动能正常工作了。对了,加载后还可以用
lsmod命令看看驱动是不是在列表里,确认一下加载成功没。六、进阶一点:写个简单的字符设备驱动
前面那个只是个 “打招呼” 的驱动,没啥实际功能。想控制硬件,得做字符设备驱动,比如模拟一个简单的设备,能读能写。
字符设备驱动得注册设备号,创建设备文件,还得实现
open、read、write这些操作函数。大概步骤是:- 在
module_init里申请设备号(用alloc_chrdev_region)。 - 定义
file_operations结构体,把自己实现的open、read等函数放进去。 - 注册字符设备(用
cdev_add),创建设备文件(可以用class_create和device_create自动创建)。 - 在
module_exit里释放资源,把申请的设备号、创建的设备文件都清理掉。
这里面细节挺多的,比如
file_operations里的函数参数,read函数要把数据从内核态拷贝到用户态,得用copy_to_user函数,不能直接赋值,不然会出问题。我第一次写的时候就直接用了赋值,结果用户态读出来全是乱码,后来才知道内核态和用户态的内存是隔离的。七、调试驱动,出了问题别慌
驱动开发嘛,不出 bug 才奇怪呢。调试的时候,
dmesg是个好东西,printk打印的信息都在里面,能帮你定位哪里错了。如果驱动加载就崩溃,可能是指针没初始化,或者访问了非法内存。这时候可以用
dmesg看看崩溃时的调用栈,大概能猜到是哪个函数出了问题。还有个小技巧,编译驱动的时候可以加
-DDEBUG选项,在代码里用DEBUG宏包一些调试信息,这样调试的时候能打印更多细节,发布的时候关掉就行,很方便。最后说点我的心得,开发 Linux 驱动,最重要的是耐心,一步一步来,别想着一口吃个胖子。刚开始可以先照着例子改,理解每个函数的作用,慢慢再自己写。内核文档也得多看,
/usr/src/linux-headers-xxx/Documentation里有很多驱动开发的说明,比瞎琢磨强多了。其实啊,驱动开发没那么神秘,就是跟内核 “打交道”,按它的规则来,它就给你干活。遇到问题别灰心,多试几次,你会发现,自己写的驱动能控制硬件的时候,那种成就感,嘿,别提多爽了。希望这些步骤能帮到你,动手试试吧!
标签: linux-headers- 调试工具
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。
还木有评论哦,快来抢沙发吧~