查看: 2772|回复: 0

linux驱动程序的数据结构

[复制链接]
发表于 2017-5-4 11:09:56 | 显示全部楼层 |阅读模式
一、linux驱动程序的数据结构

设备驱动程序实质上是提供一组供应用程序操作设备的接口函数。
各种设备由于功能不同,驱动程序提供的函数接口也不相同,但linux为了能够统一管理,规定了linux下设备驱动程序必须使用统一的接口函数 file_operations 。
所以,一种设备的驱动程序主要内容就是提供这样的一组file_operations接口函数。
那么,linux是如何管理种类繁多的设备驱动程序呢?

linux下设备大体分为块设备和字符设备两类。
内核中用2个全局数组存放这2类驱动程序。
#define MAX_CHRDEV   255
#define MAX_BLKDEV   255
struct device_struct {
    const char * name;
    struct file_operations * fops;
};
static struct device_struct chrdevs[MAX_CHRDEV];

static struct {
    const char *name;
    struct block_device_operations *bdops;
} blkdevs[MAX_BLKDEV];
//此处说明一下,struct block_device_operations是块设备驱动程序内部的接口函数,上层文件系统还是通过struct file_operations访问的。

哈哈,现在明白了吧?你的驱动程序调用 int register_chrdev(unsigned int major, const char * name, struct file_operations *fops) 就是将你提供的接口函数fops存放到chrdevs[MAX_CHRDEV]这个数组中,数组下标就是你的驱动的主设备号,数组内容包括驱动名称和驱动接口函数,这样,内核就能看到你的驱动程序了。BTW,如果你将major设为0,系统会自动给你分配一个空闲的主设备号。
那么?次设备号呢?别急,马上就出现了:)
二、设备节点如何产生?

    驱动程序运行在内核空间,应用程序访问驱动程序通常是通过系统调用文件系统接口函数的,也就是说,在linux下,和磁盘文件一样,设备也是文件,只是他们的文件属性不同而已,应用程序只能通过文件名来访问设备的驱动程序。
所以,文件系统中必须要有一个代表你的设备的文件,应用程序才能访问你的设备驱动程序。
    为了便于理解,我们可以将设备文件换个名字,叫做设备节点。
    设备节点在哪里?设备节点存在于你的文件系统中,通常在/dev目录下,当然,你也可以在其它地方创建。一般说来,我们在制作文件系统映像时就已经将可能用到的设备节点都创建好了。
    你可以打开/dev目录看一下,它下面的设备节点的数量会让你吃惊的:)

       如何创建设备节点?。
你可以用mknod命令。如使用以下命令可以创建一个mtd4的字符设备节点。
Mknod  /dev/ mtd4  c MTD_CHAR_MAJOR  4

我们创建一个普通的磁盘文件,它的内容是我们写入的数据。
那么设备节点的内容是什么?设备节点文件没有数据,它的文件大小为0,它只有文件属性,包括设备类型、主设备号、次设备号。
没有别的了?对,就这些,没别的了。

那设备节点和设备驱动程序是怎么联系起来的啊?

别着急,休息,休息一会儿:)顺便加下我的QQ:2232894713
三、应用程序是如何访问设备驱动程序的?

举个例子:我们要向nor flash的第四个分区的起始位置偏移512字节写入100字节的数据。
我们是如何做的?主要程序片断如下:
    fd = open(“/dev/mtd4”, O_RDWR);
    lseek (fd,512, SEEK_SET);
    write (fd , write_buffer, 100);
    close(fd);
上面的代码比较简单,但是似乎没有看到我们的应用程序是如何调用到驱动程序的。
没关系,接下来我将带领你们走通这条道路。

应用程序调用Open函数,这是个系统调用函数,程序会进入内核空间调用sys_open函数。
在sys_open,首先会根据文件路径“/dev/mtd4”找到这个文件节点,这部分工作是属于VFS(虚拟文件系统)的。
“/dev/mtd4”的文件属性是字符设备,于是sys_open会调用函数chrdev_open(),在这个函数里有一句话:
filp->f_op = get_chrfops(MAJOR(inode->i_rdev), MINOR(inode->i_rdev));
哈!看到了眉目吧!猜也能猜到啊,get_chrfops()里面一定会返回 chrdevs[major].fops的。
我们终于从文件系统走到驱动程序了,那么,接下来的事情就是可以理解的了。
Write()最终一定会调用到chrdevs[major].fops->write();
Read()最终一定会调用到chrdevs[major].fops->read();
各种驱动程序比较特殊的功能函数都可以通过ioctl()来得到调用。
而次设备号也会作为参数传递给你。
四、为什么要有设备文件系统?

