cn_proc(process events connector)是linux上一个直接编译进内核的驱动模块,不需要额外加载,用于发送进程事件,依赖connector模块。这里主要看一下cn_proc的工作原理和使用方式。
connector是一个依赖netlink连接用户态和内核态的通用模块。比如可以编写内核驱动和用户态程序通过connector通信,看起来与geniric netlink差不多,可以参考内核源码中的例子,这里不关心这种使用方式。

关于netlinkgeneric netlink可以参考之前的记录。

本文参考环境为 centos 8.5,4.18.0-348.el8.x86_64

cn_proc基本原理

cn_proc代码位于drivers/connector/cn_proc.c
connector代码位于drivers/connector/connector.c

代码量很小,只简略记录一下

系统启动

  • connector模块注册一个NETLINK_CONNECTOR类型的netlink socket
  • cn_proc模块调用cn_add_callback注册到connector模块,

程序注册

用户态部分

  • 用户态程序创建NETLINK_CONNECTOR类型的netlink socket
  • bind到cn_proc对应的CN_IDX_PROC广播组
  • 向cn_proc发送开始监听消息

内核部分

  • 内核收到对应的NETLINK_CONNECTOR类型的netlink消息后,回调connector模块的cn_rx_skb函数
    • connector模块遍历注册到自己的所有模块,如果其注册的cb_id类型数据与消息携带的cb_id类型数据相同,则调用注册传入的回调函数
  • cn_proc注册的回调函数为cn_proc_mcast_ctl
    • 做一些合法性检查
    • 如果消息携带的操作码为PROC_CN_MCAST_LISTEN,则增加计数器proc_event_num_listeners
    • 如果消息携带的操作码为PROC_CN_MCAST_IGNORE,则减少计数器proc_event_num_listeners

事件产生

  • 进程事件产生时,会调用cn_proc模块中对应的函数,比如fork事件,copy_process函数中会调用proc_fork_connector
    • 检查proc_event_num_listeners是否大于0,如果不大于0则结束。这也是为什么用户态程序在bind后需要发送监听消息,在结束前最好也发送取消监听消息,不然内核会有一些不必要的浪费
    • 构造并填充一个消息,外层cn_msg类型,内层proc_event类型
    • 通过connector模块的netlink socket向CN_IDX_PROC这个广播组广播。这也是为什么用户态程序bind需要指定该广播组

cn_proc使用举例

cn_proc使用例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/connector.h>
#include <linux/cn_proc.h>

// 内核里将cn_msg向后移动了4字节,使proc_event对齐8字节,应该不是必需的
//#define CN_PROC_MSG_SIZE NLMSG_LENGTH(sizeof(struct cn_msg) + sizeof(struct proc_event) + 4)
#define CN_PROC_MSG_SIZE NLMSG_LENGTH(sizeof(struct cn_msg) + sizeof(struct proc_event))

volatile int stop_flag = 0;

void signal_handler(int sig) {
stop_flag = 1;
}

const char *proc_event_type(enum what w) {
switch (w) {
case PROC_EVENT_NONE:
return "NONE";
case PROC_EVENT_FORK:
return "FORK";
case PROC_EVENT_EXEC:
return "EXEC";
case PROC_EVENT_UID:
return "UID";
case PROC_EVENT_GID:
return "GID";
case PROC_EVENT_SID:
return "SID";
case PROC_EVENT_PTRACE:
return "PTRACE";
case PROC_EVENT_COMM:
return "COMM";
case PROC_EVENT_COREDUMP:
return "COREDUMP";
case PROC_EVENT_EXIT:
return "EXIT";
default:
return "UNKNOWN";
}
}

void parse_msg(struct cn_msg *msg) {
struct proc_event *ev;
printf("==================================\n");
printf("msg->id %u:%u\n", msg->id.idx, msg->id.val);
printf("msg->len %u\n", msg->len);
printf("msg->seq %u\n", msg->seq);
printf("msg->ack %u\n", msg->ack);
if (msg->id.idx != CN_IDX_PROC || msg->id.val != CN_VAL_PROC) {
printf("unknown id\n");
return;
}
if (msg->len != sizeof(struct proc_event)) {
printf("unknown msg len\n");
return;
}
ev = (struct proc_event *)&msg->data;
printf("\n");
printf("ev->what %s\n", proc_event_type(ev->what));
printf("ev->cpu %u\n", ev->cpu);
if (ev->what == PROC_EVENT_NONE) {
printf("ev->err %u\n", ev->event_data.ack.err);
} else {
// 这两个字段是其他类型共用的,随便选类型输出一下
printf("ev->pid %d\n", ev->event_data.fork.parent_pid);
printf("ev->tgid %d\n", ev->event_data.fork.parent_tgid);
}
}

