继续linux设备驱动程序第二章的学习实践.
一、当前进程
查找当前进程.
在hello.c中增加以下代码:
printk(KERN_EMERG "the process is "%s", (pid %i)n",
current->comm,current->pid);
make后,执行以下指令:
insmod ./hello.ko
通过dmesg命令得到当前进程的命令“insmod”,其ID是3364:
二 编译和装载
书本对应hello world的makefile如下:
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
.PHONY: modules modules_install clean
else
# called from kernel build system: just declare what our modules are
obj-m := hello.o
endif
以上编译方式为makefile的驱动程序动态编译,(静态编译即将驱动直接编译进内核,动态编译即为将驱动编译成模块)。如书中所述这种方法方便于内核树之外的模块构造变得容易.
下面按我自己的理解和测试解读makefile代码:
1.这里用了ifeq ($(KERNELRELEASE),) 当KERNELRELEASE为null时执行,否则执行else.
由于只有在Linux源码根目录下的Makefile编译内核时,KERNELRELEASE宏才会被定义.所以当第一次进入makefile时,ifeq ($(KERNELRELEASE),) 为真,执行ifeq以下代码.
2.设置需要使用的内核源码目录路径:
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
3.将当前路径赋值给PWD
PWD := $(shell pwd)
4 modules为makefile的默认目标被执行(因为是第一个),而 modules_install 和clean则是伪目标,执行make时,只执行modules 目标.
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
5 .PHONY作用.这里的主要作用是避免在makefile执行的命令和工作目录下的实际文件出现名字冲突.
.PHONY: modules modules_install clean
比如我在工作目录下,建了一个文件clean,此时输入以下命令;
make clean
此时make产生的*.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions文件已删除.
如果将makefile改为
.PHONY: modules modules_install clean
则会提示"clean" is up to date.
makefile编译过程理解:
1;第一次执行makefile,KERNELRELEASE宏刚开始值为NULL,进入ifeq
2:赋值得到KERNELDIR,PWD
执行第一个目标(默认的目标)modules:
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
$(MAKE)即为make命令, -C $(KERNELDIR)即进入内核源码目录编译顶层makefile,此时KERNELRELEASE被赋值. M=$(PWD) modules 表示在构造modules目标之前返回到当前模块源码所在目录,第二次执行makefile.此时KERNELRELEASE已经被赋值,所以执行else部分.
else
# called from kernel build system: just declare what our modules are
obj-m := hello.o
自此得到可装载的模块hello.ko.
三 装载卸载模块
1.linux的系统调用
系统调用指的是操作系统提供给用户程序调用的一组特殊接口,是用户空间和内核通讯的普遍按照功能区域,linux系统调用大致分为进程控制,文件访问,系统控制,存储管理,网络管理,进程通讯等,详细说明可以通过man 2 syscalls命令查看manpage说明。
2.加载模块
linux设备驱动有两种加载方式insmod和modprobe。
insmod一次只能加载特定的一个设备驱动。写法为:
insmod hello.ko
modprobe可一次将有依赖关系的驱动全部加载到内核。驱动被安装在/lib/modules/$(uname -r)/...下。写法为:
modprob hello //
注:uname -r是终端中输入uname -r后显示的内核版本,如我的“uname -r”为5.4.0。
加载驱动模块(驱动程序模块名为hello.ko)
方法一insmod:
进入hello.ko驱动模块文件所在的目录(可按自己的喜好放在相应的目录里),然后直接
insmod hello.ko
方法二modprobe:
1)、将hello.ko文件拷贝到/lib/module/#uname -r#/目录下(这里hello.ko必须按要求拷贝到这个指定目录下)
cp /home/valian/SLAM/linux/hello_world/hello.ko /lib/modules/5.4.0/
2)、depmod(会在/lib/modules/#uname -r#/目录下生成modules.dep和modules.dep.bb文件,表明模块的依赖关系)
depmod
3)、加载
modprobe hello(注意这里无需输入.ko后缀)
4) 消息打印查找
dmesg|tail -5 显示最后5条
控制台显示界面如图:
5)、两种方法的区别:
modprobe和insmod都是动态加载驱动模块的,区别在于modprobe可以解决load module时的依赖关系,它是通过/lib/modules/#uname -r/modules.dep文件来查找依赖关系的;而insmod不能解决依赖问题。insmod可以在任何目录下执行,更方便一些。
6)、显示已载入系统的模块:
lsmod
如下图可以看到hello模块已经载入。
3.卸载驱动模块
在任何目录下,输入命令
rmmod hello //hello是模块的名称,而不需要hello.ko这个文件
四、版本依赖
如果需要编写能够在多个内核版本一起工作的模块,则必须使用宏和#ifdef来构造并编译自己的代码。在linux/version.h找到相关版本定义(linux:不同的版本显示不同,如本文为linux-5.4)
find / -type f -name "version.h" // 进入对应文件夹查找version.h文件
查到/usr/src/linux-5.4/usr/include/linux/version.h。打开文件version.h:
vim /usr/src/linux-5.4/usr/include/linux/version.h
打开version.h内容为:
#define LINUX_VERSION_CODE 328704 // 本机版本为5.4.0 即 328704=5<<16+4<<8+0
#define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c)) //
五、驱动模块设计
1、所有可装载的模块代码都必须包含以下头文件:
#include <linux/module.h>
#liclude <linux/init>
2、模块应该指定代码所使用的许可证。因此需要包含MODULE_LICENSE行:
MODULE_LICENSE("GPL"); //"GPL","GPL v2","Dual BSD/GPL"等等其他许可证
为方便于选择不同的许可证,可使用宏定义实现:
#define __license__ "GPL" // 设置许可证
MODULE_LICENSE(__license__);
3、可增加描述性定义
MODULE_AUTHOR("**@163.com>");
MODULEDESCRIPTION("Linux Kernel hello module ");
MODUALE_VERSION("drive_1.0.0")
4 初始化、关闭模块
初始化:模块的初始化负责注册模块所提供的任何设施,这里的设施是指一个可以被应用程序访问的新功能,可以是一个完整的驱动程序或仅仅是一个新的软件抽象。
(static) int hello_init(void)
{
/* 初始化 注册代码* /
}
module_init(hello_init);
清除模块:清除函数在模块被移除之前注销接口并向系统中返回所有资源。函数定义如下:
(static )void hello_exit(void)
{
/* 清除代码* /
}
module_exit(hello_exit);
如果一个模块未定义清除函数,则内核不允许卸载该模块。
六、模块参数
当硬件的I/O端口或I/O内存地址位置需要告知给我们的驱动程序时,内核允许驱动程序指定这些参数,并且这些参数可在装载驱动程序模块时改变。
书中提供的hellop.c中,在hello.c模块中增加两个参数改成hellop模块:一个为整数,howmany(次数);一个为字符串,whom。在装载模块时,向whom问候howmany次。参数定义如下:
module_param(howmany, int, S_IRUGO); // S_IRUGO 只读
module_param(whom, charp, S_IRUGO); // S_IRUGO 只读
参数定义为只读,我需要通过自己的实践,完成sysfs对参数的修改,所以在hellop.c中,修改参数定义如下:
module_param(howmany, int, S_IRUGO|S_IWUSR); //可读可写
module_param(whom, charp, S_IRUGO|S_IWUSR); //可读可写
装载模块指令如下:
insmod hellop.ko howmany=8 whom="valian" //
注意:书中少写了hellop后面的.ko,读者调试的时候需要注意。
1.通过sysfs修改参数
在linux中,每一个module(驱动)加载到kernel后,都会在/sys/module目录下生成以该模块命名的一个文件夹,即生成了文件夹,其参数则保存在/sys/module/hellop/parameters。
通过ls命令显示参数:
ls /sys/module/hellop/parameters
得到参数如下howmany,whom:
root@valian-TM1703:/home/valian/SLAM/linux/hellop# ls /sys/module/hellop/parameters
howmany whom
通过echo修改参数howmany和whom,然后通过cat指令显示修改前和修改后的参数。
root@valian-TM1703:/home/valian/SLAM/linux/hellop# cat /sys/module/hellop/parameters/howmany
8
root@valian-TM1703:/home/valian/SLAM/linux/hellop# cat /sys/module/hellop/parameters/whom
valian
root@valian-TM1703:/home/valian/SLAM/linux/hellop# echo 15 > /sys/module/hellop/parameters/howmany
root@valian-TM1703:/home/valian/SLAM/linux/hellop# cat /sys/module/hellop/parameters/howmany
15
root@valian-TM1703:/home/valian/SLAM/linux/hellop# echo "my son" > /sys/module/hellop/parameters/whom
root@valian-TM1703:/home/valian/SLAM/linux/hellop# cat /sys/module/hellop/parameters/whom
my son
从以上输出可以看出参数howmany由8改为15,whom由"valian"改为"my son"。成功的通过sysfs修改参数。
注意:sysfs文件系统一般是自动加载到/sys下的,如果在根目录下没有找到sysfs,可以通过下面的命令手工加载:
mkdir -p /sysfs //创建sysfs目录
mount -t sysfs sysfs /sysfs //挂载sysfs到/sys目录