4.1. 字符设备驱动程序

4.1.1. file_operations 结构

file_operations 结构定义在linux/fs.h中,它保存了驱动程序定义的函数指针,这些函数用于执行设备上的各种操作。结构的每个字段对应于驱动程序定义的某个函数的地址,用于处理请求的操作。

例如,每个字符驱动程序都需要定义一个从设备读取数据的函数。file_operations 结构保存着模块执行该操作的函数的地址。下面是内核的定义示例:2.4.2:

    struct file_operations {
       struct module *owner;
       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 datasync);
       int (*fasync) (int, struct file *, int);
       int (*lock) (struct file *, int, struct file_lock *);
    	 ssize_t (*readv) (struct file *, const struct iovec *, unsigned long,
          loff_t *);
    	 ssize_t (*writev) (struct file *, const struct iovec *, unsigned long,
          loff_t *);
    };
			

某些操作不由驱动程序实现。例如,处理视频卡的驱动程序不需要从目录结构中读取数据。file_operations 结构中的相应条目应设置为NULL.

有一个 gcc 扩展使分配给此结构更方便。你会在现代驱动程序中看到它,可能会让你感到惊讶。这是分配给结构的新方法:

    struct file_operations fops = {
       read: device_read,
       write: device_write,
       open: device_open,
       release: device_release
    };
			

但是,也有一种 C99 方法可以分配给结构的元素,而且这绝对比使用 GNU 扩展更受欢迎。我目前使用的 gcc 版本,2.95支持新的 C99 语法。你应该使用此语法,以防有人想要移植你的驱动程序。它将有助于兼容性。

    struct file_operations fops = {
       .read = device_read,
       .write = device_write,
       .open = device_open,
       .release = device_release
    };
			

其含义很明确,你应该意识到,你没有明确分配的结构的任何成员都将被 gcc 初始化为NULL

指向 struct file_operations 的指针通常命名为fops.

4.1.2. file 结构

每个设备在内核中都由一个 file 结构表示,该结构定义在linux/fs.h中。请注意,file 是一个内核级结构,永远不会出现在用户空间程序中。它与 glibc 定义的 FILE 不同,后者永远不会出现在内核空间函数中。此外,它的名称有点误导性;它表示一个抽象的打开“文件”,而不是磁盘上的文件,后者由名为 inode 的结构表示。

指向struct file的指针通常命名为filp。你也会看到它被称为struct file file。抵制这种诱惑。

继续查看file的定义。你看到的大部分条目,如struct dentry都没有被设备驱动程序使用,你可以忽略它们。这是因为驱动程序不直接填充file;它们只使用包含在file中的结构,这些结构是在其他地方创建的。

4.1.3. 注册设备

如前所述,字符设备通过设备文件访问,通常位于/dev[1]中。主设备号告诉你哪个驱动程序处理哪个设备文件。次设备号仅由驱动程序本身使用,以区分它正在操作的设备,以防驱动程序处理多个设备。

将驱动程序添加到你的系统意味着向内核注册它。这等同于在模块初始化期间为其分配一个主设备号。你可以使用register_chrdev函数来执行此操作,该函数由linux/fs.h.

    int register_chrdev(unsigned int major, const char *name,
       struct file_operations *fops); 
			

定义,其中unsigned int major是你想要请求的主设备号,const char *name是设备将显示在/proc/devices中的名称,以及struct file_operations *fops是指向驱动程序的file_operations表的指针。负返回值表示注册失败。请注意,我们没有将次设备号传递给register_chrdev。这是因为内核不关心次设备号;只有我们的驱动程序使用它。

现在的问题是,如何在不劫持已在使用的主设备号的情况下获取主设备号?最简单的方法是浏览Documentation/devices.txt并选择一个未使用的。这是一种不好的做法,因为你永远无法确定你选择的数字是否会在以后被分配。答案是你可以要求内核为你分配一个动态主设备号。

