最近学习设备模型的运行机制,进过书上和网上资料的训练,貌似已经修改出了自己的网络权值,所以写了下来并整理一下自己的思路。
之前的驱动程序由于硬件信息和逻辑操作是写在一起的,所以一个驱动只能适应一种平台。为了提高驱动程序的可移植性,就引出了设备模型。
那么现在面临的问题是:设备模型是怎么工作的?
一.理论
这就要谈到两个重要的结构体kobject和kset。
1.1 kobject
kobject是设备对象的基础结构体。很多的kobject对象连接在一起构成了一个分级的拓扑结构,相当于这栋建筑的钢架,负责各个对象间的连接工作。
struct kobject; struct kobject { const char *name; // struct list_head entry; //内核链表的入口,通过container_of() struct kobject *parent; //父对象,用于构建kobject对象的层级关系 struct kset *kset; //kobject对象所属的kset集合,同类型的对象会被加入同一个集合中 struct kobj_type *ktype; //属性文件及其操作函数句柄 struct sysfs_dirent *sd; //??目录结构 struct kref kref; //对象引用计数,用于计算生命周期 unsigned int state_initialized:1; //是否初始化 unsigned int state_in_sysfs:1; //是否出现在文件树中 unsigned int state_add_uevent_sent:1; // unsigned int state_remove_uevent_sent:1; unsigned int uevent_suppress:1; //是否发送通知事件 };
对应的操作函数如下
kobject_set_name(struct kobject * kobj,const char * fmt,...); kobject_init(struct kobject * kobj,struct kobj_type * ktype); kobject_add(struct kobject * kobj,struct kobject * parent,const char * fmt,...);//1).保证kobject的层次关系。2).在sysfs中建立对应目录 kobject_del(struct kobject * kobj);
其中的kobject_add()函数会将新的kobject对象添加到这栋建筑的对应层级中,且还会在sysfs中建立对应的目录。关于属性文件,它存在的意义是为用户提供了一种与驱动模型交互的方式。在之前的驱动模型中,应用层与驱动层的交互是依靠设备文件来完成,典型交互过程就是:
打开设备文件 --> 向设备文件读写操作 --> 关闭设备文件
对设备进行读写的时候,会将数据传输到内核层的驱动处理函数处。有了属性文件后就可以有另外一种交互方式:
cat /sys/(设备模型的对应目录)/(对应的属性文件)
echo ‘xx‘ > /sys/(设备模型的对应目录)/(对应的属性文件)
对属性文件的读写最终会调用到"对应的"kobject对象的ktype成员下的操作函数(show或store)。这样就通过对属性文件的操作实现了与内核中kobject对象的交流。(在下面的例子中会有演示)
1.2 kset
kset是一个容器放有所有同类型的kobject对象,相当于这栋建筑的一个楼层。
struct kset { struct list_head list; //同类型的kobject链表 spinlock_t list_lock; // struct kobject kobj; //本身所属的kobject对象 struct kset_uevent_ops *uevent_ops; //通知事件的操作函数 }; struct kset_uevent_ops { //在kobject_uevent()中先后调用函数1和函数3 int (*filter)(struct kset *kset, struct kobject *kobj); //函数1 const char *(*name)(struct kset *kset, struct kobject *kobj); int (*uevent)(struct kset *kset, struct kobject *kobj, //函数3 struct kobj_uevent_env *env); };
在有些时候我们需要将一个kobject对象的变化通知到应用层(比如热插拔事件,然后在应用层会查找并加载相应驱动程序),这时就需要调用kobject_uevent()函数。
该函数会找到该kobject对象所属的kset集合。然后分别调用uevent_ops成员下的filter函数(),和uevent函数()。最终会调用call_usermodehelper()。在call_usermodehelper会根据指定的路径将一个用户空间的程序带进内核空间执行,从而完成事件的通知。函数主干如下(注意英语注释)
kobject_uevent(struct kobject * kobj,enum kobject_action action); { ...... /* search the kset we belong to */ top_kobj = kobj; while (!top_kobj->kset && top_kobj->parent) top_kobj = top_kobj->parent; ...... kset = top_kobj->kset; uevent_ops = kset->uevent_ops; /* skip the event, if the filter returns zero. */ if (uevent_ops && uevent_ops->filter) if (!uevent_ops->filter(kset, kobj)) { pr_debug("kobject: ‘%s‘ (%p): %s: filter function " "caused the event to drop!\n", kobject_name(kobj), kobj, __func__); return 0; } ...... /* let the kset specific function add its stuff */ if (uevent_ops && uevent_ops->uevent) { retval = uevent_ops->uevent(kset, kobj, env);//完成kset对象的私人事件 if (retval) { pr_debug("kobject: ‘%s‘ (%p): %s: uevent() returned " "%d\n", kobject_name(kobj), kobj, __func__, retval); goto exit; } } ...... ...... /* 调用用户空间的程序*/ argv [0] = uevent_helper; //这里指定了应用层程序的路径 retval = call_usermodehelper(argv[0], argv, env->envp, UMH_WAIT_EXEC); ...... }
kobject_uevent与上层的交流实际就是在内核为应用层的程序建立一个线程。而这个应用程序由 uevent_helper 变量指定。那么怎么修改这个内核变量呢,这就涉及到另外一个点。
在内核运行的过程中会有一些全局变量,这些全局变量的确定着内核的运行方式。在linux内核编写时为了给用户层留出他们的接口,就将这些变量以及内核信息虚拟成了一个文件放在"/proc/sys/kernel/" 目录下。当然也有可能在 "/sys/kernel/" 目录下,他们在这个功能方面有一些重复。关于proc目录的具体信息可以 "man proc" 来查看。
现在继续回来讨论 uevent_helper变量的修改,经过查找在 "/sys/kernel" 下发现了uevent_helper,又在 "/proc/sys/kernel/" 下发现了hotplug。对这两个的修改都能修改到内核中的uevent_helper变量。
二. 例子
现在制作一个具体的测试程序来检验一下。
#include <linux/module.h> #include <linux/kernel.h> #include <linux/kobject.h> #include <linux/sysfs.h> #include <linux/slab.h> #define NAME_PARENT "dem_parent" //parent 对象 #define NAME_CHILD "dem_child" //child 对象 #define NAME_SET "dem_set" //child 对象所属的kset集合 #define NAME_CHATTR "child_attr" //child 对象的属性文件 static struct kobject *parent; static struct kobject *child; static struct kset *c_kset; static int flag = 0; static ssize_t attr_show(struct kobject *kobj, struct attribute *attr,char *buf) { ssize_t size = 0; size = sprintf( buf, "%d\n", flag); return size; } static ssize_t attr_store(struct kobject *kobj,struct attribute *attr,const char *buf, size_t len) { //printk( "kobject: %x, kchild: %x\n", kobj, child); int old_flag = flag; flag = buf[0]-‘0‘; //将从属性文件传下来的信息通知会应用层,进而验证热插拔 switch( flag) { case 0: kobject_uevent( kobj, KOBJ_ADD); break; case 1: kobject_uevent( kobj, KOBJ_REMOVE); break; case 2: kobject_uevent( kobj, KOBJ_CHANGE); break; case 3: kobject_uevent( kobj, KOBJ_MOVE); break; case 4: kobject_uevent( kobj, KOBJ_ONLINE); break; case 5: kobject_uevent( kobj, KOBJ_OFFLINE); break; default : break; } return old_flag; } //child对象的属性文件 static struct attribute kchild_attr[] = { { .name = NAME_CHATTR, .mode = S_IRUGO|S_IWUGO, } }; //child对象属性文件的操作函数 static struct sysfs_ops kchild_ops = { .show = attr_show, .store = attr_store, }; static struct kobj_type kchild_type = { .sysfs_ops = &kchild_ops, }; static int __init demo_init( void) { printk("load vision: %s\n", __TIME__); //创建一个kobject对象作为child对象的父对象 parent = kobject_create_and_add( NAME_PARENT, NULL); if( NULL==parent ) { printk("error: %s, %d\n", __FILE__, __LINE__); goto ERR_KPARENT; } //如果child对象要进行事件通知,就必须属于一个kset集合 c_kset = kset_create_and_add( NAME_SET, NULL, parent); if( NULL==c_kset) { printk("error: %s, %d\n", __FILE__, __LINE__); goto ERR_KSET; } child = kzalloc(sizeof(*child), GFP_KERNEL); if (!child) { printk("error: %s, %d\n", __FILE__, __LINE__); goto ERR_KCHILD; } int retval; child->kset = c_kset; retval = kobject_init_and_add( child, &kchild_type, parent, NAME_CHILD); if (retval) { printk("error: %s, %d\n", __FILE__, __LINE__); goto ERR_CHILDADD; } //创建child对象对应的属性文件 retval = sysfs_create_file( child, &kchild_attr); OUT: return retval; ERR_CHILDADD: kobject_put(child); child = NULL; ERR_KCHILD: kset_unregister( c_kset); c_kset = NULL; ERR_KSET: kobject_del( parent); parent = NULL; ERR_KPARENT: return -1; } static void __exit demo_exit( void) { printk("unload vision: %s\n", __TIME__); kobject_del( child); kset_unregister( c_kset); kobject_del( parent); } MODULE_LICENSE("GPL"); module_init(demo_init); module_exit(demo_exit);
程序思路按照一般的驱动加载思路执行。最终这个程序会在 /sys/目录下建立 dem_parent文件夹,再在其下建立 dem_child 和dem_set两个文件夹。并建立了属性文件/sys/dem_parent/dem_child/child_attr。当我们执行"echo ‘1‘ > /sys/dem_parent/dem_child/child_attr" 时,消息会通知到内核的child对象处,并调用store处理函数。这就完成了应用层到内核层的沟通。
而内核层到用户层的沟通,通过kobject_uevent()来实现。在store函数中,我们已经调用了该函数。其会去指定路径下找到相应的程序或脚本文件,并在内核空间中构建进程。这样就完成了内核到用户空间的交流。经过查找发现了应用层程序的路径由uevent_helper变量指定,去/sys/kernel目录下果然找到了一个文件uevent_helper,这应该就是留给应用层的接口。现在输入:"echo ‘/sbin/XXX‘ > /sys/kernel/uevent_helper",当我们再次执行"echo ‘1‘ > /sys/dem_parent/dem_child/child_attr"时就会发现自己在在应用层设置的程序或脚本被调用了(记得修改程序或脚本的可执行权限)。