第 4 章. 字符设备文件

字符设备驱动程序

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

含义很明确,您应该意识到,您未显式分配的结构的任何成员都将初始化为NULL由 gcc。

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

file 结构体

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

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

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

注册设备

如前所述,字符设备通过设备文件访问,通常位于/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中定义了一些宏,可让您增加、减少和显示此计数器

  • MOD_INC_USE_COUNT:增加使用计数。

  • MOD_DEC_USE_COUNT:减少使用计数。

  • MOD_IN_USE:显示使用计数。

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

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

为多个内核版本编写模块

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

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中用于生产驱动程序