详解linux内核的uevent机制

描述

一、kobject_uevent简介

在linux内核中,uevent机制是一种内核和用户空间通信的机制,用于通知用户空间应用程序各种硬件更改或其他事件,比如插入或移除硬件设备(如USB驱动器或网络接口)。uevent表示“用户空间事件”,当硬件事件发生时,内核会生成一个 uevent,并通过 netlink 套接字将其发送到用户空间。用户空间应用程序(例如 udev、mdev),可以监听这些事件并采取相应的操作,例如加载适当的驱动程序或执行其他配置任务。

kobject_uevent()函数可通过发送一个uevent通知用户空间:

 

int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
 return kobject_uevent_env(kobj, action, NULL);
}

 

从上述代码可知,kobject_uevent()调用kobject_uevent_env()实现核心操作,但是kobject_uevent()没有传输环境变量,故而设置kobject_uevent_env()的第三个参数为NULL。

enum kobject_action用于描述内核对象的操作,内核中定义了以下几种操作:

 

enum kobject_action {
 KOBJ_ADD,  //对象添加。表示向系统中添加了一个新的kobject对象。
 KOBJ_REMOVE, //对象移除。表示从系统中移除了一个kobject对象。
 KOBJ_CHANGE, //对象修改。表示kobject对象的属性或状态发生了变化。
 KOBJ_MOVE,  //对象移动。表示kobject对象被移动到了另一个位置。
 KOBJ_ONLINE, //对象上线。表示kobject对象已经准备好在线工作。
 KOBJ_OFFLINE, //对象离线。表示kobject对象已经离线,不再处于工作状态。
 KOBJ_MAX  //动作类型的最大值,用于边界检查。
};

 

在用户空间可使用udevadm monitor查看uevent事件:

 

udevadm monitor --udev

 

二、重要数据结构

1、struct kobj_uevent_env

struct kobj_uevent_env结构的目的是在内核中传递事件相关的参数、环境变量和数据。通过使用这个结构体,内核可以轻松地传递事件相关的信息和数据给相关的处理程序或模块。

 

struct kobj_uevent_env {
 char *argv[3];      //用于存储传递给事件的参数。通常情况下,用于表示事件的命令行参数。
 char *envp[UEVENT_NUM_ENVP];  //这是一个包含 UEVENT_NUM_ENVP 个指针的数组,用于存储传递给事件的环境变量。在Linux内核中,环境变量通常以键值对的形式传递。
 int envp_idx;      //用于跟踪环境变量数组中的当前索引。它指示下一个环境变量应该存储在数组的哪个位置。
 char buf[UEVENT_BUFFER_SIZE];  //用于存储事件的文本数据。在内核中,事件通常以文本形式表示。
 int buflen;  //用于跟踪事件数据缓冲区中的当前有效数据长度。它指示缓冲区中包含的事件数据量。
};

 

2、struct kset_uevent_ops

struct kset_uevent_ops结构体定义了 kset 的事件操作接口,使得用户可以通过提供相应的函数指针来自定义 kset 中 kobject 的事件处理行为:

 

struct kset_uevent_ops {
 int (* const filter)(struct kset *kset, struct kobject *kobj);
 const char *(* const name)(struct kset *kset, struct kobject *kobj);
 int (* const uevent)(struct kset *kset, struct kobject *kobj,
        struct kobj_uevent_env *env);
};

 

int (_ const filter)(struct kset _kset, struct kobject *kobj);:这是一个指向函数的指针,该函数用于过滤 kset 中的 kobject(内核对象)。它接受两个参数,分别是指向 kset 和 kobject 的指针,返回一个整数值,通常表示过滤操作的结果。

const char ( const name)(struct kset _kset, struct kobject _kobj);:这是一个指向函数的指针,该函数用于获取 kobject 的名称。它接受两个参数,分别是指向 kset 和 kobject 的指针,返回一个指向字符常量的指针,通常是 kobject 的名称字符串。

