Linux Module And Device Driver

慕容灿
2023-12-01

转自:http://hi.baidu.com/yopklnqbrsampsr/item/0de01ac7ea05960fac092fcb

 

这篇文章是笔者在学习Linux环境下模块和驱动程序编程时的个人总结,其中很多内容来自于参考文献。当前,这篇东西只是前三周的小结,后续进行一些练习后将进行更新,^_^。如果文中有错误之处,敬请指正,谢谢。jixu_yang@yahoo.com.cn


1 proc
在linux系统中,对于内核和模块,除了平常的方式,还可以使用特别的方式来向进程传递信息--proc文件系统。Proc文件系统设计的初衷是为了方便地访问进程信息,但是现在该文件系统得到了很大的扩展,它反映了当前运行系统的信息映像,如/proc/meminfo反映了当前内存的使用情况, /proc/modules反映了当前模块的装载情况。


1.1 proc文件系统
Proc文件系统是对在内存中运行的系统的直接反映,它使用层次式的方式来表示系统。通过proc,我们能够方便地了解内核信息和当前正在运行着的进程的信息。因此,一些shell命令可以很方便地使用这些信息来显示系统当前状况,如ps命令。设计proc文件系统的初衷是为了采用一种可读的方式来提供系统信息,而非提供相关的系统调用来获取系统信息。


1.2 内核读写操作
对于父子进程,它们的段描述符相同,为此它们之间的数据交换可以直接进行。但是如果内核需要获取某个用户进程的数据,或某个进程需要从内核中获取数据,那么就需要进行特殊的操作了,因为内核的地址空间与用户进程的地址空间不同。为此,内核提供了相关的函数来方便进行内核与应用程序之间的数据交换,如 copy_from_user(kernel_addr,user_addr,user_buf_len),copy_to_user (user_addr,kernel_addr,kernel_buf_len),get_user(kernel_char,user_addr), put_user(user_char,kernel_addr)。

 
1.3 创建proc文件系统机制
创建proc文件的方式与编写驱动程序的方法相似。你至少需要定义两个函数--init_module和cleanup_module函数,初始化函数用于创建proc文件并初始化,而退出函数用于清除proc文件并释放申请的内存空间等等。
为了创建一个proc文件,你需要在init_module函数中使用create_proc_entry来创建一个proc文件句柄。该句柄是一个 struct proc_dir_entry类型的指针。然后,你需要设置这个指针的某些函数指针域,这样当用户在应用程序中通过read/write来读写这个 proc文件时,就会调用这些指针域中的函数来进行具体的处理,就如同编写驱动程序中需要设置fops结构体一样。结构体struct proc_dir_entry的类型定义如下:

struct proc_dir_entry { 
	unsigned int low_ino; 
	unsigned short namelen; 
	const char *name; 
	mode_t mode; 
	nlink_t nlink; 
	uid_t uid; 
	gid_t gid; 
	unsigned long size; 
	struct inode_operations * proc_iops; 
	struct file_operations * proc_fops; 
	get_info_t *get_info; 
	struct module *owner; 
	struct proc_dir_entry *next, *parent, *subdir; 
	void *data; 
	read_proc_t *read_proc; 
	rite_proc_t *write_proc; 
	tomic_t count; /* use count */ 
	nt deleted; /* delete flag */ 
	oid *set; 
}; 



其中着蓝色的域是我们经常所使用的。其中的read_proc和write_proc是proc接口,通过设置这两个域,那么当我们在用户程序中读该 proc文件时,read_proc对应的函数被调用,类似的,当我们在用户程序中写该proc文件时,write_proc对应的函数被调用。它们的函数类型分别是:

int procfile_read(char *buffer, char **buffer_location, off_t offset, 
	int buffer_length, int *eof, void *data) 
int procfile_write(struct file *file, const char *buffer, 
	unsigned long count, void *data) 



1.4 通过文件系统接口来操作proc文件
在上面的小节中,我们说过,通过设置read_proc和write_proc域就可以使得用户程序能读写proc文件。但是为了进行其它高级的文件操作,如权限管理,那么就需要使用其它的两个域--proc_iops和proc_fops,它们分别用于对inode节点和文件操作进行控制。对于 proc_fops,它的使用方式和作用与驱动程序中注册设备时传递的设备文件操作结构体相同。对于proc_iops,它主要用于权限管理,其数据结构如下:

