Netlink用于内核与用户空间程序传输信息,同时也可用于用户进程间通信。它包含用户空间下基于socket的标准接口,以及用于内核模块的内核API。iproute2工具包使用该方式从用户空间与内核进行通信。这里介绍netlink的简单使用方式与其实现原理。

本文内容参考内核版本 3.10.0-862.el7.x86_64

例子代码

先看一个简单例子的代码

ps: 自定义netlink协议不推荐直接占用32个netlink协议号中的未使用号码,后面会单独文章介绍generic netlink的使用。

nl_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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

#include <linux/skbuff.h>
#include <linux/netlink.h>
#include <linux/netdevice.h>

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


#define MY_NETLINK_TYPE 26

struct sock *my_nl_sock = NULL;

static void nl_input(struct sk_buff *skb) {

pr_info("recv msg len: %d\n", skb->len);

if (skb->data[skb->len - 1] == '\0')
pr_info("recv msg str: %s\n", skb->data);

pr_info("portid : %d\n", NETLINK_CB(skb).portid);
pr_info("dst_group: %d\n", NETLINK_CB(skb).dst_group);

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


static int __init my_init(void)
{
struct netlink_kernel_cfg cfg = {
.groups = 0,
.input = nl_input,
};
pr_info("my init.\n");

my_nl_sock = netlink_kernel_create(&init_net, MY_NETLINK_TYPE, &cfg);
if (!my_nl_sock) {
pr_info("nl create failed\n");
pr_info("====================\n");
return -1;
}

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

static void __exit my_exit(void)
{
pr_info("my exit.\n");
if (my_nl_sock) {
netlink_kernel_release(my_nl_sock);
}
pr_info("====================\n");
}

module_init(my_init);
module_exit(my_exit);
nl_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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <sys/types.h>
#include <unistd.h>

#define MY_NETLINK_TYPE 26

int main() {

struct sockaddr_nl src_addr, dest_addr;
char msg[] = "hello from client.";
int err;
int fd;

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

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

/* bind */
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid();
src_addr.nl_groups = 0;

err = bind(fd, (struct sockaddr *)&src_addr, sizeof(src_addr));
if (err == -1) {
perror("bind 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, msg, sizeof(msg), 0, (struct sockaddr *)&dest_addr, (socklen_t)sizeof(dest_addr));
if (err == -1) {
perror("sendto failed");
return -1;
}

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

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

EXTRA_CFLAGS += -Wall -g

all: kernel nl_user

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

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

clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
rm -f nl_user

用户态

可以与PF_INET协议族一样使用通用socket api,甚至限制更少,比如sendto的目的地址可以设置为NULL,默认目的nl_pid和nl_groups都为0,不过仍然建议显式设置。

地址

netlink socket地址结构体为sockaddr_nl

1
2
3
4
5
6
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* port ID */
__u32 nl_groups; /* multicast groups mask */
};
  • nl_family
    固定为AF_NETLINK
  • nl_pid
    bind操作时用作本地绑定的单播源地址,用作唯一不重复的本地端口id,可以自由设置。如果设置为0,内核将自动分配一个id,优先pid,如果冲突了将会分配一个不冲突的id。
    connectsend类操作时,用作单播目的地址。内核的目的地址端口固定为0。
  • nl_groups
    bind操作时,用于表示该socket接收的组播消息的组位图,每一个非0位代表一个接收组。
    send类操作时,用于表示发送的组播组位图,但是只有最低非0位代表的一个组播组是有效的,也就是组播消息只能发给一个组。

消息格式

很多文章包括man文件中都说明netlink消息需要包含nlmsghdr头,但实际情况是内核包含的netlink协议通过该协议头解析消息层次。自定义的netlink协议并不一定需要nlmsghdr头,在例子代码中可以看到与普通SOCK_DGRAM类socket一致,可以直接通过socket发送数据。在内核netlink收发代码中也可以验证这一点。

如果要使用nlmsghdr协议头,可以参考include/uapi/linux/netlink.h中的一组宏,帮助定位数据对齐。

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

struct nlmsghdr {
/* 消息长度,包含消息头和payload。不包含payload后部为了对齐所需要的pad。 */
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process port ID */
};

#define NLMSG_ALIGNTO 4U
/* 计算对齐后的长度 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/* nlmsghdr对齐后需要占用的长度 */
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/* 长度为len的数据包含nlmsghdr头后需要占用的长度,注意这里没有包含len后面为了对齐所需要的pad */
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
/* 跟上面那个类似,区别就是增加了len后面为了对齐所需要的pad */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 参数为netlink消息,返回payload地址。 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/* nlh开始的缓冲区长度len,跳过一个netlink消息,返回下一个netlink消息地址,同时len做减法
注意这里没有对nlh重新赋值
**/
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
/* nlh开始的缓冲区长度len以内,至少包含一个合法的netlink消息 */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len <= (len))
/* 返回payload中len字节偏移的指针 */
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

内核态

内核api参考文件include/linux/netlink.h

1
2
3
4
5
static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
{
return __netlink_kernel_create(net, unit, THIS_MODULE, cfg);
}

第一个参数是net namespace网络命名空间的内核结构体,可以理解netlink socket只在该命名空间有效。另外netlink_sock可以有一个flag,NETLINK_F_LISTEN_ALL_NSID,这时就可以接收所有namespace的消息。
第二个参数是netlink协议族下具体的协议号,在include/uapi/linux/netlink.h中预定义了一些协议号,并且设置了该协议族下最多允许32个协议号,这个限制用于nl_table的创建。理论上32以内未被占用的协议号可以用于自定义的netlink协议通信,但是这种方式并不规范,而且对于未来更新版本内核可能对协议号的占用兼容性不好,更合适的自定义netlink通信的方式是使用generic netlink,也就是编号为16的NETLINK_GENERIC通信,后面会有单独文章介绍。
第三个参数是一个配置用的结构体,定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {
/* 支持的组播组数。并不是该内核netlink socket接收组播的位图,最终会使用该字段与32中的较大值确定组播组数 */
unsigned int groups;
/* 设置该netlink协议是否允许非特权用户发送组播消息或接收组播消息 */
unsigned int flags;
/* 回调函数,发送给内核的消息将回调该函数处理 */
void (*input)(struct sk_buff *skb);
/* 锁,用于netlink_dump,具体使用场景暂时不关注 */
struct mutex *cb_mutex;
/* 回调函数,当有netlink socket绑定到组播组时回调该函数通知,参数为组播号 */
int (*bind)(int group);
/* 回调函数,当有netlink socket解绑组播组时回调该函数通知,参数为组播号 */
void (*unbind)(int group);
/* 我看的内核版本中对该函数指针没有使用,应该是内核高版本功能移植时导致的。更新版本内核暂时不关注。 */
bool (*compare)(struct net *net, struct sock *sk);
};

用户态程序发送消息给内核netlink socket时,上一步配置的input回调函数将被调用。注意这里位于系统调用的上下文,也就是进程上下文。

参考include/net/netlink.h中的注释。

单模使用netlink_unicast,实现参考用户态sendto的单播实现

组播使用netlink_multicast,这只是netlink_broadcast的简单包装,实现参考用户态sendto的组播实现

先看一下netlink socket在内核中的实现的结构关系。

abc

内核在启动阶段完成netlink协议的初始化,函数为netlink_proto_init,重点为两点。

  • nl_table的内存分配与哈希表初始化。
  • 为用户态netlink socket的创建提供支持,具体为将netlink_family_ops注册到net_families[PF_NETLINK]上,这里的create函数指针(也就是函数netlink_create将在创建socket的系统调用中发挥作用)。

函数最后包含的rtnetlink_init可以作为内核netlink api使用方式的参考。

netlink_family_ops定义
1
2
3
4
5
6
7
8
9
10
11
12
struct net_proto_family {
int family;
int (*create)(struct net *net, struct socket *sock,
int protocol, int kern);
struct module *owner;
};

static const struct net_proto_family netlink_family_ops = {
.family = PF_NETLINK,
.create = netlink_create,
.owner = THIS_MODULE, /* for consistency 8) */
};
netlink协议初始化流程
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
+------------------+
|netlink_proto_init|
+---------+--------+
|
| +--------------+
+---+proto_register|
| +--------------+
| 参数为netlink_proto,将其注册到proto_list全局链表上,
| 作用是proc文件系统上/proc/net/protocols显示支持的协议。
|
|
+--- nl_table全局数组分配内存,并初始化每一个协议项的哈希表。
| netlink的操作围绕这个全局数组展开。
|
|
+--- 初始化链表头netlink_tap_all,用于挂载netlink_tap结构体。
| 看起来是用于监听,这里不做重点跟进。
|
| +--------------------------+
+---+netlink_add_usersock_entry|
| +--------------------------+
| 初始化了nl_table[NETLINK_USERSOCK]项。
| 从代码看应该是用于用户态进程间通信预留的,因为并没有内核接收函数。
|
| +-------------+
+---+sock_register|
| +-------------+
| 参数为netlink_family_ops,实际为net_families[PF_NETLINK]赋值netlink_family_ops。
| 作为用socket系统调用时,为该协议族指定创建时调用的函数指针,这里为netlink_create
|
| +----------------------+
+---+register_pernet_subsys|
| +----------------------+
| 为每个net namespace注册proc文件系统节点
|
| +--------------+
+---+rtnetlink_init|
| +--------------+
| 初始化rtnetlink,用于网络相关配置操作。这也是netlink出现的最初目的。
| 关于内核netlink的正确使用方式,可以参考这里。
|
v
返回

内核代码使用netlink_create_kernel函数创建netlink socket。实现重点是:

  • 创建关联的struct socket和struct sock,这里sock实际空间类型为struct netlink_sock。
  • 将netlink_sock插入nl_table该协议项的哈希表中。
  • 将参数中cfg配置填充入nl_table该协议项内和netlink_sock中。
内核netlink socket创建流程
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
+---------------------+
|netlink_kernel_create|
+----------+----------+
|
| +-----------------------+
+---+__netlink_kernel_create|
| +-----------+-----------+
| |
| | +----------------+
| +----+sock_create_lite|
| | +----------------+
| | 创建struct socket结构
| |
| | +----------------+
| +----+__netlink_create|
| | +----------------+
| | socket的ops赋值为netlink_ops,
| | 创建struct sock结构,
| | 实际分配空间为struct netlink_sock
| | 内部有调用sock_init_data初始化sock和sk内部字段,比如sk_data_ready函数指针
| |
| | +----------------------------------------------------------------+
| | |更新sk关联的net namespace |
| | |确定最终使用的组播组数,取cfg->groups与32中的较大值 |
| | |为组播位图listeners分配内存 |
| | | |
| | |设置sk字段函数指针sk_data_ready为netlink_data_ready |
| +----+这个函数在内核netlink_sock中是无用的,因为内核收消息流程的原因。|
| | |可以看到是一个空函数 |
| | | |
| | |设置nlk(netlink_sock)字段函数指针netlink_rcv为cfg中input函数指针|
| | |这将在内核收消息时被调用 |
| | +----------------------------------------------------------------+
| |
| | +--------------+
| +----+netlink_insert|
| | +-------+------+
| | |
| | |
| | +----> 设置netlink_sock的portid
| | |
| | | +----------------+
| | +-----+__netlink_insert|
| | | +----------------+
| | | struct sock插入nl_table特定表项的哈希表中
| | |
| | +----> 设置netlink_sock的bound为portid
| | |
| |<-----------+
| |
| | +----------------------------------------------------------------------------------+
| | |将struct sock指针转换为netlink_sock指针 |
| +----+设置flags增加标记NETLINK_F_KERNEL_SOCKET |
| | |标记这是一个内核netlink_sock,因为内核和用户态netlink_sock对接收消息的处理方式不同|
| | +----------------------------------------------------------------------------------+
| |
| | +---------------------------------------------------------+
| +----|nl_table特定表项根据struct netlink_kernel_cfg类型参数填充|
| | +---------------------------------------------------------+
|<--------------+
|
v
返回struct sock结构指针

用户态socket创建调用了net_families中该协议族的create函数指针。netlink初始化时为PF_NETLINK协议族设定了函数指针为netlink_create函数。在这里为socket的ops指针赋值为netlink_ops,这里的函数指针涉及针对socket的系统调用,定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static const struct proto_ops netlink_ops = {
.family = PF_NETLINK,
.owner = THIS_MODULE,
.release = netlink_release,
.bind = netlink_bind,
.connect = netlink_connect,
.socketpair = sock_no_socketpair,
.accept = sock_no_accept,
.getname = netlink_getname,
.poll = datagram_poll,
.ioctl = sock_no_ioctl,
.listen = sock_no_listen,
.shutdown = sock_no_shutdown,
.setsockopt = netlink_setsockopt,
.getsockopt = netlink_getsockopt,
.sendmsg = netlink_sendmsg,
.recvmsg = netlink_recvmsg,
.mmap = sock_no_mmap,
.sendpage = sock_no_sendpage,
};
用户态netlink socket创建流程
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
+--------------+
|socket系统调用|
+-------+------+
|
| +-----------+
+---+sock_create|
| +--+--------+
| |
| | +-------------+
| +--+__sock_create|
| +------+------+
| |
| | +----------+
| +--+sock_alloc| 分配struct socket
| | +----------+
| |
| | +--------------------------------+
| | |net_families[PF_NETLINK]->create|
| +--+ |
| | | 实际调用函数为netlink_create |
| | +-----+--------------------------+
| | |
| | | +----------------+
| | +-----+__netlink_create|
| | | +----------------+
| | | socket的ops赋值为netlink_ops,
| | | 创建struct sock结构,
| | | 实际分配空间为struct netlink_sock
| | | 内部有调用sock_init_data初始化sock和sk内部字段,比如sk_data_ready函数指针
| | |
| | |
| | | +----------------------------+
| | +-----+设置netlink_sock的回调函数,|
| | | |netlink_bind,netlink_unbind|
| | | +----------------------------+
| |<-------+
|<---------------+
|
|
| +-----------+
+---+sock_map_fd| struct socket结构映射文件描述符fd
| +-----------+
|
返回fd

看过前面的内容,用户态bind操作就好理解了,系统调用处理函数中调用socket的ops->bind函数指针,对于netlink协议族来说就是函数netlink_bind。这里涉及有组播的配置操作,注意区分位图与组播组数的区别就好。
bind完成后,内核态和用户态netlink socket的结构关系就基本配置完成了。

用户态netlink socket bind流程
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
+------------+
|bind系统调用|
+-----+------+
|
| +-------------------+
+---+sockfd_lookup_light|
| +-------------------+
| 通过fd查找struct socket结构
|
| +-------------------+
+---+move_addr_to_kernel|
| +-------------------+
| 将用户态内存地址中的地址参数数据拷贝到内核
|
| +-------------------------------+
| |调用socket成员ops->bind函数指针|
+---+ |
| | 实际调用函数为netlink_bind |
| +---------------+---------------+
| |
| | +-----------------------------------------------------------------------+
| | |如果bind地址中nl_groups不为0 |
| | |首先检查了是否允许非特权用户接收组播消息或该用户是否有CAP_NET_ADMIN特权|
| +---+权限检查通过后调用netlink_realloc_groups |
| | |为netlink_sock分配组播位图空间并记录位图支持组个数上限 |
| | +-----------------------------------------------------------------------+
| |
| | +----------------------------------------------------------------------+
| | |如果该netlink_sock之前有过绑定端口, |
| | |这里将会检查传入的nl_pid是否与之前绑定的端口号相同。 |
| +---+ |
| | |可以看到如果需要修改绑定的组播号,可以多次调用bind传入最新需要的组播号|
| | |只是需要注意参数中的nl_pid要与之前绑定的端口号相同 |
| | |如果是自动分配的端口号,可以通过getsockname函数获取已经绑定的端口号 |
| | +----------------------------------------------------------------------+
| |
| | +----------------------------------------------------------+
| +---+如果bind有组播,同时之前的内核netlink设置有bind回调函数 |
| | |对于接收组播的每一位调用回调函数,参数为每一位代表的组序号|
| | +----------------------------------------------------------+
| |
| | +-------------------------------------------------------------------+
| | |netlink_insert将bind地址参数中的nl_pid设置入netlink_sock, |
| +---+同时插入nl_table特定的哈希表中。 |
| | |如果传入的nl_pid为0,将调用netlink_autobind自动分配一个不冲突的值,|
| | |然后调用netlink_insert。 |
| | +-------------------------------------------------------------------+
| |
| | +----------------------------+
| | |netlink_update_subscriptions|
| | +----------------------------+
| | 更新netlink_sock成员subscriptions,记录订阅的组播组数
| | 如果存在订阅组播,需要将struct sock成员sk_bind_node插入到nl_table特定项的mc_list链表中
| |
| |
| +--- 更新组播位图
| |
| | +------------------------+
| +---+netlink_update_listeners|
| | +------------------------+
| | 更新nl_table特定表项成员listeners->masks位图记录了该协议下有哪些组存在被订阅
| |
|<------------------+
|
v
返回成功失败
用户态netlink socket sendto流程
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
+--------------+
|sendto系统调用|
+-----+--------+
|
| +-------------------+
+---+sockfd_lookup_light|
| +-------------------+
| 通过fd查找struct socket结构
|
| +-------------------+
+---+move_addr_to_kernel|
| +-------------------+
| 将用户态内存地址中的地址参数数据拷贝到内核
|
| +---------------------------------------------------------+
+---+组装一个struct msghdr结构,要发送的数据地址和长度填充其中|
| +---------------------------------------------------------+
|
| +------------------------------------------------------+
| |sock_sendmsg -> __sock_sendmsg -> __sock_sendmsg_nosec|
+---+内部调用socket成员ops->sendmsg,实际调用函数为 |
| | netlink_sendmsg |
| +---+--------------------------------------------------+
| | 这里忽略安全相关操作,不做分析
| |
| | +--------------------------------------------------------------------------+
| +------+选择目标portid和group, |
| | |如果传入的目标地址为空则使用netlink_sock结构成员(如果没有connect过应该是0)|
| | +--------------------------------------------------------------------------+
| |
| | +------------------------------------------------------+
| +------+如果没有bind过,调用netlink_autobind自动bind一个portid|
| | +------------------------------------------------------+
| |
| | +-----------------------+
| +------+netlink_alloc_large_skb|
| | | 分配一个skb内存 |
| | +-----------------------+
| |
| | +-----------------------------------------------------+
| +------+使用NETLINK_CB取得skb中的control buffer |
| | |写入当前netlink_sock的portid、dst_group、creds、flags|
| | +-----------------------------------------------------+
| |
| | +-------------------------------+
| +------+memcpy_from_msg |
| | |从用户态内存地址复制数据到skb中|
| | +-------------------------------+
| |
| | +--------------------------------------------------+
| | |如果目标地址包含组播组,首先增加skb->users引用计数|
| | |(这个引用计数将在广播完成后由函数内部消费掉) |
| | | |
| | | netlink_broadcast做广播,但是广播的返回值并不在意|
| | | |
| | +--------------------------------------------------+
| |
| | +--------------------------+
| | | |
| +------+ netlink_unicast做单播 |
| | | |
| | | 单播返回值作为函数返回值 |
| | | |
| | +--------------------------+
| |
|<------+
|
v
返回成功失败

函数定义为
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 portid, int nonblock)

  • ssk
    发送源的struct sock结构。
  • skb
    需要发送的数据。
  • portid
    目标portid。
  • nonblock
    标记是否阻塞。(如果使用阻塞方式,将取ssk的sk_sndtimeo字段作为超时时间,且可能会引起调度。如果在中断和软中断上下文中使用需要注意)。
netlink_unicast 流程
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
+---------------+
|netlink_unicast|
+-------+-------+
|
| +------------+
+---+netlink_trim|
| +------------+
| 对于使用物理内存且空闲空间超过一半的skb,做裁剪操作。
|
| +------------+
+---+取得超时时间|
| +------------+
|
| +-----------------------+
+---+netlink_getsockbyportid|
| +-----------------------+
| 取得目标struct sock结构。返回已经sock_hold增加过引用计数sk_refcnt的struct sock结构。
| 通过ssk的协议号和net namespace选择同协议同namespace下目标portid的struct sock结构。
| 如果目标sk状态为NETLINK_CONNECTED且netlink_sock成员dst_port不等于ssk的port,则返回connection refused错误。
| netlink_sock和sock的结构关系见图。
|
| +----------------------------------------------------------------+
| |netlink_is_kernel判断上一步拿到的sk是否是内核sk |
+---+方式就是判断netlink_sock字段flags是否包含NETLINK_F_KERNEL_SOCKET|
| |这个标记在内核函数netlink_kernel_create中被设置。 |
| +---------------+-------------------------------------------+----+
| | |
| 如果是内核sk| 如果不是内核sk|
| | |
| v |
| +-------------------------------------------+ |
| |netlink_unicast_kernel函数处理 | |
| |然后返回,netlink_unicast结束 | |
| | | |
+-------+如果netlink_sock没有netlink_rcv函数指针 | |
| |则释放skb,并释放sk引用计数 | |
| | | |
| |如果有设置netlink_rcv函数指针则进行以下流程| |
| |(该函数指针是netlink_kernel_create传入的) | |
| +-------------------------------------------+ |
| | |
| | +-----------------------------------------+ |
| | |netlink_skb_set_owner_r | |
| +---+将skb->sk字段设置为目标sk并设置destructor| |
| | |修改目标sk缓冲区相关成员 | |
| | +-----------------------------------------+ |
| | |
| | +-----------------------------------+ |
| +---+NETLINK_CB(skb).sk设置为源sk,既ssk| |
| | +-----------------------------------+ |
| | |
| | +---------------------------------+ |
| +---+调用netlink_rcv函数指针处理skb | |
| | |而后释放skb,释放sk引用计数,返回| |
| | +---------------------------------+ |
|<--------+ |
| |
| |
| +-------------------------------------------------------+
| |
| | +--------------------------------------------------------------+
| +----+sk_filter判断是否阻断后续处理,不确定具体情况,可能与ebpf有关 |
| | +--------------------------------------------------------------+
| |
| | +--------------------------------------------------------------------+
| | |netlink_attachskb |
| | |判断sk剩余缓冲区是否满足大小,如果有超时设置可能会引发调度。 |
| +----+ |
| | |如果大小满足,调用netlink_skb_set_owner_r,将skb->sk字段设置为目标sk|
| | |并设置destructor和修改目标sk缓冲区相关成员 |
| | +--------------------------------------------------------------------+
| |
| | +----------------------------------------------------------------+
| +----+根据上一步返回值判断是出错还是从netlink_getsockbyportid开始重试 |
| | |如果正常,返回值为0,进行下一步。 |
| | +----------------------------------------------------------------+
| |
| | +---------------+
| +----+netlink_sendskb|
| | +----+----------+
| | |
| | | +-----------------+
| | +-----+__netlink_sendskb|
| | | +-+---------------+
| | | |
| | | | +-------------------+
| | | +----+netlink_deliver_tap|
| | | | +-------------------+
| | | | tap流程不关注
| | | |
| | | | +--------------+
| | | +----+skb_queue_tail|
| | | | +--------------+
| | | | 将skb挂到sk字段sk_receive_queue上,这个函数使用了保存中断的自旋锁
| | | |
| | | | +-------------------------------------------------+
| | | +----+调用sk->sk_data_ready函数指针,参数为sk和数据长度|
| | | | +-------------------------------------------------+
| | |<------+
| | |
| | | +-----------------------------------------------------+
| | +-----+sock_put释放sk引用计数,如果不再有引用则sk_free释放sk|
| | | +-----------------------------------------------------+
| | |
| |<--------+
| |
|<------+
|
v
返回

函数定义为

int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 portid, u32 group, gfp_t allocation)

  • ssk
    发送源的struct sock结构。
  • skb
    需要发送的数据。
  • portid
    单播目标portid,作为组播处理函数的参数是为了避免重复发送。
  • group
  • allocation
netlink_broadcast 流程
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

+----------------------------------------------------------------------------------------+
|netlink_broadcast,直接调用netlink_broadcast_filterd(后两个参数过滤函数指针及数据置NULL)|
+----------------------------------------------------------------------------------------+
|
| +------------+
+---+netlink_trim|
| +------------+
|
| +--------------------------------------------+
+---+填充netlink_broadcast_data结构,变量命名info|
| +--------------------------------------------+
| 为了后续函数调用参数整洁,创建了这个结构体传递组播使用到参数及需要返回检查的多个值
| 该结构体也只有这一个使用位置。
|
| +---------------------------------------------------------------------+
| |遍历nl_table数组中该协议号成员的mc_list(这里链接了所有订阅了组播的sk)|
+---+ |
| | 对每个sk调用do_one_broadcast(sk, &info) |
| +---+-----------------------------------------------------------------+
| |
| | +-----------------------------------------------------------------------+
| | |排除源ssk,排除portid与单播portid相同的, |
| | |排除不包含组播group(这里的组播id从1开始,group为1的组对应位图0位)的sk |
| | | |
| +---+如果sk与ssk的net namespace不同 |
| | | sk对应的nlk字段flags不包含NETLINK_F_LISTEN_ALL_NSID,排除。 |
| | | 调用peernet_has_id比较sk与ssk的net返回0,排除。(这个不懂,暂时忽略)|
| | | 调用file_ns_capable检查网络广播权限返回0,排除。(这个细节暂时忽略) |
| | +-----------------------------------------------------------------------+
| |
| | +-----------------------------------------------------------------+
| +---+检查info->failure字段,如果为真说明之前对skb的skb_clone操作失败了|
| | |如果为真,调用netlink_overrun将出错消息传播给sk并返回 |
| | +-----------------------------------------------------------------+
| |
| | +-----------------------------------------------------------------------------------+
| | |检查info->skb2字段,如果未NULL则由原skb取得一个独立的skb置于skb2字段用于后续广播 |
| +---+取得独立skb的方式有两种,由原skb引用计数值区分: |
| | | 大于1,调用skb_clone克隆一个,可能会失败 |
| | | 等于1,先增加引用计数再调用skb_orphan执行destructor后将其与destructor函数指针剥离|
| | +-----------------------------------------------------------------------------------+
| |
| | +-----------------------------------------------------------------------------------------+
| | |如果info->skb2字段为NULL,说明上一步的操作失败了 |
| | |那么调用netlink_overrun将出错消息传递给sk,并将info->failure字段置1 |
| +---+ 如果sk对应的nlk字段flags包含标记NETLINK_F_BROADCAST_SEND_ERROR |
| | | 则info->delivery_failure置1 |
| | |函数返回 |
| | |(这里的netlink_overrun和failure置1,配合再前一步对failure的检查,完成了对所有sk的出错传播|
| | +-----------------------------------------------------------------------------------------+
| |
| | +----------------------------------------------------------------+
| +---+对传入参数的过滤函数指针调用(在这个调用栈里该参数为NULL,不关注)|
| | +----------------------------------------------------------------+
| |
| | +---------------------------------------------------+
| +---+sk_filter过滤检查,如果未通过将释放skb2并置NULL返回|
| | +---------------------------------------------------+
| |
| | +-------------------------------------------------------------+
| | |在sk的net字段netns_idr中查找ssk的net所对应的id |
| +---+赋值给NETLINK_CB的nsid字段,并为nsid_is_set置真 |
| | |(不明白为什么要为net对应的peer net分配id,取了又是做什么用?)|
| | +-------------------------------------------------------------+
| |
| | +-------------------------------------------+
| | | |
| | | netlink_broadcast_deliver(sk, info->skb2) |
| | | |
| | +---+---------------------------------------+
| | |
| | | +-----------------------------------------------------------------------+
| | +---+判断sk缓冲区是否满足要求及nlk->state字段是否包含标记NETLINK_S_CONGESTED|
| | | |如果不满足要求直接返回-1 |
| | | +-----------------------------------------------------------------------+
| | |
| | |
| | | +-------------------------+
| | | | |
| | | | netlink_skb_set_owner_r |
| | +---+ |
| | | | __netlink_sendskb |
| | | | |
| | | +-------------------------+
| | | 这里是真正的发送部分,
| | | 可以参考单播中同样的函数调用
| | |
| | | +----------------------------+
| |<------+---+返回sk缓冲区是否是否超过一半|
| | +----------------------------+
| |
| | +--------------------------------------------------------------------------------------------+
| | |如果上一步出错,也就是缓冲区不足,将会调用netlink_overrun对sk报错 |
| | | 同时如果nlk->flags包含标记NETLINK_F_BROADCAST_SEND_ERROR |
| | | 则将info->delivery_failure置1 |
| +---+ |
| | |如果上一步正常返回, |
| | | info->congested与返回值做或操作,也就是在遍历中记录是否存在拥塞的sk |
| | | info->delivered置1 |
| | | info->skb2置NULL(也就是说对每个组播中的sk,递交的skb2都是新的独立的,但数据不一定独立)|
| | +--------------------------------------------------------------------------------------------+
| |
| |
|<------+
|
| +---------------------------------+
+---+consume_skb(skb,也就是info->skb)|
| +---------------------------------+
|
| +------------------------------------------+
| | |
| | 如果info->delivery_failure为真 |
+---+ kfree_skb(info.skb2) |
| | 返回-ENOBUFS,也就是因为sk缓冲区不足 |
| | |
| +------------------------------------------+
|
| +----------------------+
+---+consume_skb(info.skb2)|
| +----------------------+
|
| +---------------------------------------------------------------------------------------------+
| | |
| | 如果info->delivered为真,说明存在提交成功的sk,返回0 |
+---+ 如果info->congested为真且内存分配标记包含__GFP_WAIT(也就是说当前允许调度,进程上下文) |
| | 说明存拥塞,那么在返回0前先调用yield释放cpu且会闲置一会 |
| | |
| +---------------------------------------------------------------------------------------------+
|
| +------------------------------------------------+
| | |
+---+ 如果运行到这里了,说明没有合适的接收该组播的sk |
| | 返回-ESRCH,No such process |
| | |
| +------------------------------------------------+
|
v
返回

与前文操作类似,其他系统调用操作在系统调用处理函数中会调用struct socket的ops成员响应的函数指针,跟进代码就可以了。