文章从上层应用訪问字符设备驱动開始,一步步地深入分析Linux字符设备的软件层次、组成框架和交互、怎样编写驱动、设备文件的创建和mdev原理。对Linux字符设备驱动有全面的解说。
本文整合之前发表的《Linux字符设备驱动剖析》和《 Linux 设备文件的创建和mdev》两篇文章。基于linux字符设备驱动的全部相关知识给读者一个完整的呈现。
一、从最简单的应用程序入手
1.非常简单。open设备文件。read、write、ioctl,最后close退出。例如以下:
二、/dev文件夹与文件系统
2. /dev是根文件系统下的一个文件夹文件,/代表根文件夹,其挂载的是根文件系统的yaffs格式,通过读取/根文件夹这个文件。就能分析list出其包含的各个文件夹,当中就包含dev这个子文件夹。
即在/根文件夹(也是一个文件,其真实存在于flash介质)中有一项这种数据:
是否文件夹 偏移 大小 名称 -- --
1 0xYYYY 0Xmmm dev -- --
Ls/ 命令即会使用/挂载的yaffs文件系统来读取出根文件夹文件的内容,然后list出dev(是一个文件夹)。
即这时还不须要去读取dev这个文件夹文件的内容。Cd dev即会分析dev挂载的文件系统的超级块的信息,superblock,而不再理会在flash中的dev文件夹文件的数据。
3. /dev在根文件系统构建的时候会挂载为tmpfs. Tmpfs是一个基于虚拟内存的文件系统,主要使用RAM和SWAP(Ramfs仅仅是使用物理内存)。即以后读写dev这个文件夹的操作都转到tmpfs的操作,确切地讲都是针对RAM的操作,而不再是通过yaffs文件系统的读写函数去訪问flash介质。Tmpfs基于RAM。所以在掉电后回消失。
因此/dev文件夹下的设备文件都是每次linux启动后创建的。
挂载过程:/etc/init.d/rcS
Mount –a 会读取/etc/fstab的内容来挂载,其内容例如以下:
4. /dev/NULL和/dev/console是在制作根文件系统的时候静态创建的。其它设备文件都是系统载入根文件系统和各种驱动初始化过程中自己主动创建的。当然也能够通过命令行手动mknod设备文件。
三、设备文件的创建
5. /dev文件夹下的设备文件基本上都是通过mdev来动态创建的。mdev是一个用户态的应用程序,位于busybox工具箱中。
其创建过程包含:
1) 驱动初始化或者总线匹配后会调用驱动的probe接口,该接口会调用device_create(设备类, 设备号, 设备名);在/sys/class/设备类文件夹生成唯一的设备属性文件(包含设备号和设备名等信息)。而且发送uvent事件(KOBJ_ADD和环境变量,如路径等信息)到用户空间(通过socket方式)。
2) mdev是一个work_thread线程,收到事件后会分析出/sys/class/设备类的相应文件。终于调用mknod动态来创建设备文件,而这个设备文件内容主要是设备号(这个设备文件相应的inode会记录文件的属性是一个设备(其它属性还包含文件夹,一般文件,符号链接等))。应用程序open(device_name,…)最重要的一步就是通过文件系统接口来获得该设备文件的内容—设备号。
6. 假设初始化过程中没有调用device_create接口来创建设备文件,则须要手动通过命令行调用mknod接口来创建设备文件,方可在应用程序中訪问。
7. mknod接口分析。通过系统调用后相应调用sys_mknod,其是vfs层的接口。
Sys_mknod(设备名, 设备号)
vfs通过逐一路径link_path_walk,分析出dev挂载了tmpfs,所以调用tmpfs->mknod
shmem_mknod(structinode *dir, struct dentry *dentry, int mode, dev_t dev)
inode = shmem_get_inode(dir->i_sb,dir, mode, dev, VM_NORESERVE);
inode = new_inode(sb);
switch (mode & S_IFMT) {
default:
inode->i_op =&shmem_special_inode_operations;
init_special_inode(inode,mode, dev);//下面是函数展开
break;
case S_IFREG://file
case S_IFDIR://DIR
case S_IFLNK://dentry填入inode信息,这时相应的dentry和inode都已经存在于内存中。
d_instantiate(dentry, inode);
可见,tmpfs的文件夹和文件都是像ramfs一样一般都存在于内存中。
通过ls命令来获取文件夹的信息则由dentry数据结构的内容来获取,而文件的信息由inode数据结构的内容来提供。
Inode包含设备文件的设备号i_rdev,文件属性(i_mode: S_ISCHR),inode操作集i_fop(对于设备文件来说就是怎样open这个inode)。
四、open设备文件
9. open设备文件的终于目的是为了获取到该设备驱动的file_operations操作集,而该接口集是struct file的成员。open返回file数据结构指针:
struct file {
conststruct file_operations *f_op;
unsignedint f_flags;//可读。可写等
…
};
下面是led设备驱动的操作接口。open("/dev/LED",O_RDWR)就是为了获得led_fops。
static conststruct file_operations led_fops = {
.owner =THIS_MODULE,
.open =led_open,
.write = led_write,
};
10. 细致看应用程序int fd =open("/dev/LED",O_RDWR),open的返回值是int,并非file,事实上是为了操作系统和安全考虑。fd位于应用层,而file位于内核层。它们都同属进程相关概念。在Linux中。同一个文件(相应于唯一的inode)能够被不同的进程打开多次,而每次打开都会获得file数据结构。而每一个进程都会维护一个已经打开的file数组,fd就是相应file结构的数组下标。因此,file和fd在进程范围内是一一相应的关系。
11. open接口分析。通过系统调用后相应调用sys_open,其是vfs层的接口
Sys_open(/dev/led)
SYSCALL_DEFINE3(open,const char __user *, filename, int, flags, int, mode)
do_sys_open(AT_FDCWD,/dev/tty, flags, mode);
//path_init返回时nd->dentry即为搜索路径文件名称的起点
//link_path_walk一步步建立打开路径的各个文件夹的dentry和inode
当中inode->i_fop在mknod的init_special_inode调用中被赋值为def_chr_fops。
下面该变量的定义,因此, open(inode, f)即调用到chrdev_open。其能够看出是字符设备所相应的文件系统接口。我们姑且称其为字符设备文件系统。
conststruct file_operations def_chr_fops = {
.open = chrdev_open,
};
继续分析chrdev_open:
Kobj_lookup(cdev_map,inode->i_rdev, &idx)即是通过设备的设备号(inode->i_rdev)在cdev_map中查找设备相应的操作集file_operations.关于怎样查找。我们在理解字符设备驱动怎样注冊自己的file_operations后再回头来分析这个问题。
五、字符设备驱动的注冊
12. 字符设备相应cdev数据结构:
struct cdev {
struct kobject kobj; // 每一个 cdev 都是一个 kobject
struct module*owner; // 指向实现驱动的模块
const structfile_operations *ops; // 操纵这个字符设备文件的方法
struct list_headlist; //相应的字符设备文件的inode->i_devices 的链表头
dev_t dev; // 起始设备编号
unsigned intcount; // 设备范围号大小
};
13. led设备驱动初始化和设备驱动注冊
1) cdev_init是初始化cdev结构体。并将led_fops填入该结构。
2) cdev_add
3) cdev_map是一个全家指针变量。类型例如以下:
4) kobj_map使用hash散列表来存储cdev数据结构。通过注冊设备的主设备号major来获得cdev_map->probes数组的索引值i(i = major % 255),然后把一个类型为struct probe的节点对象增加到probes[i]所管理的链表中,probes[i]->data即是cdev数据结构。而probes[i]->dev和range代表字符设备号和范围。
六、再述open设备文件
14. 通过第五步的字符设备的注冊过程,应该对Kobj_lookup查找led_ops是非常easy理解的。至此。已经获得led设备驱动的led_ops。接着立马调用file->f_ops->open即调用了led_open,在该函数中会对led用到的GPIO进行ioremap并设置GPIO方向、上下拉等硬件初始化。
15. 最后,chrdev_open一步步返回。最后到
do_sys_open的struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);返回。
Fd_install(fd, f)即是在当前进程中将存有led_ops的file指针填入进程的file数组中,下标是fd。
最后将fd返回给用户空间。
而用户空间仅仅要传入fd就可以找到相应的file数据结构。
七、设备操作
16. 这里以设备写为例,主要是控制led的亮和灭。
write(fd,val,1)系统调用后相应sys_write,其相应全部的文件写。包含文件夹、一般文件和设备文件,一般文件有位置偏移的概念。即读写之后。当前位置会发生变化,所以如要跳着读写。就须要fseek。
对于字符设备文件,没有位置的概念。所以我们重点跟踪vfs_write的过程。
1) fget_light在当前进程中通过fd来获得file指针
2) vfs_write
3) 对于led设备,file->f_op->write即是led_write。
在该接口中实现对led设备的控制。
八、再论字符设备驱动的初始化
综上所述,字符设备的初始化包含两个主要环节:
1) 字符设备驱动的注冊,即通过cdev_add向系统注冊cdev数据结构,提供file_operations操作集和设备号等信息,终于file_operations存放在全局指针变量cdev_map指向的Hash表中,其能够通过设备号索引并遍历得到。
2) 通过device_create(设备类, 设备号, 设备名)在sys/class/设备类中创建设备属性文件并发送uevent事件。而mdev利用该信息自己主动调用mknod在/dev文件夹下创建相应的设备文件,以便应用程序訪问。
那么怎样通过通过device_create来创建设备文件呢,mdev的原理又是什么呢?我们接着分析。
九、设备类相关知识
设备类是虚拟的,并没有直接相应的物理实物,仅仅是为了更好地管理同一类设备导出到用户空间而产生的文件夹和文件。整个过程涉及到sysfs文件系统。该文件系统是为了展示linux设备驱动模型而构建的文件系统,是基于ramfs,linux根文件夹中的/sysfs即挂载了sysfs文件系统。
Struct kobject数据结构是sysfs的基础,kobject在sysfs中代表一个文件夹,而linux的驱动(struct driver)、设备(struct device)、设备类(struct class)均是从kobject进行派生的,因此他们在sysfs中都相应于一个文件夹。而数据结构中附属的struct device_attribute、driver_attribute、class_attribute等属性数据结构在sysfs中则代表一个普通的文件。
Struct kset是struct kobject的容器,即Struct kset能够成为同一类struct kobject的父亲,而其自身也有kobject成员,因此其又可能和其它kobject成为上一级kset的子成员。
十、两种创建设备文件的方式
在设备驱动中cdev_add将struct file_operations和设备号注冊到系统后,为了能够自己主动产生驱动相应的设备文件,须要调用class_create和device_create,并通过uevent机制调用mdev(嵌入式linux由busybox提供)来调用mknod创建设备文件。当然也能够不调用这两个接口,那就手工通过命令行mknod来创建设备文件。
十一、设备类和设备相关数据结构
1. include/linux/kobject.h
struct kobject {
constchar *name;//名称
structlist_head entry;//kobject链表
structkobject *parent;//即所属kset的kobject
structkset *kset;//所属kset
structkobj_type *ktype;//属性操作接口
…
};
struct kset {
struct list_head list;//管理同属于kset的kobject
struct kobject kobj;//能够成为上一级父kset的子文件夹
const struct kset_uevent_ops *uevent_ops;//uevent处理接口
};
假设Kobject A代表一个文件夹。kset B代表几个文件夹(包含A)的共同的父文件夹。
则A.kset=B; A.parent=B.kobj.
2.include/linux/device.h
struct class {//设备类
const char *name;//设备类名称
struct module *owner;//创建设备类的module
structclass_attribute *class_attrs;//设备类属性
struct device_attribute *dev_attrs;//设备属性
struct kobject *dev_kobj;//kobject再sysfs中代表一个文件夹
….
struct class_private *p;//设备类得以注冊到系统的连接件
};
3.drivers/base/base.h
struct class_private {
//该设备类相同是一个kset ,包含下面的class_devices。同一时候在class_subsys填充父kset
struct kset class_subsys;
structklist class_devices;//设备类包含的设备(kobject)
…
structclass *class;//指向设备类数据结构,即要创建的本级文件夹信息
};
4.include/linux/device.h
structdevice {//设备
structdevice *parent;//sysfs/devices/中的父设备
structdevice_private *p;//设备得以注冊到系统的连接件
structkobject kobj;//设备文件夹
constchar *init_name;//设备名称
structbus_type *bus;//设备所属总线
structdevice_driver *driver; //设备使用的驱动
structklist_node knode_class;//连接到设备类的klist
structclass *class;//所属设备类
conststruct attribute_group **groups;
…
}
5. drivers/base/base.h
struct device_private {
structklist klist_children;//连接子设备
structklist_node knode_parent;//增加到父设备链表
structklist_node knode_driver;//增加到驱动的设备链表
structklist_node knode_bus;//增加到总线的链表
structdevice *device;//相应设备结构
};
6. 解释
class_private是class的私有结构,class通过class_private注冊到系统中;device_private是device的私有结构。device通过device_private注冊到系统中。
注冊到系统中也是将相应的数据结构增加到系统已经存在的链表中,可是这些链接的细节并不希望暴露给用户,也没有必要暴露出来,所以才有private的结构。而class和device则通过sysfs向用户层提供信息。
十二、创建设备类文件夹文件
1. 在驱动通过cdev_add将struct file_operations接口集和设备注冊到系统后,即利用class_create接口来创建设备类文件夹文件。
led_class = class_create(THIS_MODULE,"led_class");
__class_create(owner, name,&__key);
cls->name = name;//设备类名
cls->owner= owner;//所属module
retval =__class_register(cls, key);
structclass_private *cp;
//将类的名字led_class赋值给相应的kset
kobject_set_name(&cp->class_subsys.kobj,"%s", cls->name);
// 填充class_subsys所属的父kset:ket:sysfs/class.
cp->class_subsys.kobj.kset= class_kset;
//填充class属性操作接口
cp->class_subsys.kobj.ktype= &class_ktype;
cp->class = cls;//通过cp能够找到class
cls->p = cp;//通过class能够找到cp
//创建led_class设备类文件夹
kset_register(&cp->class_subsys);
//在led_class文件夹创建class属性文件
add_class_attrs(class_get(cls));
2. 继续展开kset_register
kset_register(&cp->class_subsys);
kobject_add_internal(&k->kobj);
// parent即class_kset.kobj, 即/sysfs/class相应的文件夹
parent =kobject_get(kobj->parent);
create_dir(kobj);
//创建一个led _class设备类文件夹
sysfs_create_dir(kobj);
该接口是sysfs文件系统接口,代表创建一个文件夹。不再展开。
3. 上述提到的class_kset 在class_init被创建
class_kset= kset_create_and_add("class", NULL, NULL);
第三个传參为NULL,代表默认在/sysfs/创建class文件夹。
十三、创建设备文件夹和设备属性文件
1.利用class_create接口来创建设备类文件夹文件后。再利用device_create接口来创建详细设备文件夹和设备属性文件。
led_device =device_create(led_class, NULL, led_devno, NULL, "led");
device_create_vargs
dev->devt = devt;//设备号
dev->class= class;//设备类led_class
dev->parent =parent;//父设备,这里是NULL
kobject_set_name_vargs(&dev->kobj,fmt, args)//设备名”led”
device_register(dev)注冊设备
2. 继续展开device_register(dev)
device_initialize(dev);
dev->kobj.kset= devices_kset;//设备所属/sysfs/devices/
device_add(dev)
device_private_init(dev)//初始化device_private
dev_set_name(dev,"%s", dev->init_name);//赋值dev->kobject的名称
setup_parent(dev,parent);//建立device和父设备的kobject的联系
//kobject_add在/sysfs/devices/文件夹下创建设备文件夹led,kobject_add是和kset_register类似的接口。仅仅只是前者针对kobject,后者针对kset。
kobject_add(&dev->kobj,dev->kobj.parent, NULL);
kobject_add_varg
kobj->parent= parent;
kobject_add_internal(kobj)
create_dir(kobj);//创建设备文件夹
//在刚创建的/sysfs/devices/led文件夹下创建uevent属性文件,名称是”uevent”
device_create_file(dev,&uevent_attr);
//在刚创建的/sysfs/devices/led文件夹下创建dev属性文件,名称是”dev”,该属性文件的内容就是设备号
device_create_file(dev,&devt_attr);
//在/sysfs/class/led_class/文件夹下建立led设备的符号连接,所以打开/sysfs/class/led_class/led/文件夹也能看到dev属性文件。读出设备号。
device_add_class_symlinks(dev);
//创建device属性文件,包含设备所属总线的属性和attribute_group属性
device_add_attrs()
bus_add_device(dev)//将设备增加总线
//触发uevent机制。并通过调用mdev来创建设备文件。
kobject_uevent(&dev->kobj,KOBJ_ADD);
//匹配设备和总线的驱动,匹配成功就调用驱动的probe接口,不再展开
bus_probe_device(dev);
3. 展开kobject_uevent(&dev->kobj, KOBJ_ADD);
kobject_uevent_env(kobj,action, NULL);
kset= top_kobj->kset;
uevent_ops = kset->uevent_ops; //即device_uevent_ops
//subsystem即设备所属的设备类的名称”led_class”
subsystem= uevent_ops->name(kset, kobj);
//devpath即/sysfs/devices/led/
devpath= kobject_get_path(kobj, GFP_KERNEL);
//增加各种环境变量
add_uevent_var(env,"ACTION=%s", action_string);
add_uevent_var(env,"DEVPATH=%s", devpath);
add_uevent_var(env,"SUBSYSTEM=%s", subsystem);
uevent_ops->uevent(kset,kobj, env);
add_uevent_var(env,"MAJOR=%u", MAJOR(dev->devt));
add_uevent_var(env,"MINOR=%u", MINOR(dev->devt));
add_uevent_var(env,"DEVNAME=%s", name);
add_uevent_var(env,"DEVTYPE=%s", dev->type->name);
//还会增加总线相关的一些属性环境变量等等。
#ifdefined(CONFIG_NET)//假设是PC的linux会通过socket的方式向应用层发送uevent事件消息。但在嵌入式linux中不启用该机制。
#endif
argv [0] = uevent_helper;//即/sbin/mdev
argv [1] = (char *)subsystem;//”led_class”
argv [2] = NULL;
add_uevent_var(env,"HOME=/");
add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
call_usermodehelper(argv[0], argv,
env->envp, UMH_WAIT_EXEC);
4. 上述提到的devices_kset在devices_init被创建
devices_kset= kset_create_and_add("devices", &device_uevent_ops, NULL);
第三个传參为NULL,代表默认在/sysfs/创建devices文件夹
5. 上述设备属性文件
staticstruct device_attribute devt_attr =
__ATTR(dev, S_IRUGO, show_dev, NULL);
static ssize_t show_dev(struct device*dev, struct device_attribute *attr,
char *buf){{
returnprint_dev_t(buf, dev->devt); //即返回设备的设备号
}
6.devices设备文件夹响应uevent事件的操作
staticconst struct kset_uevent_ops device_uevent_ops = {
.filter = dev_uevent_filter,
.name = dev_uevent_name,
.uevent = dev_uevent,
};
7.call_usermodehelper是从内核空间调用用户空间程序的接口。
8. 对于嵌入式系统来说。busybox採用的是mdev。在系统启动脚本rcS 中会使用命令
echo /sbin/mdev >/proc/sys/kernel/hotplug
uevent_helper[]数组即读入/proc/sys/kernel/hotplug文件的内容。即 “/sbin/mdev”
十四、创建设备文件
轮到mdev出场了,以上描写叙述都是在sysfs文件系统中创建文件夹或者文件,而应用程序訪问的设备文件则须要创建在/dev/文件夹下。该项工作由mdev完毕。
Mdev的原理是解释/etc/mdev.conf文件定义的命名设备文件的规则。并在该规则下依据环境变量的要求来创建设备文件。
Mdev.conf由用户层指定。因此更具灵活性。
本文无意展开对mdev配置脚本的分析。
Busybox/util-linux/mdev.c
int mdev_main(int argc UNUSED_PARAM, char**argv)
xchdir("/dev");
if (argv[1] &&strcmp(argv[1], "-s")//系统启动时mdev –s才会运行这个分支
else
action= getenv("ACTION");
env_path= getenv("DEVPATH");
G.subsystem= getenv("SUBSYSTEM");
snprintf(temp, PATH_MAX,"/sys%s", env_path);//到/sysfs/devices/led文件夹
make_device(temp,/*delete:*/ 0);
strcpy(dev_maj_min,"/dev");
//读出dev属性文件。得到设备号
open_read_close(path,dev_maj_min + 1, 64);
….
mknod(node_name,rule->mode | type, makedev(major, minor))
终于我们会跟踪到mknod在/dev/文件夹下创建了设备文件。
我们追求:
1.从上电第一行代码、系统第一行代码、模块第一行代码、应用第一行代码,深入解说嵌入式软件生命周期。
2 深刻理解硬件体系。以面向对象思维剖析各种总线和驱动框架。
3 聚焦软件层次设计和框架设计
4 知其然。知其所以然
很多其它的嵌入式linux和android、物联网、汽车自己主动驾驶等领域原创技术分享请关注微信公众号: