1、驱动程序,用于操作系统与硬件通信的软件程序。
1) 设备驱动程序的功能和用途;
2) 编写Linux内核模块;
3) Linux内核驱动分类;
4) PCI(peripheral component InterConnect,外设部件互连标准)总线
附属于操作系统的一部分,通常用于与硬件通信,每种硬件都有驱动程序,包含硬件设备的信息,操作系统通过驱动程序提供的硬件信息与硬件设备通信。驱动程序的功能相似,可归纳为3点:
1) 硬件设备的初始化。通过总线识别,访问设备寄存器、配置设备的端口、设置中断等。
2) 向操作系统提供统一的软件接口。设备驱动程序向操作系统提供了一类设备通用的软件接口,如硬盘设备向操作系统提供读写磁盘块、寻址接口等。
3) 提供辅助功能。如虚设备驱动。
Linux内核是一个整体结构,通过内核模块的方式向开发人员提供了一种动态加载程序到内核的能力。通过内核模块,开发人员可以访问内核的资源,内核还向开发人员提供了访问底层硬件和总线的接口。
2、Linux内核模块
内核可以动态加载和卸载内核模块,通过内核模块扩展内核的功能,通常内核模块被用于设备驱动、文件系统等。
2.1Linux内核是一个整体结构,当修改和添加新功能时,需要重新生成内核,如果把Linux内核设计成具有内核模块机制,效率会较前者高。从代码的角度分析,内核模块是一组可以完成某种功能的函数集合;从执行的角度分析,内核模块是一个已经编译但是没有链接的程序。从内核角度分析,模块包含了在运行时可以连接的代码,可以被连接到内核作为内核的一部分;从用户角度分析,内核模块是一个外挂组件,在需要的时候挂载到内核,也可以卸载。
内核模块也是一个应用程序,但与普通的应用程序有所不同,
1) 运行环境不同。内核模块运行于内核空间,可以访问系统的几乎所有的软硬件资源;普通应用程序运行于用户空间,访问的资源有限。
2) 功能定位不同。内核模块是为其他内核模块以及应用程序服务的,提供通用的功能;普通应用程序为完成某个特定的目标或任务。
3) 函数调用方式不同。内核模块调用内核提供的函数,若调用其他的库函数会导致运行异常;普通应用程序调用相关的库函数。
2.2内核模块结构
必需要考虑的一个问题是驱动程序的并发性,在内核空间同一时间内有可能有多个进程在运行,在设备向处理器发送中断时,导致一个内核模块被调用多次,内核模块必须支持可重入。内核的数据结构在多线程环境下易被其他线程干扰,加锁保护是必须的。
内核模块提供一个current指针,指向当前正在运行的进程。内核模块通过current指向调用自身的进程,可以把数据准确地返回给指定进程。
Linux内核模块使用物理内存,而应用程序使用虚拟内存,可以分配较大的内存。内核模块可以供使用的内存有限,最小到一个内存页面(4096 byte),需要注意内核模块对内存的分配和使用。
在Linux 2.6系列内核中,通过module_init()宏加载内核模块调用内核模块的初始化函数,通过module_exit()宏卸载内核模块调用内核模块的卸载函数。
Static int _init init_func(void); // 初始化函数
Static void _exit exit_func(void); //清除函数
函数名称可以由用户自定义,但是必须使用规定的返回值和参数格式。Static修饰符作用是函数在当前文件有效,外部不可见;_init关键字告诉编译器,该函数代码在初始化完毕后被忽略;_exit 关键字告诉编译器该代码仅在卸载模块调用。
2.3内核模块的加载和卸载
Linux内核提供了一个kmod管理内核模块,kmod模块与用户态的kmodule通信,获取内核模块的信息。
内核模块加载
通过insmod命令和modprobe命令加载内核模块。Insmod命令加载内核模块不检查内核模块的符号是否已经在内核中定义;modprobe检查内核模块的符号表和模块的依赖关系。此外,Linux内核可以在需要加载某个模块时通过kmod机制通知用户态的modprobe加载模块。
使用insmod加载内核模块,首先使用特权级系统调用查找内核输出的符号,通常,内核输出符号被保存在内核模块列表第一个模块结构。Insmod命令把内核模块加载到虚拟内存,利用内核输出符号表来修改被加载模块中没有解析的内核函数和资源地址,之后,insmod使用特权指令申请存放内核模块的空间。因为内核模块是工作在内核态的,访问用户态的资源需要做地址转换,申请成功后,insmod把内核复制到新控制,然后把模块加入到内核模块列表的尾部,并设置模块标识为UNINITIALIZED,表示模块还没有被引用。内核模块被安装到内核后,insmod使用特权指令告诉内核新增加的模块初始化和清除函数的地址供内核调用。
内核模块卸载
内核模块卸载主要是对模块引用计数的判断,内核模块被其他模块引用时,自身的引用计数器会加1,当模块卸载时,判断引用计数器是否为0,如果为0才能卸载,否则将其减1。使用rmmod命令卸载内核模块,rmmod命令从内核模块列表中查找指定的模块,判断模块引用计数是否为0,对于引用计数为0的模块,从内核模块列表中删除,然后释放占用的内存。
2.4最小内核模块
/*module_hello.c*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“zhangzr”);
Static int _init h_init(void)
{
Printk(KERN_ALERT“(init)hello,world!\n”);
Return 0;
}
Static void _exit h_exit(void)
{
Printk(KERN_ALERT“(exit)hello,world!\n”);
}
Module_init(h_init);
Module_exit(h_exit);
2.4.1编译内核模块
编译内核版本需要建立一个makefile,主要目的是使用内核头文件,因为内核模块对内核版本存在依赖关系。
Ubuntu Linux系统编译内核模块:
1)在shell下安装当前版本的Linux内核源代码
$ sudo apt-get install linux-source
编译内核模块不需要重新编译内核代码,但需要使用当前版相同的代码。
2)在module_hello.c同一目录下编写makefile:
Ifneq ($(KERNELRELEASE),)
Obj–m := module_hello.o
Else
KERNELDIR?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
Default:
$(MAKE) –C $(KERNELDIR) M=$(PWD) modules
endif
第一行检查是否定义了KERNELRELEASE环境变量,如果定义则表示该模块是内核代码的一部分,直接把模块名称添加到obj-m环境变量;如果未定义,表示在内核代码以外编译,通过设置KERNELDIR和PWD环境变量,然后通过内核脚本编译当前文件,生成内核模块文件。
3)makefile建立完毕后,在shell下输入make编译内核。
4)编译结束后,生成module_hello.ko,通过modprobe加载内核模块。
$sudo insmod ./module_hello.ko
在加载过程中可以看到hello_init()函数的输出信息。
5)对加载到内核中的内核模块,可以使用rmmod命令卸载内核模块。
$sudo rmmod module_hello.ko
在卸载过程中可以看到hello_exit()函数的输出信息。
验证一下:$sudo lsmod |grep module_hello // 查看模块列表,若无输出,则卸载完成
2.4.2 为内核模块添加参数
驱动程序需要在加载的时候提供一个或者多个参数,内核模块提供了设置参数的能力,通过module_param()宏为内核模块设置一个参数,module_param(参数名称,类型,属性),参数名称是加载内核模块是使用的参数名称,在内核模块中需要有一个同名的变量与之对应;类型是参数的类型,内核支持C语言常用的基本类型;属性是参数的访问权限。
/*module_param.c*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“zhangzr”);
Static int value = 0;
Static char * name = NULL;
Module_param(value, int,S_IRUGO);
Module_param(name, char*,S_IRUGO);
Static int hello_init(void)
{
Printk(KERN_ALERT“value = %d name = %s\n”, value, name);
Printk(KERN_ALERT“(init)hello,world!\n”);
Return 0;
}
Static void hello_exit(void)
{
Printk(KERN_ALERT“(exit)hello, world!\n”);
}
Module_init(hello_init);
Module_exit(hello_exit);
$sudo insmod ./module_param.ko value=123name=zhangzr
3、Linux设备驱动
Linux系统把设备驱动分成字符设备、块设备、和网络设备3种类型。内核为设备驱动提供了注册和管理接口,设备驱动还可以使用内核提供的其他功能以及访问内核资源。
3.1、PCI局部总线
外设部件互连标准。
3.2、设备驱动
在Linux系统中,设备驱动通常作为一类特殊的文件存放在/dev目录,在内核中使用主设备号标识一个设备,次设备号提供给设备驱动使用。在打开一个设备时,内核会根据设备的主设备号得到设备驱动,并且把次设备号传给驱动。Linux内核为所有的设备分配主设备号。
在使用一个设备时,需要建立设备文件,mknod命令格式:
mknod[OPTION] … NAME TYPE [MAJOR MINOR]
NAME是设备文件名称;TYPE是设备类型,c代表字符设备,b代表块设备;MAJOR是主设备号;MINOR是次设备号;OPTION –m参数指定设备文件访问权限。
Linux内核按照外部设备工作特点把设备分成字符设备、块设备和网络设备3种基本类型。编写设备驱动需要使用内核提供的设备驱动接口,向内核提供具体设备的操作方法。
3.3、字符设备
字符设备是Linux系统最简单的一类设备。应用程序可以像操作普通文件一样操作字符设备。常见的串口、调制解调器都是字符设备。需要使用内核提供的register_chardev()函数注册一个字符设备驱动。
Int register_chardev(unsigned int major,const char* name, struct file_operation *fops);
在/proc/devices文件为已经注册的设备信息。
Int unregister_chrdev(unsigned int major,const char* name);
Major是主设备驱动号;name是设备驱动名称。
4、开发字符设备驱动
功能:建立一个名为globalChar的虚拟设备,设备内部有一个全局变量供用户操作。设备提供了读函数读取全局变量的值并且返回给用户,写函数把用户设定的值写入全局变量。