Linux 内核通用通知机制(watch_queue)深度解析:基于管道的内核事件订阅与投递
2026/9/15 18:39:58 网站建设 项目流程

Linux 内核通用通知机制(watch_queue)深度解析:基于管道的内核事件订阅与投递

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

通用通知机制(general notification mechanism)是 Linux 内核提供的一套"内核事件 → 用户态"的异步通知设施,它以标准管道(pipe)为基础,由内核把通知消息直接"拼接"进用户态打开的特殊管道环形缓冲中,用户程序只需用 read() 即可消费事件。本文以 Documentation/core-api/watch_queue.rst 为主线,结合 kernel/watch_queue.c、include/uapi/linux/watch_queue.h 与密钥子系统源码,系统讲解该机制的架构、数据结构、内核 API、事件过滤与用户态编程方法,读完即可上手编写自己的内核通知消费者,并理解其底层实现原理。

概述:以管道为背板的零拷贝通知通道

该设施在形态上表现为一种以特殊模式打开(O_NOTIFICATION_PIPE)的管道,管道的内部环形缓冲被内核用来存放由内核自身生成的消息,用户态通过 read() 逐条取出。由于这类管道中写入的是内核通知消息,splice 及类似的接口在此类管道上被禁用——它们在某些情况下需要"回退"(revert)已追加到环形缓冲中的数据,而这可能与已交错写入的通知消息发生冲突,破坏消息流的一致性。

管道的属主(owner)必须显式告知内核希望监视哪些"源"(source)。只有已连接到该管道的源才会向其中插入消息,并且一个源可以同时绑定到多个管道,将消息同时投递给所有订阅它的队列。

在投递语义上,内核遵循"尽力而为"原则:

  • 如果环形缓冲没有空闲槽位,或没有预分配(preallocated)的消息缓冲可用,消息会被直接丢弃,内核不会等待;
  • 发生丢弃后,read() 会在当前缓冲中最后一条消息被读走之后,向输出缓冲插入一条WATCH_META_LOSS_NOTIFICATION(数据丢失)元消息,告知用户态有事件被丢弃;
  • 内核在产生通知时不等待消费者取走消息,而是立即继续执行。这意味着通知可以在持有自旋锁(spinlock)的上下文中产生,也保证内核不会因用户态故障而被无限期阻塞。

这一设计(消息预分配 + 非阻塞投递)在源码中得到印证:内核为每个通知槽预分配了独立页面(WATCH_QUEUE_NOTE_SIZE为 128 字节,见 kernel/watch_queue.c),投递时通过post_one_notification()在持有管道读等待队列锁的情况下把消息拷贝进预分配槽并推进pipe->head,全程不经过用户态调度。

该功能由内核配置项控制:

"General setup" -> "General notification queue" (CONFIG_WATCH_QUEUE)

从源码看,该机制对应的用户态头文件为 include/uapi/linux/watch_queue.h,内核侧头文件为 include/linux/watch_queue.h。

消息结构:struct watch_notification及其字段语义

所有通知消息都以一个短头(header)开始,该头按 64 位对齐,以便子类结构可以包含__u64字段:

struct watch_notification { __u32 type:24; __u32 subtype:8; __u32 info; };

各字段语义如下:

  • type(24 位):表示通知记录的来源(源类型),具体取值见 include/uapi/linux/watch_queue.h:
    • WATCH_TYPE_META = 0:特殊记录,由 watch queue 自身内部生成;
    • WATCH_TYPE_KEY_NOTIFY = 1:密钥/密钥环变更事件通知。
  • subtype(8 位):表示该来源下的记录子类型(可被过滤)。对 META 类型,有两个子类型:
    • WATCH_META_REMOVAL_NOTIFICATION:被监视对象被移除或销毁;
    • WATCH_META_LOSS_NOTIFICATION:发生了消息丢失。
  • info(32 位):承载多项信息,通过掩码与移位提取(宏定义见 include/uapi/linux/watch_queue.h):
    • WATCH_INFO_LENGTH0x0000007f,移位WATCH_INFO_LENGTH__SHIFT = 0):消息总长度(含头部),记录大小在8 到 127 字节之间;
    • WATCH_INFO_ID0x0000ff00,移位WATCH_INFO_ID__SHIFT = 8):watch ID,即调用者设定的标识,取值0~255。多个 watch 可共享同一个队列,该字段用于区分它们;
    • WATCH_INFO_TYPE_INFO0xffff0000,移位WATCH_INFO_TYPE_INFO__SHIFT = 16):类型特有字段,由通知产生方设定,表达与类型/子类型相关的特定含义;其中还细分出 8 个标志位WATCH_INFO_FLAG_0~WATCH_INFO_FLAG_7,可供产生方按位使用。

除 length 之外,info 中的其余字段都可以用于过滤(见下文"事件过滤")。

头部之后可以跟随补充信息,其格式由具体类型和子类型自行定义。内核在 include/uapi/linux/watch_queue.h 中给出了两个现成的扩展结构:

  • struct watch_notification_removal:扩展的移除通知,头部之后追加__u64 id(类型相关的对象标识),可通过长度(是否等于watch_sizeof(n))区分,用于WATCH_TYPE_META/WATCH_META_REMOVAL_NOTIFICATION
  • struct key_notification:密钥变更通知记录,头部之后追加__u32 key_id(受影响的 key/keyring)与__u32 aux(按子类型变化的辅助数据),对应WATCH_TYPE_KEY_NOTIFY

内核侧核心数据结构

在深入各 API 之前,先看内核内部维护的三类对象(定义于 include/linux/watch_queue.h):

struct watch_queue(通知输出缓冲):由管道承载的缓冲对象,字段包括:

  • filter:指向struct watch_filter的 RCU 指针,保存用户态设置的过滤规则;
  • pipe:实现缓冲的管道(队列关闭时为 NULL);
  • watches:贡献性 watch 的哈希链表头;
  • notes/notes_bitmap:预分配通知槽的页面数组与分配位图;
  • nr_notes/nr_pages:槽数量与页数量;
  • usage:kref 引用计数。

struct watch(订阅):对某个 watch list 的订阅,表示"把通知写到哪个队列":

  • info_id:8 位 ID(由用户态提供)左移WATCH_INFO_ID__SHIFT后的值,通知写入队列时被 OR 进infoWATCH_INFO_ID字段;
  • queue:投递事件的队列;
  • watch_list:所属的 watch list;
  • cred:watch 属主的凭据,用于 LSM 检查;
  • private:与 watch_list 关联的驱动私有数据,由watch_list::release_watch()清理;
  • id:源的标识。投递时 ID 不同的通知会被忽略(例如密钥的序列号)。

struct watch_list(通知源):订阅某个通知源的 watcher 列表:

  • watchers:watcher 链表;
  • release_watch:watch_list 销毁时回调,用于释放 watch_list 持有的被监视对象的引用;
  • lock:保护列表的自旋锁。

struct watch_filter:过滤规则集,含一个类型位图type_filterstruct watch_type_filter filters[]数组。

Watch List(通知源)API

"watch list" 是订阅了某个通知源的 watcher 列表。它可以附着在某个对象上(如一个 key 或一个 superblock),也可以是全局的(如设备事件)。从用户态视角看,非全局 watch list 通常通过引用其所属对象来指代——例如使用KEYCTL_NOTIFY并传入密钥序列号来监视某个具体密钥。

内核提供两个管理函数:

void init_watch_list(struct watch_list *wlist, void (*release_watch)(struct watch *wlist));

初始化一个 watch list。若release_watch不为 NULL,则表示当 watch_list 对象被销毁时应调用该函数,以丢弃 watch list 对被监视对象持有的任何引用。其实现(include/linux/watch_queue.h)为初始化 watchers 链表头、初始化自旋锁并保存回调。