int main() {
int conn_fd;
int n;
char buffer[CN_PROC_MSG_SIZE];
struct sigaction sa;
struct sockaddr_nl sa_nl;
struct nlmsghdr *nl_hdr;
struct cn_msg *msg;

printf("CN_PROC_MSG_SIZE %lu\n", CN_PROC_MSG_SIZE);

sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;

if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction failed");
return 1;
}

if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("sigaction failed");
return 1;
}

conn_fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR);
if (conn_fd == -1) {
perror("socket failed");
return 1;
}

sa_nl.nl_family = AF_NETLINK;
// 这里应该用位图,网上有些例子直接用CN_IDX_PROC也能成功,因为运气好这个数值刚好是1
sa_nl.nl_groups = 1 << (CN_IDX_PROC - 1);
sa_nl.nl_pid = getpid();

if (bind(conn_fd, (struct sockaddr*)&sa_nl, sizeof(sa_nl)) == -1) {
perror("bind failed");
close(conn_fd);
return 1;
}


// 增加监听计数
memset(buffer, 0, sizeof(buffer));
nl_hdr = (struct nlmsghdr *)buffer;
msg = NLMSG_DATA(buffer);
nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op));
nl_hdr->nlmsg_pid = getpid();
nl_hdr->nlmsg_type = NLMSG_DONE;
nl_hdr->nlmsg_seq = 22;

msg->id.idx = CN_IDX_PROC;
msg->id.val = CN_VAL_PROC;
msg->ack = 666;
msg->len = sizeof(enum proc_cn_mcast_op);
*(enum proc_cn_mcast_op *)msg->data = PROC_CN_MCAST_LISTEN;

if (send(conn_fd, buffer, nl_hdr->nlmsg_len, 0) == -1) {
perror("send PROC_CN_MCAST_LISTEN failed");
close(conn_fd);
return 1;
}
printf("PROC_CN_MCAST_LISTEN send success\n");

// 循环读取消息
while (stop_flag == 0) {
memset(buffer, 0, sizeof(buffer));
n = recv(conn_fd, buffer, sizeof(buffer), 0);
if (n == -1) {
if (errno == ENOBUFS) {
printf("recv return ENOBUFS, event loss\n");
} else {
perror("recv failed");
break;
}
} else if (n == 0) {
perror("recv return 0?");
break;
} else {
printf("recv return %d\n", n);
msg = NLMSG_DATA(buffer);
if (n < NLMSG_LENGTH(sizeof(struct cn_msg))) {
fprintf(stderr, "invalid recv len %d\n", n);
} else if (n < NLMSG_LENGTH(msg->len)) {
fprintf(stderr, "invalid recv len %d, cn_msg len %u\n", n, msg->len);
} else {
parse_msg(msg);
}
}
}

// 减少监听计数
memset(buffer, 0, sizeof(buffer));
nl_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op));
nl_hdr->nlmsg_pid = getpid();
nl_hdr->nlmsg_type = NLMSG_DONE;
nl_hdr->nlmsg_seq = 22;

msg->id.idx = CN_IDX_PROC;
msg->id.val = CN_VAL_PROC;
msg->len = sizeof(enum proc_cn_mcast_op);
*(enum proc_cn_mcast_op *)msg->data = PROC_CN_MCAST_IGNORE;

if (send(conn_fd, buffer, nl_hdr->nlmsg_len, 0) == -1) {
perror("send PROC_CN_MCAST_IGNORE failed");
close(conn_fd);
return 1;
}
printf("PROC_CN_MCAST_IGNORE send success\n");

close(conn_fd);
return 0;
}
运行输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
[huyu@centos852111 connector]$ make
gcc -Wall -g main.c -o a.out
[huyu@centos852111 connector]$ sudo ./a.out
CN_PROC_MSG_SIZE 76
PROC_CN_MCAST_LISTEN send success
recv return 76
==================================
msg->id 1:1
msg->len 40
msg->seq 262805
msg->ack 667

