10.4.3 把内核2.2移植到内核2.4

   如果你曾对Linux2.0版比较熟悉,现在要在内核2.4版下开发驱动程序,那么在了解了2.02.2内核API的变化后,还要了解2.22.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()函数获得内核映射图。

 4PCI设备驱动程序

Linux 2.4包含了具有全部特征的资源管理子系统。它提供了“即插即用”功能, PCI子系统也随之经历了改变。在Linux 2.2中,设备驱动程序搜索所驱动的设备,在2.4中,当驱动程序初始化时就注册设备的信息,当找到一个设备时PCI子系统就调用设备的初始化程序。

1)驱动程序注册

     假设要驱动的设备其商家id和设备id分配为VENDOR_IDDEVICE_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的具体内容请参看第三章的3.5.6节。

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);