struct inode_operations { 
	int (*create) (struct inode *,struct dentry *,int, struct nameidata *); 
	struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *); 
	int (*link) (struct dentry *,struct inode *,struct dentry *); 
	int (*unlink) (struct inode *,struct dentry *); 
	int (*symlink) (struct inode *,struct dentry *,const char *); 
	int (*mkdir) (struct inode *,struct dentry *,int); 
	int (*rmdir) (struct inode *,struct dentry *); 
	int (*mknod) (struct inode *,struct dentry *,int,dev_t); 
	int (*rename) (struct inode *, struct dentry *, 
	struct inode *, struct dentry *); 
	int (*readlink) (struct dentry *, char __user *,int); 
	void * (*follow_link) (struct dentry *, struct nameidata *); 
	void (*put_link) (struct dentry *, struct nameidata *, void *); 
	void (*truncate) (struct inode *); 
	int (*permission) (struct inode *, int, struct nameidata *); 
	int (*setattr) (struct dentry *, struct iattr *); 
	int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *); 
	int (*setxattr) (struct dentry *, const char *,const void *,size_t,int); 
	ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t); 
	ssize_t (*listxattr) (struct dentry *, char *, size_t); 
	int (*removexattr) (struct dentry *, const char *); 
	void (*truncate_range)(struct inode *, loff_t, loff_t); 
}; 



其中着蓝色的为进行权限管理的函数。


1.5 本章小结
本章描述了proc设计的初衷,并说明了如何创建proc文件以及如何通过proc接口和文件系统接口(inode接口)来控制proc文件。


2 Module
模块,或者说是可装载模块(Loadabel Kernel Module),指的是在Linux系统启动以后,可以通过一些命令(insmod,rmmod,modprobe)来加载一些目标文件到内核中,使之成为系统的一部分(为此它们可以使用系统的任何函数,数据结构等众多资源),而这些被加载的目标文件我们就称其为可加载模块LKM。而系统系统时的内核我们称其为基本内核(base kernel)。
模块这种东西从1995年(Linux 1.2)版本开始引人,至2000年左右开始变得成熟。LKM主要用于如下的处理:
设备驱动。用于控制硬件设备。
文件系统驱动。用于操作不同的文件系统文件。
系统调用。用于替换原有系统调用或增加自己的系统调用。
网络驱动。用于控制网卡,向外发送数据或接受数据。
TTY Line Disciplines。不太明白,主要用于扩充终端设备。
可执行程序解释器。通过引入可执行程序解释器,Linux系统能执行非ELF类型的可执行程序。


2.1 模块文件
模块也是一种obj文件。从内核2.6开始,内核把模块文件的扩展名从.o改为.ko,以区别与普通的目标文件。而且在.ko文件中增加了.modinfo section,为此我们可以通过modinfo mod.ko来查看某个模块的基本信息,类似如下:
filename: hello-4.ko
description: A simple testing driver
author: Jixu-Yang <jixuyang@gmail.com>
license: GPL v2
srcversion: 7F87DF94E6C40958B2866F5
depends:
vermagic: 2.6.15-1.2054_FC5 686 REGPARM 4KSTACKS gcc-4.1
模块的编写方式与平常的编程相似,但是它引用的外部符号必须是内核所提供的,即我们通过查看cat /proc/kallsyms所看到的符号。既然它是obj文件,所以当编译完成时,它对外部符号的引用仍然没有解决。实际上,对模块中所引用的外部符号是在insmod过程中解决的。
模块所能引用的符号与平常函数库中的符号不同,它所使用的的符号是在/proc/kallsyms中的符号,这些符号包括系统调用函数,如sys_fork,syscall_exit等等。