int (_ const uevent)(struct kset _kset, struct kobject _kobj, struct kobj_uevent_env _env);:这是一个指向函数的指针,该函数用于生成 kobject 的事件。它接受三个参数,分别是指向 kset、kobject 和 kobj_uevent_env 结构体的指针。该函数负责将 kobject 的事件信息填充到给定的环境中,并返回一个整数值,通常表示操作的成功或失败。

三、kobject_uevent_env()详细剖析

kobject_uevent_env()用于在给定的 kobject 上触发一个事件,并且传递一个额外的环境变量数组:

 

int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[])

 

struct kobject *kobj:表示要触发事件的内核对象。

enum kobject_action action:表示要执行的动作,通常是一个枚举类型,定义了可能的动作列表。这可能包括添加、删除或修改对象等操作。

char *envp_ext[]:是一个字符指针数组,用于传递额外的环境变量给事件处理程序。这些环境变量以 key=value 的形式表示,其中 key 是环境变量的名称,value 是其对应的值。

该函数将会触发一个事件,并将指定的动作和环境变量信息传递给与该对象相关联的事件处理程序。事件处理程序通常会根据传入的动作和环境变量来执行相应的操作,例如更新内核状态、通知用户空间进程等。kobject_uevent_env()实现如下(具体执行步骤见注释):

 

int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
         char *envp_ext[])
{
 struct kobj_uevent_env *env;
 const char *action_string = kobject_actions[action];
 const char *devpath = NULL;
 const char *subsystem;
 struct kobject *top_kobj;
 struct kset *kset;
 const struct kset_uevent_ops *uevent_ops;
 int i = 0;
 int retval = 0;

 /*
  * Mark "remove" event done regardless of result, for some subsystems
  * do not want to re-trigger "remove" event via automatic cleanup.
  */
    /如果动作是 KOBJ_REMOVE,则将对象的 state_remove_uevent_sent 标志设置为1,表示“remove”事件已发送。
 if (action == KOBJ_REMOVE)
  kobj->state_remove_uevent_sent = 1;
    
    //打印调试信息,包括对象的名称、指针和函数名称。
 pr_debug("kobject: '%s' (%p): %s
",
   kobject_name(kobj), kobj, __func__);

 //找该对象所属的 kset。
 top_kobj = kobj;
 while (!top_kobj->kset && top_kobj->parent)
  top_kobj = top_kobj->parent;
    
    //如果对象不属于任何 kset,则返回错误。
 if (!top_kobj->kset) {
  pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "
    "without kset!
", kobject_name(kobj), kobj,
    __func__);
  return -EINVAL;
 }

 kset = top_kobj->kset;
 uevent_ops = kset->uevent_ops;

 //检查对象的 uevent_suppress 标志是否设置,如果设置了则跳过事件发送。
 if (kobj->uevent_suppress) {
  pr_debug("kobject: '%s' (%p): %s: uevent_suppress "
     "caused the event to drop!
",
     kobject_name(kobj), kobj, __func__);
  return 0;
 }
    
 //如果 uevent_ops 和 uevent_ops->filter 存在且 filter 函数返回0,则跳过事件发送。
 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!
",
     kobject_name(kobj), kobj, __func__);
   return 0;
  }

 //获取事件的子系统名称。
 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!
