linux驱动编程--设备模型1


最近学习设备模型的运行机制,进过书上和网上资料的训练,貌似已经修改出了自己的网络权值,所以写了下来并整理一下自己的思路。

之前的驱动程序由于硬件信息和逻辑操作是写在一起的,所以一个驱动只能适应一种平台。为了提高驱动程序的可移植性,就引出了设备模型。

那么现在面临的问题是:设备模型是怎么工作的?

一.理论

这就要谈到两个重要的结构体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"时就会发现自己在在应用层设置的程序或脚本被调用了(记得修改程序或脚本的可执行权限)。

相关内容