void remove_watch_list(struct watch_list *wlist, u64 id);

移除订阅到该 watch_list 上的所有 watch 并释放它们,然后销毁 watch_list 对象本身。注意当前仓库中该函数带一个id参数,内部通过remove_watch_from_object(wlist, NULL, id, true)批量移除(见 include/linux/watch_queue.h)。

Watch Queue(通知输出缓冲)API

"watch queue" 是应用分配的、通知记录将被写入其中的缓冲。其全部内部运作都隐藏在管道设备驱动中,但要设置一个 watch,必须获得它的引用。相关 API:

struct watch_queue *get_watch_queue(int fd);

watch queue 通过实现缓冲的管道的 fd 向内核标识,因此用户态必须把该 fd 通过系统调用传给内核。此函数从 fd 解析出管道的struct pipe_inode_info,若其带有watch_queue则递增引用计数并返回不透明指针(见 kernel/watch_queue.c)。

void put_watch_queue(struct watch_queue *wqueue);

丢弃由get_watch_queue()获得的引用;引用计数归零时通过__put_watch_queue()释放预分配页面、位图与过滤器,最后 RCU 释放对象本身(kernel/watch_queue.c)。

Watch 订阅 API

"watch" 是 watch list 上的一个订阅,指明通知记录应写入的 watch queue(缓冲)。watch 对象中部分字段可由驱动(内核子模块)设置:

struct watch { union { u32 info_id; /* ID to be OR'd in to info field */ ... }; void *private; /* Private data for the watched object */ u64 id; /* Internal identifier */ ... };
  • info_id应是来自用户态的 8 位数字并左移WATCH_INFO_ID__SHIFT,通知写入关联队列缓冲时被 OR 进WATCH_INFO_ID字段;
  • private是与 watch_list 关联的驱动数据,由watch_list::release_watch()清理;
  • id是源的 ID,投递时 ID 不匹配的通知会被忽略

管理 watch 的函数:

void init_watch(struct watch *watch, struct watch_queue *wqueue);

初始化 watch 对象并设置其队列指针,使用适当的屏障避免 lockdep 报错(实现见 kernel/watch_queue.c,内部完成 kref 初始化、链表节点初始化与rcu_assign_pointer)。

int add_watch_to_object(struct watch *watch, struct watch_list *wlist);

将 watch 订阅到 watch list(通知源)上。调用前必须设置好 watch 结构中由驱动设置的字段。其实现(kernel/watch_queue.c)会:

  • 检查同队列 + 同 ID 的 watch 是否已存在(存在返回-EBUSY);
  • 检查用户 watch 总数是否超过RLIMIT_NOFILE(超过返回-EAGAIN),并持有当前凭据引用;
  • 递增队列与 watch 的 kref,分别挂入wqueue->watcheswlist->watchers
int remove_watch_from_object(struct watch_list *wlist, struct watch_queue *wqueue, u64 id, false);

从 watch list 中移除一个 watch,要求该 watch 与指定的 watch queue(wqueue)和对象标识(id)匹配。移除后向 watch queue 发送WATCH_META_REMOVAL_NOTIFICATION通知,告知 watch 已被移除(必要时附带watch_notification_removal.id扩展字段)。

int remove_watch_from_object(struct watch_list *wlist, NULL, 0, true);

移除 watch list 上的所有 watch。预期在销毁前调用,且此时 watch list 对新 watch 已不可达。同样会向每个被订阅 watch 的队列发送WATCH_META_REMOVAL_NOTIFICATION。实现中all == true时会循环处理直至列表清空,并调用wlist->release_watch()释放驱动私有数据(kernel/watch_queue.c)。

通知投递 API 与底层路径

向 watch list 投递通知使订阅的 watch 可见该事件,核心函数为:

