瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。
【公众号】迅为电子
【粉丝群】824412014(加群获取驱动文档+例程)
【视频观看】嵌入式学习之Linux驱动(第十期_热插拔_全新升级)_基于RK3568
【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板
在前一章节中,我们成功地通过uevent机制将内核事件发送到用户空间。然而,本章节我们尝试在实验中去掉了创建kset的步骤,并发现用户空间无法收到事件。在分析这个问题之前,让我们回顾一下前一章节中的实验。
在前一章节的代码示例中,我们演示了如何在内核中创建kset和kobject,并将其添加到sysfs文件系统中。这样做的目的是为了在sysfs中创建相应的目录结构,以便用户空间能够找到正确的sysfs路径来监听事件或访问设备属性。然而,当我们去掉了创建kset的步骤后,用户空间无法收到事件。这是因为没有正确的sysfs路径供用户空间监听事件。
上个章节的驱动代码修改如下图(图 114-1)所示:
图 114-1
编译之后,加载驱动程序,如下图(图 114-2)所示:
图 114-2
为了解决这个问题,我们需要在代码层面确保正确创建kset和kobject,并将其添加到sysfs文件系统中。这样,用户空间才能够通过sysfs路径找到相应的事件或属性文件。在接下来的部分,我们将通过代码分析来解释为什么没有创建kset会导致用户空间无法接收事件。
在上一小节的实验中,为什么没有创建kset会导致用户空间无法接收事件呢?要搞清楚这个问题,我们可以追踪下kobject_uevent函数。如下所示:
/**
* kobject_uevent - notify userspace by sending an uevent
*
* @kobj: struct kobject that the action is happening to
* @action: action that is happening
*
* Returns 0 if kobject_uevent() is completed with success or the
* corresponding error when it fails.
*/
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}
EXPORT_SYMBOL_GPL(kobject_uevent);
在上述代码中,可以看出kobject_uevent()函数是用于通知用户空间的,它通过发送uevent来实现。该函数接受俩个参数:kobject表示发生操作的struct kobject对象,action表示正在发生的操作。在这个函数中调用了kobject_uevent_env()函数,并将其返回值作为自己的返回值。这说明kobject_uevent()函数实际上是一个简化的接口,它将kobject_uevent_env()函数的调用参数设置为NULL,并返回相同的结果。
kobject_uevent_env函数代码比较多,接下来我们拆分为几个部分给大家讲解下。
第一部分,如下所示:
/**
* kobject_uevent_env - send an uevent with environmental data //发送带有环境变量数据的事件
*
* @kobj: struct kobject that the action is happening to //正在发生动作的对象
* @action: action that is happening //正在发生的动作
* @envp_ext: pointer to environmental data //环境变量的指针
*
* Returns 0 if kobject_uevent_env() is completed with success or the
* corresponding error when it fails.
*/
第二部分如下所示:
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
char *envp_ext[])
{
struct kobj_uevent_env *env; //指向kobj_uevent_env结构体的指针,用于存储发送的事件和环境变量
const char *action_string = kobject_actions[action]; //事件的类型
const char *devpath = NULL;//存放kobject的路径
const char *subsystem; //存放所属子系统的名称
struct kobject *top_kobj; //指向顶层top_kobj的kobject指针
struct kset *kset; //指向kset的指针,表示kobject所属的kset
const struct kset_uevent_ops *uevent_ops; //指向struct kset_uevent_ops结构体的指针
int i = 0; //计数器i,用来编译环境变量数组
int retval = 0 //表示函数的执行结果,也就是返回值
第三部分如下所示:
if (action == KOBJ_REMOVE)
kobj->state_remove_uevent_sent = 1;
pr_debug("kobject: '%s' (%p): %s\n",
kobject_name(kobj), kobj, __func__);
/* search the kset we belong to */
top_kobj = kobj;
while (!top_kobj->kset && top_kobj->parent)
top_kobj = top_kobj->parent;
if (!top_kobj->kset) {
pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "
"without kset!\n", kobject_name(kobj), kobj,
__func__);
return -EINVAL;
}
kset = top_kobj->kset;
uevent_ops = kset->uevent_ops;
以上代码中,首先检查action是否为KOBJ_REMOVE,如果是,则设置 kobj->state_remove_uevent_sent 为1,表示“remove”事件已发送。
接下来通过调用pr_debug()函数输出调试信息,显示相关的kobject名称和指针以及当前函数名称。
然后,通过循环查找kobj所属的kset,直到找到具体有效kset的顶层kobj,即kset的根节点。这是通过沿着kobj的父对象链向上遍历实现的,如果找不到有效的kset,则输出调试信息,并返回-EINVAL表示发送uevent失败。
这个过程的目的是为了确定当前kobj所属的kset,以便后面的uevent发送能够正确地通知用户空间的应用程序。因为uevent是通过netlink socket发送给用户空间的应用程序的,而netlink socket是基于kset的。
最后,将找到的kset赋值给变量kset,并将其uevent_ops字段赋值给变量uevent_ops,以便后续使用。
第四部分如下所示:
/* skip the event, if uevent_suppress is set*/
if (kobj->uevent_suppress) {
pr_debug("kobject: '%s' (%p): %s: uevent_suppress "
"caused the event to drop!\n",
kobject_name(kobj), kobj, __func__);
return 0;
}
在上面的代码中,首先检查kobj->uevent_suppress是否为1,如果设置kobj->uevent_suppress,则输出调试信息表示该事件被跳过,并返回0表示成功。
第五部分如下所示:
/* skip the event, if uevent_suppress is set*/
if (kobj->uevent_suppress) {
pr_debug("kobject: '%s' (%p): %s: uevent_suppress "
"caused the event to drop!\n",
kobject_name(kobj), kobj, __func__);
return 0;
}
/* 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;
}
/* originating subsystem */
if (uevent_ops && uevent_ops->name)
subsystem = uevent_ops->name(kset, kobj);
else
subsystem = kobject_name(&kset->kobj);
if (!subsystem) {
pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the "
"event to drop!\n", kobject_name(kobj), kobj,
__func__);
return 0;
}
在上面的代码中,检查uevent_ops是否存在,并且uevent_ops->filter是否存在。如果两者都存在,并且调用uevent_ops->filter(kset, kobj),返回值为0,则表示过滤器函数导致该事件被跳过。输出相应的调试信息,并返回0表示成功。
然后,根据uevent_ops中的name字段获取原始子系统的名称。如果uevent_ops->name存在,则调用uevent_ops->name(kset, kobj)函数获取子系统名称,否则使用kset的名称作为子系统名称。如果无法获取子系统名称,输出调试信息,并返回0表示成功。
综上所述,这部分代码主要用于检查是否应该跳过发送uevent的条件,如uevent_suppress标志、过滤器函数的返回结果以及获取原始子系统名称。如果满足这些条件,则该事件将被跳过,并返回0表示成功。
第六部分,如下所示:
if (IS_ENABLED(CONFIG_ARCH_ROCKCHIP) &&
IS_ENABLED(CONFIG_FREEZER) &&
IS_ENABLED(CONFIG_ANDROID)) {
/*
* Android healthd try to listen power_supply subsystem uevent,
* but which will block system from suspend on big.LITTLE system
* because thermal_cooling_device_unregister will called when
* cpufreq_exit. So ignore this uevent when suspend.
*/
extern bool pm_freezing;
if (pm_freezing && !strcmp(subsystem, "thermal"))
return 0;
}
上面的代码是一个条件判断,用于特定条件下跳过发送uevent的操作,它首先使用IS_ENABLED()宏进行编译时配置的条件判断,检查是否启用了CONFIG_ARCH_ROCKCHIP、CONFIG_FREEZER和CONFIG_ANDROID配置选项。如果这三个选项都启用了,那么代码将进入条件块部分。内部的注释解释了为什么需要跳过发送uevent的操作。在big.LITTLE系统上,当cpufreq_exit函数被调用时,thermal_cooling_device_unregister函数也会被调用,这可能会导致系统挂起失败,为了避免这种情况,该函数忽略了thermal子系统的uevent事件。
代码中使用了一个外部变量pm_freezing,如果pm_freezing为真(即系统正在进入挂起状态),并且子系统名称与"thermal"相同时,将返回0表示跳过发送uevent的操作。
第七部分,如下所示:
/* environment buffer */
env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
if (!env)
return -ENOMEM;
/* complete object path */
devpath = kobject_get_path(kobj, GFP_KERNEL);
if (!devpath) {
retval = -ENOENT;
goto exit;
}
/* default keys */
retval = add_uevent_var(env, "ACTION=%s", action_string);
if (retval)
goto exit;
retval = add_uevent_var(env, "DEVPATH=%s", devpath);
if (retval)
goto exit;
retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
if (retval)
goto exit;
/* keys passed in from the caller */
if (envp_ext) {
for (i = 0; envp_ext[i]; i++) {
retval = add_uevent_var(env, "%s", envp_ext[i]);
if (retval)
goto exit;
}
}
/* let the kset specific function add its stuff */
if (uevent_ops && uevent_ops->uevent) {
retval = uevent_ops->uevent(kset, kobj, env);
if (retval) {
pr_debug("kobject: '%s' (%p): %s: uevent() returned "
"%d\n", kobject_name(kobj), kobj,
__func__, retval);
goto exit;
}
}
上面代码中,首先,它分配了一个 kobj_uevent_env 结构体的内存空间,并检查是否成功分配。然后,它获取了 kobj 对象所在的路径,并检查是否成功获取。接下来,它添加了一些默认的键值对到环境变量中,包括 ACTION、DEVPATH 和 SUBSYSTEM。最后,如果有额外的键值对传入,则将其也添加到环境变量中。循环的终止条件是 envp_ext[i] 为空指针。
接下来它首先检查uevent_ops和uevent_ops->uevent是否存在。如果存在,则调用uevent_ops->uevent函数,并传递kset、kobj和env作为参数。如果uevent_ops->uevent返回非零值(表示出错),则会打印一条带有错误信息的调试消息,然后跳转到标签exit。
第八部分,如下所示:
switch (action) {
case KOBJ_ADD:
/*
* Mark "add" event so we can make sure we deliver "remove"
* event to userspace during automatic cleanup. If
* the object did send an "add" event, "remove" will
* automatically generated by the core, if not already done
* by the caller.
*/
kobj->state_add_uevent_sent = 1;
break;
case KOBJ_UNBIND:
zap_modalias_env(env);
break;
default:
break;
}
在上面的代码中,当action为KOBJ_ADD时,将kobject对象的state_add_uevent_sent成员设置为1,表示已经发送了“add”事件,以便在自动清理期间将“remove”事件传递给用户空间。如果该对象已经发送了“add”事件,内核会自动发送“remove”事件,否则需要调用者手动发送“remove”事件。
当action为KOBJ_UNBIND时,调用 zap_modalias_env函数清除环境变量中MODALIAS变量。MODALIAS是一个特殊的环境变量,用于描述设备的模块别名。在设备驱动程序加载时,内核会根据MODALIAS环境变量自动加载相应的驱动程序模块。在设备卸载时,需要清除MODALIAS环境变量,以便下次重新加载设备驱动程序时能够正确地识别设备。
第九部分,如下所示:
retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)++uevent_seqnum);
if (retval) {
mutex_unlock(&uevent_sock_mutex);
goto exit;
}
retval = kobject_uevent_net_broadcast(kobj, env, action_string,
devpath);
上述代码将一个名为 "SEQNUM" 的环境变量添加到 uevent 环境变量列表中,并将其值设置为 uevent_seqnum 的值加 1。其中,add_uevent_var 是一个内部函数,用于将一个键值对添加到 uevent 环境变量列表中。如果添加失败,函数将返回一个非零值,同时会释放 uevent_sock_mutex 互斥锁并跳转到 exit 标签处进行清理操作。这个函数的主要作用是为 uevent 事件添加一个唯一的序列号,以便在处理 uevent 事件时可以识别它们的顺序。通俗的话讲就是每次发送一个事件,都要有它的事件号,该事件号不能重复,也会被加到环境变量里面。
kobject_uevent_net_broadcast 是一个内核函数,用于将一个 uevent 事件发送到系统中所有的网络命名空间中。它的参数包括 kobj,env,action_string 和 devpath。其中,kobj 是与 uevent 事件相关的内核对象,env 是一个包含 uevent 环境变量的列表,action_string 是一个字符串,表示 uevent 事件的类型,devpath 是一个字符串,表示与 uevent 事件相关的设备路径。该函数会遍历系统中所有的网络命名空间,并将 uevent 事件发送到每个网络命名空间中。这个函数的主要作用是在内核中广播一个 uevent 事件,以便用户空间的应用程序可以接收并处理这些事件。
第十部分,如下所示:
#ifdef CONFIG_UEVENT_HELPER
/* call uevent_helper, usually only enabled during early boot */
if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
struct subprocess_info *info;
retval = add_uevent_var(env, "HOME=/");
if (retval)
goto exit;
retval = add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
if (retval)
goto exit;
retval = init_uevent_argv(env, subsystem);
if (retval)
goto exit;
retval = -ENOMEM;
info = call_usermodehelper_setup(env->argv[0], env->argv,
env->envp, GFP_KERNEL,
NULL, cleanup_uevent_env, env);
if (info) {
retval = call_usermodehelper_exec(info, UMH_NO_WAIT);
env = NULL; /* freed by cleanup_uevent_env */
}
}
#endif
在内核中调用用户空间的 uevent_helper 程序来处理 uevent 事件。uevent_helper 是一个用户空间程序,它可以在内核空间生成 uevent 事件时被调用。如果 CONFIG_UEVENT_HELPER 宏被定义,那么内核会在生成 uevent 事件时调用 uevent_helper 程序,以便在用户空间中处理这些事件。在上述代码中,如果 uevent_helper 变量不为空且 kobj_usermode_filter 函数返回 false,那么就会调用 call_usermodehelper_setup 函数来启动一个用户空间进程,并将 env 中的参数传递给该进程。在这个过程中,env 中的参数将会被转换成环境变量,并被传递给用户空间进程。
以上通过分析kobject_uevent_env()函数,了解到此函数用于在内核中生成并发送uevent事件到用户空间。如果没有创建kset,会导致用户空间无法接收事件,可以详细见代码分析的第三部分。