前文说过将32个netlink协议号中未使用的号码用于自定义内核模块通信并不是一个好主意,更规范的做法是使用generic netlink的方式达成同样的目的。generic netlink是一种netlink协议,被设计为一个通用的协议,用于承载运行于其上的各种用户自定义协议。这里介绍generic netlink的使用方式及其实现原理。

数据结构

例子代码之前先看几个需要了解的数据结构。

struct genlmsghdr
1
2
3
4
5
struct genlmsghdr {
__u8 cmd;
__u8 version;
__u16 reserved;
};

genlmsghdr是generic netlink的消息头,发送给内核的generic netlink消息中该结构必须存在,因为内核需要根据这个协议头定位该cmd对应的ops。

generic netlink 消息格式

struct genl_family
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
struct genl_family {
RH_KABI_REPLACE(unsigned int id, int id) /* private */
unsigned int hdrsize;
char name[GENL_NAMSIZ];
unsigned int version;
unsigned int maxattr;
bool netnsok;
bool parallel_ops;
int (*pre_doit)(const struct genl_ops *ops,
struct sk_buff *skb,
struct genl_info *info);
void (*post_doit)(const struct genl_ops *ops,
struct sk_buff *skb,
struct genl_info *info);
struct nlattr ** attrbuf; /* private */
const struct genl_ops * ops;
const struct genl_multicast_group *mcgrps;
unsigned int n_ops;
unsigned int n_mcgrps;
unsigned int mcgrp_offset; /* private */
RH_KABI_DEPRECATE(struct list_head, family_list)
struct module *module;

/* Reserved slots. For Red Hat usage only, modules are required to
* set them to zero. */
RH_KABI_RESERVE(1)
RH_KABI_RESERVE(2)
RH_KABI_RESERVE(3)
RH_KABI_RESERVE(4)
};

genl_family用于注册到generic netlink中,需要使用静态数据,因为后续处理需要该结构。

  • id
    family注册后最终确定的id,内核保留字段,自定义协议注册该字段保留0。
  • hdrsize
    自定义协议中自定义头的长度,会影响内核对消息体中nlattr的定位及预处理。
  • name
    family的名字,不能注册重复名字,长度在15以内(不包含’\0’结尾)。
  • maxattr
    支持的nla_type的最大值,如果在消息中出现超出该值的nlattr,将不会被解析填充到genl_info中,也不会被policy校验。
  • netnsok
    是否支持接收所有net namespace的请求。
  • parallel_ops
    是否支持并行操作,如果不支持并行操作,内核将使用一个全局锁将不支持并行操作的family的所有请求串行处理。
  • pre_doid
    如果非NULL,将在genl_ops的doit函数指针前执行。
  • post_doit
    如果非NULL,将在genl_ops的doit函数指针执行。
  • attrbuf
    内核保留字段,如果不支持并行操作,将用作genl_info字段attrs共用的缓存。
  • ops
    支持的genl_ops数组。
  • mcgrps
    family需要的组播组名字数组。
  • n_ops
    ops数组长度。
  • n_mcgrps
    mcgrps数组长度
  • mcgrp_offset
    内核保留字段,最终确定的组播组连续id的首个id。
struct genl_ops
1
2
3
4
5
6
7
8
9
10
11
struct genl_ops {
const struct nla_policy *policy;
int (*doit)(struct sk_buff *skb,
struct genl_info *info);
int (*dumpit)(struct sk_buff *skb,
struct netlink_callback *cb);
int (*done)(struct netlink_callback *cb);
u8 cmd;
u8 internal_flags;
u8 flags;
};

注册时嵌入到genl_family结构中,同样需要使用静态数据

  • policy
    用户校验该cmd下nlattr基本内容,具体看include/net/netlink.h和源码吧。
  • doit
    该cmd的处理函数。
  • dumpit
    该cmd的请求nlmsg_flags中如果包含NLM_F_DUMP,将由该处理函数处理。这里的处理逻辑支持对于一个dump请求多次调用dumpit返回响应,由dumpit的返回值决定。下面的NLM_F_DUMP 请求处理部分可以看到更多介绍。单播数据包分发流程可以看到其生效位置。
  • done
    与dumpit对应,在dumpit的一个完整逻辑完成后将会调用该处理函数。
  • cmd
    该ops对应处理的cmd,对应genlmsghdr中的cmd。
  • internal_flags
    family自定义使用,内核不关心该字段。
  • flags
    标明该ops中cmd对应的请求,需要进程拥有的权限,单播数据包分发流程可以看到有对其的检查。
组播组名字
1
2
3
struct genl_multicast_group {
char name[GENL_NAMSIZ];
};
1
2
3
4
5
6
7
8
9
10
11
struct genl_info {
u32 snd_seq;
u32 snd_portid;
struct nlmsghdr * nlhdr;
struct genlmsghdr * genlhdr;
void * userhdr;
struct nlattr ** attrs;
possible_net_t _net;
void * user_ptr[2];
RH_KABI_DEPRECATE(struct sock *, dst_sk)
};

genl_info由内核generic netlink子系统填充并传递给用户自定义的处理函数,简化用户的解析操作。

  • snd_seq
    发送方nlmsg_seq。
  • snd_portid
    发送方portid。
  • nlhdr
    nlmsghdr结构指针。
  • genlhdr
    genlmsghdr结构指针。
  • userhdr
    genl_family注册时标明长度的自定义头的地址。
  • attrs
    内核解析的nlattr指针数组,索引为nla_type,只解析最外层的nlattr。如果相同nla_type的nlattr,后面的会覆盖前面的填充。
  • _net
    skb所属sk的net namespace,取对应的net namespace使用函数genl_info_net
  • user_ptr
    这个东西应该是给用户自己使用的,内核不关心。

例子代码

comm.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define GENL_EXAMPLE_FAMILY_NAME "genl_exam_name"

enum {
MY_CMD_UNSPEC,
MY_CMD_PRINT_STR,
MY_CMD_ADD,
__MY_CMD_MAX,
#define MY_CMD_MAX (__MAX_CMD_MAX -1)
};

enum {
MY_ATTR_UNSPEC,
MY_ATTR_STR,
MY_ATTR_N1,
MY_ATTR_N2,
__MY_ATTR_MAX,
#define MY_ATTR_MAX (__MY_ATTR_MAX - 1)
};
genl_kern.c
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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

#include <linux/skbuff.h>
#include <linux/netlink.h>
#include <net/genetlink.h>
#include <net/sock.h>
#include <linux/netdevice.h>
#include "comm.h"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Hu Yu <hyuuhit@gmail.com>");
MODULE_DESCRIPTION("generic netlink example");
MODULE_VERSION("0.1");