如果你传递主设备号 0 给register_chrdev,则返回值将是动态分配的主设备号。缺点是你无法提前创建设备文件,因为你不知道主设备号是什么。有几种方法可以做到这一点。首先,驱动程序本身可以打印新分配的数字,我们可以手动创建设备文件。其次,新注册的设备将在/proc/devices中有一个条目,我们可以手动创建设备文件,也可以编写一个 shell 脚本来读取该文件并创建设备文件。第三种方法是让我们的驱动程序在成功注册后使用mknod系统调用创建设备文件,并在调用cleanup_module.

期间删除它。

我们不能允许在 root 感觉合适时 rmmod 内核模块。如果设备文件被一个进程打开,然后我们删除内核模块,则使用该文件将导致调用适当函数(读/写)以前所在的内存位置。如果我们幸运的话,没有其他代码加载到那里,我们将收到一条难看的错误消息。如果我们不走运,另一个内核模块被加载到相同的位置,这意味着跳转到内核中另一个函数的中间。这样做的结果是无法预测的,但它们不会很好。

通常,当你不想允许某些事情发生时,你会从应该执行该事情的函数返回一个错误代码(一个负数)。对于cleanup_module,这是不可能的,因为它是一个 void 函数。但是,有一个计数器跟踪有多少进程正在使用你的模块。你可以通过查看/proc/modules的第三个字段来查看其值。如果这个数字不是零,rmmod将失败。请注意,你不必从cleanup_module中检查计数器,因为系统调用sys_delete_module会为你执行检查,该系统调用定义在linux/module.c中。你不应该直接使用这个计数器,但在linux/modules.h中定义了一些宏,可以让你增加、减少和显示这个计数器

保持计数器准确非常重要;如果你丢失了正确的使用计数的跟踪,你将永远无法卸载模块;现在是重新启动的时候了,孩子们。这注定会在模块开发过程中迟早发生在你身上。

4.1.5. chardev.c

下一个代码示例创建了一个名为chardev的字符驱动程序。你可以cat它的设备文件(或用程序open该文件),驱动程序会将设备文件被读取的次数放入文件中。我们不支持写入文件(如 echo "hi" > /dev/hello),但会捕获这些尝试并告诉用户不支持该操作。如果你没有看到我们如何处理读入缓冲区的数据,请不要担心;我们没有对它做太多处理。我们只是读入数据并打印一条消息,确认我们收到了它。

示例 4-1. chardev.c

/*  chardev.c: Creates a read-only char device that says how many times
 *  you've read from the dev file
 *
 *  Copyright (C) 2001 by Peter Jay Salzman
 *
 *  08/02/2006 - Updated by Rodrigo Rubira Branco <rodrigo@kernelhacking.com>
 */

/* Kernel Programming */
#define MODULE
#define LINUX
#define __KERNEL__

#if defined(CONFIG_MODVERSIONS) && ! defined(MODVERSIONS)
   #include <linux/modversions.h>
   #define MODVERSIONS
#endif
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>  /* for put_user */
#include <asm/errno.h>

/*  Prototypes - this would normally go in a .h file */
int init_module(void);
void cleanup_module(void);
static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t, loff_t *);
static ssize_t device_write(struct file *, const char *, size_t, loff_t *);

#define SUCCESS 0
#define DEVICE_NAME "chardev" /* Dev name as it appears in /proc/devices   */
#define BUF_LEN 80            /* Max length of the message from the device */


/* Global variables are declared as static, so are global within the file. */

static int Major;            /* Major number assigned to our device driver */
static int Device_Open = 0;  /* Is device open?  Used to prevent multiple
                                        access to the device */
static char msg[BUF_LEN];    /* The msg the device will give when asked    */
static char *msg_Ptr;

static struct file_operations fops = {
  .read = device_read,
  .write = device_write,
  .open = device_open,
  .release = device_release
};


/* Functions */

int init_module(void)
{
   Major = register_chrdev(0, DEVICE_NAME, &fops);

   if (Major < 0) {
     printk ("Registering the character device failed with %d\n", Major);
     return Major;
   }

   printk("<1>I was assigned major number %d.  To talk to\n", Major);
   printk("<1>the driver, create a dev file with\n");
   printk("'mknod /dev/hello c %d 0'.\n", Major);
   printk("<1>Try various minor numbers.  Try to cat and echo to\n");
   printk("the device file.\n");
   printk("<1>Remove the device file and module when done.\n");

   return 0;
}


