Linux驱动–(二)简单的内核模块驱动程序
一、概述
-
Linux中所有的驱动都是以内核模块的形式来实现的,他们与其他所有的内核编译在一起形成一个单独的内核镜像文件(所以说Linux是一个宏内核)。当然啦,这是相对于微内核而言的(典型的微内核的代表是Windows系统),后面我会专门总结一下这两种内核的优缺点和不同,这里呢,先简单说一下红内核和微内核的区别(后面会专门写一篇博客来区分二者):
宏内核:所有的内核功能都被编译到一起,形成一个单独的内核镜像文件。其显著优点是效率非常高,内核中各个功能模块的是通过直接的函数调用来是进行的。
微内核:只实现内核中相当关键以及核心的一部分,其他功能模块被单独编译,功能模块之间的交互需要微内核微内核提供的某种通信机制来建立。
通过上面的一个简单描述描述,我们很快就能发现向Linux这种宏内核的缺点:如果我们要重新添加、删除某一个模块的时候就不得不重新去编译整个内核。为什么呀?因为某块的驱动是内核的一部分啊,它是随内核一起编译的啊。我每次修改一点都得重新编译整个内核,那不得烦死啊。可能大家没有体会,因为之前大家编写的代码都是几十行,几百行,最多几万行。但是呢,Linux截至目前位置已经有几千万行了。这是什么概念的?就是如果的你的电脑性能一般,可能光编译一个内核就得几个小时,更被说再将一些复杂的驱动模块添加进去,例如一些网络驱动的程序代码。我的电脑性能还不错,有时候编译一次也得一个小时左右。所以现在一些公司的都是用一些够高性能的服务器进行代码的编译工作的。
说这些呢,知识想让大家理解上面所说的那种方式给我们的开发巩固走带来的不便。那么有没有办法解决呢?当然有的,世界上那些Linux大神们就想到了一个简单的办法来解决。那就是动态加载内核模块。
动态加载:简单点来说就是,前面提到的一些内核模块单独编译,它在需要的时候就动态的加载进内核。从而动态的增加内核功能,在不需要的时候就卸载掉。而无论是加载和卸载都不需要编译整个内核,需要编译的仅仅是我们需要加载的模块本身。
这里要说明一下,内核模块并不一定都是驱动模块,驱动也都不一定都是以模块的形式。
相反,我们前面说的那一种方式就是静态加载咯。
二、简单的驱动模块
- 像你学习所有语言的时候先输出个"hello world"一样,我们在学习内核驱动的时候也先整一个最最简单的驱动模块。当然啦,麻雀虽小,五脏俱全。这一个小小的模块就是我们,进入驱动模块的一道大门,也是我们开始认识驱动模块的一个"引路人"。先给出代码:
/**********************hello.c**********************/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
int init_module(void)
{
printk("init module:hello world!\n");
return 0;
}
void clean_module(void)
{
printk("cleanup module")
}
-
下面来简单分析一下上面这段代码,看似很简单,实则不然。
最上面的头文件,包含的是内核源码的头文件。如:第一行<linux/init.h>就是Linux内核源码树中/include/linux/init.h头文件。
<linux/kernel.h>中包含了printk函数的原型声明;
<linux/module.h>头文件这哪是没用,后面讲。
上面程序的6~10行是模块的初始化函数,在模块被动态加载的时候调用。函数不接受参数,返回0,表示模块的初始化函数执行成功,否则通常返回一个复制表示失败。其中printk是一个内核打印函数,同我们应用程序中printf类似,但是printk支持了打印等级的设置。这在我们设置内核log打印等级的时候很有用的,很多模块驱动程序调试时都使用到了该函数打印相关log。这里的init_module()函数也不是必须的,在加载模块时,如果没有发现该函数,则不会调用。在后面我们会继续完善这一函数,在里面添加诸如:内存的分配、模块的注册等内容。
12~15行是模块的清除函数,在模块从内核中被卸载时调用调用。顾名思义,该函数主要完成清除性的操作,是初始化函数的逆操作。后面会添加诸如:内存的释放、模块的注销等操作。
三、Makefile文件
-
Makefile文件主要是完成工程的自动化编译,关于Makefile文件的编写规则,我会在其他博文中介绍的。这里呢,Makefile文件的主要作用就是完成模块的编译工作,将模块添加到内核源码树中。将代码编译到内核的原码树中有两种方式:
第一种:通过修改源码中的Makefile文件,将相关的模块编译进去。
第二种:在内核源码树下新建一个目录,然后将该模块放在该目录下。然后在该目录下重新编写一个对应的Makefile文件。
我们这里采用第二种,第一种会修改内核源码,我们这里当然不采用啦。当然也不建议大家使用。下面是Makefile文件代码:
ifeq($(KERNELRELEASE),)
ifeq($(ARCH),arm)
KERNELDIR ?= /home/linux_driver/linux-3.14.25
ROOTFS ?= /nfs/rootfs
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.o *.ko *.cmd *.mod *.modules.order Modules.symhello .tmp_versions
else
obbj-m := hello.o
endif
-
可以看到这里整个代码被if…else…endif语句分为了两个部分。
第一部分是在KERNELRELEASE变量值为空的情况下执行的执行代码第1~16行。
第二部分则相反,在KERNELRELEASE变量值不为空是执行代码17~21行
其中,KERNELRELEASE是内核源码树中顶层Makefile文件中定义的一个变量,并对其赋值为Linux内核源码的版本,该变量会用export导出,从而可以在子Makefile中使用该变量。
在模块目录下执行make指令,将会导致make工具对于当前目录下的Makefile文件进行解释执行。第一次执行Makefile文件时,代码第一行的KERNELRELESE变量没有被定义,也没有被赋值,所以ifeq条件成立,那么此时他会执行第一行及第一部分的内容也就是前面说到的1~16行。第一部分内容包含了内核源码目录的变量KERNELDIR的定义,并且根据是编译ARM平台下的驱动还是PC上运行的驱动对该变量进行了不同的赋值,这样可以在命令行中对ARCH进行赋值来选择是编译那个平台下运行的驱动。其中/home/linux_driver/linux-3.14.25是我们所选的目标板的内核源码目录。如果是编译ARM平台下驱动,则还对根文件系统目录的变量ROOTFS进行了定义和赋值。接下来是对当前模块所在的目录的变量PWD定义(代码的第九行)。Makefile文件中的第一个目标modules为默认目标(代码第十一行),执行make而不跟参数。则会默认生成该目标。生成该目标就是要执行12行的命令,在代码12行中,$(MAKE)相当于make,主要用于平台的兼容。代码第12行的含义是进入到内核源码目录编译内核源码树之外的一个目录(由-C $(PWD)指定),中的模块(由最后的modules指定)。
当编译过程折返回(退出内核源码目录,再次进入模块目录,由 M=$(PWD)指定)编译模块时,上述的Makefile第二次被解释执行。不过这一次的执行和上一次的不同,此时KERNELRELEASE变量已经被赋值,并且被导出,导致ifeq条件不成立,那么将解释执行Makefile的第二部分,即外层else和endif之间的部分。其中,obj-m表示将后面跟的目标编译成一个模块。
module_install目标表示把编译之后的模块安装到指定的目录,安装的目录为$(INSTALL_MOD_PATH)/lib/modules/$(KERNELRELEASE),在没对INSTALL_MOD_PATH赋值的情况下,模块将会被安排到/lib/modules/$(KERNELRELEASE)目录下。
有关modules和modules_install,这里不多做解释,后面会详细讲解,这里只要会用即可。
clean的目标是清楚make生成的中间文件。
好啦,上面已经编译好了相关的Makefile文件,下面来讲讲该文案该如何运行。
使用以下两个命令使文件开始运行:
# make # make modules_install
注意,一般的话根据运行平台的不同,安装的目录会不同,有时候可能需要root权限才能安装。
比如,我们想要让它运行在ARM目标机上面,因为前面已经留了ARCH这一变量所以这里我们可以使用下面的命令:
# make ARCH=arm # make ARCH=arm module_install
编译完成后,会自当前的目录下生成一个.ko的文件。安装成功后,则会根据平台的不同将其赋值到不同的目录下。
四、内核模块的相关工具
-
模块的加载
**insmod:**加载指定目录下的一个.ko文件到内核。比如加载刚编译好的模块,可以用下面命令中的一个
# insmod hello.ko # insmod /lib/modules/3.14-32-generic/estra/hello
木块加载成功后使用dmesg命令,可以查看相应的内核log。这里要说一下啦,这个dmesg指令,我们平常在编写驱动的时候可能京城会用到来打印一些先关的内核log。
**modprobe:**自动加载模块到内核,相对于insmod来说这个就更加的智能了。但是其前提是模块要孩子要执行安装操作,在运行该命令前最好运行一次depmod命令来跟新模块的依赖信息。
# depmod # modprobe hello
-
查看模块信息
**modinfo:**查看模块的信息,比如:
# modinfo hello
-
模块的卸载
**rmod:**卸载指定的模块,前提是内核配置为允许卸载内核模块
# rmod hello