Linux设备管理(二):内核中字符设备的管理
/************************************************************************************
*本文为个人学习记录,如有错误,欢迎指正。
* https://www.cnblogs.com/embedded-tzp/p/4507240.html
* http://www.169it.com/tech-qa-linux/article-5682294992603241339.html
* https://blog.csdn.net/zhoujiaxq/article/details/7646013
* http://www.cnblogs.com/xiaojiang1025/p/6196198.html
************************************************************************************/
1. 字符设备的管理框架
Linux内核对设备的管理是基于kobject来进行的,详见Linux设备管理:kobject, kset, ktype分析。Linux对字符设备的管理框架依赖于struct kobj_map、struct cdev、dev_t dev、struct file_operations等数据结构。如下图所示。
2. 字符设备数据结构
Linux内核中关于字符设备的操作函数存放在 "/kernel/fs/char_dev.c" 文件中。
2.1 dev_t dev
一个字符设备或块设备都有一个主设备号(major)和一个次设备号(minor)。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
Linux内核中,使用dev_t来描述设备号。
typedef u_long dev_t; // 在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。
Linux内核中提供以下几个宏来操作dev_t。
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) // 从设备号中提取主设备号 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) // 从设备号中提取次设备号 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) // 将主、次设备号拼凑为设备号
2.2 struct cdev
Linux内核中,使用struct cdev结构体来描述一个字符设备。
<include/linux/cdev.h> struct cdev { struct kobject kobj; //内嵌的内核对象 struct module *owner; //该字符设备所在的内核模块(所有者)的对象指针,一般为THIS_MODULE,主要用于模块计数 const struct file_operations *ops;//该结构描述了字符设备所能实现的操作集(打开、关闭、读/写、...),是极为关键的一个结构体 struct list_head list; //用来将已经向内核注册的所有字符设备形成链表 dev_t dev; //字符设备的设备号,由主设备号和次设备号构成(如果是一次申请多个设备号,此设备号为第一个) unsigned int count; //隶属于同一主设备号的次设备号的个数 };
2.3 struct file_operations
Linux内核中,使用file_operations结构来管理设备驱动程序的函数,这个结构的每一个成员的名字都对应着一个函数调用。
用户进程利用在对设备文件进行操作时(read/write等),系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取其file_operations结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。
struct file_operations { struct module *owner; /* 模块拥有者,一般为 THIS——MODULE */ ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); /* 从设备中读取数据,成功时返回读取的字节数,出错返回负值(绝对值是错误码) */ ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); /* 向设备发送数据,成功时该函数返回写入字节数。若为被实现,用户调层用write()时系统将返回 -EINVAL*/ int (*mmap) (struct file *, struct vm_area_struct *); /* 将设备内存映射内核空间进程内存中,若未实现,用户层调用 mmap()系统将返回 -ENODEV */ long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg); /* 提供设备相关控制命令(读写设备参数、状态,控制设备进行读写...)的实现,当调用成功时返回一个非负值 */ int (*open) (struct inode *, struct file *); /* 打开设备 */ int (*release) (struct inode *, struct file *); /* 关闭设备 */ int (*flush) (struct file *, fl_owner_t id); /* 刷新设备 */ loff_t (*llseek) (struct file *, loff_t, int); /* 用来修改文件读写位置,并将新位置返回,出错时返回一个负值 */ int (*fasync) (int, struct file *, int); /* 通知设备 FASYNC 标志发生变化 */ unsigned int (*poll) (struct file *, struct poll_table_struct *); /* POLL机制,用于询问设备是否可以被非阻塞地立即读写。当询问的条件未被触发时,用户空间进行select()和poll()系统调用将引起进程阻塞 */ };
2.4 struct kobj_map
Linux内核中,所有的字符设备都会记录在一个cdev_map 变量中。cdev_map是一个struct kobj_map类型的指针,其中包含着一个struct probe*类型、大小为255的数组,数组的每个元素指向的一个probe结构封装了一个设备号和相应的设备对象(cdev)。
struct kobj_map { struct probe { struct probe *next; // 这样形成了链表结构 dev_t dev; //设备号 */ unsigned long range; // 设备号的范围 struct module *owner; kobj_probe_t *get; int (*lock) (dev_t, void *); void *data; //指向struct cdev对象 } *probes[255]; struct mutex *lock; }
字符设备驱动程序通过调用cdev_add把它所管理的字符设备对象的指针嵌入到一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map所实现的哈希链表中。对系统而言,当设备驱动程序成功调用了cdev_add之后,就意味着一个字符设备对象已经加入到了系统,在需要的时候,系统就可以找到它。对用户态的程序而言,cdev_add调用之后,就已经可以通过文件系统的接口调用该设备的驱动程序(具体调用流程详见Linux字符设备:应用程序调用字符设备驱动程序的流程)。
int cdev_add(struct cdev *p, dev_t dev, unsigned count) { p->dev = dev; p->count = count; /*申请并填充struct probe,再通过要加入系统的设备的主设备号major(major=MAJOR(dev))来获得probes数组的索引值i(i = major % 255), 然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中*/ return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); }
cdev_map对字符设备的管理方式有两种:
(1)一个cdev对象对应这一个/多个设备号的情况
在cdev_map中, 一个probes对象就对应一个主设备号;多个设备号对应一个cdev时,其实只是次设备号在变,主设备号还是一样的,所以是同一个probes对象。
(2)主设备号超过255的情况
当主设备号超过255时,会进行probe复用,此时probe->next就派上了用场,比如probe[200]可以表示设备号200,455...3895等所有对255取余是200的数字。