从前面的章节,我们可以看到以主次设备号的形式管理设备驱动程序存在很大的缺点。
首先,设备节点的创建是独立于内核的,是在建立文件系统时就把所有要用到的设备节点都创建好了的,通常我们不会去刻意删除哪些节点,因为我们不知道系统将来会不会用到它们。由于每个设备节点代表唯一的主次设备号,所以每个可能存在的子设备都对应一个设备节点,可见,这样的设备节点数量是很大的,这些数量庞大的设备节点都(文件)存在于存储介质中,对文件系统的效率也是个影响。
其次,文件系统中存在哪些设备节点,并不代表内核中就有这种设备的驱动程序,也不代表系统中有这种设备,因为设备节点不是动态创建的,它是制作文件系统时建立的。因此,/dev目录下的信息大多对我们是无用的,而且那么多的设备节点都平铺在/dev目录下,阅读起来也不直观。
最后,目前主次设备号都是用8位整数表示的,也就是说内核最多管理256种字符设备和256种块设备。现在计算机外设种类越来越多,这样的限制已经不够了。

由于目前的主次设备号的管理形式有以上几个缺点,linux内核小组在2.4版本以后加入了设备文件系统来改进这些缺点。
设备文件系统的思想就是想让设备节点可以动态创建、删除,这样系统中有哪些设备驱动程序就可以一目了然;还要能够把设备节点组织成一棵目录树,方便阅读;最后,希望能够扩大主次设备号的限制,不再限制在256种设备以内。
五、设备文件系统如何实现?

       要想在内核中方便的做到动态创建、删除设备文件(在这里,我们把设备节点称为设备文件会更恰当些),最自然的做法就是在RAM中创建一个文件系统,内核启动时这个文件系统是空的,以后每加载一种设备驱动程序,就在这个文件系统中创建一个对应的设备文件;卸载设备驱动程序时,再删除这个设备文件。而且,我们可以在这个文件系统中创建目录,一类设备文件放在同一个目录中,甚至把一种设备的多个子设备文件放在同一个目录下,方便阅读。
    在设备文件系统中,我们在注册设备文件时可以把设备驱动程序的ops直接挂到设备文件的inode中,以后访问驱动程序就可以摆脱主次设备号的限制了,不需要再访问chrdev[]数组,这样就突破了256种设备的限制。
    我们把设备文件系统mount到/dev目录下,这样,看起来跟以前的方案就很相似了,也方便老的应用程序的移植。

六、如何使用设备文件系统?

       以前我们写驱动程序时要调用int register_chrdev(unsigned int major, const char * name, struct file_operations *fops)将你提供的接口函数fops存放到chrdevs[MAX_CHRDEV]这个数组中,然后在文件系统中用mknod创建有相同主设备号的设备节点就可以了。
    那么现在有了设备文件系统,我们的驱动程序该如何写呢?
    很简单,follow me!

1、调用devfs_handle_t devfs_mk_dir (devfs_handle_t dir, const char *name, void *info)创建设备文件所在的目录。Dir是要创建目录的父目录句柄,如为NULL,就是设备文件系统的根目录(/dev);最后一个参数info通常为NULL。
2、调用devfs_handle_t devfs_register (devfs_handle_t dir, const char *name,
                  unsigned int flags,
                  unsigned int major, unsigned int minor,
                  umode_t mode, void *ops, void *info)
注册具体的设备,并在指定目录下创建子设备节点。
这里的dir目录名是要创建的设备文件所在的目录名,目录名一般不能为NULL,因为子设备文件名name通常是以0、1、2、3等数字命名的,会和其它设备文件冲突。
    3、卸载驱动程序时调用void devfs_unregister (devfs_handle_t de)删除创建的目录和子设备文件。

七、具体设备驱动程序分析

       这节以mtdchar设备驱动程序来具体分析驱动程序的写法。
    Mtdchar字符设备是管理flash驱动程序的,是各种flash驱动程序的抽象层。
    Mtdchar的主程序是driver/mtd/mtdchar.c;

1、驱动程序初始化时,要注册设备节点,创建子设备文件

驱动程序为了兼容以前的方案,通常会既注册设备节点,又创建子设备文件,这样不管内核支持不支持设备文件系统,驱动程序都可以工作。