static void dump_nlhdr(struct nlmsghdr *nlhdr) {
pr_info("nlh->nlmsg_len: %u\n", nlhdr->nlmsg_len);
pr_info("nlh->nlmsg_type: %u\n", nlhdr->nlmsg_type);
pr_info("nlh->nlmsg_flags: %u\n", nlhdr->nlmsg_flags);
pr_info("nlh->nlmsg_seq: %u\n", nlhdr->nlmsg_seq);
pr_info("nlh->nlmsg_pid: %u\n", nlhdr->nlmsg_pid);
}

static void dump_genlhdr(struct genlmsghdr *genlhdr) {
pr_info("genlh->cmd: %u\n", genlhdr->cmd);
pr_info("genlh->version: %u\n", genlhdr->version);
}

static void dump_genl_info(struct genl_info *info) {
dump_nlhdr(info->nlhdr);
dump_genlhdr(info->genlhdr);
pr_info("snd_seq: %u\n", info->snd_seq);
pr_info("snd_portid: %u\n", info->snd_portid);
pr_info("attrs: %s\n", info->attrs ? "NOT NULL" : "NULL");
if (info->attrs != NULL) {
pr_info("%s - %p\n", "MY_ATTR_STR", info->attrs[MY_ATTR_STR]);
pr_info("%s - %p\n", "MY_ATTR_N1", info->attrs[MY_ATTR_N1]);
pr_info("%s - %p\n", "MY_ATTR_N2", info->attrs[MY_ATTR_N2]);
}
}

static int my_pre_doit(const struct genl_ops *ops, struct sk_buff *skb, struct genl_info *info) {
pr_info("my_pre_doit\n");
return 0;
}

static int print_doit(struct sk_buff *skb, struct genl_info *info) {
pr_info("print_doit\n");
dump_genl_info(info);
if (info->attrs[MY_ATTR_STR]) {
pr_info("print result: %s\n", (char *)nla_data(info->attrs[MY_ATTR_STR]));
}
pr_info("end print_doit =========================\n");
return 0;
}

static int add_doit(struct sk_buff *skb, struct genl_info *info) {
pr_info("add_doit\n");
dump_genl_info(info);
if (info->attrs[MY_ATTR_N1] && info->attrs[MY_ATTR_N2]) {
int n = *(u8 *)nla_data(info->attrs[MY_ATTR_N1]);
n += *(u8 *)nla_data(info->attrs[MY_ATTR_N2]);
pr_info("add result: %d\n", n);

}
pr_info("end add_doit =========================\n");
return 0;
}

static void my_post_doit(const struct genl_ops *ops, struct sk_buff *skb, struct genl_info *info) {
pr_info("my_post_doit\n");
}

struct nla_policy my_policy[] = {
[MY_ATTR_STR] = {
.type = NLA_NUL_STRING,
}
};

static struct genl_ops my_ops[] = {
{
.policy = my_policy,
.doit = print_doit,
.dumpit = NULL,
.done = NULL,
.cmd = MY_CMD_PRINT_STR,
.internal_flags = 0,
.flags = 0,
},
{
.doit = add_doit,
.cmd = MY_CMD_ADD,
},
};

static struct genl_multicast_group groups[] = {
{
.name = "g1",
},
{
.name = "g2",
},
};

static struct genl_family family = {
.id = 0,
.name = GENL_EXAMPLE_FAMILY_NAME,
.hdrsize = 0,
.version = 1,
.maxattr = MY_ATTR_MAX,
.netnsok = true,
.parallel_ops = false,
.pre_doit = &my_pre_doit,
.post_doit = &my_post_doit,
.ops = my_ops,
.n_ops = ARRAY_SIZE(my_ops),
.mcgrps = groups,
.n_mcgrps = ARRAY_SIZE(groups),
.module = THIS_MODULE,
};

static int __init my_init(void)
{

pr_info("my init.\n");

if(genl_register_family(&family)) {
pr_info("genl_register_family failed\n");
pr_info("====================\n");
return -1;
}

pr_info("====================\n");
return 0;
}

static void __exit my_exit(void)
{
pr_info("my exit.\n");

if(genl_unregister_family(&family)) {
pr_info("genl_unregister_family failed\n");
}

pr_info("====================\n");
}

module_init(my_init);
module_exit(my_exit);
genl_user.c 重复代码很多,懒得管了
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/genetlink.h>
#include <linux/netlink.h>
#include <sys/types.h>
#include <unistd.h>

#include "comm.h"

static int family_id = -1;

/**
* nla_data - head of payload
* @nla: netlink attribute
*/
static inline void *nla_data(const struct nlattr *nla)
{
return (char *) nla + NLA_HDRLEN;
}

/**
* nla_ok - check if the netlink attribute fits into the remaining bytes
* @nla: netlink attribute
* @remaining: number of bytes remaining in attribute stream
*/
static inline int nla_ok(const struct nlattr *nla, int remaining)
{
return remaining >= (int) sizeof(*nla) &&
nla->nla_len >= sizeof(*nla) &&
nla->nla_len <= remaining;
}

/**
* nla_next - next netlink attribute in attribute stream
* @nla: netlink attribute
* @remaining: number of bytes remaining in attribute stream
*
* Returns the next netlink attribute in the attribute stream and
* decrements remaining by the size of the current attribute.
*/
static inline struct nlattr *nla_next(const struct nlattr *nla, int *remaining)
{
int totlen = NLA_ALIGN(nla->nla_len);

*remaining -= totlen;
return (struct nlattr *) ((char *) nla + totlen);
}

static int get_family_id_fill_request(char *family, void *buf, int *buf_len) {

struct nlmsghdr *nlh;
struct genlmsghdr *glh;
struct nlattr *nla;

int len = 0;

// attr 长度
len += NLA_ALIGN(NLA_HDRLEN + strlen(family) + 1) ;
// + genelmsghdr + pad 长度
len += GENL_HDRLEN;
// + nlmsghdr + pad 长度
len = NLMSG_SPACE(len);

if (len <= *buf_len) {
*buf_len = len;
} else {
return -1;
}

nlh = buf;
glh = NLMSG_DATA(buf);
nla = (struct nlattr *)((char *)glh + GENL_HDRLEN);

nlh->nlmsg_len = len;
nlh->nlmsg_type = GENL_ID_CTRL;
nlh->nlmsg_flags = NLM_F_REQUEST;
// 用于判断请求响应对应情况,先随便填一个。
nlh->nlmsg_seq = 299;
nlh->nlmsg_pid = 0;

glh->cmd = CTRL_CMD_GETFAMILY;
glh->version = 2;

nla->nla_type = CTRL_ATTR_FAMILY_NAME;
nla->nla_len = NLA_HDRLEN + strlen(family) + 1;
memcpy((void *)nla + NLA_HDRLEN, family, strlen(family) + 1);

return 0;
}

