10.4.2 从版本2.02.2内核API的变化

     Linux2.0相比,2.2在性能、资源的利用、健壮性以及可扩展性等方面已经有了很大的改善,这些改善势必导致内核API的变化。所谓内核API就是指为进行内核扩展而提供的编程接口,内核的编程指编写驱动程序、文件系统及其他的内核代码。有关驱动程序的内容请参见下一章。

1.   用户空间与内核空间之间数据的拷贝

早在Linux2.1.x版本时,Liuns就提出了一种有效的办法来改善内核空间与用户空间之间数据的拷贝。我们知道,内核空间与用户空间之间数据的拷贝要通过一个缓冲区,在以前的内核中,对这个缓冲区有效性的检查是通过verify_area()函数的,如果这个缓冲区有效,则调用memcpy_tofs()把数据从内核空间拷贝到用户空间。但是,verify_area()函数是低效的,因为它必须检查每一个页面,看其是否是一个有效的映射。

2.1.x(以及后来的版本)中,取消了对用户空间缓冲区每个页面的检查,取而代之的是用异常来处理非法的缓冲区。这就避免了在SMP上的竞争条件及有效性检查。verify_area()函数现在仅仅用来检查缓冲区的范围是否合法,这是一个快速的操作。

   因此,如果你要把数据拷贝到用户空间,就使用copy_to_user()函数,其用法如下:

 

    if ( copy_to_user (ubuff, kbuff, length) ) return -EFAULT;

 

   这里,ubuff是用户空间的缓冲区,kbuff是内核空间的缓冲区,而length是要拷贝的字节数。如果copy_to_user()函数返回一个非0值,就意味着某些数据没有被拷贝(由于无效的缓冲区)。在这种情况下,返回-EFAULT以表示缓冲区是无效的。类似地,从用户空间拷贝到内核空间的用法如下:

    if ( copy_from_user (kbuff, ubuff, length) ) return -EFAULT;

  注意,这两个函数都自动调用verify_area()函数,你没必要自己调用它。