void post_watch_notification(struct watch_list *wlist, struct watch_notification *n, const struct cred *cred, u64 id);
  • 通知应预先格式化,传入头部指针n。通知可能大于头部,大小(以缓冲槽为单位)记录在n->info & WATCH_INFO_LENGTH中;
  • cred表示源(主体)的凭据,会传给 LSM(如 SELinux),根据每个队列(客体)的凭据允许或抑制记录;
  • id是源对象的 ID(如密钥上的序列号),只有设置了相同 ID 的 watch 才会看到该通知。

该函数是内联包装(include/linux/watch_queue.h),实际实现为__post_watch_notification()(kernel/watch_queue.c),完整调用链如下:

  1. 校验WATCH_INFO_LENGTH非零,否则WARN_ON(1)并返回;
  2. 在 RCU 读锁保护下遍历wlist->watchers
  3. ID 匹配watch->id != id的 watch 直接跳过;
  4. n->infoWATCH_INFO_ID字段替换为watch->info_id(即把调用者设定的 8 位 ID OR 进每条消息);
  5. 过滤:若队列带过滤器且filter_watch_notification()判定不通过,则跳过;
  6. LSM 检查security_post_notification(watch->cred, cred, n) < 0则跳过;
  7. 加锁后调用post_one_notification()实际写入。

post_one_notification()(kernel/watch_queue.c)是真正的"入队"动作:在管道读等待队列锁下检查环形缓冲是否满(满则标记PIPE_BUF_FLAG_LOSS并丢弃)、从notes_bitmap找到空闲预分配槽、把消息memcpy进对应页面偏移、构造struct pipe_buffer(页面、偏移、长度、专属watch_queue_pipe_buf_ops),用smp_store_release推进pipe->head以便与pipe_read()同步,最后唤醒读端(EPOLLIN | EPOLLRDNORM)并发送SIGIO信号。页面的释放回调会把槽位重新标记回位图,实现槽的循环复用。

事件源:密钥/密钥环通知(WATCH_TYPE_KEY_NOTIFY)

任一缓冲都可以从多个源接收消息。当前仓库中已接入的源为:

  • WATCH_TYPE_KEY_NOTIFY:密钥与密钥环变更通知,包括密钥环内容变化或密钥属性变化。详见 Documentation/security/keys/core.rst。

该源由密钥子系统实现。密钥变化时,security/keys/internal.h 中的notify_key()内联函数构造struct key_notificationtype = WATCH_TYPE_KEY_NOTIFYinfo = watch_sizeof(n),携带key_idaux)并调用post_watch_notification(key->watchers, ...)投递。各子类型定义于 include/uapi/linux/watch_queue.h:

子类型含义
NOTIFY_KEY_INSTANTIATED密钥被实例化(aux 为错误码)
NOTIFY_KEY_UPDATED密钥被更新
NOTIFY_KEY_LINKED密钥(aux 中给出)被加入被监视的密钥环
NOTIFY_KEY_UNLINKED密钥(aux 中给出)被移出被监视的密钥环
NOTIFY_KEY_CLEARED密钥环被清空
NOTIFY_KEY_REVOKED密钥被吊销
NOTIFY_KEY_INVALIDATED密钥失效
NOTIFY_KEY_SETATTR密钥属性被修改

这些子类型对应的触发点散布于密钥子系统:例如 security/keys/key.c 中实例化、更新、吊销、失效路径,security/keys/keyring.c 中KEYCTL_LINK/UNLINK路径(如notify_key(keyring, NOTIFY_KEY_LINKED, key_serial(key))),以及 security/keys/keyctl.c 中的KEYCTL_SETATTR路径。

