如果你曾对Linux2.0版比较熟悉,现在要在内核2.4版下开发驱动程序,那么在了解了2.0到2.2内核API的变化后,还要了解2.2到2.4的变化。
1. 使用设备文件系统(DevFS)
DevFS设备文件系统是Linux
2.4一个全新的功能,它主要为了有效的管理/dev目录而开发的。我能知道,Unix/Linux中所有的目录都是层次结构,唯独/dev目录是一维结构(没有子目录),这就直接影响着访问的效率及管理的方便与否。另外,/dev目录下的节点并不是按实际需要创建的,因此,该目录下存在大量实际不用的节点,但一般也不能轻易删除。
理想的/dev目录应该是层次的、其规模是可伸缩的。Devfs就是为达到此目的而设计。它在底层改写了用户与设备交互的方式和途径。它会给用户在两方面带来影响。首先,几乎所有的设备名称都做了改变,例如:“/dev/hda”是用户的硬盘,现在可能被定位于“/dev/ide0/...”。这一修改方案增大了设备可用的名字空间,且容许USB类和类似设备的系统集成。其次,不再需要用户自己创建设备节点。DevFS的 /dev目录最初是空的,里面特定的文件是在系统启动时、或是加载模块后驱动程序装入时建立的。当模块和驱动程序卸载时,文件就消失了。为保持和旧版本的兼容,可以使用一个用户空间守护程序“devfsd”,以使先前的设备名称能继续使用。
目前,Devfs的使用还只是一个实验性选项,由一个编译选项CONFIG_DEVFS_FS加以选择。
(1) 注册和注销字符设备驱动程序
如前所述,一个新的文件系统要加入系统,必须进行注册。那么,一个新的驱动程序要加入系统,也必须进行注册。在下一章我们会看到,我们把设备大体分为字符设备和块设备。字符设备的注册和注销调用register_chrdev()和unregister_chrdev()函数。注册了设备驱动程序以后,驱动程序应该调用devfs_register()登记设备的入口点,所谓设备的入口点就是设备所在的路径名;在注销设备驱动程序之前,应该调用devfs_unregister()取消注册。
devfs_register()和devfs_unregister() 函数原型为:
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);
其中devfs_handle_t表示Devfs的句柄(一个结构类型),每个参数的含义如下:
dir : 我们要创建的文件所在的Devfs的句柄。NULL意味着这是Devfs的根,即
/dev。
flags :设备文件系统的标志,缺省值为DEVFS_FL_DEFAULT。
major : 主设备号,普通文件不需要这一参数。
minor : 次设备号, 普通文件也不需要这一参数
mode : 缺省的文件模式(包括属性和许可权)。
ops : 指向 file_operations 或
block_device_operations结构的指针
info : 任意一个指针,这个指针将被写到file结构的private_data域。
例如,如果我们要注册的设备驱动程序叫做DEVICE_NAME,其主设备号为MAJOR_NR,次设备号为MINOR_NR,缺省的文件操作为device_fops:则该设备驱动程序的init_module()函数和cleanup_module()函数如下:
int
init_module(void)
{
int ret;
if ((ret = register_chrdev(MAJOR_NR,
DEVICE_NAME, &device_fops)) == 0)
return ret;
}
void
cleanup_module(void)
{
unregister_chrdev(MAJOR_NR, DEVICE_NAME);
}
对以上代码进行改写以支持设备文件系统(假定设备入口点的名字为DEVICE_ENTRY)
#include
<linux/devfs_fs_kernel.h>
devfs_handle_t devfs_handle;
int
init_module(void)
{
int ret;
if
((ret = devfs_register_chrdev(MAJOR_NR, DEVICE_NAME, &device_fops)) == 0)
return
ret;
devfs_handle = devfs_register(NULL,
DEVICE_ENTRY, DEVFS_FL_DEFAULT,
MAJOR_NR, MINOR_NR,
S_IFCHR | S_IRUGO | S_IWUSR,
&device_fops, NULL);
}
void
cleanup_module(void)
{
devfs_unregister_chrdev(MAJOR_NR, DEVICE_NAME);
devfs_unregister(devfs_handle);
}
(2)
在Devfs名字空间中创建一个目录
devfs_mk_dir()用来创建一个目录,这个函数返回Devfs的句柄,这个句柄用作devfs_register的参数dir。
例如,为了在“/dev/mydevice”目录下创建一个设备设备入口点,则进行如下操作:
devfs_handle = devfs_mk_dir(NULL, "mydevice", NULL);
devfs_register(devfs_handle,
DEVICE_ENTRY, DEVFS_FL_DEFAULT,
MAJOR_NR, MINOR_NR, S_IFCHR | S_IRUGO | S_IWUSR,
&device_fops, NULL);
(3)
注册一系列设备入口点
如果一个设备有几个次设备号,就说明同一个设备驱动程序控制了几个不同的设备,例如主IDE硬盘的主设备号为3,但其每个分区都有一个从设备号,例如/dev/had2的从设备号为2。在Devfs下,每个从次设备号也有一个目录,例如/dev/ide0/,/dev/ide1/等,也就是说,每个次设备号都有一个设备入口点,于是就可以调用devfs_register_series来创建一系列的设备入口点。设备入口点的名字以printf()函数中format参数的形式来创建。
注册DEVICE_NR设备入口点(次设备号从MINOR_START开始)的操作如下:
devfs_handle = devfs_mk_dir(NULL, "mydevice", NULL);
devfs_register_series(devfs_handle, "device%u", max_device,
DEVFS_FL_DEFAULT,
MAJOR_NR, MINOR_START, S_IFCHR | S_IRUGO |
S_IWUSR,
&device_fops, NULL);
(4)块设备
注册和注销块设的函数为:
devfs_register_blkdev()
devfs_unregister_blkdev
()
3. 使用 /proc 文件系统
/proc是一个特殊的文件系统,其安装点一般都固定为/proc。这个文件系统中所有的文件都是特殊文件,其内容不存在于任何设备上。每当创建一个进程时,系统就以其pid为文件名在这个目录下建立起一个特殊文件,使得通过这个文件就可以读/写相应进程的用户空间,而当进程退出时则将此文件删除。
/proc文件系统中的目录项结构dentry,在磁盘上没有对应结构,而以内存中的proc_dir_entry结构来代替,在include/linux/proc_fs.h中定义如下:
struct proc_dir_entry {
unsigned short 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;
write_proc_t *write_proc;
atomic_t count;
/* use count */
int deleted; /*
delete flag */
kdev_t
rdev;
};
注册和注销/proc文件系统的机制已经发生了变化。在Linux2.2中, proc_dir_entry结构是静态定义和初始化的,而在2.4中,这个数据结构被动态地创建。
(1)
传送的数据小于一个页面的大小
当传送的数据小于一个页面大小时,/proc文件系统的实现可以通过proc_dir_entry中的read_proc 和write_proc方法来实现。
假定我们要注册的/proc文件系统名为“foo”,在Linux2.2中的代码如下:
foo_proc_entry结构的初始化:
struct
proc_dir_entry foo_proc_entry = {
namelen: 3,
name : "foo",
mode : S_IRUGO | S_IWUSR,
read_proc : foo_read_proc,
write_proc : foo_write_proc,
};
/proc文件系统根节点,即目录项 proc_root的初始化为:
struct
proc_dir_entry proc_root = {
low_ino:
PROC_ROOT_INO,
namelen: 5,
name:
"/proc",
mode:
S_IFDIR | S_IRUGO | S_IXUGO,
nlink:
2,
proc_iops:
&proc_root_inode_operations,
proc_fops:
&proc_root_operations,
parent:
&proc_root,
};
注册:
proc_register(&proc_root, &foo_proc_entry);
注销:
proc_unreigster(&proc_root, foo_proc_entry.low_ino);
在Linux2.4中:
注册:
struct proc_dir_entry
*ent;
if ((ent =
create_proc_entry("foo", S_IRUGO | S_IWUSR, NULL)) != NULL) {
ent->read_proc
= foo_read_proc;
ent->write_proc
= foo_write_proc;
}
注销:
remove_proc_entry("foo",
NULL);
(2)传送数据大于一个页面大小
当传送数据大于一个页面大小时,/proc文件系统的实现应当通过完整的file结构来实现:
在Linux2.2中:
相关数据结构为:
struct
file_operations foo_file_ops = {
......
};
struct
inode_operations foo_inode_ops = {
default_file_ops : &foo_file_ops;
};
struct
proc_dir_entry foo_proc_entry = {
namelen: 3,
name : "foo",
mode : S_IRUGO | S_IWUSR,
ops : &foo_inode_ops,
};
注册为:
proc_register(&proc_root,
&foo_proc_entry);
注销为
proc_unreigster(&proc_root,
foo_proc_entry.low_ino);
在Linux 2.4中:
相关数据结构为:
struct
file_operations foo_file_ops = {
......
};
struct
inode_operations foo_inode_ops = {
......
};
注册为:
struct proc_dir_entry
*ent;
if ((ent =
create_proc_entry("foo", S_IRUGO | S_IWUSR, NULL)) != NULL) {
ent->proc_iops
= &foo_inode_ops;
ent->proc_fops
= &foo_file_ops;
}
注销为:
remove_proc_entry("foo",
NULL);
3. 块设备驱动程序
块设备驱动程序的界面有了很大的变化,新引入了block_device_operations结构,缓冲区高速缓存的接口也发生了变化。
(1)设备注册
在Linux2.2中,块设备与字符设备驱动程序的注册基本相同,都是通过file_operations结构进行的。在2.4中,引入了新结构block_device_operations。例如,块设备的名字为DEVICE_NAME,主设备号为MAJOR_NR,则在2.2中:
数据结构为:
struct
file_operations device_fops = {
open : device_open,
release : device_release,
read : block_read,
write : block_write,
ioctl :
device_ioctl,
fsync : block_fsync,
};
注册为:
register_blkdev(MAJOR_NR,
DEVICE_NAME, &device_fops);
在2.4中:
数据结构为:
#include
<linux/blkpg.h>
struct
block_device_operations device_fops = {
open : device_open,
release : device_release,
ioctl : device_ioctl,
};
注册为:
register_blkdev(MAJOR_NR,
DEVICE_NAME, &device_fops);
(2)缓冲区高速缓存接口
在块设备驱动程序中,有一个“请求函数”来处理缓冲区高速缓存的请求。在2.2中,请求函数的注册和定义如下:
函数原型为:
void
device_request(void);
注册为:
blk_dev[MAJOR_NR].request_fn
= &device_request;
请求函数的定义为:
void
device_request(void)
{
while (1) {
INIT_REQUEST;
......
switch
(CURRENT->cmd) {
case
READ :
//
read
break;
case
WRITE :
//
write
break;
default
:
end_request(0);
continue;
}
end_request(1);
}
}
在2.4中:
函数原型为:
int
device_make_request(request_queue_t *q, int rw, struct buffer_head *sbh);
注册:
blk_queue_make_request(BLK_DEFAULT_QUEUE(MAJOR_NR),
&device_make_request);
请求函数的定义为:
int
device_make_request(request_queue_t *q, int rw, struct buffer_head *sbh)
{
char *bdata;
int ret = 0;
......
bdata = bh_kmap(sbh);
switch (rw) {
case READ :
// read
break;
case READA :
// read ahead
break;
case WRITE :
// write
break;
default :
goto
fail;
}
ret = 1;
fail:
sbh->b_end_io(sbh, ret);
return 0;
}
其中request_queue_t类型的定义请参见下一章,bh_kmap()函数获得内核映射图。
4.PCI设备驱动程序
Linux 2.4包含了具有全部特征的资源管理子系统。它提供了“即插即用”功能, PCI子系统也随之经历了改变。在Linux 2.2中,设备驱动程序搜索所驱动的设备,在2.4中,当驱动程序初始化时就注册设备的信息,当找到一个设备时PCI子系统就调用设备的初始化程序。
(1)驱动程序注册
假设要驱动的设备其商家id和设备id分配为VENDOR_ID和DEVICE_ID,在Linux2.2中,设备初始化函数如下:
struct pci_dev *pdev =
NULL;
while ((pdev =
pci_find_device(VENDOR_ID, DEVICE_ID, pdev)) != NULL) {
// initialize each device
}
在Linux 2.4中
数据结构:
struct
pci_device_id device_pci_tbl[] __initdata = {
{ VENDOR_ID, DEVICE_ID, PCI_ANY_ID, PCI_ANY_ID
},
{ 0, 0, 0, 0 },
};
int
device_init_one(struct pci_dev *dev, const struct pci_device_id *ent);
void
device_remove_one(struct pci_dev *pdev);
struct
pci_driver device_driver = {
name : DEVICE_NAME,
id_table : device_pci_tbl,
probe : device_init_one,
remove : device_remove_one,
suspend : device_suspend,
resume : device_resume,
};
注册为:
if
(pci_register_driver(&device_driver) <= 0)
return
-ENODEV;
注销为:
pci_unregister_driver(&device_driver);
5.文件系统的移植问题已经在上一章进行了介绍
6.下半部分(bottom half)处理程序、软中断(softirq)及tasklets
为了处理硬件中断之外的中断,Linux2.2提供了下半部分。Linux 2.4提供了两种新的机制:软中断及tasklet。软中断在SMP上不是串行化执行,而是同一个处理程序可以在多个CPU上同时执行。为了提高SMP的性能,软中断机制现在主要用于网络子系统。对于tasklet来说,多个tasklet可以在多个CPU上执行,但一个CPU一次只能处理一个tasklet。下半部分(bh)是由内核串行执行的,即使在SMP环境下,一个CPU也只能处理一个下半部分。因此,下半部分变得过时,一般情况下,使用tasklet就足够了。把下半部分移植到tasklet的具体内容请参看第三章的
7.链表及等待队列
(1)通用双向链表
Linux 2.2是以宏和内联函数的形式来定义通用链表的。Linux 2.2主要在文件系统中使用了这种链表,而Linux 2.4使用的更加普遍(例如等待队列)。
在include/linux/list.h中定义的通用链表list_head如下:
struct list_head {
struct list_head *next, *prev;
};
如果我们定义一个整型数据的链表,则其定义如下:
struct
foo_list {
int data;
struct list_head list;
};
然后,链表的头应该定义如下:
LIST_HEAD(foo_list_head);
或者为
struct list_head foo_list_head =
LIST_HEAD_INIT(data_list_head);
或者为:
struct
list_head foo_list_head;
INIT_LIST_HEAD(&foo_list_head);
现在,我们可以用list_add()为链表增加一个节点,用list_del()删除一个节点
struct
foo_list data;
list_add(&data.list,
&foo_list_head);
list_del(&data.list);
使用list_for_each()和 list_entry()来遍历链表。
struct
list_head *head, *curr;
struct
foo_list *element;
head
= &foo_list_head;
curr
= head->next;
list_for_each(curr,
head)
element = list_entry(curr, struct foo_list,
list);
(2)等待队列
Linux 2.2以单链表实现了任务的等待队列,而Linux 2.4用通用双向链表实现了等待队列。
在Linux 2.2中:
队列的定义为
struct
wait_queue *wq = NULL;
睡眠和唤醒为:
interruptible_sleep_on(&wq);
wake_up_interruptible(&wq);
在Linux 2.4中,等待队列的定义发生了变化,但实现函数还是一样的:
队列定义为:
DECLARE_WAIT_QUEUE_HEAD(wq);
或者为
wait_queue_head_t
wq;
init_waitqueue_head(&wq);