the number of different uevent actions that can be emitted by the kernel these days is limited, and seems to be fixed.

from include/linux/kobject.h:

   40 /*
   41  * The actions here must match the index to the string array
   42  * in lib/kobject_uevent.c
   43  *
   44  * Do not add new actions here without checking with the driver-core
   45  * maintainers. Action strings are not meant to express subsystem
   46  * or device specific properties. In most cases you want to send a
   47  * kobject_uevent_env(kobj, KOBJ_CHANGE, env) with additional event
   48  * specific variables added to the event environment.
   49  */
   50 enum kobject_action {
   51         KOBJ_ADD,
   52         KOBJ_REMOVE,
   53         KOBJ_CHANGE,
   54         KOBJ_MOVE,
   55         KOBJ_ONLINE,
   56         KOBJ_OFFLINE,
   57         KOBJ_MAX
   58 };

devs are effectively “gently nudged” towards using KOBJ_ADD+KOBJ_ONLINE and KOBJ_OFFLINE+KOBJ_REMOVE on start and stop, respectively, of whatever they’re representing by their kobject-s, with KOBJ_CHANGE acting as the wildcard/void* of uevents. whatever else you want to tell userspace - you’re pretty much stuck with KOBJ_CHANGE.

using just one action for all the interesting events might seem a little limiting at first. but there are two ways in which you can “enrich” the “context” of the events you’re emitting:

  1. make sure that by the time you’re emitting the event you set some kobject attr of interest to the new value, and let udev rule match the subtype of the uevent based on the value of that attr:

    SYSFS{attribute}=="some_value"
    

    this is simpler implementation-wise (less code to write in kernel, trivial to set up in a udev rule), but has the disadvantage of being slightly slower and not atomic. i.e. if the kobject attr might be changing reasonably fast, by the time udev will get to match the contents of the corresponding sysfs file to the udev rule, the attr might have changed to, e.g., whatever your next uevent wanted to communicate to userspace. on the upside, it’s easier to debug - sysfs files are there for userspace scripts to peek at, whereas uevent env is ephemeral, if your udev rule was wrong - you just missed the uevent.

  2. when emitting uevent-s, you can add your own KEY=val to the env. while the performance aspect of choosing this is usually frowned upon, the atomicity argument in favor of this approach is perfectly valid and rather important. the tradeoff is pretty much the opposite of the above - slightly more to write on the kernel side, atomicity, and the env is ephemeral. the latter obstacle can be partially overcome at dev time by making udevadm spill the beans on incoming uevent-s to stdout:

    udevadm monitor --kernel --property
    

    you can also add --subsystem-match= and --tag-match= to narrow down what you want to see.

since the former approach is rather self explanatory - just maintain kobject attrs neatly prior to emitting uevent-s, the rest of the text deals with the latter approach: directly enriching uevent env.

a simple example comes dock/undock notifications, which just emit the events with the specified env in one go. instead of the plain vanilla kobject_uevent(), dock is using the underlying kobject_uevent_env().

from lib/kobject_uevent.c:

  321 /**
  322  * kobject_uevent - notify userspace by sending an uevent
  323  *
  324  * @action: action that is happening
  325  * @kobj: struct kobject that the action is happening to
  326  *
  327  * Returns 0 if kobject_uevent() is completed with success or the
  328  * corresponding error when it fails.
  329  */
  330 int kobject_uevent(struct kobject *kobj, enum kobject_action action)
  331 {
  332         return kobject_uevent_env(kobj, action, NULL);
  333 }
  334 EXPORT_SYMBOL_GPL(kobject_uevent);

  120 /**
  121  * kobject_uevent_env - send an uevent with environmental data
  122  *
  123  * @action: action that is happening
  124  * @kobj: struct kobject that the action is happening to
  125  * @envp_ext: pointer to environmental data
  126  *
  127  * Returns 0 if kobject_uevent_env() is completed with success or the
  128  * corresponding error when it fails.
  129  */
  130 int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
  131                        char *envp_ext[])