char *nla_ctrl_type_to_string(__u16 nla_type) {
switch (nla_type) {
case CTRL_ATTR_FAMILY_NAME:
return "family name";
case CTRL_ATTR_FAMILY_ID:
return "family id";
case CTRL_ATTR_VERSION:
return "version";
case CTRL_ATTR_HDRSIZE:
return "hdrsize";
case CTRL_ATTR_MAXATTR:
return "maxattr";
case CTRL_ATTR_OPS:
return "ops";
case CTRL_ATTR_MCAST_GROUPS:
return "mcast_groups";
default:
return "unknown";
}
}

void parse_nla_msg_mcast_group_content(const struct nlattr *nla, int len) {
for (; nla_ok(nla, len); nla = nla_next(nla, &len)) {
if (nla->nla_type == CTRL_ATTR_MCAST_GRP_ID) {
printf(", id: %u", *(__u32*)nla_data(nla));
} else if (nla->nla_type == CTRL_ATTR_MCAST_GRP_NAME) {
printf(", name: %s", (char *)nla_data(nla));
} else {
printf(", unknown type %u", nla->nla_type);
}
}
printf("\n");
}

void parse_nla_msg_mcast_groups_array(const struct nlattr *nla, int len) {
for (; nla_ok(nla, len); nla = nla_next(nla, &len)) {
printf(" %u group", nla->nla_type);
parse_nla_msg_mcast_group_content(nla_data(nla), nla->nla_len);
}
}

void parse_nla_msg_op_content(const struct nlattr *nla, int len) {
for (; nla_ok(nla, len); nla = nla_next(nla, &len)) {
if (nla->nla_type == CTRL_ATTR_OP_ID) {
printf(", id: %u", *(__u8*)nla_data(nla));
} else if (nla->nla_type == CTRL_ATTR_OP_FLAGS) {
int exist = 0;
__u8 flags = *(__u8*)nla_data(nla);
printf(", flags: 0x%02x - ", flags);

if (flags & GENL_CMD_CAP_DUMP) {
printf("GENL_CMD_CAP_DUMP ");
exist = 1;
}
if (flags & GENL_CMD_CAP_DO) {
if (exist)
printf("| ");
printf("GENL_CMD_CAP_DO ");
exist = 1;
}
if (flags & GENL_CMD_CAP_HASPOL) {
if (exist)
printf("| ");
printf("GENL_CMD_CAP_HASPOL ");
exist = 1;
}
if (flags & (~(GENL_CMD_CAP_DUMP | GENL_CMD_CAP_DO | GENL_CMD_CAP_HASPOL))) {
if (exist)
printf("| ");
printf("0x%02x", flags & (~(GENL_CMD_CAP_DUMP | GENL_CMD_CAP_DO | GENL_CMD_CAP_HASPOL)));
}
}
}
printf("\n");
}

void parse_nla_msg_ops_array(const struct nlattr *nla, int len) {
for (; nla_ok(nla, len); nla = nla_next(nla, &len)) {
printf(" %u op", nla->nla_type);
parse_nla_msg_op_content(nla_data(nla), nla->nla_len);
}
}

void parse_nla_msg(struct nlattr *nla, int len) {
for (; nla_ok(nla, len); nla = nla_next(nla, &len)) {
printf("len: %u, type: %u - %s: ", nla->nla_len, nla->nla_type, nla_ctrl_type_to_string(nla->nla_type));
if (nla->nla_type == CTRL_ATTR_FAMILY_NAME) {
printf("%s\n", (char *)nla + NLA_HDRLEN);
} else if (nla->nla_type == CTRL_ATTR_FAMILY_ID) {
__u16 num = *(__u16 *)((void *)nla + NLA_HDRLEN);
printf("%u\n", num);
family_id = num;
} else if (nla->nla_type == CTRL_ATTR_VERSION ||
nla->nla_type == CTRL_ATTR_HDRSIZE ||
nla->nla_type == CTRL_ATTR_MAXATTR) {
__u32 num = *(__u32 *)((void *)nla + NLA_HDRLEN);
printf("%u\n", num);
} else if (nla->nla_type == CTRL_ATTR_OPS) {
printf("\n");
parse_nla_msg_ops_array(nla_data(nla), nla->nla_len);
} else if (nla->nla_type == CTRL_ATTR_MCAST_GROUPS) {
printf("\n");
parse_nla_msg_mcast_groups_array(nla_data(nla), nla->nla_len);
} else {
printf("\n");
}
}
}

char *get_ctrl_cmd_name(int cmd) {
switch (cmd) {
case CTRL_CMD_UNSPEC:
return "CTRL_CMD_UNSPEC";
case CTRL_CMD_NEWFAMILY:
return "CTRL_CMD_NEWFAMILY";
case CTRL_CMD_DELFAMILY:
return "CTRL_CMD_DELFAMILY";
case CTRL_CMD_GETFAMILY:
return "CTRL_CMD_GETFAMILY";
case CTRL_CMD_NEWOPS:
return "CTRL_CMD_NEWOPS";
case CTRL_CMD_DELOPS:
return "CTRL_CMD_DELOPS";
case CTRL_CMD_GETOPS:
return "CTRL_CMD_GETOPS";
case CTRL_CMD_NEWMCAST_GRP:
return "CTRL_CMD_NEWMCAST_GRP";
case CTRL_CMD_DELMCAST_GRP:
return "CTRL_CMD_DELMCAST_GRP";
case CTRL_CMD_GETMCAST_GRP:
return "CTRL_CMD_GETMCAST_GRP";
default:
return "unknown";
}
}

void parse_genl_msg(struct genlmsghdr *glh, int len, int ctrl) {
struct nlattr *nla = (struct nlattr *)((char *)glh + GENL_HDRLEN);
int nlalen;

if (len < GENL_HDRLEN) {
return;
}

nlalen = len - GENL_HDRLEN;

printf("glh cmd : %u", glh->cmd);
if (ctrl)
printf(" - %s\n", get_ctrl_cmd_name(glh->cmd));
else
printf("\n");

printf("glh version: %u\n", glh->version);
printf("nla all len: %d\n", nlalen);
printf("-----------------------\n");

parse_nla_msg(nla, nlalen);
}

