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