深度解析:基于管道的内核事件订阅与投递)
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; };各字段语义如下type24 位表示通知记录的来源源类型具体取值见 include/uapi/linux/watch_queue.hWATCH_TYPE_META 0特殊记录由 watch queue 自身内部生成WATCH_TYPE_KEY_NOTIFY 1密钥/密钥环变更事件通知。subtype8 位表示该来源下的记录子类型可被过滤。对 META 类型有两个子类型WATCH_META_REMOVAL_NOTIFICATION被监视对象被移除或销毁WATCH_META_LOSS_NOTIFICATION发生了消息丢失。info32 位承载多项信息通过掩码与移位提取宏定义见 include/uapi/linux/watch_queue.hWATCH_INFO_LENGTH0x0000007f移位WATCH_INFO_LENGTH__SHIFT 0消息总长度含头部记录大小在8 到 127 字节之间WATCH_INFO_ID0x0000ff00移位WATCH_INFO_ID__SHIFT 8watch 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_NOTIFICATIONstruct key_notification密钥变更通知记录头部之后追加__u32 key_id受影响的 key/keyring与__u32 aux按子类型变化的辅助数据对应WATCH_TYPE_KEY_NOTIFY。内核侧核心数据结构在深入各 API 之前先看内核内部维护的三类对象定义于 include/linux/watch_queue.hstruct watch_queue通知输出缓冲由管道承载的缓冲对象字段包括filter指向struct watch_filter的 RCU 指针保存用户态设置的过滤规则pipe实现缓冲的管道队列关闭时为 NULLwatches贡献性 watch 的哈希链表头notes/notes_bitmap预分配通知槽的页面数组与分配位图nr_notes/nr_pages槽数量与页数量usagekref 引用计数。struct watch订阅对某个 watch list 的订阅表示把通知写到哪个队列info_id8 位 ID由用户态提供左移WATCH_INFO_ID__SHIFT后的值通知写入队列时被 OR 进info的WATCH_INFO_ID字段queue投递事件的队列watch_list所属的 watch listcredwatch 属主的凭据用于 LSM 检查private与 watch_list 关联的驱动私有数据由watch_list::release_watch()清理id源的标识。投递时 ID 不同的通知会被忽略例如密钥的序列号。struct watch_list通知源订阅某个通知源的 watcher 列表watcherswatcher 链表release_watchwatch_list 销毁时回调用于释放 watch_list 持有的被监视对象的引用lock保护列表的自旋锁。struct watch_filter过滤规则集含一个类型位图type_filter与struct watch_type_filter filters[]数组。Watch List通知源APIwatch 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通知输出缓冲APIwatch queue 是应用分配的、通知记录将被写入其中的缓冲。其全部内部运作都隐藏在管道设备驱动中但要设置一个 watch必须获得它的引用。相关 APIstruct 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 订阅 APIwatch 是 watch list 上的一个订阅指明通知记录应写入的 watch queue缓冲。watch 对象中部分字段可由驱动内核子模块设置struct watch { union { u32 info_id; /* ID to be ORd 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-watches与wlist-watchers。int remove_watch_from_object(struct watch_list *wlist, struct watch_queue *wqueue, u64 id, false);从 watch list 中移除一个 watch要求该 watch 与指定的 watch queuewqueue和对象标识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完整调用链如下校验WATCH_INFO_LENGTH非零否则WARN_ON(1)并返回在 RCU 读锁保护下遍历wlist-watchersID 匹配watch-id ! id的 watch 直接跳过将n-info的WATCH_INFO_ID字段替换为watch-info_id即把调用者设定的 8 位 ID OR 进每条消息过滤若队列带过滤器且filter_watch_notification()判定不通过则跳过LSM 检查security_post_notification(watch-cred, cred, n) 0则跳过加锁后调用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_id与aux并调用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的流程为watch_id须在-1到0xff之间否则返回-EINVAL以KEY_NEED_VIEW权限查找目标 key即监视密钥需要 View 权限通过get_watch_queue(watch_queue_fd)取得队列引用若watch_id 0分配并初始化 watchwatch-id key-serialwatch-info_id watch_id WATCH_INFO_ID__SHIFT经security_watch_key()安全检查后在key-sem写锁下add_watch_to_object()若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_NOTIFYinfo_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_lock与rcu_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_id与aux字段。安全模型与资源限制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_listpost_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),仅供参考