Linux字符设备驱动

来源:互联网 发布:cf游戏数据异常怎么办 编辑:程序博客网 时间:2024/05/19 17:58

1. Linux设备类型

Linux内核中的设备可分为三类:字符设备、块设备和网络设备。
字符设备(Character device):适合面向字符的数据交换,因其数据传输量较低。对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据流。他不具备缓冲区,所以对这种设备的读写是实时的,如终端、磁带机、串口、键盘等。
块设备(Block device):是一种具有一定结构的随机存取设备,对这种设备的读写是按固定大小的数据块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或从设备中一次性读出放入到缓冲区。块设备通常都是以安装文件系统的方式使用的——这也是块设备一般的访问方式。
每个字符设备和块设备都有与之对应的设备文件,这种文件并不指向磁盘或其他存储介质上的数据,只是用来建立与某个设备驱动程序的关联,让应用程序可以像访问普通文件一样来访问和操作设备,而无需过多关注设备之间的差异。正如下图所示:
这里写图片描述

Linux驱动程序中字符设备和块设备的几点区别:

  • 字符设备只能以字节为最小单位访问,而块设备以固定长度的块为单位,例如512字节,1024字节等,即使只请求一个字节的数据,也会从设备中取出完整块的数据;
  • 块设备可以随机访问(在数据中的任何位置进行访问),但是字符设备不做要求(有些字符设备可以提供数据的随机访问,驱动程序可选择是否实现);
  • 块设备的读写会有大规模的缓存,已经读取的数据会保存在内存中,如果再次读取则直接从内存中获得,写入操作也使用了缓存以便延迟处理,减少了IO次数和占用的CPU时间。而字符设备每次的读写请求必须与设备交互才能完成,因此没有必要使用缓存。

网络设备(Network device):网络设备用于管理系统中的(物理或虚拟)网卡,处理网口上网络数据的收发,并提供协议栈和特定网卡之间关联的统一接口。和字串设备/块设备不同的是,网络设备在/dev下面不会有对应的设备文件,而是通过net_device结构来定义网卡提供的服务并可供用户程序读取和配置(如配置IP地址等)。和字符设备类似,网络设备不会关联到实际的存储介质或特定文件系统上。

2. 设备文件

2.1 文件属性

一个设备文件对应的设备并不是通过其文件名标识,而是通过文件的主、从设备号标识的。这些号码在系统中作为特别的文件属性管理。

root@openwrt:/bin# ls -l /dev/mtdblock* /dev/ttyS*brw-r--r-- 1 root  root  31,  0 Jan  1  1970 /dev/mtdblock0brw-r--r-- 1 root  root  31,  1 Jan  1  1970 /dev/mtdblock1brw-r--r-- 1 root  root  31,  2 Jan  1  1970 /dev/mtdblock2crw-rw-rw- 1 root  root  4,  64 Jan  1  1970 /dev/ttyS0crw------- 1 root  root  4,  65 Aug 24 17:53 /dev/ttyS1

上面打印出了/dev中的几个设备文件,这些文件的属性和普通文件有两处很重要的差别:
 文件类型(访问权限之前的字母)是b或c,分别表示块设备和字符设备。
 设备文件没有文件长度,而增加了另外两个值:[主设备号, 从设备号],二者共同形成一个唯一的号码,内核可由此查找对应的设备驱动程序。

2.2 主从设备号

内核通过主从设备号来标识匹配的驱动程序。主设备号用于寻址设备驱动程序自身,系统中可能存在几个同样类型的设备,他们由同一个设备驱动程序管理,也就是说,一个主设备号对应一个驱动程序,一个次设备号对应驱动程序所实现的某个设备实例。例如上面的ttyS0和ttyS1两个设备的主设备号是同一个,而驱动程序管理的各个设备则通过不同的从设备号指定。
为驱动程序和设备分配的主从设备号,主要是通过一个半官方的组织管理,设备号的当前列表可以从http://www.lanana.org或内核源码中的Documentation/devices.txt中获取,而内核源码的

MAJOR(dev_t dev); //从dev_t中提取主设备号MINOR(dev_t dev); //从dev_t中提取从设备号MKDEV(int major, int minor); //根据主从设备号产生一个dev_t类型的值

3. 字符设备创建过程

3.1 管理字符设备