void parse_nl_msg(struct nlmsghdr *nlh, int len) {
if (len < NLMSG_HDRLEN) {
return;
}
for (;NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
printf("nlmsg_len : %u\n", nlh->nlmsg_len);
printf("nlmsg_type : %u", nlh->nlmsg_type);
if (nlh->nlmsg_type == GENL_ID_CTRL)
printf(" - %s\n", "GENL_ID_CTRL");
else
printf("\n");
printf("nlmsg_flags: %u\n", nlh->nlmsg_flags);
printf("nlmsg_seq : %u\n", nlh->nlmsg_seq);
printf("nlmsg_pid : %u\n", nlh->nlmsg_pid);
printf("-----------------------\n");

if (nlh->nlmsg_type == NLMSG_ERROR) {
struct nlmsgerr *err = NLMSG_DATA(nlh);
printf("errno %d, ", err->error);
printf("%s\n", strerror(abs(err->error)));
printf(">>>\n");
parse_nl_msg(&(err->msg), nlh->nlmsg_len - NLMSG_HDRLEN);
printf("<<<\n");
} else if (nlh->nlmsg_type == GENL_ID_CTRL) {
parse_genl_msg(NLMSG_DATA(nlh), nlh->nlmsg_len - NLMSG_HDRLEN, 1);
} else {
parse_genl_msg(NLMSG_DATA(nlh), nlh->nlmsg_len - NLMSG_HDRLEN, 0);
}
printf("-----------------------\n\n");
}
}

int test_print_request(unsigned int family_id, int fd, const char *str) {
struct nlmsghdr *nlh;
struct genlmsghdr *glh;
struct nlattr *nla;
char buf[1500];
struct sockaddr_nl dest_addr;

int len = 0;
int err;

// attr 长度
len += NLA_ALIGN(NLA_HDRLEN + strlen(str) + 1) ;
// + genelmsghdr + pad 长度
len += GENL_HDRLEN;
// + nlmsghdr + pad 长度
len = NLMSG_SPACE(len);

nlh = (struct nlmsghdr *)buf;
glh = NLMSG_DATA(buf);
nla = (struct nlattr *)((char *)glh + GENL_HDRLEN);

nlh->nlmsg_len = len;
nlh->nlmsg_type = family_id;
nlh->nlmsg_flags = NLM_F_REQUEST;
// 用于判断请求响应对应情况,先随便填一个。
nlh->nlmsg_seq = 300;
nlh->nlmsg_pid = 0;

glh->cmd = MY_CMD_PRINT_STR;
glh->version = 1;

nla->nla_type = MY_ATTR_STR;
nla->nla_len = NLA_HDRLEN + strlen(str) + 1;
memcpy((void *)nla + NLA_HDRLEN, str, strlen(str) + 1);


memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0;
dest_addr.nl_groups = 0;

err = sendto(fd, buf, len, 0, (struct sockaddr *)&dest_addr, (socklen_t)sizeof(dest_addr));
if (err == -1) {
perror("sendto failed");
return -1;
}

return 0;
}

int test_add_request(unsigned int family_id, int fd, __u8 n1, __u8 n2) {
struct nlmsghdr *nlh;
struct genlmsghdr *glh;
struct nlattr *nla;
char buf[1500];
struct sockaddr_nl dest_addr;

int len = 0;
int err;

// attr 长度
len += NLA_ALIGN(NLA_HDRLEN + sizeof(__u8)) ;
len += NLA_ALIGN(NLA_HDRLEN + sizeof(__u8)) ;
// + genelmsghdr + pad 长度
len += GENL_HDRLEN;
// + nlmsghdr + pad 长度
len = NLMSG_SPACE(len);

nlh = (struct nlmsghdr *)buf;
glh = NLMSG_DATA(buf);
nla = (struct nlattr *)((char *)glh + GENL_HDRLEN);

nlh->nlmsg_len = len;
nlh->nlmsg_type = family_id;
nlh->nlmsg_flags = NLM_F_REQUEST;
// 用于判断请求响应对应情况,先随便填一个。
nlh->nlmsg_seq = 301;
nlh->nlmsg_pid = 0;

glh->cmd = MY_CMD_ADD;
glh->version = 1;

nla->nla_type = MY_ATTR_N1;
nla->nla_len = NLA_HDRLEN + sizeof(__u8);
memcpy((void *)nla + NLA_HDRLEN, &n1, sizeof(__u8));

nla = (struct nlattr *)((char *)nla + NLA_ALIGN(NLA_HDRLEN + sizeof(__u8)));
nla->nla_type = MY_ATTR_N2;
nla->nla_len = NLA_HDRLEN + sizeof(__u8);
memcpy((void *)nla + NLA_HDRLEN, &n2, sizeof(__u8));

memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0;
dest_addr.nl_groups = 0;

err = sendto(fd, buf, len, 0, (struct sockaddr *)&dest_addr, (socklen_t)sizeof(dest_addr));
if (err == -1) {
perror("sendto failed");
return -1;
}

return 0;
}


int main() {

struct sockaddr_nl dest_addr;
char buf[1500];
int buf_len = sizeof(buf);
int err;
int fd;

printf("my pid: %d\n", getpid());

printf("NLMSG_HDRLEN: %d\n", NLMSG_HDRLEN);
printf("GENL_HDRLEN: %d\n", GENL_HDRLEN);
printf("NLA_HDRLEN: %d\n", NLA_HDRLEN);

/* create */
fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_GENERIC);
if (fd < 0) {
perror("socekt failed");
return -1;
}

if (get_family_id_fill_request(GENL_EXAMPLE_FAMILY_NAME, buf, &buf_len)) {
fprintf(stderr, "get_family_id_fill_request failed");
return -1;
}

/* send */
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0;
dest_addr.nl_groups = 0;

err = sendto(fd, buf, buf_len, 0, (struct sockaddr *)&dest_addr, (socklen_t)sizeof(dest_addr));
if (err == -1) {
perror("sendto failed");
return -1;
}
printf("send len %d\n", err);

err = recv(fd, buf, sizeof(buf), 0);
if (err == -1) {
perror("recv failed");
return -1;
}
printf("recv len %d\n", err);
parse_nl_msg((struct nlmsghdr *)buf, err);

if (family_id > 0) {
if (test_print_request(family_id, fd, "test genl print str request")) {
fprintf(stderr, "test_print_request failed\n");
return -1;
}
if (test_add_request(family_id, fd, 2, 5)) {
fprintf(stderr, "test_add_request failed\n");
return -1;
}
}

return 0;
}
Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
obj-m := genl_kern.o

PWD:=$(shell pwd)
KVER:=$(shell uname -r)
KDIR:=/lib/modules/$(KVER)/build