用户态通过KEYCTL_WATCH_KEY控制监视(需CONFIG_KEY_NOTIFICATIONS,见 security/keys/Kconfig)。其实现keyctl_watch_key()(security/keys/keyctl.c)的流程为:

  1. watch_id须在-10xff之间,否则返回-EINVAL
  2. KEY_NEED_VIEW权限查找目标 key(即监视密钥需要 View 权限);
  3. 通过get_watch_queue(watch_queue_fd)取得队列引用;
  4. watch_id >= 0:分配并初始化 watch(watch->id = key->serialwatch->info_id = watch_id << WATCH_INFO_ID__SHIFT),经security_watch_key()安全检查后在key->sem写锁下add_watch_to_object()
  5. watch_id == -1:取消监视,调用remove_watch_from_object()移除匹配队列与序列号的 watch。

事件过滤:IOC_WATCH_QUEUE_SET_FILTER

创建 watch queue 后,可以设置一组过滤器来限制接收的事件:

struct watch_notification_filter filter = { ... }; ioctl(fd, IOC_WATCH_QUEUE_SET_FILTER, &filter)

过滤器描述结构为:

struct watch_notification_filter { __u32 nr_filters; __u32 __reserved; struct watch_notification_type_filter filters[]; };
  • nr_filtersfilters[]中过滤器的个数;
  • __reserved:必须为 0。

filters数组元素类型:

struct watch_notification_type_filter { __u32 type; __u32 info_filter; __u32 info_mask; __u32 subtype_filter[8]; };

各字段规则:

  • type:要过滤的事件类型,例如WATCH_TYPE_KEY_NOTIFY
  • info_filter 与 info_mask:对通知记录 info 字段的过滤,仅当满足下式时才写入缓冲:
(watch.info & info_mask) == info_filter

例如,可以利用它忽略挂载树中不在精确监视点上的事件(当同一类型事件携带类型特有位置信息时);

  • subtype_filter:表示感兴趣子类型的位掩码,subtype_filter[0]的 bit 0 对应子类型 0、bit 1 对应子类型 1,以此类推。

若 ioctl 参数为 NULL,则移除过滤器,被监视源的所有事件都将通过。

内核侧实现watch_queue_set_filter()(kernel/watch_queue.c)的约束值得注意:

  • nr_filters必须为 1~16,__reserved必须为 0,否则返回-EINVAL
  • 校验info_filter的位不能超出info_masktf[i].info_filter & ~tf[i].info_mask非零则拒绝),且info_mask不得包含WATCH_INFO_LENGTH
  • 未知类型(type >= WATCH_TYPE__NR)会被静默忽略(不计入内部过滤器);
  • 参数为 NULL 时置wfilter = NULL,随后经pipe_lockrcu_replace_pointer原子替换队列过滤器,旧过滤器用kfree_rcu延迟释放。

实际过滤判定在filter_watch_notification()(kernel/watch_queue.c):先看类型位图type_filter,再遍历过滤器条目,命中"类型匹配 + 子类型位命中 + info 位匹配"即放行;一旦设置了过滤器,默认行为是拒绝未匹配的事件

用户态完整示例:监视密钥环变更

创建缓冲:以特殊标志创建管道并设定缓冲大小:

pipe2(fds, O_TMPFILE); ioctl(fds[1], IOC_WATCH_QUEUE_SET_SIZE, 256);

需要说明的是:O_NOTIFICATION_PIPE在 UAPI 头中定义为O_EXCL(见 include/uapi/linux/watch_queue.h),实际开发时应使用pipe2(fds, O_NOTIFICATION_PIPE)以表达语义。IOC_WATCH_QUEUE_SET_SIZE的参数是以 128 字节通知槽为单位的个数;内核实现(watch_queue_set_size(),kernel/watch_queue.c)要求nr_notes在 1~512 之间,按每页 32 个槽(WATCH_QUEUE_NOTES_PER_PAGE)向上取整分配页面、通过account_pipe_buffers()计入用户管道缓冲配额、以pipe_resize_ring()调整环形缓冲大小(向上取 2 的幂),并将配额同步到pipe->max_usage/pipe->nr_accounted。注意若队列已分配过槽(wqueue->notes非空),再次调用会返回-EBUSY