2. 文件操作的方法

    在内核2.1.42版本以后,增加了一个目录高速缓存(dcache)层,这个层加速了目录搜索操作(大约能提高4倍),但同时也需要改变文件操作接口。对驱动程序的编写者,这个变化相对比较简单:原来传递给file_operations某些方法的参数为struct inode *,现在改为struct dentry *。如果你的驱动程序要引用inode,下面代码就足够了:

    struct inode *inode = dentry->d_inode;

   假定dentry是目录项的变量名。实际上,有些驱动程序就不涉及inode,因此可忽略这一步。然而,你必须改变的是,重新声明file_operations中的函数。注意,某些方法还是把inode而不是dentry作为参数来传递。

   有些方法甚至没有提供dentry,仅仅提供了struct file *,在这种情况下,你可以用下面的代码提取出dentry

    struct dentry *dentry = file->f_dentry;

    假定file是指向file指针的变量名。

    下面是内核2.2.x文件操作的方法:

    loff_t llseek (struct file *, loff_t, int);

    ssize_t read (struct file *, char *, size_t, loff_t *);

    ssize_t write (struct file *, const char *, 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 fasync (int, struct file *, int);

    int check_media_change (kdev_t dev);

    int revalidate (kdev_t dev);

    int lock (struct file *, int, struct file_lock *);

 

    在你声明自己的file_operations结构时,应当确保把自己的方法放置在与上面一致的位置。不过,还有另外一种我们提到过的方法,其形式如下:

static struct file_operations mydev_fops = {

    open:    mydev_open,

    release: mydev_close,

    read:    mydev_read,

    write:   mydev_write,

};

  gcc编译程序能够把这些方法放在正确的位置,并把未定义的方法置为NULL

  另外还值得注意的是,Linux2.2中引入了pread() pwrite()系统调用,这就允许进程可以从一个文件的指定位置进行读和写,这与另一个lseek()系统调用类似但不完全相同。其不同之处是,pread() pwrite()系统调用能对一个文件进行并发访问。为了对这些新的系统调用进行支持,在read()write()方法中增加了第4个(或最后一个)参数,这个参数是指向offset的一个指针。作为驱动程序的编写者,你不必关心文件的位置,因此可以忽略这个参数。不过,为了正确性期间,你最好在你的驱动程序中避免使用新的系统调用,就像你不必支持llseek()方法一样,你也可以在read()write()方法的最顶行增加如下行来避免新旧系统调用的差异:

    if (ppos != &file->f_pos) return ESPIPE

假定ppos是指向offset指针的变量名,file是指向struct file结构的变量名。对于read() write()系统调用,传递给参数offset的为file->f_pos的地址,而对于pread() pwrite()系统调用,传递给offset的为ppos的地址,由此可以很容易地区别这种两种情况。

如果你确实关心文件的位置,那就必须使用和更新由ppos所指向的值,以跟踪进程正读在何处。

3. 信号的处理

  新增加的signal_pending()函数时的信号的处理更加容易和健壮。2.0处理方式是:

    if (current->signal & ~current->blocked)

  2.2:

    if ( signal_pending (current) )

4 I/O空间映射

任何系统都会有输入输出,因此就会涉及对外部设备的访问。在早期的计算机中,外设通常只有几个寄存器,通过这几个寄存器就可以完成对外设的所有操作。而现在的情况则大不一样,外设通常自带几MB的存储器,从自PCI总线出现以后,更是如此。所以,必须将外设卡上的存储器映射到内存空间,实际上是虚存空间(3GB以上)。在以前的Linux内核版本中,这样的映射是通过vremap()建立的,现在改名为ioremap(),以更确切地反映其真正的意图。

5 I/O事件的多路技术

    select() poll()系统调用可以让一个进程同时处理多个文件描述符,也就是说可以使进程检测同时等待的多个I/O设备,当没有设备准备好时,select()阻塞,其中任设备准备好时,select()就返回。在Linux2.0中,驱动程序通过在file_operations结构中提供select()方法来支持这种技术,而在2.2中,驱动程序必须提供的是poll()方法,这种方法具有更大的灵活性。

6.丢弃初始化函数和数据

   当内核初始化全部完成以后,就可以丢弃以后不再需要的函数和数据,这意味着存放这些函数和数据的内存可以重新得到使用。但这仅仅应用在编译进内核的驱动程序,而适合于可安装模块。

定义一个以后要丢弃的变量的形式为:

 

static int mydata __initdata = 0;

 

定义一个以后要丢弃的函数的形式为:

 

    __initfunc(void myfunc (void))

    {

     }

__initdata __initfunc关键字把代码和数据放在一个特殊的“初始化”区段。较理想的做法应当是,尽可能地把更多的代码和数据放在初始化区段,当然,这里的代码和数据指的是初始化以后(当init进程启动时)不再使用的。

7.定时的设定

     新增加了一些定时设定函数。2.0设定定时是这样的:

      current->timeout = jiffies + timeout;

      schedule ();

 

    2.2是:

    timeout = schedule_timeout (timeout);

    同理,如果你需要在一个等待队列上睡眠,但需要定时,2.0操作是:

     current->timeout = jiffies + timeout;

     interruptible_sleep_on (&wait);

   2.2是:

    timeout = interruptible_sleep_on_timeout (&wait, timeout);

注意,这些新函数返回的是剩余时间的多少。在某些情况下,这些函数在定时时间还没到就返回。

8.向后兼容的宏

    你可以把下面的代码包含进自己编写的代码中,这样就不必费神维护是为Linux2.2.x 还是为 2.0.x所编译的驱动程序。

 

    #include <linux/version.h>

    #ifndef KERNEL_VERSION

    #  define KERNEL_VERSION(a,b,c) (((a) << 16) + ((b) << 8) + (c)

    #endif

    #if (LINUX_VERSION_CODE < KERNEL_VERSION(2,1,0))

    #  include <linux/mm.h>

    static inline unsigned long copy_to_user (void *to, const void *from,

                     unsigned long n)

    {

        if ( !verify_area (VERIFY_WRITE, to, n) ) return n;

        memcpy_tofs (to, from, n);

        return 0;

     }

     static inline unsigned long copy_from_user (void *to, const void *from,

                       unsigned long n)

    {

         if ( !verify_area (VERIFY_READ, from, n) ) return n;

         memcpy_fromfs (to, from, n);

         return 0;

     }

    #  define __initdata

    #  define __initfunc(func) func

    #else

    #  include <asm/uaccess.h>

    #endif

    #ifndef signal_pending

    #  define signal_pending(p) ( (p)->signal & ~(p)->blocked )

    #endif