EXTRA_CFLAGS += -Wall -g

.PHONY:all
all: kernel genl_user

.PHONY:kernel
kernel:
$(MAKE) -C $(KDIR) M=$(PWD) modules

genl_user: genl_user.c
gcc $(EXTRA_CFLAGS) $^ -o $@

.PHONY:clean
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
rm -f genl_user
genl_user 输出
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
my pid: 26758
NLMSG_HDRLEN: 16
GENL_HDRLEN: 4
NLA_HDRLEN: 4
send len 40
recv len 160
nlmsg_len : 160
nlmsg_type : 16 - GENL_ID_CTRL
nlmsg_flags: 0
nlmsg_seq : 299
nlmsg_pid : 26758
-----------------------
glh cmd : 1 - CTRL_CMD_NEWFAMILY
glh version: 2
nla all len: 140
-----------------------
len: 19, type: 2 - family name: genl_exam_name
len: 6, type: 1 - family id: 27
len: 8, type: 3 - version: 1
len: 8, type: 4 - hdrsize: 0
len: 8, type: 5 - maxattr: 3
len: 44, type: 6 - ops:
1 op, id: 1, flags: 0x0a - GENL_CMD_CAP_DO | GENL_CMD_CAP_HASPOL
2 op, id: 2, flags: 0x02 - GENL_CMD_CAP_DO
len: 44, type: 7 - mcast_groups:
1 group, id: 4, name: g1
2 group, id: 5, name: g2
-----------------------
dmesg 输出
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
[985691.316473] my init.
[985691.316502] ====================
[985694.636004] my_pre_doit
[985694.636009] print_doit
[985694.636011] nlh->nlmsg_len: 52
[985694.636012] nlh->nlmsg_type: 27
[985694.636013] nlh->nlmsg_flags: 1
[985694.636014] nlh->nlmsg_seq: 300
[985694.636015] nlh->nlmsg_pid: 0
[985694.636016] genlh->cmd: 1
[985694.636017] genlh->version: 1
[985694.636018] snd_seq: 300
[985694.636019] snd_portid: 26758
[985694.636021] attrs: NOT NULL
[985694.636022] MY_ATTR_STR - ffff967ef6a1b214
[985694.636024] MY_ATTR_N1 - (null)
[985694.636025] MY_ATTR_N2 - (null)
[985694.636026] print result: test genl print str request
[985694.636027] end print_doit =========================
[985694.636028] my_post_doit
[985694.636030] my_pre_doit
[985694.636031] add_doit
[985694.636032] nlh->nlmsg_len: 36
[985694.636033] nlh->nlmsg_type: 27
[985694.636034] nlh->nlmsg_flags: 1
[985694.636035] nlh->nlmsg_seq: 301
[985694.636036] nlh->nlmsg_pid: 0
[985694.636037] genlh->cmd: 2
[985694.636038] genlh->version: 1
[985694.636039] snd_seq: 301
[985694.636040] snd_portid: 26758
[985694.636041] attrs: NOT NULL
[985694.636042] MY_ATTR_STR - (null)
[985694.636043] MY_ATTR_N1 - ffff967ef6a1b214
[985694.636044] MY_ATTR_N2 - ffff967ef6a1b21c
[985694.636045] add result: 7
[985694.636046] end add_doit =========================
[985694.636047] my_post_doit
[985706.885696] my exit.
[985706.885711] ====================

generic netlink的实现代码位于/net/netlink/genetlink

实现原理上可以简单描述如下:

  • 组播依赖netlink的原生组播功能,generic netlink负责对组播组id进行分配管理。
  • 单播由netlink传递到generic netlink消息总线(这个消息总线就是一个统一的处理入口函数),通过family id定位到genl_family,进一步通过cmd定位到genl_ops进行处理。
  • controller服务负责对family name和family id进行名字翻译处理,controller的family id是固定的GENL_ID_CTRL

下面是generic netlink howto(文章中数据结构已经比较旧)中描述的基本架构图。

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
    +---------------------+      +---------------------+
| (3) application "A" | | (3) application "B" |
+------+--------------+ +--------------+------+
| |
\ /
\ /
| |
+-------+--------------------------------+-------+
| : : | user-space
=====+ : (5) kernel socket API : +================
| : : | kernel-space
+--------+-------------------------------+-------+
| |
+-----+-------------------------------+----+
| (1) Netlink subsystem |
+---------------------+--------------------+
|
+---------------------+--------------------+
| (2) Generic Netlink bus |
+--+--------------------------+-------+----+
| | |
+-------+---------+ | |
| (4) controller | / \
+-----------------+ / \
| |
+------------------+--+ +--+------------------+
| (3) kernel user "X" | | (3) kernel user "Y" |
+---------------------+ +---------------------+

初始化

generic netlink的初始化工作由函数genl_init完成。

genl_init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int __init genl_init(void)
{
int err;

err = genl_register_family(&genl_ctrl);
if (err < 0)
goto problem;

err = register_pernet_subsys(&genl_pernet_ops);
if (err)
goto problem;

return 0;

problem:
panic("GENL: Cannot register controller: %d\n", err);
}

这个函数内容很少,两个功能。

  • 通过函数genl_register_family在generic netlink协议下注册了一个controller名字服务。
    • genl_register_family的实现包含了generic netlink如果通过自定义协议名承载·
    • controller与用户的自定义协议使用同样的api注册,实现了通过自定义协议名查询相关信息的功能(比如id和广播组)。
  • 为每个net namespace注册了函数调用。
    这个函数会对每个net namespace(包括未来创建的新的net namespace)执行,用于在netlink协议族上注册generic netlink协议。

这两个工作完成的先后并没有严格的依赖关系。先看对每个net namespace注册的函数。

1
2
3
4
static struct pernet_operations genl_pernet_ops = {
.init = genl_pernet_init,
.exit = genl_pernet_exit,
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int __net_init genl_pernet_init(struct net *net)
{
struct netlink_kernel_cfg cfg = {
.input = genl_rcv,
.flags = NL_CFG_F_NONROOT_RECV,
};

/* we'll bump the group number right afterwards */
net->genl_sock = netlink_kernel_create(net, NETLINK_GENERIC, &cfg);

if (!net->genl_sock && net_eq(net, &init_net))
panic("GENL: Cannot initialize generic netlink\n");

if (!net->genl_sock)
return -ENOMEM;

return 0;
}

到函数genl_pernet_init就很好理解了,结合之前netlink的介绍,就是在netlink协议族上注册协议号为NETLINK_GENERIC的协议,允许非特权用户接收广播,数据包在内核的接收处理函数为genl_rcv,每个net namespace中创建的sk赋值给net->genl_sock用于内核发送generic netlink数据包时使用。用户态发送给内核的所有generic netlink数据包,不区分协议名,入口处理函数都是genl_rcv,这个函数我们晚一点再看,先看一下genl_register_family如何注册一个自定义协议到generaic netlink上,然后再看genl_rcv如何将数据包分发给不同的自定义协议。

自定义协议注册

genl_register_family主要完成两个功能:

  • 为注册的genl_family分配一个唯一id,并将指针保存起来后续将使用
  • 如果注册有组播,还会为组播分配连续id的组播组,其实组播组id记录在mcgrp_offset字段中
genl_register_family 流程
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

+--------------------+
|genl_register_family|
+---+----------------+
|
| +-----------------+
+------+genl_validate_ops|
| +-----------------+
| 通过genl_family字段n_ops指明的genl_ops个数,遍历所有ops
| 确保每个ops中dumpid和doit两个函数指针至少有一个不为NULL
| 同时确保各个ops中要处理的cmd没有冲突
|
| +-------------------------------------------------------------+
+------+通过函数genl_family_find_by_name查找是否存在同名的genl_family|
| |如果存在同名,返回-EEXIST错误 |
| +-------------------------------------------------------------+
|
| +--------------------------------------------------------+
+------|如果传入的是内核预留的对几个协议,那么做特殊处理,确定id|
| +--------------------------------------------------------+
| PS:自定义协议不应该使用任何固定id,而应该在注册时将id置0,让系统自动分配id
|
| +-------------------------------------------------------------------------+
+------+如果maxattr不为0,同时parallel_ops为假 |
| |那么为attrbuf字段分配内存,大小为maxattr+1个指针,指针类型为struct nlattr|
| +-------------------------------------------------------------------------+
| 内核对数据包的处理位于系统调用也就是进程上下文,是有能力并发的,但是genl提供了串行的选项,
| 当串行时,就可以利用attrbuf作为该协议所有请求的genl_info中attrs字段的缓存,反复利用。
| 这样就避免了多余的内存分配。genl_info的填充将在数据包分发中介绍。
|
| +---------+
+------+idr_alloc|
| +---------+
| 为genl_family的id字段,分配一个generic netlink协议内唯一的id。
| 这个id用于唯一定位一个自定义协议
|
| +-----------------------------+
+------+genl_validate_assign_mc_group|
| +----+------------------------+
| |
| | +-------------------------------------------------------+
| +--------+根据genl_family中n_mcgrps遍历mcgrps字段, |
| | |确保每个多播组名字字符串长度大于0小于16(包含结尾'\0')|
| | +-------------------------------------------------------+
| |
| | +--------------------------------+
| +--------+如果传入的是内核预留的几个协议 |
| | |那么做特殊处理,确定使用的组播id|
| | +--------------------------------+
| |
| | +----------------------------+
| +--------+genl_allocate_reserve_groups|
| | +----------------------------+
| | 组播涉及三个全局变量
| | mc_groups是组播位图
| | mc_groups_longs是组播位图占用了几个long类型
| | mc_group_start没啥大用,只是mc_groups初始指向的地址,占用一个long
| | 这个函数在mc_groups位图中按顺序查找未被占用的连续n_mcgrps位,
| | 如果位图空间不足则会扩充mc_groups空间,直到找到连续的n_mcgrps位,并标记为占用。
| | 然后将起始的位数传递回去。
| |
| | +----------------------------------------------+
| +--------+上一步传回的组播组起始id赋值给mcgrp_offset, |
| | |这个值与n_mcgrps配合记录了该协议的所有组播组id|
| | +----------------------------------------------+
| |
| | +------------------------+
| +--------+__netlink_change_ngroups|
| | +------------------------+
| | 这个函数会比较传入的组播组数与nl_table中相应的组播组数,
| | 在需要时更新nl_table中的组播位图空间。
| | 这里在genl_family字段netnsok时遍历了所有的net namespace,
| | 但nl_table是全局数组,这个遍历相比只执行一次应该不会有额外的作用。
| |
| | +--------------------------------+
|<----------+--------+没有出错的情况下,函数可以返回了|
| +--------------------------------+
| 可以看到generic netlink的组播完全依赖netlink的组播机制,
| generic netlink只是自己管理了自定义协议组播组id的分配,
| 然后根据id的分配情况,扩充netlink的组播组位图。
|
| +---------------+
+------+genl_ctrl_event|
| +---------------+
| 生成一个CTRL_CMD_NEWFAMILY事件组播发送给预留组播组GENL_ID_CTRL
| 为每一个新增的组播组生成一个CTRL_CMD_NEWMCAST事件发送给预留组播组GENL_ID_CTRL
|
v
返回

单播数据包分发

通过前面的注册流程可以看到generic netlink组播依赖netlink的组播机制,这里继续看单播数据包的是如何分发的。之前分析过netlink内核收包流程,这里直接分析generic netlink在内核的收包处理函数genl_rcv

genl_rcv 流程
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
+--------+
|genl_rcv|
+---+----+
|
| +-----------------------------------------------------------+
| | netlink_rcv_skb(skb, &genl_rcv_msg) |
| | |
| |这个函数中做了一些对netlink消息头nlmsghdr的合法性检查, |
+---+之前说过netlink协议单纯发数据并不依赖nlmsghdr头, |
| |但在generic netlink使用中,必须遵循generic netlink的要求, |
| |因此nlmsghdr和genlmsghdr头都是必须包含且填写正确的。 |
| +--+--------------------------------------------------------+
| |
| | +-------------------------------------------------+
| +---+根据skb数据长度循环处理每个nlmsghdr及其包含的数据|
| | +--+----------------------------------------------+
| | |
| | | +--------------------------------------------------------------------------------+
| | | |检查skb剩余数据长度是否足够一个NLMSG_HDRLEN |
| | | |检查nlmsg_len是否足够NLMSG_HDRLEN |
| | +---+检查skb剩余数据长度是否足够nlmsg_len |
| | | |检查nlmsg_flags是否包含NLM_F_REQUEST |
| | | |检查nlmsg_type是否小于NLMSG_MIN_TYPE(因为小于这个值的都是控制消息,不应主动发送)|
| | | +--------------------------------------------------------------------------------+
| | |
| | | +--------------------------+
| | +---+调用传入的函数指针,实际为|
| | | | genl_rcv_msg |
| | | +--+-----------------------+
| | | |
| | | | +-----------------------------------------------------------------+
| | | | | genl_family_find_byid |
| | | +---+ |
| | | | |以nlmsghdr字段nlmsg_type作为id查找之前是否有注册过genl_family结构|
| | | | +-----------------------------------------------------------------+
| | | |
| | | | +--------------------------------------------------------------+
| | | | | genl_family_rcv_msg(family, skb, nlh) |
| | | +---+ |
| | | | |如果family中parallel_ops为假,这个函数执行前后会有全局锁的操作|
| | | | |使所有不支持并行操作的family中所有操作全局串行 |
| | | | +--+-----------------------------------------------------------+
| | | | |
| | | | | +-----------------------------------------------------------------+
| | | | | |如果family字段netnsok为假,检查skb来源net namesapce是不是init_net|
| | | | | | |
| | | | | |检查nlh空间是否足够包含GENL_HDRLEN和family的自定义消息头hdrsize |
| | | | | | |
| | | | +---+检查family是否有处理指定cmd的genl_ops |
| | | | | | |
| | | | | |如果ops字段flags包含GENL_ADMIN_PERM, |
| | | | | |检查在init_user_ns是否有CAP_NET_ADMIN权限 |
| | | | | | |
| | | | | |如果ops字段flags包含GENL_UNS_ADMIN_PERM |
| | | | | |检查在net所属user_namespace是否有CAP_NET_ADMIN权限 |
| | | | | +-----------------------------------------------------------------+
| | | | |
| | | | |
| | | | | +---------------------------------------------------------------+
| | | | | |如果nlmsg_flags包含NLM_F_DUMP,将进入一块独立的处理逻辑, |
| | | | | |这里涉及struct genl_ops中的dumpit和done函数。下面小节单独介绍。|
| | | | +---+ |
| | | | | |该块处理逻辑完成后直接从函数genl_family_rcv_msg返回, |
| | | | | |可以看出dumpit和doit是并列且有优先级的,dumpit条件满足则运行, |
| | | | | |dumpit条件不满足才运行doit。 |
| | | | | +---------------------------------------------------------------+
| | | | |
| | | | |
| | | | | +-----------------------------------------------------+
| | | | +---+检查ops字段doit是否为NULL,这里马上就要进入主要工作了|
| | | | | +-----------------------------------------------------+
| | | | |
| | | | | +-----------------------------------------------------------------+
| | | | | |如果注册时声明了非并行操作,说明family字段attrbuf已经分配过内存了|
| | | | +---+使用该字段作为genl_info的attrs缓存。 |
| | | | | | |
| | | | | |如果注册时声明了并行操作,也就没有共用的缓存了,这时要分配内存 |
| | | | | +-----------------------------------------------------------------+
| | | | |
| | | | | +-----------------------------------------------------------------+
| | | | | | nlmsg_parse -> nla_parse |
| | | | | |这个函数的目的是校验nlattr,同时为genl_info中的attrs做预处理 |
| | | | | | |
| | | | | |这里会遍历每一个nlattr, |
| | | | +---+对于nla_tyoe大于0小于等于genl_family字段maxattr的nlattr, |
| | | | | |如果ops存在policy,会使用该policy校验nlattr是否符合要求, |
| | | | | |最后将nlattr的指针填充近上一步所说的缓存指定槽位中, |
| | | | | |如果在最外层存在相同nla_type的nlattr,后面的将覆盖槽位中的指针 |
| | | | | |这里填充的指针数组,会设置给genl_info作为nlattr的简单访问入口 |
| | | | | |但如果你希望用更复杂的结构,就需要自己解析nlh了,不过我不会这么做|
| | | | | |这里的实现也说明了,不要把nla_type的类型设置为0 |
| | | | | +-----------------------------------------------------------------+
| | | | |
| | | | | +---------------------------------------------------------------+
| | | | | |填充genl_info结构 |
| | | | | | |
| | | | | |snd_seq是nlmsg_seq |
| | | | | |snd_portid是发送方portid |
| | | | +---+nlhdr是nlmsghdr结构指针 |
| | | | | |genlhdr是genlmsghdr结构指针 |
| | | | | |userhdr是generic netlink注册时标明长度的自定义头的地址 |
| | | | | |attrs是上一步填充的nlattr指针数组 |
| | | | | |_net是skb所属sk的net namespace,取这个值使用函数genl_info_net |
| | | | | |user_ptr置0,这个东西应该是给用户自己使用的,内核不关心如何使用|
| | | | | +---------------------------------------------------------------+
| | | | |
| | | | |
| | | | | +------------------------------------------------------------------+
| | | | | | |
| | | | | | ops->doit(skb, &info) |
| | | | +---+ |
| | | | | | 终于运行doit函数指针了 |
| | | | | | 如果ops字段pre_doit和post_doit不为NULL,在doit前后还会运行这两个 |
| | | | | | |
| | | | | +------------------------------------------------------------------+
| | | | |
| | | | |
| | | | | +-----------------------------------+
| | | | +---+如果之前分配了attrs内存,这里释放掉|
| | | | | +-----------------------------------+
| | | | |
| | |<-----+<-----+
| | |
| | | +--------------------------------------------------------------+
| | +---+如果nlmsg_flags包含NFM_F_ACK或者上一步对nlmsghdr的处理返回错误|
| | | |将调用netlink_ack返回一个netlink消息,包含nlmsgerr结构 |
| | | +--------------------------------------------------------------+
| | |
| | | +---------------------------+
|<-----+<-----+<--+全部nlmsghdr遍历完成后返回0|
| +---------------------------+
|
v
完成

NLM_F_DUMP 请求处理

dump的处理涉及一个结构体struct netlink_callback

struct netlink_callback
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
struct netlink_callback {
/* 当次dump请求所在的skb */
struct sk_buff *skb;
/* 档次dump请求所在的netlink消息指针 */
const struct nlmsghdr *nlh;
/* dumpit函数指针。如果不支持并行这里将会是genl_lock_dumpit,使用锁包裹dumpit执行。 */
int (*dump)(struct sk_buff * skb,
struct netlink_callback *cb);
/* done函数指针。如果不支持并行这里将会是genl_lock_done,使用锁包裹done执行。 */
int (*done)(struct netlink_callback *cb);
/* netlink协议自定的数据。generic netlink这里固定填充genl_ops */
void *data;
/* the module that dump function belong to */
struct module *module;
u16 family;
/* dumpit最小需要的空间,generic netlink中为0,使用默认大小 */
u16 min_dump_alloc;
/**
* 用于检查在一次完整的dump中,是否有意外导致中断而插入了新的dump请求
* 如果检查出错会在netlink消息中加入标记NLM_F_DUMP_INTR。
*
* 该检查由函数nl_dump_check_consistent完成,
* 但是generic netlink中由于使用错误,该检查无效。
* 也由于generic netlink中不允许一个socket并发多个dump请求,因此该检查并不需要。
* 正确的使用方式参考函数nl_dump_check_consistent的注释
*/
unsigned int prev_seq, seq;
/* 用户自由使用,可以在一次完整dump的多次调用时保存状态 */
long args[6];
};

dumpitdone函数中作为参数传递的struct netlink_callback类型指针指向的实际是struct netlink_sock中成员cb,同时nlk成员cb_running标记了一次dump正在运行中,如果在一次dump未完成时有另外的dump请求到来将返回-EBUSY,注意这里仅仅阻止同一个socket的并发dump,不同socket间并不影响。

dump部分的调用栈为

genl_family_rcv_msg -> __netlink_dump_start -> netlink_dump

