4.Linux内核模块
4.1 Linux内核模块简介
如果把所有需要的功能都编译到Linux内核。这回导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。
现在我们需要的是一种机制使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核中。
Linux提供了这样的一种机制,这种机制被称为模块(Module)。模块具有这样的特点:
模块本身不被编译如内核映像,从而控制内核的大小。
模块一旦被加载,它就和内核中的其他部分完全一样。
内核模块编译后会生成*.ko目标文件,通过“insmod ./*.ko”命令可以加载内核模块,通过“rmmod *”命令可以卸载它。内核模块中用于输出的函数是内核空间的printk()而非用户空间的printf()。printk()的用法和printf基本相似,但前者可定义输出级别。printk可作为一种最基本的内核调试手段。
在linux中,使用lsmod命令可以获得系统中加载了的所有模块以及模块间的依赖关系。lsmod命令实际上读取并分析“/proc/modules”文件。
内核中已加载模块的信息也存在于/sys/module目录下,加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下包含一个refcnt文件和一个sections目录。
modprobe命令比insmod命令要强大,它在加载某模块时,会同时加载该模块所依赖的其他模块。使用modprobe命令加载的模块若以“modprobe -r filename”的方式卸载将同时卸载其依赖的模块。
使用modinfo<模块名>命令可以获得模块的信息,包括模块作者、模块说明、模块所支持的参数以及vermagic。
4.2 Linux内核模块程序结构
一个Linux内核模块主要由如下几个部分组成。
(1)模块加载函数(一般需要)
当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
(2)模块卸载函数(一般需要)
当通过remod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。
(3)模块许可证声明(必须)
许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将受到内核被污染(kernel tainted)的警告。
在Linux2.6内核中,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、"Dual MPL/GPL"和“Proprietary”。
大多数情况下,内核模块遵循GPL兼容许可证。Linux 2.6内核模块最常见是以MODULE_LICENSE("Dual BSD/GPL")语句声明模块采用BSD/GPL双LICENSE。
(4)模块参数(可选)
模块参数是模块被加载的时候可以被传递给他的值,它本身对应模块内部的全局变量。
(5)模块导出符号(可选)
内核模块可以导出符号(symbol,对应于函数和变量),这样其他模块可以使用本模块中的变量或函数。
(6)模块作者等信息声明(可选)
语句声明块是:MODEULE_AUTHOR、MODULE_LICENSE、MODULE_DESCRIPTION、MODULE_ALIAS。
4.3 模块加载函数
Linux内核模块加载函数一般以__init标识声明。典型的模块加载函数的形式如下下:
static int __init initialization_function(void)
{
/*=初始化代码*/
}
module_init(initialization_function);
模块加载函数必须以“module_init(函数名)”的形式被指定。它返回整数值,若初始化成功,应返回0。而在初始化失败时,应该返回错误编码。在Linux内核里,错误编码是一个负值,在
在Linux 2.6内核中,可以利用request_module(const char *fmt, ...)函数加载内核模块,驱动开发人员可以通过request_module(module_name)或request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev))这种灵活的方式加载其他内核模块。
在Linux中,所有标识为__init的函数在连接的时候都放在.init.text这个区段内,此外,所有的__init函数在区段.initcall.init中还保存了一份指针,在初始化时内核会通过这些函数指针调用这些__init函数,并在初始化完成后,释放init区段(包括.init.text、.initcall.init等)。
4.4 模块卸载函数
Linux内核模块卸载函数一般以__exit标识声明。典型的模块卸载函数的形式如下:
static void __exit cleanup_function(void)
{
/*释放代码*/
}
module_exit(cleanup_function);
模块卸载函数在模块卸载的时候执行,不返回任何值,必须以“module_exit(函数名)”的形式来指定。通常来说,模块卸载要完成与模块加载相反的功能:
若模块加载函数注册了XXX,模块卸载函数应注销XXX。
若模块加载函数动态申请了内存,则模块卸载函数应释放该内存。
若模块加载函数申请了硬件资源(中断、DMA通道、I/O端口和I/O内存等)的占用,则模块卸载函数应释放这些硬件资源。
若模块加载函数开启了硬件,则卸载函数中一般要关闭之。
和__init一样,__exit也可以使对应函数的运行完成后自动回收内存。实际上,__init和__exit都是宏,其定义分别为:
#define __init __attribute__((__section__(".init.text")))
#ifdef MODULE
#define __exit __attribute__((__section__(".exit.text")))
#else
#define __exit __attribute_used__attribute__((__section__(".exit.text")))
#endif
数据也可以被定义为__initdata和__exitdata,这两个宏分别为:
#define __initdata __attribute__((__section__(".init.data")))
#define __exitdata __attribute__((__section__(".exit.data")))
4.5 模块参数
我们可以用“module_param(参数名,参数类型,参数读/写权限)”为模块定义一个参数:
static char *book_name="dissecting Linux Device Driver";
static int num=4000;
module_param(num, int, S_IRUGO);
module_param(book_name, charp, S_IRUGO);
在装载内核模块时,用户可以向模块传递参数,形式为”insmode(或modprobe)模块名 参数名=参数值“,如果不传递,参数将使用模块内定义的缺省值。
参数类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符类型)、bool或invbool(布尔的反),在模块被编译时会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。
模块被加载后,在/sys/module/目录下将出现以此模块名命名的目录。当“参数读/写权限”为0时,表示此参数不存在sysfs文件系统下对应的文件节点。如果此模块存在“参数读/写权限”不为0的命令行参数,在此模块的目录下还将出现paramters目录,包含一系列以参数名命名的文件节点,这些文件的权限值就是传入module_parame()的“参数读/写权限”,而文件的内容为参数的值。
除此之外,模块也可以拥有参数数组,形式为“module_param_array(数组名,数组类型,数组长,参数读/写权限)”从2.6.0~2.6.10版本,需将数组长变量名赋给“数组长”,从2.6.10版本开始,需将数组长变量的指针赋给“数组长”,当不需要保存实际输入的数组元素个数时,可以设置“数组长”为NULL。
运行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。
通过查看“/var/log/messages”日志文件可以看到内核的输出。
4.6 导出符号
Linux 2.6的“/proc/kallsyms”文件对应着内核符号表,它记录了符号以及符号所在的内存地址。
模块可以使用如下宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GOL(符号名);
导出的符号将可以被其他模块使用,使用前声明一下即可。EXPORT_SYMBOL_GPL()只适合用于包含GPL许可权的模块。
4.7 模块声明与描述
在Linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别声明模块的作者、描述、版本、设备表和别名。
对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,表明该驱动模块所支持的设备。
4.8 模块的使用计数
Linux 2.4内核中,模块自身通过MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT宏来管理自己的使用计数。
Linux 2.6内核提供了模块计数管理接口try_module_get(&module)和module_put(&module)。从而取代Linux 2.4内核中的模块使用计数管理宏。模块的使用计数一般不必由模块自身管理,而且模块计算管理还考虑了SMP与PREEMPT机制的影响。
int try_module_get(struct module *module);
该函数用于增加模块使用计数;若返回为0,表示调用失败,希望使用的模块没有被加载或正在被卸载中。
void module_put(struct module *module);
该函数用于减少模块使用计数。
try_module_get()与module_put()的引入与使用与Linux 2.6内核下的设备模型密切相关。Linux 2.6内核为不同类型的设备定义了struct module *owner域,用来指向管理此设备的模块。当开始使用某个设备时,内核使用try_module_get(dev->owner)去增加管理此设备的owner模块的使用计数;当不再使用此设备时,内核使用module_put(dev->owner)减少对管理此设备的owner模块的使用计数。这样,当设备在使用时,管理此设备的模块将不能被卸载。只有当设备不再被使用时,模块才允许被卸载。
在Linux 2.6内核下,对于设备驱动工程师而言,很少需要亲自调用try_module_get()与module_put(),因此此时开发人员所写的驱动通常为支持某具体设备的owner模块,对此设备owner模块的计数管理由内核里更底层的代码如总线驱动或是此类设备共用的核心模块来实现,从而简化了设备驱动开发。
4.9 模块的编译
我们可以为代码清单的模板编写一个简单的Makefile:
KVERS=$(shell uname -r)
#Kernel module
obj-m+=hello.o
#Specify flags for the module compilation
#EXTRA_CFLAGS=-G -O0
build:kernel_modules
kernel_modules:make -C /lib/module/$(KVERS)/build M=$(CURDIR) modules
clean: make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
该Makefile文件应该与源代码hello.c位于同一个目录,开启其中的EXTRA_CFAGS=-g -O0可以得到包含调试信息的hello.ko模块。运行make命令得到的模块可直接在PC上运行。
如果一个模块包括多个.c文件(如file1.c、file2.c),则应该以如下方式编写Makefile:
obj-m:=modulename.o
modulename-objs:=file1.o file2.o
4.10 使用模块绕开GPL
对于企业自己编写的驱动等内核代码,如果不编译为模块则无法绕开GPL,编译为模块后企业在产品中使用模块,则公司对外不再需要提供对应的源代码,为了使公司产品所使用的Linux操作系统支持模块,需要完成如下工作:
在内核编译时应该选上“可以加载模块”,嵌入式产品一般不需要动态卸载模块,所以“可以卸载模块”不用选。
将我们编译的内核模块.ko文件按应该放置在目标文件系统的相关目录下。
产品的文件系统中应该包含了支持新内核的insmod、lsmod、rmmod等工具,由于嵌入式产品中一般不需要建立模块间依赖关系,所以modprobe可以不要,一般也不需要卸载模块,所以rmmod也可以不要。
在使用中用户可使用insmod命令手动加载模块,如insmod xxx.ko。
但是一般而言,产品在启动过程中应该加载模块,在嵌入式产品Linux的启动过程中,加载企业自己的模块的最简单的方法是修改启动过程的rc脚本,增加insmod /.../xxx.ko这样的命令。用busybox做出的文件系统,通常修改etc/init.d/rcS文件。
严格意义上讲,不使用GPL许可权的驱动模块不宜使用标准的驱动架构,如V4L2、ALSA、Framebugger等,否则仍然可能存在license问题。
5.Linux文件系统与设备文件系统
5.1 Linux文件操作
5.1.1 文件操作系统调用
Linux的文件操作系统调用(在Windows编程领域,习惯称操作系统提供的接口为API)涉及创建、打开、读写和关闭文件。
1.创建
int creat(const char *filename, mode_t mode);
参数mode指定新建文件的存取权限,它同umake一起决定文件的最终权限(mode&umask),其中umask代表了文件在创建时需要去掉的一些存取权限。unask可以通过系统调用umask()来改变。
int unask(int newmask);
该调用将umask设置为newmask,然后返回旧的umask,它只影响读、写和执行权限。
2.打开
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open函数有两个形式,其中pathname是我们要打开的文件名(包含路径名称,缺省是认为在当前路径下面)。flags可以是图中的一个值或者是几个组合:
O_RDONLY、O_WRONLY、O_RDWR三个标志只能使用任意的一个。
如果使用了O_CREATE标志,使用的函数是int open(const char* pathname, int flags, mode_t mode);还要指定mode标志,用来表示文件的访问权限。mode可是是下图中的值的组合:
除了可以通过上述宏进行“或”逻辑产生标志以外,我们也可以自己用数字来表示,Linux用5个数字来表示文件的各种权限;第一位表示设置用户ID;第二位表示设置组ID;第三位表示用户自己的权限位;第四位表示组的权限;最后一位表示其他人的权限。
open("test", 10705);等价于open("test", O_CREAT, S_IRWXU|S_IROTH|S_IXOTH|S_ISUID);
如果文件打开成功,open函数会返回一个文件描述符,以后对该文件的所有操作就可以通过对这个文件描述符进行操作来实现。
3.读写
在文件打开以后,我们才可以对文件进行读写,Linux中提供文件读写的系统调用时read、write函数:
int read(int fd, const void *buf, size_t length);
int write(int fd, const void *buf, size_t length);
其中参数buf为指向缓存区的指针,length为缓存区的大小(以字节外单位)。函数read实现从文件描述符fd所指定的文件中读取length个字节到buf所指向的缓存区中,返回值为实际读写的字节数。函数write实现将把length个字节从buf指向的缓存区中写到文件描述符fd所指向的文件中,返回值为实际写入的字节数。
以O_CREAT为标志的open实际实现了文件创建的功能,下面的函数等同creat函数:int open(pathname, O_CREAT|O_WRONLY|O_TRUNC, mode);
4.定位
对于随机文件,我们可以随机地制定位置读写,使用如下函数进行定位:
int lseek(int fd, offset_t offset, int whence);
lseek将文件读写指针相对whence移动offset个字节。操作成功时,返回文件指针相对于文件包的位置。参数whence可使用下述值:
SEEK_SET:相对文件开头。
SEEK_CUR:相对文件读写指针的当前位置。
SEEK_END:相对文件末尾
offset可取负值,表示向前移动。
由于lseek函数的返回值为文件指针相对文件头的位置,因此下列调用的返回值就是文件的长度:lseek(fd, 0, SEEK_END);
5.关闭
当我们操作完成后,我们要关闭文件了,只要调用close就可以了,其中fd是我们要关闭的文件描述符:
int close(int fd);
5.1.2 C库文件操作
C库函数的文件操作实际上是独立于具体的操作系统平台的,不管是DOS、Windows、Linux还是在VxWorks中都是这些函数。
1.创建和打开
FILE *fopen(const char *path, const char *mode);
C库函数中支持的打开模式如下:
其中b用于区分二进制文件和文本文件,这一点在DOS、Windows系统中是有区分的,但Linux不区分二进制文件和文本文件。
2.读写
C库函数支持以字符、字符串等为单位,支持按照某种格式进行文件的读写,这一组函数为:
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
char *fgets(char *s, int n, FILE *stream);
int fputs(const char *s, FILE *stream);
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream);
read()实现从流stream中读取加n个字段,每个字段为size字节,并将读取的字段放入ptr所指的字符数组中,返回实际已读取的字段数。在读取的字段数小于num时,可能是在函数调用时出现错误,也可能是读到文件结尾。所以要通过调用feof()和ferror()来判断。
write()实现从缓存区ptr所指的数组中把n个字段写到流stream中,每个字段长为size个字节,返回实际写入的字段数。
另外,C库函数还提供了读写过程中的定位能力,这些函数包括:
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);
int fseek(FILE *stream, long offset, int whence);
3.关闭
利用C库函数关闭文件依然是很简单的操作:
int fclose(FILE *stream);
5.2 Linux文件系统
5.2.1 Linux文件系统目录结构
进入Linux根目录(即“/”,Linux文件系统的入口,也是出于最高一级的目录)运行“ls -l”命令,看到Linux包含以下目录。
/bin:包含基本命令,如ls、cp、mkdir等,这个目录中的文件都是可执行的。
/sbin:包含系统命令,如modprobe、hwclock、ifconfig等,大多是涉及系统管理的命令,这个目录中的文件都是可执行的。
/dev:设备文件存储目录,应用程序通过对这些文件的读写和控制就可以访问实际的设备。
/etc:系统配置文件的所在地,一些服务器的配置文件也在这里,如用户帐号及密码配置文件。busybox的启动脚本也存放在该目录。
/lib:系统库文件存放目录,如LDD6410包含libc-2.6.1.so、libpthread-2.6.1.so、libthread-db-1.0.so等。
/mnt:一般是用于存放挂在存储设备的挂在目录的。可以参看/etc/fstab的定义。有时候我们让系统开机自动挂载文件系统,把挂载点放在这里也是可以的。
/opt:opt是”可选“的意思,有些软件包会被安装在这里。
/proc:操作系统运行时,进程及内核信息(比如CPU、硬盘分区、内存信息等)存放在这里。/proc目录为伪文件系统proc的挂载目录,proc并不是真正的文件系统,它存在于内存之中。
/tmp:有时用户运行程序的时候,会产生临时文件,/tmp就用来存放临时文件的。
/usr:是系统存放程序的目录,比如用户命令、用户库等。
/var:var表示的是变化的意思,这个目录的内容经常变动,如/var的/var/log目录被用来存放系统日志。
/sys:Linux 2.6内核所支持的sysfs文件系统被影射在此目录。Linux设备驱动模型中的总线、驱动和设备可以在sysfs文件系统中找到对应的节点。当内核检测到在系统中出现了新的设备后,内核会在sysfs文件系统中为该设备生成一项新的记录。
5.2.2 Linux文件系统与设备驱动
下图为Linux中虚拟文件系统、磁盘文件(存放于Ramdisk、Flash、ROM、SD卡、U盘等文件系统中的文件也属于此列)及一般设备文件与设备驱动程序之间的关系:
应用程序与VFS之间的接口是系统调用,而VFS语磁盘文件系统以及普通设备之间的接口是file_operations结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。
由于字符设备的上层没有磁盘文件系统,所以字符设备的file_operations成员函数就直接由设备驱动提供了。
而对于块存储设备而言,ext2、fat、jffs2等文件系统中会实现针对VFS的file_operations成员函数,设备驱动层将看不到file_operations的存在。磁盘文件系统和设备驱动会将对磁盘上文件的访问最终转换成对磁盘上柱面和扇区的访问。
设备驱动程序的设计中,一般而言,会关心file和inode着两个结构体。
1.file结构体
file结构体代表一个打开的文件(设备对应于设备文件),系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件使创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file的指针通常被命名为file或filp(file pointer)。
struct file
{
union{
struct list_head fu_list;
struct rcu_head fu_rcuhead;
}f_u;
struct dentry *f_dentry; /*与文件关联的目录入口(dentry)结构*/
struct vfsmount *f_vfsmnt;
struct file_operations *f_op; /*和文件关联的操作*/
atomic_t f_count;
unsigned int f_flags; /*文件标志,如O_RDONLY、O_NONBLOCK、O_SYNC*/
mode_t f_mode; /*文件读/写模式, FMODE_READ和FMODE_WRITE*/
loff_t f_pos; /*当前读写位置*/
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
struct file_ra_state f_ra;
unsigned long f_version;
void *f_security;
/*tty驱动需要,其他的也许需要*/
void *private_data; /*文件私有数据*/
...
struct address_space *f_mapping;
};
文件读/写模式mode、标志f_flags都是设备驱动关心的内容,而私有数据指针private_data在设备驱动中被广泛应用,大多数指向设备驱动自定义用于描述设备的结构体。
驱动程序中经常会使用如下类似的代码来检测用户打开文件的读写方式:
if(file->f_mode & FMODE_WRITE){/*用户要求可写*/
}
if(file->f_mode & FMODE_WRITE){/*用户要求可读*/
}
下面的代码可以用于判断以阻塞还是非阻塞方式打开设备文件:
if(file->f_flags & O_NONBLOCK) /*非阻塞*/
pr_debug("open:non-bolcking\n");
else /*阻塞*/
pr_debug("open:blocking\n");
2.inode结构体
VFS inode包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁,inode结构体的定义如下:
struct inode{
...
unode_t i_mode; /*inode的权限*/
uid_t i_uid; /*inode拥有者的id*/
gid_t i_gid; /*inode所属的群组id*/
dev_t i_rdev; /*若是设备文件,此字段记录设备的设备号*/
loff_t i_size; /*inode所代表的文件大小*/
struct timespec i_atime; /*inode最近一次的存取时间*/
struct timespec i_mtime; /*inode最近一次的修改时间*/
struct timespec i_ctime; /*inode的产生时间*/
unsigned long i_blksize; /*inode在做I/O时的区块大小*/
unsigned long i_blocks; /*inode所使用的block数,一个block为512byte*/
struct block_device *i_bdev; /*若是块设备,为其对应的block_device结构体指针*/
struct cdev *i_dev; /*若是字符设备,为其对应的cdev结构体指针*/
...
};
对于表示设备文件的inode结构,i_rdev字段包含设备编号。Linux2.6设备编号分为主设备编号和次设备编号,前者为dev_t的高12位,后者为dev_t的低20位。下列操作用于从一个inode中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
查看/proc/devices文件可以获知系统中注册的设备,第一列为主设备号,第二列为设备号。
查看/dev目录可以获知系统中包含的设备文件,日期的前两列给出了对应设备的主设备号和次设备号。
主设备号是与驱动对应的概念,同一类设备一般使用相同的主设备号,不同类的设备一般使用不同的主设备号(但是也排除在同一主设备号下包含有一定差异的设备)。因为同一驱动可支持多个同类设备,因此用次设备号来描述使用该驱动的设备的序号,序号一般从0开始。
内核Documents目录下的devices.txt文件描述了Linux设备号的分配情况。
5.3 devfs设备文件系统
devfs(设备文件系统)是由Linux 2.4内核引入的,引入是被许多工程师给予了高度评价,它的出现使得设备驱动程序能自主管理它自己的设备文件。具体来说,devgs具有如下优点:
(1)可以通过程序在设备初始化时在/dev目录下创建设备文件,卸载设备时将它删除。
(2)设备驱动程序可以指定设备名、所有者和权限位,用户空间程序仍可以修改所有者和权限位。
(3)不需要为设备驱动程序分配主设备号及次设备号,在程序中直接给register_chrdev()传递0主设备号获得可用的主设备号,在devfs_register指定次设备号。
创建设备目录:devfs_handle_t devfs_mk_dir(devfs_handle_t dir, const *name, void *info);
创建设备文件:devfs_handle_t devfs_register(devfs_handle_t dir, const char* name, unsigned int flags, unsigned int major, unsigned int minor, umode_t mode, void* ops, void *info);
撤销设备文件:void devfs_unregister(devfs_handle_t de);
在Linux 2.4的设备驱动编程中,分别在模块加载和卸载函数中创建和撤销设备文件是被普遍采用并值得大力推荐的好方法。
使用的register_chrdev()和unregister_chrdev在Linux 2.6内核中虽然仍然被支持,但这是过时的做法。
5.4 udev设备文件系统
5.4.1 udev与devfs的区别
尽管devfs有这样和那样的优点,但是在Linux 2.6内核中,devfs被认为是过时的方法,并最终被抛弃,udev取代了它。Linux VFS内核维护者AI Viro指出了几点udev取代devfs的原因:
devfs所做的工作被确信可以在用户态来完成。
devfs被加入内核时,大家寄望它的质量可以迎头赶上。
devfs被发现了一些可修复和无法修复的bug。
对于可修复的bug,几个月前就已经被修复,其维护者认为一切良好。
对于不可修复的bug,同样是相当长一段时间以来没有改观了。
devfs的维护者和作者对它感到失望并且已经停止了对代码的维护工作。
udev完全在用户态工作,利用设备加入或移除时内核所发出的热插拔事件(hotplug event)来工作。在热插拔时,设备的详细信息会由内核输出到位于/sys的sysfs文件系统。udev的设备命名策略、权限控制和事件处理都是在用户态下完成的,它利用sysfs中的信息来进行创建设备文件节点等工作热插拔时输出到sysfs中的设备的详细信息就是相亲对象的资料(外贸、年龄、性格、籍贯等),设备命名策略等就是择偶标准。devfs是个蹩脚的婚姻介绍所,它直接指定了谁和谁谈恋爱,而udev则聪明地多,它只是把资料交给了客户,让客户根据这些资料去选择和谁谈恋爱。
由于udev根据系统中硬件设备的状态动态更新设备文件,进行设备文件的创建和删除等,进行设备文件的创建和删除等,因此,在使用udev后,/dev目录下就会只包含系统中真正存在的设备了。
devfs与udev的另一个显著区别在于:采用devfs,当一个并不存在的/dev节点被打开的时候,devfs能自动加载对应的驱动,而udev则不这么做。这是因为udev的设计者认为Linux应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候。udev的设计者认为devfs所提供的打开/dev节点时自动加载驱动的功能对于一个配置正确的计算机是多余的。系统中所有的设备都应该产生热插拔事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。
5.4.2 sysfs文件系统与Linux设备模型
Linux 2.6的内核引入了sysfs文件系统,sysfs被看成是与proc、devfs和devpty同类别的文件系统,该文件系统是一个虚拟的文件系统,它可以产生一个包含所有系统硬件的层级视图,与提供进程和状态信息的proc文件系统十分类似。
sysfs把链接在系统上的设备和总线组织成为一个分级的文件,它们可以由用户空间存取,向用户空间导出内核数据结构以及它们的属性。sysfs的一个目的就是展示设备驱动模型中各组件的层次关系,其顶级目录包括block、device、bus、drivers、class、power和firmware。
block目录包含所有的块设备:devices目录包含系统所有的设备,并根据设备挂在的总线类型组织层次结构;bus目录包含系统中所有的总线类型;drivers目录包含内核中所有已注册的设备驱动程序;class目录包含系统中的设备类型(如网卡设备、声卡设备、输入设备等)。在/sys目录运行tree会得到一个相当长的树形目录。
在/sys/bus的pci等子目录下,又会再分出drivers和devices目录,而devices目录中的文件是对/sys/devices目录中文件的符号链接。同样地,/sys/class目录下也包含对许多对/sys/devices下文件的链接。下图所示,这与设备、驱动、总线和类的现实状况是直接对应的,也正符合Linux 2.6的设备模型。
随着技术的不断进步,系统的拓扑结构越来越复杂,对智能电源管理、热插拔以及即插即用的支持要求越来越高,Linux 2.4内核已经难以满足这些需求。为适应这种形势的需要,Linux 2.6内核开发了上述全新的设备、总线、类和驱动环环相扣的设备模型。下图表示了Linux驱动模型中设备、总线和类之间的关系。
大多数情况下,Linux 2.6内核中的设备模型代码会作为“幕后黑手”处理好这些关系,内核中总线级和其他内核子系统会完成与设备模型的交互,这是的驱动工程师几乎不需要关心设备模型。
Linux内核中,分别使用bus_type、device_driver和device来描述总线、驱动和设备。这3个结构体定义于include/linux/device.h头文件中。
device_driver和device分别表示驱动和设备,而这两者都必须依附于一种总线,因此都包含struct bus_type指针。在Linux内核中,设备和驱动是分开注册的,注册1个设备的时候并不需要驱动已经存在,而1个驱动被注册的时候,也不需要对应的设备已经被注册。设备和驱动各自涌向内核,而每个设备和驱动涌入的时候都会寻找自己的另一半。茫茫人海,何处觅踪?正是bus_type的match()成员函数将两者捆绑在一起,简单地说,设备和驱动就是红尘漂浮的男女,而bus_type的match()则是牵引红线的月老,它可以识别什么设备与什么驱动可以配对。
注意,总线、驱动和设备都最终会落实为sysfs中的一个目录,因为进一步追踪代码会发现,它们实际上都可以认为是kobject的派生类(device结构体直接包含了kobject kobj成员,而bus_type和device_driver则透过bus_type_private、driver_private间接包含kobject),kobject可看作所有总线、设备和驱动的抽象基类,一个kobject对应sysfs中的一个目录。
总线、设备和驱动中的各个attribute则直接落实为sysfs中的一个文件,attribute会伴随show()和store()这两个函数,分别用于读和写该attribute对应的sysfs文件节点。
事实上,udev规则中各信息的来源实际上就是bus_type、device_driver、device以及attribute等所对应的sysfs节点。
5.4.3 udev的组成
udev的主页位于:http;//www/kernek.org/pub/linux/utils/kenel/hotplug/udev.html,上面包含可关于udev的详细介绍,从http;/www.us.kernel.org/pub/linux/utils/kernek/hotplug上下载罪行的udev包。udev的设计目标如下:
在用户空间执行。
动态建立/删除设备文件。
允许每个人都不用关心主/次设备号。
提供LSB标准名称。
如果需要,可提供固定的名称。
为了提供这些功能,udev以3个分割的子计划发展:namedev、libsysfs和udev。namedev为设备命名子系统,libsvsfs提供访问sysfs文件系统从中获取信息的标准接口,udev提供/dev设备节点文件的动态创建和删除策略。udev程序背负与namedev和libsysfs库交互的任务,当/sbin/hotplug程序被内核调用时,udev将被运行。udev的工作过程如下:
(1)当内核检测到系统中出现了新设备后,内核会在sysfs文件系统中为该设备生成新纪录并导出一些设备特定的信息及所发生的事件。
(2)udev获取内核导出的信息,它调用namedev决定应该给该设备指定的名称,如果是新插入设备,udev将调用libsysfs决定应该为该设备的设备文件指定的主/次设备号,并用分析获得的设备名称和主/次设备号创建/dev中的设备文件;如果是设备移除,则之间已经被创建的/dev文件将被删除。
在namedev中使用5步序列来决定指定设备的命名:
(1)标签(label)/序号(serial):这一步检查设备是否有唯一的识别记号,例如USB设备有唯一的USB序号,SCSI有唯一的UUID。如果namedev找到与这种唯一编号相对应的规则,它将使用该规则提供名称。
(2)设备总线号:这一步会检查总线设备编号,对于不可热插拔的环境,这一步足以辨别设备。例如,PCI总线编号在系统的使用期间内很少变更。如果namedev找到相对应的规则,规则中的名称就会被使用。
(3)总线上的拓扑:当设备在总线上的位置匹配用户指定的规则时,就会使用该规则指定指定名称。
(4)替换名称:当内核提供的名称匹配指定的替代字符串时,就会使用替代字符串指定的名称。
(5)内核提供的名称:这一步“保罗万象”,如果以前的几个步骤的没有被提供,默认的内核将被指定给该设备。
5.4.4 udev规则文件
udev的规则文件以行为单位,以“#”开头的行代表注释行。其余的每一行代表一个规则。每个规则分成一个或多个匹配和赋值部分。匹配部分用匹配专用的关键字来表示,相应的赋值部分用赋值专用的关键字来表示。匹配关键字包括:ACTION(行为)、KERNEL(匹配内核设备名)、BUS(匹配总线类型)、SYSFS(匹配从sysfs得到的信息,比如label、vendor、USB序列号)、SUBSYSTEM(匹配子系统名)等,赋值关键字包括:NAME(创建的设备文件名)、SYMLINK(符号创建链接名)、OWNER(设置设备的所有者)、GROUP(设置设备的组)、IMPORT(调用外部程序)等。
例如,如下规则:
SUBSYSTEM==“net”,ACTION==“add”,SYSFS(address)==“00:0d:87:f6:59:f3”,IMPORT=“/sbin/rename_netiface %k eth0”
其中的“匹配”部分有3项,分别是SUBSYSTEM、ACTION和SYSFS。而“赋值”部分有一项,是IMPORT。这个规则的意思是:当系统中出现的新硬件属于net子系统范畴,系统对该硬件采取的动作是加入这个硬件,切这个硬件在sysfs文件系统中的“address”信息等于“00:0d:87:f6:59:f3”时,这个硬件在udev层次施行的动作是调用外部程序/sbin/rename_netiface,并传递给该程序两个参数,一个是“%k”,代表内核对该新设备定义的名称,另一个是“eth0”。
多个设备采用基于它们的序列号或者其他标识信息的办法来进行确定的映射,使用下面的规则可以做到:
BUS=“usb”,SYSFS(serial)="HXOLL0012202323480",NAME="lp_epson",SYMLINK="printers/epson_stylus"
该规则中的匹配项目有BUS和SYSFS,赋值项目为NAME和SYMLINK,它意味着当一台USB打印机的序列号为“HXOLL0012202323480”的USB打印机不管何时被插入,对应的设备名都是/dev/lp_epson,而devfs显然无法实现设备的这种固定命名。
udev规则的写法非常灵活,在匹配部分,可以通过“*”、"?"、[a~c]、[1~9]等shell通配符来灵活匹配多个项目。*类似于shell中的*通配符,代替任意长度的任意字符串,?代替一个字符,[x~y]是访问定义,此外,%k就是KENEL,%n则是设备的KERNEL序号(如存储设备的分区号)。
可以借助udev中的udevinfo工具查找规则文件可以利用的信息。如运行“udevinfo -a -p/sys/block/sda”。
5.4.5 创建和配置mdev
在嵌入式系统中,通常可以用udev的轻量级版本mdev,mdev集成与busybox中。在busybox的源代码目录运行,make menuconfig,进入“LInux System Utilities”子选项,选中mdev相关项目。
LDD6410根文件系统中/etc/init.d/rcS包含的如下内容即是为了使用mdev的功能:
/bin/mount -t sysfs sysfs /sys
/bin/mount -t tmpfs mdev /dev
echo /bin/mdev > /proc/sys/kernel/hotplug
mdev -s
其中“mdev -s”的含义是扫描/sys中所有的类设备目录,如果在目录中含有名为“dev”的文件,切文件中包含的是设备号,则mdev就利用这些信息为该设备在/dev下创建设备节点文件。
“echo /sbin/mdev > /proc/sys/kernel/hotplug”的含义是当有热插拔事件产生时,内核就会调用位于/sbin目录的mdev。这时mdev通过环境变量中的ACTION和DEVPATH,来确定此次热插拔事件的动作以及影响了/sys中的那个目录。接着会看看这个目录中是否有“dev”的属性文件,如果有就利用这些信息为这个设备在/dev下创建设备节点文件。
若要修改mdev的规则,可通过修改/etc/mdev.cong文件实现。
5.5 LDD6410的SD和NAND文件系统
LDD6410的SD卡分为两区,其中的第二个分区为ext3文件系统,存在LDD6410的文件数据,其制作方法如下:
(1)在安装了Linux的PC机上通过fdisk给一张空的SD卡分为2个区(如果SD卡中本身已经包含,请通过fdisk的“d”命令全部删除)
(2)格式化SD卡的分区1和分区2:mkfs.vfat /dev/sdb1 mkfs.ext3 /dev/sdb2 fsck.ext3 /dev/sdb2
(3)通过moviNAND Fusing_Tool.exe烧写SD卡的U-BOOT和zImage。
更新NAND中U-BOOT的方法如下:
(1)通过tftp或nfs等方式获取新的U-BOOT,如:
#tftp -r u-boot-movi.bin -g 192.168.1.111
(2)运行:
#flashcp u-boot-movi.bin /dev/mtd0
更新NAND中zImage的方法如下:
(1)通过tftp或nfs等方式获取新的zImage,如:
#tftp -r zImage-fix -g 192.168.1.111
(2)运行:
#flashcp zImage-fix /dev/mtd1
更新NAND中文件系统的方法如下:
在PC上将做好的新的根文件系统拷贝到SD卡或NFS的某个目录,下面我们以
以SD卡或NFS为根文件系统启动系统,运行如下命令擦除/dev/mtd2分区:#flash_eraseall /dev/mtd2
然后将NAND的该分区mount到/mnt:#mount /dev/mtdblock2 -t yaffs2 /mnt/
将新的文件系统拷贝到/mnt:#cp -fa
6.字符设备驱动
6.1 Linux字符设备驱动结构
6.1.1 cdev结构体
在Linux 2.6内核中,使用cdev结构体描述一个字符设备,cdev结构体的定义如下:
struct cdev{
struct kobject kobj; /*内嵌的kobject对象*/
struct module *owner; /*所属模块*/
struct file_operations * ops; /*文件操作结构体*/
struct list_head list;
dev_t dev; /*设备号*/
unsigned int count;
};
cdev结构体的dev_t成员定义了设备号,为32位,其中12位主设备号,20位次设备号。使用下列宏可以从dev_t获得主设备号和次设备号:
MAJOR(dev_t dev)
MANOR(dev_t dev)
而使用下列宏则可以通过主设备号和次设备号生成dev_t:
MKDEV(int major, int minor)
cdev结构体的另一个重要成员file_operations定义了字符设备驱动提供给虚拟文件系统的接口函数。Linux 2.6内核提供了一组函数用于操作cdev结构体:
void cdev_init(struct cdev *cdev, struct file_operations *fops) /*用于初始化cdev的成员,并建立cdev和file_operations之间的连接*/
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops=fops; /*将传入的文件操作系统结构体指针赋值给cdev的ops*/
}
struct cdev *cdev_alloc(void) /*用语动态申请一个cdev内存*/
{
struct cdev *p=kzalloc(sizeof(struct cdev), GFP_KERNEL);
if(p){
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
void cdev_put(struct cdev *p);
int cdev_add(struct cdev*, dev_t, unsigned); /*向系统添加一个cdev并完成设备的注册,调用通常发生在字符设备驱动模块加载函数中*/
void cdev_del(struct cdev*); /*向系统删除一个cdev并完成设备的注销,调用通常发生在字符设备驱动模块卸载函数中*/
6.1.2 分配和释放设备号
在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号,函数原型:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放在第一个参数dev中。alloc_chrdev_region()与register_chrdev_region()对比的优点在于它会自动避开设备号重复的冲突。
相反地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,函数的原型:
void unregister_chrdev_region(dev_t from, unsigned count);
6.1.3 file_operations结构体
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close()等系统调用时最终被调用。file_operations结构体目前已经比较庞大,定义如下:
struct file_operations{
struct module *owner; /*拥有该结构的模块的指针,一般为THIS_MODULES*/
loff_t(*llseek)(struct file *, loff_t, int); /*用来修改文件当前的读写位置*/
ssize_t(*read)(struct file *, char __user*, size_t, loff_t*); /*从设备中同步读取数据*/
ssize_t(*write)(struct file *, const char __user*, size_t, loff_t*); /*向设备发送数据*/
ssize_t(*aio_read)(struct file *, char __user*, size_t, loff_t); /*初始化一个异步的读取操作*/
ssize_t(*aio_write)(struct file *, const char __user*, size_t, loff_t); /*初始化一个异步的写入操作*/
int (*readdir)(struct file*, void*, filldir_t); /*仅用于读取目录,对于设备文件,该字段为NULL*/
unsigned int(*poll)(struct file*, struct_poll_table_struct*); /*轮训函数,判断目前是否可以进行非阻塞的读写或写入*/
int(*ioctl)(struct inode*, struct file*, unsigned int, unsigned long); /*执行设备I/O控制命令*/
long(*unlocked_ioctl)(struct file*, unsigned int, unsigned long); /*不使用BLK的文件系统,将使用此种函数指针代替ioctl*/
long(*compat_ioctl)(struct file*, unsigned int, unsigned long); /*在64位系统上,32位的ioctl调用将使用此函数指针代替*/
int(*mmp)(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); /*异步fsync*/
int(*fasync)(int, struct file*, int); /*通知设备FASYNC标志发生变化*/
int(*lock)(struct file*, int, struct file_lock*)
ssize_t(*sendpage)(struct file*, struct page*, int, size_t, loff_t*, int); /*通常为NULL*/
unsigned long(*get_unmapped_area)(struct file*,unsigned long,unsigned long,unsigned long,unsigned long);/*当前进程地址空间找1未映射个内存段*/
int(*check_flags)(int); /*允许模块检查传递给fcntl(F_SETEL...)调用的标志*/
int(*dir_notify)(struct file *filp, unsigned long arg); /*对文件系统有效,驱动程序不必实现*/
int(*flock)(struct file*, int, struct file_lock*);
ssize_t(*splice_write)(struct pipe_inode_info*, struct file*, loff_t*, size_t, unsigned int); /*由VFS调用,将管道数据粘接到文件*/
ssize_t(*splice_read)(struct file*, loff_t*, struct pipe_inode_info*, size_t, unsigned int); /*由VFS调用,将文件数据粘接到管道*/
int(*setlease)(struct file*, long, struct file_lock**);
};
下面我们对file_operations结构体中的主要成员进行分析:
llseek函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write函数想设备发送数据,成功是该函数返回写入的字节数,如果此函数未被实现,当用户write系统调用时,将得到-EINVAL返回值。
readdir函数仅用于目录,设备节点不需要实现它。
ioctl提供设备相关控制命令的实现(既不是读操作也不是写操作),当调用成功时,返回给调用程序一个非负值。
mmap函数将设备内存映射到进程内存中,如果设备驱动未实现此函数,当mmap系统调用时获得-ENODEV返回值。此函数对于帧缓冲等设备特别有意义。
当用户空间调用LInux API函数open打开设备文件时,设备驱动的open函数最终被调用。驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。与open函数对应的是release函数。
poll函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select和poll系统调用将引起进程的阻塞。
aio_read和aio_write函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符调用aio_read、aio_write等系统调用进行读写。
6.1.4 Linux字符设备驱动的组成
1.字符设备驱动模块加载和卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中应实现设备号的释放和cdev的注销。
工程师通常习惯为设备定义一个设备相关的结构体,其包含该设备所涉及的cdev、私有数据及信号量等信息。常见的设备结构体、模块加载和卸载函数形式如下:
/*设备结构体*/
struct xxx_dev_t{
struct cdev cdev;
...
}
/*设备驱动模块加载函数*/
static int __init xxx_init(void)
{
cdev_init(&xxx_dev.cdev, &xxx_fops); /*初始化cdev*/
xxx_dev.cdev.owner=THIS_MODULE;
/*获取字符设备号*/
if(xxx_major){
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
}else{
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
}
ret=cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /*注册设备*/
}
/*设备驱动模块卸载函数*/
static void __exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); /*释放占用的设备号*/
cdev_del(&xxx_dev.cdev); /*注销设备*/
}
2.字符设备驱动的file_operations结构体中成员函数
file_operations结构体中成员函数是字符设备驱动与内核的接口,是用户空间对Linux进行系统调用最终的落实者。大多数字符设备驱动会read()、write()、ioctl()函数,常见的字符设备驱动的这3个函数的形式如下实现:
/*读设备*/
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_to_user(buf, ..., ...);
...
}
/*写设备*/
ssize_t xxx_write(struct file *flip, const char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_from_user(..., buf, ...);
...
}
/*ioctl函数*/
int xxx_ioctl(struct inode *inode, struct file *flip, unsigned int cmd, unsigned long arg)
{
...
switch(cmd){
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default: /*不能支持的命令*/
return -ENOTTY;
}
return 0;
}
设备驱动的读函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址在内核空间不能直接读写,count是要读的字节数,f_pos是读的位置相对于文件开头的偏移。
设备驱动的写函数中,filp是文件结构体指针,buf是用户空间内存的地址,该地址的内核空间不能直接读写,count是要写的字节数,f_pos是写的位置相对于文件开头的偏移。
由于内核空间与用户空间的内存不能直接互访,因此借助了函数copy_from_user()完成用户空间到内核空间的拷贝,以及copy_to_user()完成内核空间到用户空间的拷贝。原型分别为:
unsigned long copy_from_user(void* to, const void __user *from, unsigned long count);
unsigned long copy_to_user(void __user * to, const void *from, unsigned long count);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。
如果要复制的内存是简单的类型,如char、int、long等,则可以使用简单的put_user()和get_user()。
读和写函数中的__user是一个宏,表明其后的指针指向用户空间,这个宏定义为:
#ifdef __CHECKER__
#degine __user __attribute__((noderef, address_space(1)))
#else
#define __user
#endif
I/O控制函数的cmd参数为事先定义的I/O控制命令,而arg为对应于该命令的参数。例如对于串行设备,如果SET_BAUDRATE是一道设置波特率的命令,那后面的arg就应该是一个波特率值。
在字符设备驱动中,需要定义一个file_operations的实例,并将具体设备驱动的函数赋值给file_operations的成员,如下代码实现:
struct file_operations xxx_fops={
.owner=THIS_MODULE,
.read=xxx_read,
.write=xxx_write,
.ioctl=xxx_ioctl,
...
};
通过cdev_init(&xxx_dev.cdev, &xxx_fops)语句被建立与cdev的连接。
下图所示为字符设备驱动的结构、字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系。
6.2 golbalmem虚拟设备实例描述
从本章开始,后续的数章都将基于虚拟的globalmem设备进行字符设备驱动的讲解。globalmem意味着“全局内存”,在globalmem字符设备驱动中会分配一片大小为GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过Linux系统调用访问这片内存。
实际上,这个虚拟的globalmem设备几乎没有任何实用价值,仅仅是一种为了讲解问题的方便而凭空制造的设备。当然,它也并非百无一用,由于global可被两个或两个以上的进程同时访问,其中的全局内存可作为用户空间进程进行通信的一种蹩脚的手段。
6.3 globalmem设备驱动
6.3.1 头文件、宏及设备结构体
在globalmem字符设备驱动中,应包含它要使用的头文件,并定义globalmem设备结构体及相关宏。
6.3.2 加载与卸载设备驱动
6.3.3 读写函数
globalmem设备驱动的读写函数主要是让那个设备结构体的mem[]数组与用户空间交互数据,并随着访问的字节数变更返回给用户的文件读写偏移位置。
6.3.4 seek函数
seek函数对文件定位的起始死值可以是文件开头(SEEK_SET, 0)、当前位置(SEEK_CUR,1)和文件尾(SEEK_END, 2)globalmem支持从我呢间开头和当前位置相对偏移。
在定位的时候,应该检查用户请求的合法性,若不合法,函数返回-EINVAL,合法时返回文件的当前位置。
6.3.5 ioctl函数
1.globalmem设备驱动的ioctl()函数
globalmem设备驱动的ioctl函数接受MEM_CLEAR命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl函数应该返回-EINVAL。
2.ioctl命令
Linux建议如下图所示的方式定义ioctl的命令。
命令吗的设备类型字段为一个“幻数”,可以是0~0xff之间的值,内核中的ioctl-number.txt给出了一些推荐的和已经被使用的“幻数”,新设备驱动定义“幻数”的时候要避免与其冲突。
命令码的序列号也是8位宽。
命令码的方向字段为2位,该字段表示数据传送的方向,可能的值是_IOC_NONE(无数据传输)、_IOC_READ(读)、_IOC_WRITE(写)和_IOC_READ|_IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。
命令码的数据长度字段表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常是13或者14位。
内核还定义了_IO()、_IOR()、_IOW()和_IOWR()这4个宏来辅助生成命令,这4个宏的通用定义如下:
#define _IOC(dir, type, nr, size) (((dir)<< _IOC_DIRSHIFT)|((type)<<_IOC_TYPESHIFT)|((nr<<_IOC_NRSHIFT)|((size)<<_IOC_SIZESHIFT))
#define _IO(type, nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type, nr, size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type, nr, size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type, nr, size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr), (_IOC_TYPECHECK(size)))
由此可见,这几个宏的作用是根据传入的type(设备类型字段)、nr(序列号字段)和size(数据长度字段)和宏名隐含的方向字段移位组合生成命令码。
由于globalmem的MEM_CLEAR命令不涉及数据传输,因此它可定义为:
#define GLOBALMEM_MAGIC ...
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0)
3.预定义命令
内核中预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会被当作预定义命令被内核处理而不是被设备驱动处理,如定义命令有如下4种。
(1)FIOCLEX:即File IOctl Close on Exec,对文件设置专用标志,通知内核当exec()系统调用发生时自动关闭打开的文件。#define FIONCLEX 0x5450
(2)FIONCLEX:即File IOctl Not Close on Exec,,与FIOCLEX标志相反,清除由FIOCLEX命令设置的标志。#define FIOCLEX 0x5451
(3)FIOQSIZE:获得一个文件或者目录的大小,当用于设备文件时,返回一个ENOTTY错误。#define FIOQSIZE 0x5460
(4)FIONBIO:即File IOctl Non-Blocking I/O,这个调用修改在filp->f_flags中的O_NONBLOCK标志。#define FIONBIO 0x5421
6.3.6 使用文件私有数据
大多数Linux驱动工程师遵循一个“潜规则”,那就是将文件的私有数据private_data指向设备结构体,在read()、write()、ioctl()、llseek()等函数通过private_data访问设备结构体。
container_off()的作用是通过结构体成员的指针找到对应结构体的指针,这个技巧在Linux内核编程中十分常用。在container_of(inode->i_cdev, struct globalmem_dev, cdev)语句中,传给container_of()的第一个参数是结构体成员的指针,第2个参数为整个结构体的类型,第3个参数为传入的第1个参数即结构体成员的类型,container_of返回值为整个结构体的指针。
6.4 globalmem驱动在用户空间的验证
在对应目录通过make命令编译globalmem的驱动,得到globalmem.ko文件。运行:
~/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6$sudo su
~/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6$ insmod globalmem.ko
命令加载模块,通过lsmod命令,发现globalmem模块已被加载。再通过cat/proc/devices命令查看,发现多出了主设备号为250的globalmem字符设备驱动。
接下来,通过命令:
/home/lihacker/develop/svn/ldd6410-read-only/trainling/kernel/drivers/globalmem/ch6$mkmod /dev/globalmem c 250 0
创建/dev/globalmem设备节点,并通过echo 'hello world'>/dev/globalmem命令和cat /dev/globalmem命令分别验证设备的写和读,结果证明hello world字符串被正确地写入globalmem字符设备:
/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6#echo "hello world" > /dev/globalmem
/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6#cat /dev/globalmem
如果启用了sysfs文件系统,将发现多出了/sys/module/globalmem目录。
refcnt记录了globalmem模块的引用计数,sections下包含的数个文件则给出了globalmem所包含的BSS、数据段和代码段等的地址及其他信息。