订阅密钥环变更通知

keyctl(KEYCTL_WATCH_KEY, KEY_SPEC_SESSION_KEYRING, fds[1], 0x01);

第四个参数0x01即调用者自定义的 8 位 watch ID,将出现在每条通知的WATCH_INFO_ID字段中,用于区分同一队列上的多个 watch。

消费通知:参考原文档给出的消费者循环(为可编译起见,示例中的n->应为n.n.,即取联合体中的struct watch_notification成员):

static void consumer(int rfd, struct watch_queue_buffer *buf) { unsigned char buffer[128]; ssize_t buf_len; while (buf_len = read(rfd, buffer, sizeof(buffer)), buf_len > 0 ) { void *p = buffer; void *end = buffer + buf_len; while (p < end) { union { struct watch_notification n; unsigned char buf1[128]; } n; size_t largest, len; largest = end - p; if (largest > 128) largest = 128; memcpy(&n, p, largest); len = (n.n.info & WATCH_INFO_LENGTH) >> WATCH_INFO_LENGTH__SHIFT; if (len == 0 || len > largest) return; switch (n.n.type) { case WATCH_TYPE_META: got_meta(&n.n); break; case WATCH_TYPE_KEY_NOTIFY: saw_key_change(&n.n); break; } p += len; } } }

该循环的关键点:一次 read() 可能返回多条消息,因此需要用WATCH_INFO_LENGTH逐条解析;消息长度字段是解析边界(8~127 字节);WATCH_TYPE_META分支需处理移除(WATCH_META_REMOVAL_NOTIFICATION)与丢包(WATCH_META_LOSS_NOTIFICATION)两类元事件;WATCH_TYPE_KEY_NOTIFY分支中可将struct watch_notification强转为struct key_notification读取key_idaux字段。

安全模型与资源限制

  • LSM 集成:投递路径在写入前调用security_post_notification(watch->cred, cred, n),以"队列属主凭据(客体)× 事件源凭据(主体)"的二元关系交给 SELinux 等 LSM 裁决,实现按队列粒度的权限控制;
  • 资源配额:每个用户可建立的 watch 总数受RLIMIT_NOFILE约束(add_one_watch()atomic_inc_return(&cred->user->nr_watches) > task_rlimit(current, RLIMIT_NOFILE)时返回-EAGAIN);
  • 消息丢弃语义:环形缓冲满或预分配槽耗尽时消息静默丢弃,并最终以WATCH_META_LOSS_NOTIFICATION告知消费者;内核因此在持锁上下文中也可安全投递,不会因用户态缓慢而被拖住;
  • 关闭清理:管道释放时watch_queue_clear()(kernel/watch_queue.c)置空wqueue->pipe阻止新通知,并逐一解绑队列上的 watch、回调release_watch()释放驱动数据。

结语

通用通知机制把"管道"这一最基础、最成熟的用户态 IPC 设施改造为内核事件分发通道:预分配的 128 字节消息槽 + 非阻塞投递保证持锁安全,WATCH_INFO_ID支持一队列多 watch 复用,IOC_WATCH_QUEUE_SET_FILTER提供类型/子类型/info 三层过滤,而cred贯穿的 LSM 检查让通知具备与内核其他对象一致的强制访问控制能力。当前仓库以密钥/密钥环通知(WATCH_TYPE_KEY_NOTIFY)为首个生产级事件源,内核 API 已抽象为通用的 watch list / watch queue / watch 三元模型(include/linux/watch_queue.h),后续内核模块只需实现init_watch_list+post_watch_notification即可接入新的通知源。相关实现与接口文件:内核实现 kernel/watch_queue.c、UAPI 定义 include/uapi/linux/watch_queue.h、密钥源实现 security/keys/keyctl.c 与 security/keys/internal.h。

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询