通用通知机制¶
通用通知机制构建在标准管道驱动程序之上,它有效地将来自内核的通知消息拼接到用户空间打开的管道中。这可以与以下内容结合使用
* Key/keyring notifications
可以通过以下方式启用通知缓冲区
“通用设置”/“通用通知队列”(CONFIG_WATCH_QUEUE)
本文档包含以下部分
概述¶
此工具表现为一个以特殊模式打开的管道。管道的内部环形缓冲区用于保存由内核生成的消息。然后,这些消息通过 read() 读出。由于 splice 和类似的操作在某些情况下想要恢复它们对环的添加,这可能会与通知消息交错,因此在此类管道上禁用 splice 和类似操作。
管道的所有者必须告知内核它希望通过该管道监视哪些源。只有已连接到管道的源才会将消息插入其中。请注意,一个源可以绑定到多个管道,并同时将消息插入所有管道中。
过滤器也可以放置在管道上,以便在不感兴趣的情况下可以忽略某些源类型和子事件。
如果环中没有可用的槽位或没有可用的预分配消息缓冲区,则消息将被丢弃。在这两种情况下,在读取缓冲区中最后一个消息后,read() 将在输出缓冲区中插入 WATCH_META_LOSS_NOTIFICATION 消息。
请注意,在生成通知时,内核不会等待使用者收集它,而是继续执行。这意味着可以在持有自旋锁时生成通知,并且还可以保护内核免受用户空间故障造成的无限期阻塞。
消息结构¶
通知消息以一个简短的头部开始
struct watch_notification {
__u32 type:24;
__u32 subtype:8;
__u32 info;
};
“type” 表示通知记录的来源,“subtype” 表示来自该来源的记录类型(请参阅下面的监视源部分)。类型也可以是 “WATCH_TYPE_META”。这是一种由监视队列内部生成的特殊记录类型。有两种子类型
WATCH_META_REMOVAL_NOTIFICATION
WATCH_META_LOSS_NOTIFICATION
第一个表示安装监视的对象已被删除或销毁,第二个表示某些消息已丢失。
“info” 表示一系列内容,包括
消息的长度(以字节为单位),包括头部(使用 WATCH_INFO_LENGTH 屏蔽并按 WATCH_INFO_LENGTH__SHIFT 移位)。这表示记录的大小,可以在 8 到 127 字节之间。
监视 ID(使用 WATCH_INFO_ID 屏蔽并按 WATCH_INFO_ID__SHIFT 移位)。这表示调用者的监视 ID,可以在 0 到 255 之间。多个监视可以共享一个队列,这提供了一种区分它们的方法。
特定于类型的字段(WATCH_INFO_TYPE_INFO)。这是由通知生产者设置的,用于指示特定于类型和子类型的含义。
除了长度之外,info 中的所有内容都可以用于过滤。
头部后面可以跟补充信息。此信息的格式由类型和子类型定义。
监视列表(通知源)API¶
“监视列表”是订阅通知源的监视器的列表。列表可以附加到对象(例如键或超级块)或可以是全局的(例如用于设备事件)。从用户空间的角度来看,非全局监视列表通常通过引用它所属的对象来引用(例如使用 KEYCTL_NOTIFY 并提供要监视的特定键的密钥序列号)。
要管理监视列表,提供了以下函数
void init_watch_list(struct watch_list *wlist, void (*release_watch)(struct watch *wlist));初始化监视列表。如果
release_watch
不为 NULL,则表示在销毁 watch_list 对象以丢弃监视列表对被监视对象持有的任何引用时应调用的函数。
void remove_watch_list(struct watch_list *wlist);
这将删除所有订阅到 watch_list 的监视并释放它们,然后销毁 watch_list 对象本身。
监视队列(通知输出)API¶
“监视队列”是应用程序分配的缓冲区,通知记录将写入其中。其工作原理完全隐藏在管道设备驱动程序内部,但必须获得对其的引用才能设置监视。这些可以通过以下方式管理
struct watch_queue *get_watch_queue(int fd);
由于监视队列通过实现缓冲区的管道的 fd 指示给内核,因此用户空间必须通过系统调用传递该 fd。这可以用于从系统调用中查找监视队列的不透明指针。
void put_watch_queue(struct watch_queue *wqueue);
这将丢弃从
get_watch_queue()
获得的引用。
监视订阅 API¶
“监视”是监视列表上的订阅,指示监视队列,以及通知记录应写入其中的缓冲区。监视队列对象还可以携带由用户空间设置的该对象的过滤规则。监视结构体的某些部分可以由驱动程序设置
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
值应该是从用户空间获得并按 WATCH_INFO_ID__SHIFT 移位的 8 位数字。当且仅当通知写入关联的监视队列缓冲区时,此值将 OR 到 struct watch_notification::info 的 WATCH_INFO_ID 字段中。
private
字段是驱动程序与 watch_list 关联的数据,并由 watch_list::release_watch()
方法清理。
id
字段是源的 ID。使用不同 ID 发布的通知将被忽略。
提供了以下函数来管理监视
void init_watch(struct watch *watch, struct watch_queue *wqueue);
初始化监视对象,将其指针设置为监视队列,使用适当的屏障来避免 lockdep 投诉。
int add_watch_to_object(struct watch *watch, struct watch_list *wlist);
将监视订阅到监视列表(通知源)。在调用此函数之前,必须设置监视结构中的驱动程序可设置字段。
int remove_watch_from_object(struct watch_list *wlist, struct watch_queue *wqueue, u64 id, false);从监视列表中删除监视,其中监视必须与指定的监视队列 (
wqueue
) 和对象标识符 (id
) 匹配。将通知 (WATCH_META_REMOVAL_NOTIFICATION
) 发送到监视队列,以指示监视已删除。
int remove_watch_from_object(struct watch_list *wlist, NULL, 0, true);
从监视列表中删除所有监视。预计将在销毁之前调用此函数,并且到此时监视列表对于新监视将不可访问。将通知 (
WATCH_META_REMOVAL_NOTIFICATION
) 发送到每个订阅的监视的监视队列,以指示该监视已删除。
通知发布 API¶
要将通知发布到监视列表以便订阅的监视可以看到它,应使用以下函数
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_TYPE_KEY_NOTIFY
这种类型的通知表示密钥和密钥环的更改,包括密钥环内容的更改或密钥的属性。
有关详细信息,请参阅 内核密钥保留服务。
事件过滤¶
一旦创建了监视队列,就可以应用一组过滤器来限制使用以下命令接收的事件
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_filters”是 filters[] 中的过滤器数量,“__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] 的位 0 对应于子类型 0,位 1 对应于子类型 1,依此类推。
如果 ioctl() 的参数为 NULL,则将删除过滤器,并且将接收来自所有被监视源的所有事件。
用户空间代码示例¶
使用以下类似内容创建缓冲区
pipe2(fds, O_TMPFILE);
ioctl(fds[1], IOC_WATCH_QUEUE_SET_SIZE, 256);
然后可以将其设置为接收密钥环更改通知
keyctl(KEYCTL_WATCH_KEY, KEY_SPEC_SESSION_KEYRING, fds[1], 0x01);
然后可以使用以下类似内容来使用通知
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->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);
case WATCH_TYPE_KEY_NOTIFY:
saw_key_change(&n.n);
break;
}
p += len;
}
}
}