void cleanup_module(void)
{
   /* Unregister the device */
   int ret = unregister_chrdev(Major, DEVICE_NAME);
   if (ret < 0) printk("Error in unregister_chrdev: %d\n", ret);
}


/* Methods */

/* Called when a process tries to open the device file, like
 * "cat /dev/mycharfile"
 */
static int device_open(struct inode *inode, struct file *file)
{
   static int counter = 0;
   if (Device_Open) return -EBUSY;

   Device_Open++;
   sprintf(msg,"I already told you %d times Hello world!\n", counter++);
   msg_Ptr = msg;
   MOD_INC_USE_COUNT;

   return SUCCESS;
}


/* Called when a process closes the device file */
static int device_release(struct inode *inode, struct file *file)
{
   Device_Open --;     /* We're now ready for our next caller */

   /* Decrement the usage count, or else once you opened the file, you'll
                    never get get rid of the module. */
   MOD_DEC_USE_COUNT;

   return 0;
}


/* Called when a process, which already opened the dev file, attempts to
   read from it.
*/
static ssize_t device_read(struct file *filp,
   char *buffer,    /* The buffer to fill with data */
   size_t length,   /* The length of the buffer     */
   loff_t *offset)  /* Our offset in the file       */
{
   /* Number of bytes actually written to the buffer */
   int bytes_read = 0;

   /* If we're at the end of the message, return 0 signifying end of file */
   if (*msg_Ptr == 0) return 0;

   /* Actually put the data into the buffer */
   while (length && *msg_Ptr)  {

        /* The buffer is in the user data segment, not the kernel segment;
         * assignment won't work.  We have to use put_user which copies data from
         * the kernel data segment to the user data segment. */
         put_user(*(msg_Ptr++), buffer++);

         length--;
         bytes_read++;
   }

   /* Most read functions return the number of bytes put into the buffer */
   return bytes_read;
}


/*  Called when a process writes to dev file: echo "hi" > /dev/hello */
static ssize_t device_write(struct file *filp,
   const char *buff,
   size_t len,
   loff_t *off)
{
   printk ("<1>Sorry, this operation isn't supported.\n");
   return -EINVAL;
}

MODULE_LICENSE("GPL");

4.1.6. 编写用于多个内核版本的模块

系统调用是内核向进程显示的主要接口,通常在不同版本之间保持不变。可能会添加一个新的系统调用,但通常旧的系统调用的行为与以前完全相同。这是向后兼容所必需的 -- 新的内核版本不应该破坏常规进程。在大多数情况下,设备文件也将保持不变。另一方面,内核中的内部接口可能会在不同版本之间发生变化。

Linux 内核版本分为稳定版本(n.$<$偶数$>$.m)和开发版本(n.$<$奇数$>$.m)。开发版本包括所有很酷的新想法,包括那些将被认为是错误的想法,或者在下一个版本中重新实现的想法。因此,你不能相信接口在这些版本中保持不变(这就是我懒得在本书中支持它们的原因,这太多的工作,而且很快就会过时)。另一方面,在稳定版本中,我们可以期望接口保持不变,而不管错误修复版本(m 数字)如何。

不同的内核版本之间存在差异,如果你想支持多个内核版本,你会发现自己必须编写条件编译指令。执行此操作的方法是将宏LINUX_VERSION_CODE与宏KERNEL_VERSION进行比较。在内核的a.b.c版本中,该宏的值将是 $2^{16}a+2^{8}b+c$。请注意,该宏未为内核 2.0.35 及更早版本定义,因此如果你想编写支持真正旧内核的模块,你必须自己定义它,如

示例 4-2. 某个标题

    #if LINUX_KERNEL_VERSION >= KERNEL_VERSION(2,2,0)
        #define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
    #endif

当然,由于这些是宏,你也可以使用 #ifndef KERNEL_VERSION 来测试宏是否存在,而不是测试内核版本。

注释

[1]

这是一种惯例。编写驱动程序时,可以将设备文件放在当前目录中。只需确保将其放置在/dev中,以用于生产驱动程序