", kobject_name(kobj), kobj,
    __func__);
  return 0;
 }

 //分配并初始化一个 kobj_uevent_env 结构体
 env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
 if (!env)
  return -ENOMEM;

 //获取对象的路径。
 devpath = kobject_get_path(kobj, GFP_KERNEL);
 if (!devpath) {
  retval = -ENOENT;
  goto exit;
 }

 //添加默认的环境变量,包括动作(ACTION)、设备路径(DEVPATH)和子系统(SUBSYSTEM)。
 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;

 //如果有传入的额外环境变量,则将它们添加到事件环境中
 if (envp_ext) {
  for (i = 0; envp_ext[i]; i++) {
   retval = add_uevent_var(env, "%s", envp_ext[i]);
   if (retval)
    goto exit;
  }
 }

 //调用 uevent_ops->uevent 函数,如果存在,以允许 kset 特定的操作添加额外的环境变量。
 if (uevent_ops && uevent_ops->uevent) {
  retval = uevent_ops->uevent(kset, kobj, env);
  if (retval) {
   pr_debug("kobject: '%s' (%p): %s: uevent() returned "
     "%d
", kobject_name(kobj), kobj,
     __func__, retval);
   goto exit;
  }
 }
  
    //根据动作执行额外的操作。如果是 KOBJ_ADD,则标记对象已发送 state_add_uevent_sent 为1。
 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;
 }
    
    
 mutex_lock(&uevent_sock_mutex);
 /* we will send an event, so request a new sequence number */
    //获取一个新的序列号并将其添加到事件环境中
 retval = add_uevent_var(env, "SEQNUM=%llu", ++uevent_seqnum);
 if (retval) {
  mutex_unlock(&uevent_sock_mutex);
  goto exit;
 }
    
    //通过网络广播发送事件。
 retval = kobject_uevent_net_broadcast(kobj, env, action_string,
           devpath);
 mutex_unlock(&uevent_sock_mutex);
    
    //如果配置了CONFIG_UEVENT_HELPER,则调用 uevent_helper 来执行额外的操作。
#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

exit:
 kfree(devpath);
 kfree(env);
 return retval;
}

 

总而言之,kobject_uevent_env()调用add_uevent_var()构建默认的信息(包括:ACTION、DEVPATH、SUBSYSTEM和自定义数据envp_ext),然后调用kobject_uevent_net_broadcast()发送uevent。

1、add_uevent_var()

add_uevent_var()是一个用于在内核中构建uevent环境的辅助函数,它允许向 uevent 环境中添加一个键值对,表示内核对象事件中的一个属性或信息。该函数实现如下:

 

