linux内核模块编程4

Chapter 4. Character Device Files
字符设备文件
关于file_operations结构体
结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对 设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。

举个例子,每个字符设备需要定义一个用来读取设备数据的函数。结构体 file_operations中存储着内核模块中执行这项操作的函数的地址。一下是该结构体 在内核2.6.5中看起来的样子:

struct file_operations {
        struct module *owner;
         loff_t(*llseek) (struct file *, loff_t, int);
         ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
         ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
         ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
         ssize_t(*aio_write) (struct kiocb *, const char __user *, 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 (*aio_fsync) (struct kiocb *, 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 *);
         ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t,
                             void __user *);
         ssize_t(*sendpage) (struct file *, struct page *, int, size_t,
                             loff_t *, int);
        unsigned long (*get_unmapped_area) (struct file *, unsigned long,
                                            unsigned long, unsigned long,
                                            unsigned long);
};
                        
驱动内核模块是不需要实现每个函数的。像视频卡的驱动就不需要从目录的结构 中读取数据。那么,相对应的file_operations重的项就为 NULL。

gcc还有一个方便使用这种结构体的扩展。你会在较现代的驱动内核模块中见到。 新的使用这种结构体的方式如下:

struct file_operations fops = {
        read: device_read,
        write: device_write,
        open: device_open,
        release: device_release
};
                        
同样也有C99语法的使用该结构体的方法,并且它比GNU扩展更受推荐。我使用的版本为 2.95为了方便那些想移植你的代码的人,你最好使用这种语法。它将提高代码的兼容性: 

struct file_operations fops = {
        .read = device_read,
        .write = device_write,
        .open = device_open,
        .release = device_release
};
                        
这种语法很清晰,你也必须清楚的意识到没有显示声明的结构体成员都被gcc初始化为NULL。

指向结构体struct file_operations的指针通常命名为fops。

关于file结构体
每一个设备文件都代表着内核中的一个file结构体。该结构体在头文件linux/fs.h定义。注意,file结构体是内核空间的结构体, 这意味着它不会在用户程序的代码中出现。它绝对不是在glibc中定义的FILE。 FILE自己也从不在内核空间的函数中出现。它的名字确实挺让人迷惑的。 它代表着一个抽象的打开的文件,但不是那种在磁盘上用结构体 inode表示的文件。

指向结构体struct file的指针通常命名为filp。 你同样可以看到struct file file的表达方式,但不要被它诱惑。

去看看结构体file的定义。大部分的函数入口,像结构体 struct 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定义的宏 来增加,减小和读取该计数器:


try_module_get(THIS_MODULE): Increment the use count.

try_module_put(THIS_MODULE): Decrement the use count.

保持该计数器时刻精确是非常重要的;如果你丢失了正确的计数,你将无法卸载模块, 那就只有重启了。不过这种情况在今后编写内核模块时也是无法避免的。

chardev.c
下面的代码示范了一个叫做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
*/

#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 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;
        try_module_get(THIS_MODULE);

        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. 
         */
        module_put(THIS_MODULE);

        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,        /* see include/linux/fs.h   */
                           char *buffer,        /* buffer to fill with data */
                           size_t length,        /* length of the buffer     */
                           loff_t * offset)
{
        /*
         * 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 so "*" 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 

在上一个版本中该文档还保留了详细的如何向后兼容老内核的介绍,现在我们决定打破这个传统。 对为老内核编写驱动感兴趣的读者应该参考对应版本的LKMPG,也就是说,2.4.x版本的LKMPG对应 2.4.x的内核,2.6.x版本的LKMPG对应2.6.x的内核。 

Notes
[1] 这只是习惯上的。将设备文件放 在你的用户目录下是没有问题的。但是当真正提供成熟的驱动模块时,请保证将设备文 件放在/dev下。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值