ev->what NONE
ev->cpu 3
ev->err 0

recv return 76
==================================
msg->id 1:1
msg->len 40
msg->seq 274902
msg->ack 0

ev->what FORK
ev->cpu 1
ev->pid 1763
ev->tgid 1763
recv return 76
==================================
msg->id 1:1
msg->len 40
msg->seq 269032
msg->ack 0

ev->what EXEC
ev->cpu 2
ev->pid 360189
ev->tgid 360189
recv return 76
==================================
msg->id 1:1
msg->len 40
msg->seq 269033
msg->ack 0

ev->what EXIT
ev->cpu 2
ev->pid 360189
ev->tgid 360189
^Crecv failed: Interrupted system call
PROC_CN_MCAST_IGNORE send success
[huyu@centos852111 connector]$

recv与ENOBUFS

与其他类型socket不同,netlink socket的用户态接收端是可能收到ENOBUFS错误的,上面的例子中可以看到相关的处理代码。这是因为在向netlink socket发送数据包时,如果该socket缓存空间不足且socket相应设置未阻止接收该错误,则sock结构sk_err字段会被写为ENOBUFS,该错误码会被socket上的下一次recv接收到。

缓存空间不足设置错误码的调用栈,以fork为例
1
2
3
4
5
6
7
8
9
10
11
          <...>-284188 [001] .... 587549.835287: netlink_overrun+0x0/0x40 <-netlink_broadcast_filtered+0x238/0x400
<...>-284188 [001] .... 587549.835292: <stack trace>
=> 0xffffffffc036006a
=> netlink_overrun+0x5/0x40
=> netlink_broadcast_filtered+0x238/0x400
=> netlink_broadcast+0xf/0x20
=> proc_fork_connector+0xd6/0x100
=> copy_process+0x16b6/0x1aa0
=> _do_fork+0x8f/0x350
=> do_syscall_64+0x5b/0x1a0
=> entry_SYSCALL_64_after_hwframe+0x65/0xca
设置错误代码的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
static void netlink_overrun(struct sock *sk)           
{
struct netlink_sock *nlk = nlk_sk(sk);

if (!(nlk->flags & NETLINK_F_RECV_NO_ENOBUFS)) {
if (!test_and_set_bit(NETLINK_S_CONGESTED,
&nlk_sk(sk)->state)) {
sk->sk_err = ENOBUFS;
sk->sk_error_report(sk);
}
}
atomic_inc(&sk->sk_drops);
}
recv时如何取到的错误码
1
2
3
4
5
6
7
:tags
# TO tag FROM line in file/text
1 1 netlink_recvmsg 1 /usr/src/debug/kernel-4.18.0-348.el8/linux-4.18.0-348.el8.x86_64/net/netlink/af_netlink.c
2 2 skb_recv_datagram 1954 /usr/src/debug/kernel-4.18.0-348.el8/linux-4.18.0-348.el8.x86_64/net/netlink/af_netlink.c
3 2 __skb_recv_datagram 326 /usr/src/debug/kernel-4.18.0-348.el8/linux-4.18.0-348.el8.x86_64/net/core/datagram.c
4 2 __skb_try_recv_datagram 306 /usr/src/debug/kernel-4.18.0-348.el8/linux-4.18.0-348.el8.x86_64/net/core/datagram.c
5 1 sock_error 258 /usr/src/debug/kernel-4.18.0-348.el8/linux-4.18.0-348.el8.x86_64/net/core/datagram.c
取得错误码的函数
1
2
3
4
5
6
7
8
static inline int sock_error(struct sock *sk)
{
int err;
if (likely(!sk->sk_err))
return 0;
err = xchg(&sk->sk_err, 0);
return -err;
}

验证这个情况也比较容易

  • 运行用户态程序
  • 向其发送SIGSTOP信号将其置为停止状态
  • 运行下面脚本生成数量较大的进程事件填满用户态程序socket接收缓冲区
  • 向用户态程序发送SIGCONT信号将其置为运行状态

这时就可以看到针对ENOBUFS的输出了

信号部分,对于当前会话前台可以使用快捷键<Ctrl-c>发送停止信号,fg命令发送继续运行信号且将进程置为前台

验证脚本
1
2
3
4
5
6
7
8
#!/usr/bin/bash
run_ls_loop() {
for ((i = 1; i <= 2000; i++)); do
ls > /dev/null
done
}

run_ls_loop