  • __netlink_dump_start
    负责检查nlk的cb_running判断是否有dump未完成,填充cb各成员,并调用netlink_dump

  • netlink_dump
    分配了一个skb准备交给dumpit函数填充dump内容,调用cb->dump也就是dumpit。这里对dumpit返回值的判断值得注意:

    • 返回值大于0
      含义为当次dump未结束,需要后续继续调用dumpit,因此直接发送skb,并不会修改cb_running状态。
    • 返回值小于等于0
      含义为当次dump已经完成,0为正常完成,负数为错误码的负值。由于当次dump已经完成,因此会在skb中添加一个类型为NLMSG_DONE的netlink消息用于向用户程序标记当次dump已经完成,并修改cb_running为false,释放cb保存的相关资源。

通过上面的分析,可以看到一次完整的dump可能不会在该次请求的上下文中完成,因此会有其他位置继续完成该次dump过程。这个位置是函数netlink_recvmsg,也就是用户程序后续接收消息时将会继续dump过程,该调用栈为:

netlink_recvmsg -> netlink_dump

如果一个dump未完成,而对应的socket已经关闭,则不会继续调用dumpit,而是在close时调用一次done函数通知内核该次dump到此为止。该调用栈为:

netlink_release中调用call_rcu传入deferred_put_nlk_sk

deferred_put_nlk_sk -> sock_put -> sk_free -> __sk_free -> sk_destruct -> __sk_destruct -> netlink_sock_destruct
其中netlink_sock_destruct是由函数指针sk->sk_destruct调用的。该函数指针在__netlink_create中被设置,参考netlink相关部分

通过dump部分内核代码分析,及参考controller中ctrl_dumpfamily的实现,可以推荐dumpit函数实现方式。

