结构体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扩展更受推荐。我是用的版本为2.95的gcc同样也支持C99语法。为了方便那些想移植你的代码的人,你最好使用这种语法。它将提高代码的兼容性:
struct file_operations fops = { .read = device_read, .write = device_write, .open = device_open, .release = device_release }; |
这种语法很清晰,你也必须清楚的意识到没有显示声明的结构体成员都被gcc初始化为NULL。
指向结构体file_operations的指针通常命名为fops。
每一个设备文件都代表着内核中的一个file结构体。该结构体在头文件linux/fs.h定义。注意,file结构体是内核空间的结构体,这意味着它不会在用户程序的代码中出现。它绝对不是在glibc中定义的FILE。FILE自己也从不在内核空间的函数中出现。它的名字确实挺让人迷惑的。它代表着一个抽象的打开的文件,但不是那种在磁盘上用结构体inode表示的文件。
指向结构体 file 的指针通常命名为 filp。 你同样可以看到 struct file file的表达方式。但不要被它诱惑。
去看看结构体 file 的定义。 大部分的函数入口,像结构体 dentry 并没有被设备驱动模块使用,你大可忽略它们。这是因为设备驱动模块并不自己直接填充结构体 file ; 它们只是使用在别处建立的结构体 file 中的数据。
如同先前讨论的,字符设备通常通过在路径 /dev [1] 设备文件访问。主设备号告诉你哪些驱动模块是用来操纵哪些硬件设备的。从设备号是驱动模块自己使用来区别它操纵的不同设备,当此驱动模块操纵不只一个设备时。
将内核驱动模块加载入内核意味着要向内核注册自己。这个工作是和驱动模块获得主设备号时初始化一同进行的。你可以使用头文件 linux/fs.h 中的函数 register_chrdev 来实现。
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 表的指针。负的返回值意味着注册失败。注意注册并不需要提供从设备号。内核本身并不在意从设备号。
现在的问题是你如何申请到一个没有被使用的主设备号?最简单的方法是查看文件 Documentation/devices.txt 并从中挑选一个没有被使用的。这不是一劳永逸的方法因为你无法得知该主设备号在将来会被占用。最终的方法是让内核为你动态分配一个。
如果你向函数 register_chrdev 传递为0的主设备号,那么返回的就是动态分配的主设备号。副作用就是既然你无法得知主设备号,你就无法预先建立一个设备文件。 有多种解决方法。第一种方法是新注册的驱动模块会输出自己新分配到的主设备号,所以我们可以手工建立需要的设备文件。第二种是利用文件 /proc/devices 新注册的驱动模块的入口,要么手工建立设备文件,要么编一个脚本去自动读取该文件并且生成设备文件。第三种是在我们的模块中,当注册成功时,使用 mknod 系统调用建立设备文件并且调用 rm 删除该设备文件在驱动模块调用函数 cleanup_module 前。
即使时root也不能允许随意卸载内核模块。当一个进程已经打开一个设备文件时我们卸载了该设备文件使用的内核模块,我们此时再对该文件的访问将会导致对已卸载的内核模块代码内存区的访问。幸运的话我们最多获得一个讨厌的错误警告。如果此时已经在该内存区加载了另一个模块,倒霉的你将会在内核中跳转执行意料外的代码。结果是无法预料的,而且多半是不那么令人愉快的。
平常,当你不允许某项操作时,你会得到该操作返回的错误值(一般为一负的值)。但对于无返回值的函数 cleanup_module 这是不可能的。然而,却有一个计数器跟踪着有多少进程正在使用该模块。你可以通过查看文件 /proc/modules 的第三列来获取这些信息。如果该值非零,则卸载就会失败。你不需要在你模块中的函数 cleanup_module 中检查该计数器,因为该项检查由头文件linux/module.c 中定义的系统调用 sys_delete_module 完成。你也不应该直接对该计数器进行操作。你应该使用在文件 linux/modules.h 定义的宏来增加,减小和读取该计数器:
MOD_INC_USE_COUNT: Increment the use count.
MOD_DEC_USE_COUNT: Decrement the use count.
MOD_IN_USE: Display the use count.
保持该计数器时刻精确是非常重要的;如果你丢失了正确的计数,你将无法卸载模块,那就只有重启了。不过这种情况在今后编写内核模块时也是无法避免的。
下面的代码示范了一个叫做 chardev 的字符设备。你可以用 cat 输出该设备文件的内容(或用别的程序打开它)时,驱动模块会将该设备文件被读取的次数显示。目前对设备文件的写操作还不被支持(像 echo "hi" > /dev/hello ),但会捕捉这些操作并且告诉用户该操作不被支持。不要担心我们对读入缓冲区的数据做了什么;我们什么都没做。我们只是读入数据并输出我们已经接收了数据的信息。
Example 4-1. chardev.c
/* chardev.c: Creates a read-only char device that says how many times * you've read from the dev file */ #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 */ /* 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; } |
系统调用,也就是内核提供给进程的接口,基本上是保持不变的。也许会添入新的系统调用,但那些已有的不会被改动。这对于向下兼容是非常重要的。在多数情况下,设备文件是保持不变的。但内核的内部在不同版本之间还是会有区别的。
Linux内核分为稳定版本(版本号中间为偶数)和试验版本(版本号中间为奇数)。试验版本中可以试验各种各样的新而酷的主意,有些会被证实是一个错误,有些在下一版中会被完善。总之,你不能依赖这些版本中的接口(这也是我不在本文档中支持它们的原因,它们更新的太快了)。在稳定版本中,我们可以期望接口保持一致,除了那些修改代码中错误的版本。
如果你要支持多版本的内核,你需要编写为不同内核编译的代码树。可以通过比较宏 LINUX_VERSION_CODE 和宏 KERNEL_VERSION 来实现。在版本号为 a.b.c 的内核中,该宏的值可以为 2^16×a+2^8×b+c 。注意在内核 2.0.35 及之前是没有该宏的。如果你确实要支持很老的内核,你必须自己定义该宏,就像:
Example 4-2. some title
#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 下。 |