from drivers/acpi/dock.c:

  392 static void dock_event(struct dock_station *ds, u32 event, int num)
  393 {
  394         struct device *dev = &ds->dock_device->dev;
  395         char event_string[13];
  396         char *envp[] = { event_string, NULL };
  397         struct dock_dependent_device *dd;
  398
  399         if (num == UNDOCK_EVENT)
  400                 sprintf(event_string, "EVENT=undock");
  401         else
  402                 sprintf(event_string, "EVENT=dock");
  403
  404         /*
  405          * Indicate that the status of the dock station has
  406          * changed.
  407          */
  408         if (num == DOCK_EVENT)
  409                 kobject_uevent_env(&dev->kobj, KOBJ_CHANGE, envp);
  410
  411         list_for_each_entry(dd, &ds->hotplug_devices, hotplug_list)
  412                 if (dd->ops && dd->ops->uevent)
  413                         dd->ops->uevent(dd->handle, event, dd->context);
  414
  415         if (num != DOCK_EVENT)
  416                 kobject_uevent_env(&dev->kobj, KOBJ_CHANGE, envp);
  417 }

a little more complex example comes from GFS2 that installs custom struct kset_uevent_ops to enrich the env of every event it emits. see the overview at:

Documentation/filesystems/gfs2-uevents.txt

and impl, including some handy inspiration of start/stop events as well:

fs/gfs2/sys.c

from Documentation/kobject.txt:

    If a kset wishes to control the uevent operations of the kobjects
    associated with it, it can use the struct kset_uevent_ops to handle it:

    struct kset_uevent_ops {
            int (*filter)(struct kset *kset, struct kobject *kobj);
            const char *(*name)(struct kset *kset, struct kobject *kobj);
            int (*uevent)(struct kset *kset, struct kobject *kobj,
                          struct kobj_uevent_env *env);
    };
        [...]
    The name function will be called to override the default name of the kset
    that the uevent sends to userspace.  By default, the name will be the same
    as the kset itself, but this function, if present, can override that name.

    The uevent function will be called when the uevent is about to be sent to
    userspace to allow more environment variables to be added to the uevent.

and it’s the uevent() that GFS2 overrides to add several common KEY=val entries to the env of all the events it emits.

from fs/gfs2/sys.c:

  630 static int gfs2_uevent(struct kset *kset, struct kobject *kobj,
  631                        struct kobj_uevent_env *env)
  632 {
  633         struct gfs2_sbd *sdp = container_of(kobj, struct gfs2_sbd, sd_kobj);
  634         struct super_block *s = sdp->sd_vfs;
  635         const u8 *uuid = s->s_uuid;
  636
  637         add_uevent_var(env, "LOCKTABLE=%s", sdp->sd_table_name);
  638         add_uevent_var(env, "LOCKPROTO=%s", sdp->sd_proto_name);
  639         if (!test_bit(SDF_NOJOURNALID, &sdp->sd_flags))
  640                 add_uevent_var(env, "JOURNALID=%d", sdp->sd_lockstruct.ls_jid);
  641         if (gfs2_uuid_valid(uuid))
  642                 add_uevent_var(env, "UUID=%pUB", uuid);
  643         return 0;
  644 }
  645
  646 static const struct kset_uevent_ops gfs2_uevent_ops = {
  647         .uevent = gfs2_uevent,
  648 };
  649
  650 int gfs2_sys_init(void)
  651 {
  652         gfs2_kset = kset_create_and_add("gfs2", &gfs2_uevent_ops, fs_kobj);
  653         if (!gfs2_kset)
  654                 return -ENOMEM;
  655         return 0;
  656 }
  657
  658 void gfs2_sys_uninit(void)
  659 {
  660         kset_unregister(gfs2_kset);
  661 }