int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...)
{
 va_list args;
 int len;

 if (env->envp_idx >= ARRAY_SIZE(env->envp)) {
  WARN(1, KERN_ERR "add_uevent_var: too many keys
");
  return -ENOMEM;
 }

 va_start(args, format);
 len = vsnprintf(&env->buf[env->buflen],
   sizeof(env->buf) - env->buflen,
   format, args);
 va_end(args);

 if (len >= (sizeof(env->buf) - env->buflen)) {
  WARN(1, KERN_ERR "add_uevent_var: buffer size too small
");
  return -ENOMEM;
 }

 env->envp[env->envp_idx++] = &env->buf[env->buflen];
 env->buflen += len + 1;
 return 0;
}

 

add_uevent_var()函数的作用是根据指定的格式将键值对添加到 uevent 环境中,该函数使用vsnprintf()函数将格式化字符串和参数组合成一个字符串,并将该字符串添加到 kobj_uevent_env 结构体的缓冲区中,如果成功添加键值对,则返回0;如果缓冲区空间不足导致添加失败,则返回-ENOSPC。

使用add_uevent_var()函数可以方便地构建uevent环境,向用户空间发送有关内核对象事件的信息。通常情况下,该函数用于在kobject_uevent_env()函数中构建uevent环境,以向用户空间发送内核对象事件。

2、kobject_uevent_net_broadcast()

`kobject_uevent_net_broadcast()用于在网络命名空间上广播内核对象事件:

 

int kobject_uevent_net_broadcast(struct kobject *kobj,
     struct kobj_uevent_env *env,
     const char *action_string,
     const char *devpath)

 

(1)首先,它检查是否启用了网络支持 (CONFIG_NET),因为这个功能是基于网络命名空间的。

(2)它获取与给定 kobject 相关联的命名空间类型操作 (ops),以确定是否存在网络命名空间。

(3)如果 ops 存在,且命名空间类型是网络命名空间 (KOBJ_NS_TYPE_NET),则获取该命名空间。

(4)如果存在网络命名空间,它调用 uevent_net_broadcast_tagged 函数,将事件广播到指定的网络命名空间。

(5)如果没有找到网络命名空间,或者网络命名空间不支持事件广播,它调用 uevent_net_broadcast_untagged 函数,将事件广播到所有未标记的网络命名空间。

(6)最后,它返回广播函数的返回值,指示事件广播的成功或失败。总的来说,这个函数负责根据对象的网络命名空间属性,将内核对象事件广播到适当的网络命名空间中。uevent_net_broadcast_tagged()和uevent_net_broadcast_untagged()本质上都调用netlink_broadcast()实现核心功能。

3、call_usermodehelper_setup()

call_usermodehelper_setup 函数是 Linux 内核中的一个函数,用于设置和准备调用用户空间辅助程序(usermode helper)。这个函数并不直接执行用户空间辅助程序,而是为其设置参数和环境,并返回一个 subprocess_info 结构体,用于后续的执行。

这个函数通常在内核中的一些子系统中使用,例如 kobject_uevent_net_broadcast 中的调用。函数原型如下:

 

struct subprocess_info *call_usermodehelper_setup(char *path, char **argv,
                                                  char **envp, gfp_t gfp_mask,
                                                  int (*init)(struct subprocess_info *info,
                                                              struct cred *new),
                                                  void (*cleanup)(struct subprocess_info *info),
                                                  void *data);

 

path:用户空间辅助程序的路径。

argv:参数数组,用于传递给用户空间辅助程序的命令行参数。

envp:环境变量数组,用于传递给用户空间辅助程序的环境变量。

gfp_mask:内存分配标志。

init:一个可选的初始化函数,用于在用户空间辅助程序执行前进行一些初始化操作。

cleanup:一个可选的清理函数,用于在用户空间辅助程序执行完毕后进行清理操作。

data:可选的附加数据,可以在初始化和清理函数中使用。

这个函数的作用是为用户空间辅助程序设置参数和环境,以及提供初始化和清理函数。返回的 subprocess_info 结构体包含了执行用户空间辅助程序所需的所有信息,包括路径、参数、环境等。一旦设置完成,用户空间辅助程序就可以通过call_usermodehelper_exec 函数执行。

4、call_usermodehelper_exec()

call_usermodehelper_exec 函数是 Linux 内核中与执行用户空间辅助程序(usermode helper)相关的一个函数。它负责实际执行用户空间辅助程序,并监控其执行状态。通常情况下,它会在 call_usermodehelper_setup 函数之后被调用。

下面是call_usermodehelper_exec 函数原型:

 

int call_usermodehelper_exec(struct subprocess_info *sub_info, enum umh_wait wait);

 

sub_info:指向 subprocess_info 结构体的指针,该结构体包含了执行用户空间辅助程序所需的所有信息,包括路径、参数、环境等。

wait:指定是否等待用户空间辅助程序执行完成的标志,可以是 UMH_WAIT_EXEC(等待执行完成)或 UMH_NO_WAIT(不等待执行完成)。

call_usermodehelper_exec 函数的作用是执行用户空间辅助程序,并等待其执行完成(如果需要)。在执行期间,它会监控用户空间辅助程序的执行状态,并在适当的时候返回执行结果。

这个函数通常在 Linux 内核中的一些子系统中使用,例如 kobject_uevent_net_broadcast 中的调用。通过调用用户空间辅助程序,内核可以执行一些需要借助用户空间程序完成的任务,例如配置、初始化等。

四、uevent_helper机制

在 Linux 内核中,uevent_helper 是一个用户空间辅助程序,用于处理内核对象事件(uevent)。它通常在内核启动期间使用,负责处理设备管理相关的事件,例如设备的插入、拔出、状态变化等。下面是一些使用uevent_helper的典型实践:

设备热插拔管理: 当一个设备被插入或拔出时,内核会生成相应的 uevent,并调用 uevent_helper 来处理这些事件。uevent_helper 可以根据事件类型执行一些特定的操作,例如加载适当的设备驱动、更新设备管理信息等。

自动配置和初始化: 在系统启动期间,内核可能需要执行一些自动配置和初始化任务,例如挂载文件系统、加载网络配置、启动服务等。uevent_helper 可以被用于执行这些任务,并根据事件类型执行相应的初始化操作。

系统监控和管理: uevent_helper 还可以被用于系统监控和管理任务,例如记录事件日志、生成警报、执行故障排除操作等。

用户空间通知: uevent_helper 还可以与用户空间的其他程序进行通信,例如向用户空间的监控程序发送通知或触发相应的操作。

具体使用 uevent_helper 的方式取决于系统的需求和设计。通常情况下,它会被配置为一个可执行文件或脚本,并在系统启动时由内核调用。在使用uevent_helper时,需要确保其具有足够的权限来执行所需的操作,并确保其安全性和稳定性。

五、mdev

mdev是Linux系统中用于设备管理的工具,具有以下特征:

mdev 是一个更轻量级的设备管理器,通常用于嵌入式系统和一些轻量级 Linux 发行版中。

由 BusyBox 提供,并用于在启动时自动创建设备节点。

mdev 不支持复杂的规则配置,而是基于简单的设备名匹配规则来创建设备节点。

所以综上所述,mdev 更适用于嵌入式系统或者对资源有限的系统,因为它更加轻量级,但功能也更加有限。

参考busybox源码,mdev实现如下:

 

int mdev_main(int argc UNUSED_PARAM, char **argv)
{
 enum {
  MDEV_OPT_SCAN       = 1 << 0,
  MDEV_OPT_SYSLOG     = 1 << 1,
  MDEV_OPT_DAEMON     = 1 << 2,
  MDEV_OPT_FOREGROUND = 1 << 3,
 };
 int opt;
 RESERVE_CONFIG_BUFFER(temp, PATH_MAX + SCRATCH_SIZE);

 INIT_G();

 /* We can be called as hotplug helper */
 /* Kernel cannot provide suitable stdio fds for us, do it ourself */
 bb_sanitize_stdio();

 /* Force the configuration file settings exactly */
 umask(0);

 xchdir("/dev");

 opt = getopt32(argv, "^"
  "sS" IF_FEATURE_MDEV_DAEMON("df") "v"
  "�"
  "vv",
  &G.verbose);

#if ENABLE_FEATURE_MDEV_CONF
 G.filename = "/etc/mdev.conf";
 if (opt & (MDEV_OPT_SCAN|MDEV_OPT_DAEMON)) {
  /* Same as xrealloc_vector(NULL, 4, 0): */
  G.rule_vec = xzalloc((1 << 4) * sizeof(*G.rule_vec));
 }
#endif

 if (opt & MDEV_OPT_SYSLOG) {
  openlog(applet_name, LOG_PID, LOG_DAEMON);
  logmode |= LOGMODE_SYSLOG;
 }

#if ENABLE_FEATURE_MDEV_DAEMON
 if (opt & MDEV_OPT_DAEMON) {
  /* Daemon mode listening on uevent netlink socket. Fork away
   * after initial scan so that caller can be sure everything
   * is up-to-date when mdev process returns.
   */
  int fd = daemon_init(temp);

  if (!(opt & MDEV_OPT_FOREGROUND)) {
   /* there is no point in logging to /dev/null */
   logmode &= ~LOGMODE_STDIO;
   bb_daemonize_or_rexec(0, argv);
  }

  daemon_loop(temp, fd);
 }
#endif
 if (opt & MDEV_OPT_SCAN) {
  /*
   * Scan: mdev -s
   */
  //初始化扫描
  initial_scan(temp);
 } else {
  //处理action
  process_action(temp, getpid());

  dbg1("%s exiting", curtime());
 }

 if (ENABLE_FEATURE_CLEAN_UP)
  RELEASE_CONFIG_BUFFER(temp);

 return EXIT_SUCCESS;
}

 

上述代码中,具体实现步骤如下:

使用位掩码枚举定义了几个选项(MDEV_OPT_SCAN、MDEV_OPT_SYSLOG、MDEV_OPT_DAEMON、MDEV_OPT_FOREGROUND)。

初始化一些配置变量并设置环境(INIT_G()、bb_sanitize_stdio()、umask(0)、xchdir("/dev"))。

解析了命令行参数,根据参数的不同执行不同的操作,比如扫描设备、启用系统日志、以守护进程模式运行等。

根据程序运行的不同情况,执行相应的操作,比如进行初始扫描、处理动作等。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分