每个字符设备都有一个struct cdev实例与之对应,全局变量cdev_map是一个散列表,用来跟踪系统中所有的字符设备对象。(块设备也是这种做法,每个块设备的struct genhd实例都由全局变量bdev_map来跟踪维护。)
针对字符设备还有一个设备号数据库,即全局数组chrdevs[CHRDEV_MAJOR_HASH_SIZE]。仍然使用散列表来记录所有已分配的设备号范围,使用主设备作为散列键,散列方法很简单:(major % 255)。
数组的每个散列元素以及冲突链表的每个元素都是一个struct char_device_struct结构,定义如下:

static struct char_device_struct {    struct char_device_struct *next;    unsigned int major; //主设备号    unsigned int baseminor; //子设备号的起始值    int minorct; //子设备号的个数    char name[64];    struct cdev *cdev;      /* will die */} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

结构体中的next指针指向冲突链表中的下一个元素。每个散列值的冲突链表由major从小到大排列,major相同的则由子设备号从小到大排列。不会也不允许存在设备号重叠的情况。

3.2 注册字符设备

通过MKDEV我们可以得到一个dev_t类型的设备编号,这个编号被作为要分配的设备编号范围的起始值,其次设备号通常为0。申请多个连续的设备编号的必要函数为:

int register_chrdev_region(dev_t from, unsigned count, const char *name);

这个函数有三个参数:first是设备编号范围的起始值,即上面通过MKDEV获得的dev_t类型的值,count是申请连续设备编号的个数,name是与该设备范围关联的设备名称,它将出现在/proc/devices和sysfs中。分配成功该函数返回0,失败返回错误码。
该函数将设备号 first ~ (first+count-1) 全部占为己有,字符设备的设备号数据库会将这些设备号记录为已分配,不能再被其他驱动程序申请使用。如果在申请过程中发现其中某些设备编号已经被分配过了(设备号重叠),register_chrdev_region会返回-EBUSY错误码。
对应的释放设备号的方法为:

void unregister_chrdev_region(dev_t from, unsigned count);

另外还有下面两个函数:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) [1]unregister_chrdev_region(dev_t from, unsigned count) [2]

函数[1]可以动态分配主设备编号,当我们不确定要使用哪个主设备号时可以使用这个函数,该函数中basenimor和count用于请求次设备号,获取到的主设备号以及baseminor组成的dev_t存放在第一个参数dev中,同样的,分配成功该函数返回0,失败返回错误码。函数[2]用于归还设备编号。
注意alloc_chrdev_region只能申请一个major,而register_chrdev_region申请的设备号范围可以包含多个major,即同时申请多个major。

3.3 激活字符设备

在获取了设备号范围后,需要将设备添加到系统的字符设备数据库(即上面讲到的全局变量cdev_map)中,以激活设备。这就需要用cdev_init初始化一个struct cdev实例,并调用cdev_add添加到系统中(对应的删除方法为cdev_del())。

void cdev_init(struct cdev *cdev, const struct file_operations *fops);int cdev_add(struct cdev *p, dev_t dev, unsigned count);

cdev_init的参数cdev即字符设备实例,可以静态定义或使用cdev_alloc()动态分配。参数fops指向与设备实际通信的函数集合,下面会讲到。

3.4 创建设备文件

用户程序通过设备文件名来操作设备(如open/read/write/ioctl/close),因此要将设备文件(在/dev下)创建好。
如果文件系统必须是只读的,则只能在打包镜像期间,通过mknod来预创建设备文件,mknod命令要指定设备类型、主从设备号和文件名。这种做法的问题是,所有设备文件都要手动创建,如果设备数非常多则是一件很无聊的事;其次,由于设备文件被放到磁盘文件系统中,如果一个设备已经不需要了,就会残留在系统中;最后,驱动程序必须使用mknod约定好的主从设备号来创建设备,降低了机动性。
可以使用udev机制来动态创建设备节点,它依赖于用户态程序udevd来监听内核的消息,并根据udev规则(/etc/udev/udev.conf中配置以及/etc/udev/rules.d/下的规则)创建设备文件。而/dev被挂载为tmpfs,这样在系统重启后,原有的设备文件被清空,由驱动程序更新。
在内核中注册并激活设备后,要调用device_create(),该函数在/sys中注册相应设备,并发送一个add device的通知,udevd便可收到该通知并在/dev下创建设备文件。

struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);