static int __init init_mtdchar(void)
{
    //为了兼容以前的方案,要注册mtdchar的主设备号、设备名以及fops
    if (register_chrdev(MTD_CHAR_MAJOR, "mtd", &mtd_fops))
{
       printk(KERN_NOTICE "Can't allocate major number %d for Memory Technology Devices.\n",
              MTD_CHAR_MAJOR);
       return -EAGAIN;
    }

    //如果内核支持设备文件系统,在这个函数里会创建子设备文件。
    mtdchar_devfs_init();
    return 0;
}

static inline void mtdchar_devfs_init(void)
{
    //创建设备节点的父目录,/dev/mtd/
    devfs_dir_handle = devfs_mk_dir(NULL, "mtd", NULL);
    //在这个函数里会调用devfs_register()创建子设备
    register_mtd_user(¬ifier);
}


notifier定义如下,主要是提供创建和删除子设备文件的接口函数

static struct mtd_notifier notifier = {
    .add   = mtd_notify_add,        //创建一个子设备
    .remove    = mtd_notify_remove, //删除一个子设备
};
static void mtd_notify_add(struct mtd_info* mtd)
{
    char name[8];

    if (!mtd)
       return;
    // mtd是一个子设备,代表flash上的一个逻辑分区
    sprintf(name, "%d", mtd->index);
    //这里调用devfs_register创建子设备文件,如/dev/mtd/0
    devfs_rw_handle[mtd->index] = devfs_register(devfs_dir_handle, name,
           DEVFS_FL_DEFAULT, MTD_CHAR_MAJOR, mtd->index*2,
           S_IFCHR | S_IRUGO | S_IWUGO,
           &mtd_fops, NULL);

    //下面注册的是只读子设备,无关紧要。
    sprintf(name, "%dro", mtd->index);
    //创建只读子设备,如 /dev/mtd/0ro
    devfs_ro_handle[mtd->index] = devfs_register(devfs_dir_handle, name,
           DEVFS_FL_DEFAULT, MTD_CHAR_MAJOR, mtd->index*2+1,
           S_IFCHR | S_IRUGO,
           &mtd_fops, NULL);
}

static void mtd_notify_remove(struct mtd_info* mtd)
{
    if (!mtd)
       return;
    //删除mtdchar子设备文件和mtdchar子设备文件
    devfs_unregister(devfs_rw_handle[mtd->index]);
    devfs_unregister(devfs_ro_handle[mtd->index]);
}

mtd驱动程序中会将一片flash划分为多个逻辑分区,这样的每个逻辑分区也可以被看做是一个子设备,具体flash驱动程序添加逻辑分区时会在数组mtd_table[]中记录分区的位置和大小。

void register_mtd_user (struct mtd_notifier *new)
{
    int i;

    down(&mtd_table_mutex);
    list_add(&new->list, &mtd_notifiers);
    __module_get(THIS_MODULE);

    // mtd_table[]是个全局数组,每个元素都是一个逻辑分区,记录着分区的起始位置和大小,我们将每个逻辑分区创建为一个单独的mtd子设备文件
    for (i=0; i< MAX_MTD_DEVICES; i++)
       if (mtd_table)
           new->add(mtd_table);

    up(&mtd_table_mutex);
}

2、驱动程序卸载时要注销设备节点,删除设备文件

static void __exit cleanup_mtdchar(void)
{
    mtdchar_devfs_exit();
    //注销chrdevs[major]
    unregister_chrdev(MTD_CHAR_MAJOR, "mtd");
}


static inline void mtdchar_devfs_exit(void)
{
    //在这个函数里会调用devfs_unregister()删除子设备
    unregister_mtd_user(¬ifier);
    //删除父目录
    devfs_unregister(devfs_dir_handle);
}



int unregister_mtd_user (struct mtd_notifier *old)
{
    int i;

    down(&mtd_table_mutex);

    module_put(THIS_MODULE);

    for (i=0; i< MAX_MTD_DEVICES; i++)
     if (mtd_table)
           old->remove(mtd_table);

    list_del(&old->list);
    up(&mtd_table_mutex);
    return 0;
}
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们  -  服务条款  -  使用指南  -  站点地图  -  友情链接  -  联系我们
电子工程网 © 版权所有   京ICP备16069177号 | 京公网安备11010502021702
快速回复 返回顶部 返回列表