基于Linux平台PCI设备驱动程序设计 (4)
.4 模块的编译和加载
我们可以使用makefile来编译内核可以加载的目标代码(具体使用方法可参阅有关介绍makefile的资料)。如果所编写的模块不是非常庞大,目标代码文件数量较少时,还有一种更为简单的方法可编译模块目标代码:直接使用gcc来编译目标代码,当然,在使用gcc时必须包含编译内核模块所需的所有参数,如-DMODULE、-D_KERNEL_和-DLINUX等。
模块编译好后,有两种方法可以载入模块:一种是使用命令insmod手工载入;另一种方法则更为灵活,是在需要时自动载入,当内核发现需要载入某个模块时,它会要求内核守护程序去载入相应的模块。
内核守护程序是一个拥有超级用户权限的进程,它的主要工作是载入和卸载模块,它也做其他一些任务,如打开和关闭PPP连接。内核守护程序并非亲自做这些工作,而是调用相应的程序(如insmod)来完成,它只是一个内核代理,自动地安排调度各项工作。
卸载模块的方法很简单,用rmmod命令即可。但对于在需要时载入的模块,当其不再需要时,会由kerneld自动将其从系统中删除。
第四章 驱动程序框架
在编写驱动程序之前,我们首先需要确定驱动程序能提供给用户程序何种能力。
以下我们将以字符设备为主要介绍对象,这类设备的驱动程序适用于大多数简单的PCI设备。
4.1 获得主设备号
向系统增加一个驱动程序时,要赋予它一个主设备号。这一赋值过程应该在驱动程序的初始化过程中完成。调用如下函数可完成此过程,这个函数定义在
int register_chrdev(unsigned int major,
const char *name,
struct file_operatoins *fops);
当出错时返回一个负值;成功时返回零或正值。参数major是所请求的主设备号,name是设备的名字,它将在/proc/devices中出现,fops是一个指向跳转表的指针,利用这个跳转表完成对设备函数的调用。
接下来的问题就是如何给程序一个它们可以请求的设备驱动程序的名字。这个名字必须在/dev目录中,并与驱动程序的主设备号和次设备号相连。用mknod命令在文件系统上创建一个设备节点,如:
mknod /dev/mydevice c 120 0
创建了一个名字为“mydevice”的字符设备(c),主设备号是120,次设备号是0。
上述是静态地分配主设备号的方法,事先为设备选取主设备号会出现个问题――可配置的设备要比主设备号多得多,主设备号可能会不够分配。
我们还可以使用动态分配机制来获得主设备号。
由于动态分配机制不能保证每次获得的主设备号总是一样的,似乎无法事先创建设备节点了。其实,一旦分配了设备号,我们总可以从/proc/devices读到,因此可以先从/proc/devices获得新分配的主设备号,再创建节点。上述过程需要编写脚本程序。
4.2 释放主设备号
当从系统中卸载一个模块时,应该释放该模块占用的主设备号。这一操作可以在cleanup_module中调用如下函数完成:
int unregister_chrdev(unsigned int major,const char *name);
参数major是要释放的主设备号,name是相应的设备名。
还需要在卸载驱动程序时删除设备节点。如果设备节点是在加载时创建的,可以写一个简单的脚本在卸载时删除它们。
4.3 文件操作
Linux内核内部用file结构来识别设备,它代表了一个“打开的文件”,此结构定义在
以下是我使用系统中的file结构的原型:
struct file {
struct file *f_next, **f_pprev;
struct dentry *f_dentry;
struct file_operations *f_op;
mode_t f_mode;
loff_t f_pos;
unsigned int f_count, f_flags;
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
int f_error;
unsigned long f_version;
/* needed for tty driver, and maybe others */
void *private_data;
};
其中的重要结构项罗列如下:
mode_t f_mode;
用户需要在ioctl函数中查看这个域来检查读/写权限,但由于内核在调用驱动程序的read和write前已经检查了权限,无需在这两个方法中检查权限。例如,一个不允许的写操作在驱动程序还不知道的情况下就被已经内核拒绝了。
loff_t f_pos;
为下一步读写操作设定当前文件的位置。loff_t是一个64位数值。如果驱动程序需要这个值,可以直接读取这个字段。如果定义了lseek方法,应该更新f_pos的值。当传输数据时,read和write也应该更新这个值。
unsigned int f_flags;
文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC。驱动程序为了支持非阻塞型操作需要检查这个标志。注意,检查读/写权限应该查看f_mode而不是f_flags。所有这些标志都定义在中。
struct file_operations *f_op;
与文件操作对应的指针。内核在完成open时对这个指针赋值,以后需要对文件进行操作时就访问此指针。f_op中的值并不保存,也就是说可以在需要的时候修改文件所对应的操作,下一次再调用此打开文件的相应操作时就会调用新方法。这种技巧有助于在不增加系统调用负担的情况下方便地识别主设备号相同的设备,这在面向对象编程技术中称为“方法重载”。
void *private_data;
系统在调用驱动程序的open方法前将这个指针置为NULL。驱动程序可以将这个字段用于任意目的,也可以忽略这个字段。驱动程序可以用这个字段指向已分配的数据,但是一定要在内核释放file结构前的release方法中清除它。
下面介绍驱动程序能够对它管理的设备完成哪些操作。
我们可以设想驱动程序与操作系统内核之间存在一个接口,这个接口是通过数据结构file_operations来完成的。内核使用该结构访问驱动程序的函数。
下面列举了应用程序能够对设备进行的部分操作,这些操作通常被称为“方法”,它们返回0时表示成功,发生错误时返回一个负的错误编码。
loff_t (*llseek)(struct file *, off_t, int);
方法llseek的功能是修改一个文件的当前读写位置,并将新位置做为(正的)返回值返回。出错时返回一个负值。
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
用来从设备中读取数据。当其为NULL指针时,read系统调用返回-EINVAL(“非法参数”)。函数返回一个非负值时表示成功地读取了多少字节。
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
向设备发送数据。如果没有这个函数,write系统调用返回-EINVAL。如果返回值非负,表示成功写入的字节数。
int (*readdir)(struct file *, void *, filldir_t);
对于设备节点来说,这个字段应该为NULL,因为它仅用于目录。
int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
系统调用ioctl提供了一种调用设备相关命令的方法(比如软盘的格式化命令,既不是读操作也不是写操作)。另外,内核还识别一部分ioctl命令,而不必调用结构file_operations中的ioctl。如果设备不提供ioctl入口点,对于任何内核没有定义的请求,ioctl系统调用将返回-EINVAL。当调用成功时,返回给调用程序一个非负值。
int (*mmap)(struct file *, struct vm_area_struct *);
mmap用来将设备内存映射到进程内存中。如果设备不支持这个方法,mmap系统调用将返回-ENODEV。
int (*open)(struct inode *, struct file *);
尽管此方法总是操作设备节点所需的第一个步骤,然而并不要求驱动程序一定要声明这个方法。如果该项为NULL,设备的打开操作永远成功,但是系统不会通知驱动程序。
void (*release)(struct inode *, struct file *);
当节点被关闭时调用这个操作。与open相仿,也可以不用声明release。
int (*fsync) (struct file *, struct dentry *);
功能是刷新设备。如果驱动程序不支持,fsync系统调用返回-EINVAL。
int (*fasync) (int, struct file *, int);
这个操作用来通知设备FASYNC标志的变化。fasync调用在设备已经完全刷新数据后才返回。如果设备不支持异步触发,该字段可以是NULL。
int (*check_media_change)(kdev_t dev);
方法check_media_change只用于块设备。内核调用此方法来判断设备中的物理介质(如软盘)自最近一次操作以来发生了变化(返回1)或是没有(返回0)。而字符设备无需实现这个函数。
int (*revalidate)(kdev_t dev);
这一项与前面提到的那个方法一样,也只适用于块设备。revalidate与高速缓存区有关。
下面简要介绍一下以上述的open、release、write、read和ioctl五项方法以及中断处理应该包含哪些内容。
open方法
open方法是驱动程序用来为以后的操作完成初始化准备工作的。此外,open还会增加设备计数值,以防止文件在关闭前模块被卸载出内核。
open完成如下工作:
检查设备相关错误(诸如设备未就绪或相似的硬件问题)。
如果是首次打开,初始化设备。
标别次设备号,如有必要更新f_op指针。
分配和填写要放在filp->private_data里的数据结构。
增加使用计数。
release方法
release方法的作用正好与open相反,这个方法有时也称为close。它完成如下工作:
使用计数减1。
释放open分配于private_data中的内存。
做最后一次关闭操作时关闭设备。
read和write 方法
读写设备意味着要进行内核空间到用户进程空间的数据传输,以下函数可完成这些功能:
void memcpy_fromfs(void *to,const void *from,unsigned long count);
void m