- 写在前面
- 整体环境
- 学习笔记
- 模块
- 字符设备驱动的代码
- 字符设备驱动的Makefile(构建模块)
- 管理模块的位置
- 驱动的插入和卸载
之前做项目的时候,有前辈告诉自己,要去学一下Linux内核,对很多方面都有帮助,现在闲下来,来花时间学一下这一部分的知识点,也算是一个学习笔记
目前跟着B站UP主——简说linux 的教程《Linux内核开发100讲》学习,链接如下:
简说linux个人空间
本章参考学习的链接如下:
Makefile中($(KERNELRELEASE),)执行分析
《Linux内核设计与实现》
为了学习代码,我们需要一个一套Linux环境,因为为了方便自己记笔记和学习,没有用双系统,直接在windows10下面用VMware建了一个虚拟机进行试验。
开发环境:VMWare虚拟机 Ubuntu 18.04
Linux源码版本:linux4.9.229
Linux是“单块内核”(monolithic)的操作系统,即整个系统内核都运行在一个单独的保护域中,但其内核时模块化组成在,在运行的过程中可以动态的向其中插入或从中删除代码。这些代码(包括相关的子例程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中(即后文中的Makefile文件中的obj-m := helloDev.o这一个操作也就是.ko文件),这个就叫做模块。
字符设备驱动的代码首先,UP主给出了最简单的一个字符设备驱动的代码,具体代码如下,学习过程中的笔记就当写注释了
#include#include #include #include #include #include #include #include #define BUFFER_MAX (10) #define OK (0) #define ERROR (-1) struct cdev *gDev; struct file_operations *gFile; dev_t devNum; unsigned int subDevNum = 1; int reg_major = 232; int reg_minor = 0; char *buffer; int flag = 0; int hello_open(struct inode *p, struct file *f) { printk(KERN_EMERG"hello_openrn"); return 0; } ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l) { printk(KERN_EMERG"hello_writern"); return 0; } ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l) { printk(KERN_EMERG"hello_readrn"); return 0; } int hello_init(void) { devNum = MKDEV(reg_major, reg_minor); //根据主次设备号生成一个devNum,具体实现是将主设备号作一个偏移,然后将其或上次设备号 //主次设备号用来唯一标识一个设备,主:标识一类设备 次:该类设备下的不同设备 if(OK == register_chrdev_region(devNum, subDevNum, "helloworld")){ //register_chrdev_region是将设备注册到内核里面 //将设备号注册到内核里面后,该设备号其他人就无法再注册 printk(KERN_EMERG"register_chrdev_region ok n"); }else { printk(KERN_EMERG"register_chrdev_region error n"); return ERROR; } printk(KERN_EMERG" hello driver init n"); gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL); gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL); //声明一个file_operation类型的变量gFile //struct file_operations结构体里面声明了许多对文件操作的函数 gFile->open = hello_open; gFile->read = hello_read; gFile->write = hello_write; //对gFile中的三个操作进行指向,最终由应用层请求进行调用 gFile->owner = THIS_MODULE; cdev_init(gDev, gFile); //建立了gDev和gFile之间的联系 //注:在学习内核的过程中,有些函数特别复杂,我们可以不去细究。类似这样的,我们知道这个函数将两者建立了联系即可 cdev_add(gDev, devNum, 3); //建立gDev和设备号之间的联系 //至此,建立了设备号与gDev与gFile的联系 return 0; } void __exit hello_exit(void) //驱动卸载函数 { cdev_del(gDev); //与cdev_add操作对应 unregister_chrdev_region(devNum, subDevNum);//与register_chrdev_region操作对应 return; } module_init(hello_init); //声明驱动的入口函数是hello_init module_exit(hello_exit); //声明驱动的出口函数是hello_exit MODULE_LICENSE("GPL"); //声明了版权
学习笔记
- 字符设备通常缩写为cdev,它是不可寻址的,仅提供数据的流式访问,就是一个个字符或者一个个字节。
- hello_init()就是这一个模块的入口点,它通过module_init()注册到系统中,在内核装载到时候被调用。而module_init()实际上不是一个函数调用,而是一个宏调用。唯一的参数就是模块的初始化函数。模块的所有初始化函数必须满足int my_init(void)这样的格式
- hello_exit()是模块的出口函数,由module_exit()注册到系统中,当模块从内存卸载的时候,便会调用此函数。即对这个模块的清理工作,其退出函数必须负荷void my_exit(void)格式
- 由于init和exit函数通常不会被外部函数直接调用,因此我们不必导出该函数,因此这两个函数都可以标记为static类型
ifneq ($(KERNELRELEASE),) obj-m := helloDev.o #给内核的编译系统识别,内核系统会将所有obj-m的二进制文件变为驱动文件 #此处执行就是将helloDev.c生成为helloDev.o文件,再将其编译为驱动文件即.ko文件 else PWD := $(shell pwd) #得到当前目录 #KDIR:= /lib/modules/4.4.0-31-generic/build #自己编译的一个内核下的驱动,插入编译的这个内核的驱动中 KDIR := /lib/modules/`uname -r`/build #当前运行的Ubuntu的系统所在的地方,插入到自己系统的驱动中 all: make -C $(KDIR) M=$(PWD) #make -C $(KDIR)表示进入内核目录,并执行其中的Makefile文件 #M=$(PWD) 表示执行完了之后返回到当前的目录之下继续读入、执行当前的Makefile文件 clean: rm -rf *.o *.ko *.mod.c *.symvers *.c~ *~ endif
笔记
- 首先是最简单ifneq的使用:ifneq($(变量名), 变量值),ifneq是为了比较两个参数是否不相同,不相等为true,相等为false
第二个变量是NULL即为空。即如果变量为空,则为false,进入else语句
补充一点 Makefile笔记
ifeq 判断参数是否不相等,相等为 true,不相等为 false。
ifneq 判断参数是否不相等,不相等为 true,相等为 false。
ifdef 判断是否有值,有值为 true,没有值为 false。
ifndef 判断是否有值,没有值为 true,有值为 false。
- 在执行这个Makefile文件的时候,如果使用的make指令,那么Makefile文件就会执行all:后面的语句,即上方的make -C $(KDIR) M=$(PWD),如果使用make clean指令,则会执行clean:后面的语句
- 而这个文件最关键的是会执行两次该Makefile文件,原因如下:
当第一次执行ifneq ($(KERNELRELEASE),) 时,此时的KERNELRELEASE变量还没有被定义,因此此时判断为fasle进入else后面的执行语句,然后进入all:后面的语句,all:后面的语句执行过程如下
make -C $(KDIR)表示进入内核目录,并执行其中的Makefile文件,此时,KERNELRELEASE变量已被定义,不再为空
M=$(PWD) 表示执行完了之后返回到当前的目录之下继续读入、执行当前的Makefile文件
再次执行当前Makefile文件之后,ifneq判断为true,执行obj-m := helloDev.o
在前面的这些知识里面我们可以知道,.ko文件就是一个文件镜像,Linux内核系统就是把这样的一个镜像文件插入到运行的内核当中。而这里面有一个重要的点是,我们在哪里管理我们的模块,也就是把.ko文件放在哪?
-
放到内核源代码树种
在前面Linux内核学习(一)里面我们知道,在Linux内核代码里面设备驱动程序有一个专门的目录即/drivers,在内部有许多的子目录,而我们也可以将我们自己编写的设备驱动模块放在其内部。这里面有drivers/char(存放字符设备),/drivers/usb(存放USB设备)注:这个规矩并不是墨守成规的,许多usb设备也属于字符设备,但这样的目录规则有助于我们理解各个设备的关系
但是我们如果将我们的文件放到这里面的目录之下,该目录下会同时存在大量的C源代码文件和其他文件目录,不便于自己编写。
因此,我们也可以自己在/drivers/char下创建一个自己的一个目录,例如/drivers/char/helloDev,如果是这样的话,我们就需要在drivers/char/Makefile里面加入一行obj-m += helloDev/,意思是编译模块的时候,要进入helloDev目录。然后我们需要在drivers/char/helloDev里面加入一个Makefile文件,并包含obj-m += helloDev.o(即将helloDev.c编译为helloDev.ko文件) -
放在内核代码之外
我们也可以将其放在一个自己的文件夹里面,来进行维护,那么就只需要在你当前的目录之下建立一个Makefile文件,里面包含
obj-m :=helloDev.o,这样就把文件编译为.ko文件了,当然,如果你有多个元件文件需要编译到helloDev.o里面,我们就需要加入helloDev-objs := helloDev.o goodbye.o,这样helloDev.c和goodbye.c就被编译到helloDev.ko里面了
当然我们也要让内核知道我们要编译的模块在哪
可以选择在Makefile里面加入make -C $(KDIR) M=$(PWD)例如前面的文件种
或者使用make指令的时候使用make -C /kernel/source/location SUBDIRS=$PWD modules
有了上述的准备工作之后,我们就可以将我们的helloDev.ko驱动加入到我们的Linux进程当中啦
首先,我们先把我们的内核日志中的所有内容清理一下,便于我们查看后续插入我们自己的驱动而输出的信息
sudo dmesg -C #需要在root权限下清理日志 dmesg # 查看日志内容
当dmesg没有输出内容之后,代表已经清理完了
输入sudo insmod helloDev.ko将我们的设备驱动加入到Linux内核当中
然后我们输入lsmod查看我们是否加入成功
发现已经存在了,代表已经插入成功
这个时候,我们再使用dmesg 查看我们插入驱动的输出信息,发现确实已经输出了
当我们想要把这个设备卸载的时候,我们使用sudo rmmod helloDev即可以把驱动卸载,卸载之后,再用lsmod就看不到啦
但这两个指令并不只能,先进工具modprobe更智能,它还会自动加载任何安装的模块及任何它所依赖的模块
插入模块指令modprobe module [ module parameters] 卸载模块指令modprobe -r modules,都需要在root权限下运行