另外,在嵌入式设备上,还有一个轻量级的工具mdev,机制和用法和udev类似,以及WRT系统上使用的hotplug2,有兴趣可以用用看。
其他接口
还有一个旧的注册字符设备的函数register_chrdev,这个函数从register到add一气呵成,不过新的代码不应该使用该函数,一来太自动化,驱动程序无法知道cdev的任何信息,而且该函数不支持大于255的子设备号。
可以把一些简单的字符设备驱动初始化为混杂驱动程序,所有混杂设备的主设备号都是10。混杂驱动程序只需调用misc_register()即可完成一个字符设备的完整注册过程,例如:

static struct miscdevice gpio_smi_dev = {   .minor       = MISC_DYNAMIC_MINOR,   .name        = "gpiosmi",   .fops        = &gpiosmi_fops,};static int __init gpio_smi_init(void){int ret;ret = misc_register(&gpio_smi_dev);    if(0 != ret)    {        return -1;    }    return 0;}static void __exit gpio_smi_exit(void){    misc_deregister(&gpio_smi_dev);}module_init(gpio_smi_init);module_exit(gpio_smi_exit);

在struct miscdevice结构体中,可以通过minor指定子设备号,或者指定为MISC_DYNAMIC_MINOR来让系统动态分配一个从设备号,分配好后会重新赋值给minor。
每一个混杂驱动程序自动出现在/sys/class/misc/目录下,而不必在驱动程序中再创建。

3.5 操作设备文件

关联到inode
我们说用户程序可以想操作普通文件一样来操作设备文件,那么每个设备文件肯定要关联到虚拟文件系统中的一个inode。

struct inode {    umode_t     i_mode;    ... ...    dev_t           i_rdev;  ... ...    const struct file_operations    *i_fop;    ... ...    struct list_head    i_devices;    union {        ... ...        struct block_device *i_bdev;        struct cdev     *i_cdev;    };    ... ...};

其中,i_mode存储了文件类型(普通文件、目录文件、字符设备、块设备、套接字等),而i_rdev中存储了主从设备号。i_fop是一组函数指针集合,包括许多文件操作(open/read/write等)来下发各种具体文件操作。内核根据inode表示字符设备还是块设备来使用i_bdev或i_cdev指向更多具体信息,i_devices是一个链表节点,以字符设备为例,cdev的list成员是一个链表,设备文件被打开一次,就会创建一个inode并插入到这个链表中,inode的i_devices成员即为链表元素。
寻找文件操作
在打开一个设备文件时,各文件系统的实现会调用init_special_inode函数为设备创建一个inode并设置默认的文件操作处理函数。

void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev){    inode->i_mode = mode;    if (S_ISCHR(mode)) {        inode->i_fop = &def_chr_fops;        inode->i_rdev = rdev;    } else if (S_ISBLK(mode)) {        inode->i_fop = &def_blk_fops;        inode->i_rdev = rdev;    } else if (S_ISFIFO(mode))        inode->i_fop = &pipefifo_fops;    else if (S_ISSOCK(mode))        inode->i_fop = &bad_sock_fops;    else        printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"                  " inode %s:%lu\n", mode, inode->i_sb->s_id,                  inode->i_ino);}EXPORT_SYMBOL(init_special_inode);

可见对于所有的字符设备,VFS层的文件操作集合由def_chr_fops提供,其中打开文件的函数为chrdev_open(),该函数通过inode->i_cdev或inode->i_rdev获取到字符设备的struct cdev实例,接着调用特定于该设备的文件操作集合cdev-> ops的open方法(如果在该设备的驱动程序中有定义的话)。
额外说一下,为设备定义特定文件操作通常的做法是,首先为主设备号设置一个特定的文件操作集合(例如misc的所有设备都指向misc_fops),接下来如果某个设备需要对某些操作做补充,则定义特定于该从设备号的操作函数来覆盖原操作。通常要提供的操作有open/read/write/ unlocked_ioctl/release等。

4. 伪字符设备驱动

顺便介绍一些并没有关联到实际设备的字符驱动,例如/dev/null,/dev/random,他们可以提供一些简单常用的服务,下面这些字符设备的主设备号都是1(MEM_MAJOR),定义在drivers/char/mem.c中。
/dev/null
接收你不想在命令行上显示的数据,该设备的write不处理数据,只是返回写的长度。
/dev/zero
获取一串0字符,相当于将一段内存memset为’\0’。
/dev/mem
让我们可以直接操作或映射物理地址空间,可赋与mmap()并操作返回的区域。
/dev/random/dev/urandom
随机数发生器,从random读取的随机数的随机性要高于urandom。但random可能输出一定数量随机数后阻塞停止(可以在一个新终端操作键盘或鼠标补充熵池让random继续产生随机数)。

原创粉丝点击