  • 使用cb->args数组做状态保存。
    • 如果对dump存在cb->args之外的资源分配和状态保存,需要在done函数中做清理。
    • 如果对dump没有cb->args之外的资源分配和状态保存,则可以不使用done函数。
  • 如果有数据需要dump,填充到skb中。
    • 如果skb空间不足,则填充一部分数据,并返回skb中数据长度。
    • 如果skb空间充足,则填充数据,同样返回skb中数据长度。
  • 如果不再需要填充数据,返回0。

这种实现方式的好处是,最后一次调用dumpit时不会填充skb,因此会保留足够的空间给NLMSG_DONE类型的netlink消息使用,不会因为空间不足而导致失败,内核对NLMSG_DONE类型netlink消息空间不足的处理并不友好。

由于此小节是后续补充的内容,因此代码片段单独列出供参考。

dumpit内核部分测试代码片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int add_dumpit(struct sk_buff *skb, struct netlink_callback *cb) {
void *hdr;
int i;
pr_info("%s\n", __FUNCTION__);

i = cb->args[0] ++;
if (i < 5) {
hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid, cb->nlh->nlmsg_seq, &family,
NLM_F_MULTI, MY_CMD_ADD);
if (hdr == NULL) {
goto err_out;
}
// 第i次调用保留i字节nla数据空间用于验证
if (nla_reserve(skb, MY_ATTR_N1, i) == NULL) {
goto err_out;
}
genlmsg_end(skb, hdr);
}
return skb->len;

err_out:
genlmsg_cancel(skb, hdr);
return -ENOBUFS;
}
dumpit用户态测试代码片段
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
int test_dump(unsigned int family_id, int fd) {
struct nlmsghdr *nlh;
struct genlmsghdr *glh;
char buf[1500];
struct sockaddr_nl dest_addr;

int len = 0;
int err;

// + genelmsghdr + pad 长度
len += GENL_HDRLEN;
// + nlmsghdr + pad 长度
len = NLMSG_SPACE(len);

nlh = (struct nlmsghdr *)buf;
glh = NLMSG_DATA(buf);

nlh->nlmsg_len = len;
nlh->nlmsg_type = family_id;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
// 用于判断请求响应对应情况,先随便填一个。
nlh->nlmsg_seq = 302;
nlh->nlmsg_pid = 0;

glh->cmd = MY_CMD_ADD;
glh->version = 1;

memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0;
dest_addr.nl_groups = 0;

err = sendto(fd, buf, len, 0, (struct sockaddr *)&dest_addr, (socklen_t)sizeof(dest_addr));
if (err == -1) {
perror("sendto failed");
return -1;
}

while (1) {
len = recv(fd, buf, sizeof(buf), 0);
printf("recv len %d\n", err);
parse_nl_msg((struct nlmsghdr *)buf, len);
}

return 0;
}

controller服务

controller的工作是提供名字服务,提交genl_family的name,返回相关信息,最主要的就是family的id。

从下面的数据结构可以看到controller服务仅接受一个cmd,也就是CTRL_CMD_GETFAMILY,与用户自定义协议不同之处在于这里id是固定的,因为controller服务是首个服务,也只有固定id,才能让后面的查询请求可以准确发送到controller,才能完成名字查询服务。具体流程这里不描述了,返回内容格式可以参考例子代码中用户态解析

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
static struct genl_ops genl_ctrl_ops[] = {
{
.cmd = CTRL_CMD_GETFAMILY,
.doit = ctrl_getfamily,
.dumpit = ctrl_dumpfamily,
.policy = ctrl_policy,
},
};

static struct genl_multicast_group genl_ctrl_groups[] = {
{ .name = "notify", },
};

static struct genl_family genl_ctrl = {
.module = THIS_MODULE,
.ops = genl_ctrl_ops,
.n_ops = ARRAY_SIZE(genl_ctrl_ops),
.mcgrps = genl_ctrl_groups,
.n_mcgrps = ARRAY_SIZE(genl_ctrl_groups),
.id = GENL_ID_CTRL,
.name = "nlctrl",
.version = 0x2,
.maxattr = CTRL_ATTR_MAX,
.netnsok = true,
};