2.2 版本检测
2.6以前,系统使用genksyms命令来产生.ver文档,这些文档用于采用宏定义的方式来重命名内核符号(如#define printk _set_ver(printk),这样将使printk函数变为printk_R454463b5),目的是使模块编译时的内核版本与其加载时的内核版本相同,这样可以保证系统的模块与base-kernel的一致。但是,从2.6起,因为在目标文件中引人了modinfo section,所以使得在加载模块时能够使用这个section所提供的信息来进行内核版本的审核,达到检验一致性的目标(这是我自己看相关文档总结出来的,但还没有看到相关的文档有正式的说明),所以在fc5上已经没有genksyms命令了。


2.3 认证检测
从linux-2.4以后,系统引人了模块认证机制。即如果你的模块是私有的(即没有宣告为GPL或BSD等等),而你却使用了GPL所提供的服务,那么你的模块将不能被加载到系统中,系统会提示你unresolved symbol。产生这个错误的原因是:对于不同的认证,对于Linux-2.4,系统会在提供服务的函数中加入前缀或后缀来判断,引出符号时需要使用 EXPORT_SYMBOL或EXPORT_SYMBOL_GPL;对于Linux-2.6,系统使用目标文件中的modinfo section来判断,使用MODULE_LICENSE来说明。
在当前的2.6版本中,支持的认证有五个:
GPL
GPL v2
GPL and additional rights
Dual BSD/GPL
Dual MPL/GPL


2.4 初始化和退出函数
2.4.1 申明函数方式
从kernel-2.3.13开始,内核引人了新的模块机制,这样,用户写module时就不需要必须把初始化函数和清除函数写作init_module (返回类型必须是int)和cleanup_module(返回类型必须是void),而可以使用两个宏module_init(init_func)和 module_exit(exit_func)来设置初始化函数和退出函数。当然,用户仍然可以使用原来在kernel-2.3.13以前的函数定义方式。那么内核如何来保证用户的两种方式都有效呢?通过查看实际通过看include/linux/init.h源代码,我们可以发现,对于动态加载的模块,其module_init和module_exit定义为:

#define module_init(initfn) \ 
	static inline initcall_t __inittest(void) \ 
	{ return initfn; } \ 
	int init_module(void) __attribute__((alias(#initfn))); 
#define module_exit(exitfn) \ 
	static inline exitcall_t __exittest(void) \ 
	{ return exitfn; } \ 
	void cleanup_module(void) __attribute__((alias(#exitfn))); 



从如上的定义我们可以看到,系统把init/cleanup_module函数申明为函数initfn或exitfn的别名函数,即调用initfn或exitfn时,相当于调用init_module或cleanup_moudle函数。


2.4.2 可以没有init/cleanup_module函数
如果没有初始化函数,那么可以不定义init_module函数或不使用moudle_inti来申明初始化函数。而且,如何你希望module是 unloadabel的,即该模块被装载之后就不能被unload,那么你还可以不定义cleanup_module函数或不使用module_exit 来申明清除函数。此时,当cat /proc/modules时,你会看到该模块的信息中有[permanent]的输出,这就表明该模块是不可协作的,即永久存在于系统,除非重新启动 (仅仅注销是不行的,必须重新启动才可)。


2.5 __init和__exit
通过看include/linux/init.h,我们能够看到如下的定义:

#define __init __attribute__ ((__section__ (".init.text"))) 
#define __initdata __attribute__ ((__section__ (".init.data"))) 



这意味着如果某个函数带有__init或变量带有__initdata,那么它们在linker阶段会被放置在.init.text或. init.data区域。内核启动时会调用init函数,它又会调用free_initmem函数(与硬件体系结构相关),这个函数会释放在ELF文件中.init section的空间(实际变量也可以使用__initdata来申明存放在init section),实际释放的地址空间为链接时从__init_begin到__init_end之间的内核空间。而__exit和__exitdata 的作用与__init差不多。


2.6 moudle_init和module_exit
对于被静态编译进内核的module,它们都使用了__init或__initdata宣告,其module_init定义为:

typedef int (*initcall_t)(void); 
#define __define_initcall(level,fn) \ 
	static initcall_t __initcall_##fn __attribute_used__ \ 
	__attribute__((__section__(".initcall" level ".init"))) = fn 
#define device_initcall(fn) __define_initcall("6",fn) 
#define __initcall(fn) device_initcall(fn) 
#define module_init(x) __initcall(x); 



根据上面我们对__init和__initdata的说明,我们能够知道,使用这些属性声明的函数或变量会被放置在ELF文件的.init区。通过上面对 moudle_init的定义,我们同时也知道,凡是通过该函数寄存的函数皆会在.initcall6.init中定义一个函数指针。实际上在系统启动初始化过程中(通过调用init函数)对这些初始化函数的调用皆是通过这些函数指针而间接调用的。而在函数free_initmem中进行内存释放时,不仅会释放.init区中的函数代码或数据,而且还会释放这些函数指针区域(可以参考www.linuxforum.net上Linux内核技术主题下的精华文章:内核调用__init函数的顺序和Linux kernel 2.4 Internals中的1.8小节)。module_exit的作用与之类似。
但是对于loadable的module(即系统启动后可以通过insmod或modprobe来装载的模块),虽然你使用了__init,虽然它也会被放置在.init区,但是没有相应的内存释放函数来释放这个区的空间,所以它不会被释放,但是你编程时还是应该加入这个属性,这样可以保证如果后来你的程序静态编译进内核,那么它就是有价值的了(可以参考The Linux Kernel Module Programming Guide文档中的2.4小节)。


2.7 给模块传递参数
有时,我们需要给模块传递参数来设置模块中一些变化的初值,此时我们需要使用宏MODULE_PARM,使用该宏来说明参数的名字和类型。该宏支持的类型有b(bool类型),h(短整形),i(整数),l(长整数),s(字符串类型)。同时,还可以使用数组,如果类型前面包括一个整数,那么表示该数组的最大长度,如果是通过'-'字符分开的两个整数,那么表示数组的最小和最大长度。如以下的变量:

int myint = 3; MODULE_PARM(myint, "i"); 
char *mystr; MODULE_PARM(mystr, "s"); 



对于数组的示例如下:

int myshortArray[4]; 



如果为MODULE_PARM(myintArray, "4i"),那么表示数组的最大长度为4,如果为MODULE_PARM(myintArray, "2-4i"),那么表示最小长度为2,最大长度为4。
启动系统时,我们可以给内核传递参数,这些参数有些就是就将被内核传递给各个内核模块以作为其内核模块参数。当我们使用insmod来加载模块时,也可以给模块传递参数。在Linux-2.6中,模块文件中有modinfo section,这个section包括有模块使用宏MODULE_PARM所定义的模块参数,insmod命令使用这些信息来给模块参数赋值。


2.8 模块加载原理
对于Linux-2.4和Linux-2.6,虽然它们都使用insmod来加载模块,但是它们的处理方式是不同的。对于2.4,insmod扮演 linker的作用,它先对模块文件进行符号重定位,然后再把经过重定位的模块传递给内核。对于2.6,insmod进行版本、认证和参数检查后,直接把模块传递给内核,它自己不进行符号重定位。


2.9 modprobe工作原理
在linux-2.2以前,内核使用守护进程kerneld来根据需要自动加载模块,如当系统打开一个MS-DOS的文件时,如果当前系统没有加载 msdoc.ko模块,那么系统自动加载该模块,然后再进行文件操作。从Linux-2.2开始,内核使用modprobe命令来进行自动加载,该命令最终调用insmod来进行加载。系统使用/proc/sys/kernel/modprobe来记录该命令所存在的位置。
当使用该命令来加载模块时,不需要指定模块的绝对路径,而它照样能加载模块,这是通过什么机制实现的呢?而且,它通过什么方式来解决模块之间的依赖关系呢?
实际上,但该命令接受到参数时,它会在系统文件/etc/modules.conf和/etc/modprobe.d/目录下(这些文件定义模块的别名,如alias eth0 b44,表示模块eth0的别名是模块b44)寻找是否存在这个模块名。如果找到别名,那么该命令会通过/lib/modules/<kernle -version>/modules.dep(该文件使用绝对路径来指明模块的位置和其依赖模块的位置,所以可以调用insmod来进行加载)来查看该模块依赖于哪些模块,然后使用insmod命令来依次加载这些模块。如果没有找到别名,那么直接在/lib/modules/<kernle- version>目录下以传入的模块参数名来寻找模块,如果找到,那么使用insmod命令来进行加载,否则打印出“FATAL: Module XXX not found.”的错误信息。

 
2.10 把自己的模块安装到系统
通常,各种手册中总是推荐我们使用modprobe来加载或卸载模块,因为它可以根据模块之间的依赖关系来自动地加载或卸载所依赖的模块。但是,我们编译并安装了自己的模块后(如果需要安装target,那么需要在你的模块的Makefile中加入make -C /lib/module/$(shell uname -r)/build M=$(PWD) modules_install),仍然不能使用该命令来加载模块,这是为什么呢?
这是因为,当我们安装了自己的模块后(该模块会被安装到/lib/module/<kernel-version>/extra目录下),我们并没有更新modules.dep文件。此时,我们仅仅需要在命令下运行depmod命令(不需要在任何特殊的目录),modules.dep文件就会得到更新,此时我们的模块会被加入到该文件的顶端(例如/lib/modules/2.6.15-1.2054_FC5/extra/jixu.ko: )。在冒号后面没有东西,表示它不依赖于其它模块,如果有东西,那么表示它依赖于其它模块,这样modprobe就可以根据这个依赖关系来装载模块了,如 /lib/modules/2.6.15-1.2054_FC5/extra/hello-4.ko: /lib/modules/2.6.15-1.2054_FC5/extra/hello.ko,表示如果要加载hello-4.ko模块,那么必须先加载hello.ko模块。


2.11 模块间的依赖关系
我们可以通过修改modules.dep文件来显示地设置模块之间的依赖关系。但是如果两个模块之间实际不存在依赖关系,那么虽然在使用modprobe进行加载时会加载所依赖的模块,但是却可以在任何时候卸载该被依赖的模块,系统不会提示错误信息,这是为什么呢?
我的想法是:如果两个模块真正依赖,那么当用户想卸载被依赖的模块时,其引用计数一定不为0,故系统能提示错误信息。而如果两个模块实际上并不依赖(如我自己写的测试程序,hello-4并不依赖于hello),那么当卸载被依赖的模块时,由于其引用计数为0,故是可以卸载的,系统也不会输出错误信息。


2.12 重新编译系统
根据FC5的发行notes,我们知道该系统安装时是不会安装内核源代码的,但是如果需要进行内核开发,那么我们必须安装源代码,而且还需要对这个源代码进行编译(请参考linux/Documentation/kbuild/modules.txt文件的第二小节)。我安装的FC5使用的内核版本是 linux-2.6.15。安装的方法有两种:
(1)使用kernel-2.6.15-1.2054_FC5.src.rpm来进行安装。
(2)使用linux-2.6.15.1.tar.bz2来进行安装。


2.12.1 使用源代码rpm包
对于这种方法,Fedora Core 5 发行注记上有详细的说明:
1.在个人目录准备 RPM 软件包构建环境,运行下面的命令:
su -c 'yum install fedora-rpmdevtools'
fedora-buildrpmtree
2.从下列来源之一获取 kernel-version.src.rpm 文件:
- SRPMS 文件,包含在合适的 SRPMS CD iso 镜像文件中。
- 下载内核软件包的 HTTP 或 FTP 站点
- 使用yum,此时需要执行如下命令:
su -c 'yum install yum-utils'
su -c 'yumdownloader --source kernel'
3.安装 <kernel-version>.src.rpm,运行如下命令:
rpm -Uvh kernel-<version>.src.rpm
这个命令将 RPM 内容写到 ${HOME}/rpmbuild/SOURCES 和{HOME}/rpmbuild/SPECS,这里 ${HOME} 是您的个人目录。但是我使用这个命令时没有把RPM文件的内容写到HOME目录。
4.使用这样的命令来准备内核源代码:
cd ~/rpmbuild/SPECS
rpmbuild -bp --target $(uname -m) kernel-2.6.spec
内核源码树位于 ${HOME}/rpmbuild/BUILD/kernel-<version>/ 目录。
5.Fedora Core 附带的内核配置文件在 configs/ 目录。例如,i686 SMP 配置文件被命名为 configs/kernel-version-i686-smp.config。使用下列命令来将需要的配置文件复制到合适的位置,用来编译:
cp configs/<desired-config-file> .config
您也可以在 /lib/modules/version/build/.config 这个位置找到与您当前的内核匹配的 .config 文件。
6.每个内核的名字都包含了它的版本号,这也是 uname -r 命令显示的值。内核 Makefile 的前四行定义了内核的名字。为了保护官方的内核不被破坏,Makefile 经过了修改,以生成一个与运行中的内核不同的名字。在一个模块插入运行中的内核前,这个模块必须针对运行中的内核进行编译。为此,您必须编辑内核的 Makefile。 例如,如果 uname -r 返回字符串 2.6.15-1.1948_FC5,就将 EXTRAVERSION 定义从:
EXTRAVERSION = -prep
修改为:
EXTRAVERSION = -1.1948_FC5
也就是最后一个连字符后面的所有内容。
7.执行原始配置命令:
make oldconfig
8.执行编译命令:
make


2.12.2 使用标准内核发布代码
如果你使用原始的内核代码来编译模块,那么当你使用insmod命令把该模块插入到内核时,会出现如下的错误信息:
insmod: error inserting 'jixu.ko': -1 Invalid module format
而且,你还可以在/var/log/messages中看到如下的提示信息:
Jun 4 22:07:54 localhost kernel: poet_atkm: version magic '2.6.5-1.358custom 686 REGPARM 4KSTACKS gcc-3.3' should be '2.6.5-1.358 686 REGPARM 4KSTACKS gcc-3.3'
每个模块都有一个叫做.modinfo的section,在该section中保存有内核版本,编译器版本等信息(vermagic字段),这样当一个模块被插入内核时,内核就会使用这个信息来检查被插入模块是否与内核的版本信息相同。如果不同,那么就会输出如上的信息,否则就允许模块被插入。当然,用户可以使用modprobe的—force-vermagic选项来强制插入模块,但这样做是非常不安全的。
为此,我们需要当前运行系统的原始代码。但有时,我们可能没有这样的原始代码,如我使用的fc的代码为kernel-2.6.15- 1.2054_FC5,但是你在内核的标准发布中只能找到kernel-2.6.15。此时,我们就可以使用当前运行系统的配置文件和标准的内核发布来建立与当前运行系统相同的编译版本(也需要与当前运行系统有相同的内核版本,如kernel-2.6.15,而1.2.54_FC5表示在2.6.15基础上进行修改的版本号)。其步骤如下(假设你的标准内核源代码放置在/usr/src/linux/2.6.15目录下):
1.拷贝原始编译的配置文件到标准内核源代码目录下:
cp /boot/config-`uname -r` /usr/src/linux/2.6.15/.config
2.修改标准内核源代码目录下Makefile文件中的EXTRAVERSION为使用uname -r命令输出的修改版本号。如在我的fc5机器上运行uname -r的输出是:
2.6.15-1.2054_FC5
那么把EXTRAVERSION修改为-1.2054_FC5
3.使用/lib/modules/`uname -r`/build/include/linux/version.h文件覆盖标准内核源代码目录下的/usr/src/linux/2.6.15/include/linux/version.h文件。
4.最后在标准内核源代码目录下运行make即可,此时会输出如下的编译信息:
CHK include/linux/version.h
UPD include/linux/version.h
SYMLINK include/asm -> include/asm-i386
SPLIT include/linux/autoconf.h -> include/config/*
HOSTCC scripts/basic/fixdep
HOSTCC scripts/basic/split-include
5.如果你不想完全编译内核,那么当运行完SPLIT之后,你就可以使用CTRL-C来中断编译过程。因为此时我们进行内核编译所需要的文件已经生成了。
2.13 使用自己编写的module替代系统调用
当我们自己写的模块被insmod到内核后,我们的模块就成为了系统的一部分,所以我们可以使用内核的任何资源,如修改调度策略,设置系统原始的各种功能等等,这也可以想像如果一个包含恶意的模块被载人了内核,那么它可以为所欲为,如格式化你的硬盘等等。在这里,我们只是说明如何让自己的模块取代系统的原始功能(这当然是有风险的,因为如果自己的模块存在错误,那么可能导致系统崩溃)。
这里我们说明如何使用自己的模块来替代原始的系统调用。在2.6以前,内核会引出sys_call_tab符号,它记录所有系统调用的函数指针(从2.6 后,为了系统的安全性,内核已经不在引出这个符号,为此如果你要在2.6内核上进行这样的尝试,那么你需要引出sys_call_tab符号)。为此,如果在我们的模块中重新设置sys_call_tab,那么我们就可以使用我们自己的函数来替代原始的系统调用。当然,为了系统的稳定性而言,我们还需要在卸载模块时把sys_call_tab恢复成其原始的状态。
这种方式听起来好像稳妥,但是却还是存在着风险。试想,如果模块B被载入以前,已经有其它的模块A被载入,而且替代了原始的系统调用XXX,那么当模块B 载人时,它会用自身的函数替代模块A的替代函数。然后,如果B先退出,那么它会使用A的替代函数恢复原始的系统调用,然后当A退出时,它会用原始的系统调用恢复系统最初的系统调用。但是,如果是A先退出,那么它先把系统恢复成原始的状态,而如果此时应用程序调用XXX系统调用的话,没有问题,它使用的是原始的系统调用。而当B退出时,它会用A的替代函数覆盖掉原始的系统调用。最后,当应用程序调用XXX时,由于A已经被卸载,那么此时对XXX的调用将不知道会发生什么事情,系统崩溃在所难免。所以,我们可以在卸载模块时进行一点额外的判断,即判断当前的系统调用是否与原来该模块替代该系统调用时的函数指针相同,如果不同,那么报警,否则进行函数指针恢复。


2.14 THIS_MODULE
在linux/module.h中定义有一个宏THIS_MODULE,该宏实际定义为__this_module指针。最终该模块在被insmod时,会为之分配一个 module的结构,而该指针就指向该分配的结构体。


2.15 特别注意
自己在编写驱动程序的过程中,出现了一些失误,为此单独开辟一个小节来描述自己的一些切身体会。


2.15.1 误写退出函数名
刚开始写驱动程序时,你可能把cleanup_module函数写作clean_module。虽然这样,但是照样能编译通过,且能够使用insmod或 modprobe挂载这个module,但是如果使用rmmod或modprobe -r来卸载这个module,就会因为我们编写的清除函数名不正确,所以导致不能卸载,提示信息如下:ERROR: Removing 'module': Device or resource busy。
但是,如果你的module不需要unload,那么你可以不设置module_exit函数,这样一旦这个module被load,那么就不可以被unload。

 
2.16 本章小结
本章描述了模块实现机制和编写方法。

 
3 Driver
驱动程序也是一种模块,不过它是专门用于与硬件进行通讯的模块。每个硬件设备都通过/dev目录下的一个文件表示,这种文件称为设备文件。通过这些设备文件,用户可以如对文件系统的操作一样访问硬件设备。从系统的视角,驱动程序在内核中的体系结构如图1所示:
图1:内核视图[来源于LDD-3]
3.1 设备文件
在/dev目录在存在很多文件,这些文件是设备文件,而并非普通的文本文件。通过使用ls -l命令,你可以看到其权限输出域中第一个字符为c或b,它们分别表示该设备文件是字符设备文件和块设备文件。这些文件的大小为0,即它们不占用文件系统的空间,但是它们在文件系统中有相应的inode,而在该inode中保存有该设备文件的信息。Inode节点中使用一个stat结构记录该文件的基本信息:

struct stat { 
	mode_t st_mode; /* file type & mode (permissions) */ 
	ino_t st_ino; /* i-node number (serial number) */ 
	dev_t st_dev; /* device number (file system) */ 
	dev_t st_rdev; /* device number for special files */ 
	nlink_t st_nlink; /* number of links */ 
	uid_t st_uid; /* user ID of owner */ 
	gid_t st_gid; /* group ID of owner */ 
	off_t st_size; /* size in bytes, for regular files */ 
	time_t st_atime; /* time of last access */ 
	time_t st_mtime; /* time of last modification */ 
	time_t st_ctime; /* time of last file status change */ 
	blksize_t st_blksize; /* best I/O block size */ 
	blkcnt_t st_blocks; /* number of disk blocks allocated */ 
}; 



其中,st_mode记录了文件的类型和访问权限,而对于设备文件,st_dev域记录了该设备文件的主设备号(major number)和次设备号(minor number)。对于内核而言,它使用主设备号来得知使用那个驱动程序访问硬件。如果一个驱动程序能控制多个硬件或硬件的不同部分(如一个硬盘had被划分为多个分区,had1、hda2、hda10、hda12等等),那么驱动程序使用次设备号来区分这些不同的部分。对系统而言,某些主设备号已经缺省保留为系统所使用,你可以查看linux/Documentation/devices.txt来了解具体的信息。
设备文件包括两种类型:字符设备文件和块设备文件。块设备文件的特点是使用缓冲区buffer,为此它可以cache一些数据,以至于它能更快的找到所需要的数据。另一个特点是块设备的输入和输出皆是block(其大小可变)。
你可以使用命令mknod来创建设备文件,其参数如下。当然,设备文件并非一定要放置在/dev目录下,但是系统经过把设备文件放置在这个目录下。当然,在测试你自己驱动程序的过程中,你创建一个在你当前目录下的设备文件可能更方便调试。
mkdir /dev/device_file [b|c] major minor


3.2 注册设备
用户所使用的各种硬件资源,都是通过操作系统来进行实施,而操作系统又是通过驱动程序来真正使用硬件。为此,当我们加入一个驱动程序到内核时,我们必须向内核进行注册,用以说明该驱动程序的作用。进行驱动程序的注册应该调用如下的函数:

int register_chrdev(unsigned int major, const char *name, 
	struct file_operations *fops); 



其中,major用于说明该设备的主设备号,而name是显示在/proc/devices下面的设备名称,而fops用于说明用户能够对该设备所实施的各种操作,其数据结构请参考linux/include/fs.h文件。


3.2.1 选择设备号
我们知道,很多主设备号已经被系统所使用,那么我们如何来指定一个主设备号呢。方法是查看 linux/Documentation/devices.txt文件来查看哪些主设备号被占用,然后在选取一个未使用的主设备号来使用。但是这种方法是不灵活的,因为如果以后系统升级或你的驱动程序拿到其它系统时,如果该系统已经安装了一些第三方的驱动程序,那么它们就会与你的程序冲突。所以好的方式是让系统动态地为我们分配一个空闲的主设备号,其方法是设置参数major为0。
但是如果动态分配主设备号,那么我们就不能预先创建设备文件,解决的方法有如下三个:
(1)注册驱动程序时会返回系统分配的主设备号,我们可以让驱动程序printk这个值。这样,当挂载该驱动程序后,我们能够通过/var/log/messages来获得该主设备号,然后根据该主设备号建立设备文件。
(2)当加载驱动程序后,通过cat /proc/devices,我们能够发现该设备的主设备号,然后根据该设备号建立设备文件。
(3)在编写驱动程序时,在调用register_chrdev函数之后,如果调用成功,那么说明注册驱动程序成功,而该函数的返回值就是主设备号。然后,我们根据该函数返回值来调用系统调用mknod来建立该设备的设备文件。最后,当调用cleanup_module函数时,我们删除该设备文件。Perfect方案!!

 
3.2.2 设置设备操作函数
从传统的UNIX系统开始,为此简化对设备的操作,系统设计者统一把设备看作是普通的文件,而对设备的操作皆通过普通的文件操作来实现,如open, read,write等等。在register设备时,我们需要传递一个操作设备的接口函数集合,这通过第三个参数fops来实现。该结构的定义如下:

struct file_operations { 
	struct module *owner; 
	loff_t(*llseek) (struct file *, loff_t, int); 
	ssize_t(*read) (struct file *, char __user *, size_t, loff_t *); 
	ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t); 
	ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *); 
	ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t); 
	int (*readdir) (struct file *, void *, filldir_t); 
	unsigned int (*poll) (struct file *, struct poll_table_struct *); 
	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 
	int (*mmap) (struct file *, struct vm_area_struct *); 
	int (*open) (struct inode *, struct file *); 
	int (*flush) (struct file *); 
	int (*release) (struct inode *, struct file *); 
	int (*fsync) (struct file *, struct dentry *, int datasync); 
	int (*aio_fsync) (struct kiocb *, int datasync); 
	int (*fasync) (int, struct file *, int); 
	int (*lock) (struct file *, int, struct file_lock *); 
	ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); 
	ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); 
	ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *); 
	ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 
	unsigned long (*get_unmapped_area) (struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 
}; 



通过设置这个结构体中的函数指针,用户就可以在用户程序中使用普通的如同操作文件的方式来操作硬件设备了。如果设置了read函数,那么当用户使用read函数来读取设备时,系统最终会调用此处设置的read函数来做与设备相关的具体操作。
为了简化对这种类型的赋值操作,GNU对C语法进行了如下的扩充:

struct file_operations fops = { 
	.read = device_read, 
	.write = device_write, 
	.open = device_open, 
	.release = device_release 
}; 



通过这样的语法,我们可以仅仅对关注的操作设置相关的操作函数,而其它的函数指针皆被编译器设置为null。


3.3 注销设备
当使用rmmod命令来卸载驱动程序时,系统会调用cleanup_module函数。但是在调用该函数之前,系统会调用 sys_delete_module函数来判断当前是否有其它进程正在使用这个驱动程序。如果当前有进程正在使用该驱动程序,那么你将会得到不能释放该驱动程序的提示信息。
实际上,系统使用对模块的引用计数来判断模块是否被使用,相关的函数是try_module_get和module_put,它们分别用于增加对模块的引用和递减对模块的引用。你可以通过cat /proc/modules文件输出的第三个域来查看某个模块的引用计数。
与3.2节所描述的注册设备相对应,如果你在init_module函数中使用了register_chrdev函数注册了设备(实际是让系统动态分配一个设备号),那么你还需要在cleanup_module函数中调用unregister_chrdev函数来释放这个系统动态分配的设备号,其参数是:

int unregister_chrdev (unsigned int major, const char *name) 



其中,major就是系统给设备分配的设备号,而name就是在寄存设备时传递的设备名。如果该函数返回值为0,那么释放正确,否则错误。


3.4 使用ioctl
作为通常的控制,用户可以通过在注册设备时传递fops结构体(包括各种对设备文件进行处理的函数指针)来操作设备。但是,外部设备多种多样,fops中的函数是不能完全覆盖外部设备所需要的操作的,为此系统提供了ioctl函数来提供其它所不能提供的杂七杂八的操作。在驱动程序中,其设置的方式与设置对设备的读写函数相同,参考如上的struct file_operations数据结构,其中存在一个ioctl函数指针域,真是该域用于接受驱动程序进行ioctl处理的函数指针。
在驱动程序中,ioctl的函数类型如下:

int device_ioctl(struct inode *inode, struct file *file, unsigned int ioctl_num, unsigned long ioctl_param) 



其中inode指向设备文件的inode节点,而file指向设备文件,而ioctl_num是要执行的ioctl命令编号,而device_ioctl 函数中往往有一个switch语句,它往往根据这个编号来进行具体的命令处理,而最后一个参数ioctl_param正是被用于具体的命令处理中。
在用户应用程序中,用户使用三个参数来调用ioctl函数--打开设备文件时的文件描述符,ioctl编号和ioctl参数。而ioctl参数正是对应于在驱动程序中的第四个参数ioctl_param。

 
3.5 本章小节
驱动程序实际是模块程序的一种,只是对于驱动程序,它一般有一个设备文件。该设备文件可以被应用程序所使用,使用户程序可以如操作常规文件一样操作硬件设备。需要注意的是,为了创建设备文件,驱动程序设计者需要对设备进行注册。

 
4 References
[1]Peter Jay Salzman,Ori Pomerantz. The Linux Kernel Module Programming Guide[EB/OL]. http://www.faqs.org/docs/kernel/index.html.
[2]Chengzhu. 内核调用__init函数的顺序[EB/OL]. http://www.linuxforum.net/forum/gshowflat.php?Cat=&Board=linuxK&Number=563349&page=3&view=collapsed&sb=5&o=all&fpart=.
[3]Tigran Aivazian. Linux Kernel 2.4 Internals[EB/OL]. http://www.faqs.org/docs/kernel_2_4/lki.html.
[4]Bryan Henderson. Linux Loadable Kernel Module HOWTO[EB/OL]. http://mirrors.kernel.org/LDP/HOWTO/Module-HOWTO/index.html.
[5]Uresh Vahaha. UNIX Internals: The New Frontiers[M]. 北京:人民邮电出版社,2003.
[6]Robert Love著,陈莉君等译. Linux内核设计与实现[M]. 北京:机械工业出版社,2007.
[7]Jonathan Corbet等著,魏永明等译. Linux设备驱动程序[M]. 北京:中国电力出版社,2006.

 类似资料:

相关阅读

相关文章

相关问答