1.什么是 Netlink
什么是Netlink?Netlink是linux供给的用于内核和用户态进程之间的通讯方法。可是留意尽管Netlink首要用于用户空间和内核空间的通讯,可是也能用于用户空间的两个进程通讯。仅仅进程间通讯有其他许多方法,一般不必Netlink。除非需求用到Netlink的播送特性时。
那么Netlink有什么优势呢?一般来说用户空间和内核空间的通讯方法有三种:/proc、ioctl、Netlink。而前两种都是单向的,可是Netlink能够完成双工通讯。
Netlink协议依据BSDsocket和AF_NETLINK地址簇(addressfamily),运用32位的端口号寻址(曾经称作PID),每个Netlink协议(或称作总线,man手册中则称之为netlinkfamily),一般与一个或一组内核服务/组件相相关,如NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送告知等。netlink具有以下特色:
①支撑全双工、异步通讯(当然同步也支撑)
②用户空间可运用标准的BSDsocket接口(但netlink并没有屏蔽掉协议包的结构与解析进程,引荐运用libnl等第三方库)
③在内核空间运用专用的内核API接口
④支撑多播(因而支撑“总线”式通讯,可完成音讯订阅)
⑤在内核端可用于进程上下文与中止上下文
怎样学习Netlink?我觉得最好的方法便是将Netlink和UDPsocket比照学习。由于他们真的很对当地类似。AF_NETLINK和AF_INET对应,是一个协议族,而NETLINK_ROUTE、NETLINK_GENERIC这些是协议,对应于UDP。
那么咱们首要重视Netlink和UDPsocket之间的不同点,其间最重要的一点便是:运用UDPsocket发送数据包时,用户无需结构UDP数据包的包头,内核协议栈会依据原、意图地址(sockaddr_in)填充头部信息。可是Netlink需求咱们自己结构一个包头(这个包头有什么用,咱们后边再说)。
一般咱们运用Netlink都要指定一个协议,咱们能够运用内核为咱们预留的NETLINK_GENERIC(界说在linux/netlink.h中),也能够运用咱们自界说的协议,其实便是界说一个内核还没有占用的数字。下面咱们用NETLINK_TEST做为咱们界说的协议写一个比方(留意:自界说协议不必定非要添加到linux/netlink.h中,只需用户态和内核态代码都能找到该界说就行)。咱们知道运用UDP发送报文有两种方法:sendto和sendmsg,相同Netlink也支撑这两种方法。下面先看运用sendmsg的方法。
2.用户态数据结构
首要看一下几个重要的数据结构的联系:
2.1structmsghdr
msghdr这个结构在socket变成中就会用到,并不算Netlink专有的,这儿不在过多阐明。只阐明一下怎样更好了解这个结构的功用。咱们知道socket音讯的发送和接纳函数一般有这几对:recv/send、readv/writev、recvfrom/sendto。当然还有recvmsg/sendmsg,前面三对函数各有各的特色功用,而recvmsg/sendmsg便是要包括前面三对的一切功用,当然还有自己特别的用处。msghdr的前两个成员便是为了满意recvfrom/sendto的功用,中心两个成员msg_iov和msg_iovlen则是为了满意readv/writev的功用,而最终的msg_flags则是为了满意recv/send中flag的功用,剩余的msg_control和msg_controllen则是满意recvmsg/sendmsg特有的功用。
2.2Structsockaddr_ln
Structsockaddr_ln为Netlink的地址,和咱们一般socket编程中的sockaddr_in作用相同,他们的结构比照如下。
structsockaddr_nl{}的详细界说和描绘如下:
1
2
3
4
5
6
7
struct sockaddr_nl
{
sa_family_t nl_family; /*该字段总是为AF_NETLINK */
unsigned short nl_pad; /* 现在未用到,填充为0*/
__u32 nl_pid; /* process pid */
__u32 nl_groups; /* multicast groups mask */
};
(1)nl_pid:在Netlink标准里,PID全称是Port-ID(32bits),其首要作用是用于仅有的标识一个依据netlink的socket通道。一般状况下nl_pid都设置为当时进程的进程号。前面咱们也说过,Netlink不只能够完成用户-内核空间的通讯还可使实际用户空间两个进程之间,或内核空间两个进程之间的通讯。该特点为0时一般指内核。
(2)nl_groups:假如用户空间的进程期望参加某个多播组,则有必要履行bind()体系调用。该字段指明晰调用者期望参加的多播组号的掩码(留意不是组号,后边咱们会详细解说这个字段)。假如该字段为0则表明调用者不期望参加任何多播组。关于每个隶属于Netlink协议域的协议,最多可支撑32个多播组(由于nl_groups的长度为32比特),每个多播组用一个比特来表明。
2.3structnlmsghdr
Netlink的报文由音讯头和音讯体构成,structnlmsghdr即为音讯头。音讯头界说在文件里,由结构体nlmsghdr表明:
1
2
3
4
5
6
7
8
struct nlmsghdr
{
__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 PID */
};
音讯头中各成员特点的解说及阐明:
(1)nlmsg_len:整个音讯的长度,按字节核算。包括了Netlink音讯头本身。
(2)nlmsg_type:音讯的类型,便是数据仍是操控音讯。现在(内核版别2.6.21)Netlink仅支撑四种类型的操控音讯,如下:
a)NLMSG_NOOP-空音讯,什么也不做;
b)NLMSG_ERROR-指明该音讯中包括一个过错;
c)NLMSG_DONE-假如内核经过Netlink行列回来了多个音讯,那么行列的最终一条音讯的类型为NLMSG_DONE,其他一切音讯的nlmsg_flags特点都被设置NLM_F_MULTI位有用。
d)NLMSG_OVERRUN-暂时没用到。
(3)nlmsg_flags:附加在音讯上的额定阐明信息,如上面说到的NLM_F_MULTI。
那音讯体怎样设置呢?能够运用NLMSG_DATA,详细见后边比方。
3.用户态典范一
客户端1
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_PAYLOAD 1024 // maximum payload size
#define NETLINK_TEST 25 //自界说的协议
int main(int argc, char* argv[])
{
int state;
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL; //Netlink数据包头
struct iovec iov;
struct msghdr msg;
int sock_fd, retval;
int state_smg = 0;
// Create a socket
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(sock_fd == -1){
printf(“error getTIng socket: %s”, strerror(errno));
return -1;
}
// To prepare binding
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = 100; //A:设置源端端口号
src_addr.nl_groups = 0;
//Bind
retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
if(retval < 0){
printf(“bind failed: %s”, strerror(errno));
close(sock_fd);
return -1;
}
// To orepare create mssage
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if(!nlh){
printf(“malloc nlmsghdr error!\n”);
close(sock_fd);
return -1;
}
memset(&dest_addr,0,sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; //B:设置意图端口号
dest_addr.nl_groups = 0;
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = 100; //C:设置源端口
nlh->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlh),”Hello you!”); //设置音讯体
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
//Create mssage
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
//send message
printf(“state_smg\n”);
state_smg = sendmsg(sock_fd,&msg,0);
if(state_smg == -1)
{
printf(“get error sendmsg = %s\n”,strerror(errno));
}
memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
//receive message
printf(“waiting received!\n”);
while(1){
printf(“In while recvmsg\n”);
state = recvmsg(sock_fd, &msg, 0);
if(state<0)
{
printf(“state<1");
}
printf(“Received message: %s\n”,(char *) NLMSG_DATA(nlh));
}
close(sock_fd);
return 0;
}
上面程序首要向内核发送一条音讯;“Helloyou”,然后进入循环一向等候读取内核的回复,并将收到的回复打印出来。假如看上面程序感觉很费劲,那么应该首要温习一下UDP中运用sendmsg的用法,特别时structmsghdr的结构要清楚,这儿再赘述。下面首要剖析与UDP发送数据包的不同点:
1.socket地址结构不同,UDP为sockaddr_in,Netlink为structsockaddr_nl;
2.与UDP发送数据比较,Netlink多了一个音讯头结构structnlmsghdr需求咱们结构。
留意代码注释中的A、B、C三处别离设置了pid。首要解说一下什么是pid,网上许多文章把这个字段说成是进程的pid,其实这完全是断章取义。这儿的pid和进程pid没有什么联系,仅仅相当于UDP的port。关于UDP来说port和ip标明一个地址,那对咱们的NETLINK_TEST协议(留意Netlink本身不是一个协议)来说,pid就仅有标明了一个地址。所以你假如用进程pid做为标明当然也是能够的。当然相同的pid关于NETLINK_TEST协议和内核界说的其他运用Netlink的协议是不抵触的(就像TCP的80端口和UDP的80端口)。
下面剖析这三处设置pid别离有什么作用,首要A和B方位的比较好了解,这是在地址(sockaddr_nl)上进行的设置,便是相当于设置源地址和意图地址(其实是端口),仅仅留意B处设置pid为0,0就代表是内核,能够了解为内核专用的pid,那么用户进程就不能用0做为自己的pid吗?这个只能说假如你非要用也是能够的,仅仅会发生一些问题,后边在剖析。接下来看为什么C处的音讯头依然需求设置pid呢?这儿首要要知道一个条件:内核不会像UDP相同依据咱们设置的原、意图地址为咱们结构音讯头,所以咱们不在包头写入咱们自己的地址(pid),那内核怎样知道是谁发来的报文呢?当然假如内核仅仅处理音讯不需求回复进程的话舍不设置这个音讯头pid都能够。
所以每个pid的设置功用不同:A处的设置是要设置发送者的源地址,有人会说已然源地址又不会自动填充到报文中,咱们为什么还要设置这个,由于你还可能要接纳回复啊。就像寄信,你连“门牌号”都没有,即便你在写信时分写上你的地址是100号,对方回信意图地址也是100号,可是邮局发现底子没有这个地址怎样可能把信送到你手里呢?所以A的首要作用是注册源地址,确保能够收到回复,假如不需求回复当然能够简略将pid设置为0;B处天然便是收信人的地址,pid为0代表内核的地址,假如有一个进程在101号上注册了地址,并调用了recvmsg,假如你将B处的pid设置为101,那数据包就发给了另一个进程,这就完成了运用Netlink进行进程间通讯;C相当于你在信封上写的源地址,一般状况下这个应该和你的实在地址(A)处注册的源地址相同,当然你要是不想收到回信,又想恶搞一下或许有特别需求,你能够写成其他进程注册的pid(比方101)。这和咱们实际中寄信是相同的,你给你朋友写封情书,把写信人写成你的另一个好基友,然后成果你懂得……
好了,有了这个比方咱们就大约知道用户态怎样运用Netlink了,至于咱们没有用到的nl_groups等其他信息后边讲到再说,下面看下内核是怎样处理Netlink的。
4.内核 Netlinkapi
4.1创立 netlinksocket
1
2
3
4
struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,struct module *module);
参数阐明:
(1)net:是一个网络姓名空间namespace,在不同的姓名空间里边能够有自己的转发信息库,有自己的一套net_device等等。默许状况下都是运用init_net这个全局变量。
(2)unit:表明netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX。
(3)groups:多播地址。
(4)input:为内核模块界说的netlink音讯处理函数,当有音讯抵达这个netlinksocket时,该input函数指针就会被引证,且只要此函数回来时,调用者的sendmsg才干回来。
(5)cb_mutex:为拜访数据时的互斥信号量。
(6)module:一般为THIS_MODULE。
4.2发送单播音讯 netlink_unicast
1
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)
参数阐明:
(1)ssk:为函数netlink_kernel_create()回来的socket。
(2)skb:寄存音讯,它的data字段指向要发送的netlink音讯结构,而skb的操控块保存了音讯的地址信息,宏NETLINK_CB(skb)就用于便利设置该操控块。
(3)pid:为接纳此音讯进程的pid,即方针地址,假如方针为组或内核,它设置为0。
(4)nonblock:表明该函数是否为非堵塞,假如为1,该函数将在没有接纳缓存可利用时当即回来;而假如为0,该函数在没有接纳缓存可利用守时睡觉。
4.3发送播送音讯 netlink_broadcast
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)
前面的三个参数与netlink_unicast相同,参数group为接纳音讯的多播组,该参数的每一个位代表一个多播组,因而假如发送给多个多播组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不能够睡觉),而GFP_KERNEL用于非原子上下文。
4.4开释 netlinksocket
1
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)
5.内核态程序典范一
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
#include
#include
#include
#include
#include
#include
#include
#define NETLINK_TEST 25
#define MAX_MSGSIZE 1024
int stringlength(char *s);
int err;
struct sock *nl_sk = NULL;
int flag = 0;
//向用户态进程回发音讯
void sendnlmsg(char *message, int pid)
{
struct sk_buff *skb_1;
struct nlmsghdr *nlh;
int len = NLMSG_SPACE(MAX_MSGSIZE);
int slen = 0;
if(!message || !nl_sk)
{
return ;
}
printk(KERN_ERR “pid:%d\n”,pid);
skb_1 = alloc_skb(len,GFP_KERNEL);
if(!skb_1)
{
printk(KERN_ERR “my_net_link:alloc_skb error\n”);
}
slen = stringlength(message);
nlh = nlmsg_put(skb_1,0,0,0,MAX_MSGSIZE,0);
NETLINK_CB(skb_1).pid = 0;
NETLINK_CB(skb_1).dst_group = 0;
message[slen]= '\0';
memcpy(NLMSG_DATA(nlh),message,slen+1);
printk(“my_net_link:send message '%s'.\n”,(char *)NLMSG_DATA(nlh));
netlink_unicast(nl_sk,skb_1,pid,MSG_DONTWAIT);
}
int stringlength(char *s)
{
int slen = 0;
for(; *s; s++)
{
slen++;
}
return slen;
}
//接纳用户态发来的音讯
void nl_data_ready(struct sk_buff *__skb)
{
struct sk_buff *skb;
struct nlmsghdr *nlh;
char str[100];
struct completion cmpl;
printk(“begin data_ready\n”);
int i=10;
int pid;
skb = skb_get (__skb);
if(skb->len >= NLMSG_SPACE(0))
{
nlh = nlmsg_hdr(skb);
memcpy(str, NLMSG_DATA(nlh), sizeof(str));
printk(“Message received:%s\n”,str) ;
pid = nlh->nlmsg_pid;
while(i–)
{//咱们运用completion做延时,每3秒钟向用户态回发一个音讯
init_completion(&cmpl);
wait_for_completion_timeout(&cmpl,3 * HZ);
sendnlmsg(“I am from kernel!”,pid);
}
flag = 1;
kfree_skb(skb);
}
}
// Initialize netlink
int netlink_init(void)
{
nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 1,
nl_data_ready, NULL, THIS_MODULE);
if(!nl_sk){
printk(KERN_ERR “my_net_link: create netlink socket error.\n”);
return 1;
}
printk(“my_net_link_4: create netlink socket ok.\n”);
return 0;
}
static void netlink_exit(void)
{
if(nl_sk != NULL){
sock_release(nl_sk->sk_socket);
}
printk(“my_net_link: self module exited\n”);
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_AUTHOR(“yilong”);
MODULE_LICENSE(“GPL”);
附上内核代码的Makefile文件:
1
2
3
4
5
6
7
8
ifneq ($(KERNELRELEASE),)
obj-m :=netl.o
else
KERNELDIR ?=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
6.程序结构剖析
咱们将内核模块insmod后,运转用户态程序,成果如下:
这个成果复合咱们的预期,可是运转进程中打印出“state_smg”卡了良久才输出了后边的成果。这时分检查客户进程是处于D状况的(不了解D状况的同学能够google一下)。这是为什么呢?由于进程运用Netlink向内核发数据是同步,内核向进程发数据是异步。什么意思呢?也便是用户进程调用sendmsg发送音讯后,内核会调用相应的接纳函数,可是必定到这个接纳函数履行完用户态的sendmsg才干够回来。咱们在内核态的接纳函数中调用了10次回发函数,每次都等候3秒钟,所以内核接纳函数30秒后才回来,所以咱们用户态程序的sendmsg也要等30秒后才回来。相反,内核回发的数据不必等候用户程序接纳,这是由于内核所发的数据会暂时寄存在一个行列中。
再来回到之前的一个问题,用户态程序的源地址(pid)能够用0吗?我把上面的用户程序的A和C处pid都改为了0,成果一运转就死机了。为什么呢?咱们看一下内核代码的逻辑,收到用户音讯后,依据音讯中的pid发送回去,而pid为0,内核并不认为这是用户程序,认为是本身,一切又将回发的10个音讯发给了自己(内核),这样就陷入了一个死循环,而用户态这时分进程一向处于D。
别的一个问题,假如一起发动两个用户进程会是什么状况?答案是再调用bind时犯错:“Addressalreadyinuse”,这个同UDP相同,同一个地址同一个port假如没有设置SO_REUSEADDR两次bind就会犯错,之后我用相同的方法再Netlink的socket上设置了SO_REUSEADDR,可是并没有什么作用。
7.用户态典范二
之前咱们说过UDP能够运用sendmsg/recvmsg也能够运用sendto/recvfrom,那么Netlink相同也能够运用sendto/recvfrom。详细完成如下:
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_PAYLOAD 1024 // maximum payload size
#define NETLINK_TEST 25
int main(int argc, char* argv[])
{
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
int sock_fd, retval;
int state,state_smg = 0;
// Create a socket
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(sock_fd == -1){
printf(“error getting socket: %s”, strerror(errno));
return -1;
}
// To prepare binding
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = 100;
src_addr.nl_groups = 0;
//Bind
retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
if(retval < 0){
printf(“bind failed: %s”, strerror(errno));
close(sock_fd);
return -1;
}
// To orepare create mssage head
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if(!nlh){
printf(“malloc nlmsghdr error!\n”);
close(sock_fd);
return -1;
}
memset(&dest_addr,0,sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0;
dest_addr.nl_groups = 0;
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = 100;
nlh->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlh),”Hello you!”);
//send message
printf(“state_smg\n”);
sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
if(state_smg == -1)
{
printf(“get error sendmsg = %s\n”,strerror(errno));
}
memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
//receive message
printf(“waiting received!\n”);
while(1){
printf(“In while recvmsg\n”);
state=recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,NULL,NULL);
if(state<0)
{
printf(“state<1");
}
printf(“Received message: %s\n”,(char *) NLMSG_DATA(nlh));
memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
}
close(sock_fd);
return 0;
}
了解UDP编程的同学看到这个程序必定很了解,除了多了一个Netlink音讯头的设置。可是咱们发现程序中调用了bind函数,这个函数再UDP编程中的客户端不是有必要的,由于咱们不需求把UDPsocket与某个地址相关,一起再发送UDP数据包时内核会为咱们分配一个随即的端口。可是关于Netlink有必要要有这一步bind,由于Netlink内核可不会为咱们分配一个pid。再着重一遍音讯头(nlmsghdr)中的pid是告知内核接纳端要回复的地址,可是这个地址存不存在内核并不关怀,这个地址只要用户端调用了bind后才存在。
再说一个别外话,咱们看到这两个比方都是用户态首要建议的,那Netlink是否支撑内核态自动建议的状况呢?当然是能够的,仅仅内核一般需求事情触发,这儿,只需和用户态约好号一个地址(pid),内核直接调用netlink_unicast就能够了。