什么是Netlink通信机制

一:什么是netlink通信机制
netlink是linux提供的用于内核和用户态进程之间的通信方式。
但是注意虽然netlink主要用于用户空间和内核空间的通信,但是也能用于用户空间的两个进程通信。只是进程间通信有其他很多方式,一般不用netlink。除非需要用到netlink的广播特性时。
那么netlink有什么优势呢?
一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、netlink。而前两种都是单向的,但是netlink可以实现双工通信。
netlink协议基于bsd socket和af_netlink地址簇(address family),使用32位的端口号寻址(以前称作pid),每个netlink协议(或称作总线,man手册中则称之为netlink family),通常与一个或一组内核服务/组件相关联,如netlink_route用于获取和设置路由与链路信息、netlink_kobject_uevent用于内核向用户空间的udev进程发送通知等。
netlink具有以下特点:
① 支持全双工、异步通信(当然同步也支持)
② 用户空间可使用标准的bsd socket接口(但netlink并没有屏蔽掉协议包的构造与解析过程,推荐使用libnl等第三方库)
③ 在内核空间使用专用的内核api接口
④ 支持多播(因此支持“总线”式通信,可实现消息订阅)
⑤ 在内核端可用于进程上下文与中断上下文
二:用户态数据结构
首先看一下几个重要的数据结构的关系:
1.struct msghdr
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.struct sockaddr_ln
struct sockaddr_ln为netlink的地址,和我们通常socket编程中的sockaddr_in作用一样,他们的结构对比如下:
struct sockaddr_nl的详细定义和描述如下:
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比特),每个多播组用一个比特来表示。
3.struct nlmsghdr
netlink的报文由消息头和消息体构成,struct nlmsghdr即为消息头。消息头定义在文件里,由结构体nlmsghdr表示:
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。
三:用户空间netlink socket api
1.创建socket
int socket(int domain, int type, int protocol)
domain指代地址族,即af_netlink;
套接字类型为sock_raw或sock_dgram,因为netlink是一个面向数据报的服务;
protocol选择该套接字使用哪种netlink特征。
以下是几种预定义的协议类型:
netlink_route,
netlink_firewall,
netlink_aprd,
netlink_route6_fw。
可以非常容易的添加自己的netlink协议。
为每一个协议类型最多可以定义32个多播组。
每一个多播组用一个bitmask来表示,1nlmsg_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_smgn);
state_smg = sendmsg(sock_fd,&msg,0);
if(state_smg == -1)
{
printf(get error sendmsg = %sn,strerror(errno));
}
memset(nlh,0,nlmsg_space(max_payload));
//receive message
printf(waiting received!n);
while(1){
printf(in while recvmsgn);
state = recvmsg(sock_fd, &msg, 0);
if(state<0)
{
printf(statelen >= nlmsg_space(0))
{
nlh = nlmsg_hdr(skb);
memcpy(str, nlmsg_data(nlh), sizeof(str));
printk(message received:%sn,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 exitedn);
}
module_init(netlink_init);
module_exit(netlink_exit);
module_author(zhao_h);
module_license(gpl);
附上内核代码的makefile文件:
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
我们将内核模块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时出错:“address already in use”,这个同udp一样,同一个地址同一个port如果没有设置so_reuseaddr两次bind就会出错,之后我用同样的方式再netlink的socket上设置了so_reuseaddr,但是并没有什么效果。
七:用户态范例二
之前我们说过udp可以使用sendmsg/recvmsg也可以使用sendto/recvfrom,那么netlink同样也可以使用sendto/recvfrom。具体实现如下:
#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 nlmsg_len = nlmsg_space(max_payload);
nlh->nlmsg_pid = 100;
nlh->nlmsg_flags = 0;
strcpy(nlmsg_data(nlh),hello you!);
//send message
printf(state_smgn);
sendto(sock_fd,nlh,nlmsg_length(max_payload),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
if(state_smg == -1)
{
printf(get error sendmsg = %sn,strerror(errno));
}
memset(nlh,0,nlmsg_space(max_payload));
//receive message
printf(waiting received!n);
while(1){
printf(in while recvmsgn);
state=recvfrom(sock_fd,nlh,nlmsg_length(max_payload),0,null,null);
if(state<0)
{
printf(state<1);
}
printf(received message: %sn,(char *) nlmsg_data(nlh));
memset(nlh,0,nlmsg_space(max_payload));
}
close(sock_fd);
return 0;
}
熟悉udp编程的同学看到这个程序一定很熟悉,除了多了一个netlink消息头的设置。但是我们发现程序中调用了bind函数,这个函数再udp编程中的客户端不是必须的,因为我们不需要把udp socket与某个地址关联,同时再发送udp数据包时内核会为我们分配一个随即的端口。但是对于netlink必须要有这一步bind,因为netlink内核可不会为我们分配一个pid。再强调一遍消息头(nlmsghdr)中的pid是告诉内核接收端要回复的地址,但是这个地址存不存在内核并不关心,这个地址只有用户端调用了bind后才存在。
我们看到这两个例子都是用户态首先发起的,那netlink是否支持内核态主动发起的情况呢?
当然是可以的,只是内核一般需要事件触发,这里,只要和用户态约定号一个地址(pid),内核直接调用netlink_unicast就可以了。

有关IBIS 6.1的知识信息简介
骁龙835产能问题小米6延后?小米5C采用自主研发松果性价比极高
雅虎在中国大陆停止产品与服务 雅虎在中国用不了了
传感器在洗碗机中的应用方案
一文带你了解:光电液位传感器
什么是Netlink通信机制
iPhone8: 全面屏+悬浮屏幕+无线充电, 红色版本颜值最高
这么快iPhone12就跌落神坛了?
TCL发布了X10系列QLED 8K TV 是彩电行业首款全程8K电视
开关稳压器评估之输出电压
水处理设施远程监控运营管理系统解决方案
门禁改装读码器 盗取银行卡存款惊现新手段
风向风速记录仪的应用领域有哪些,它的效果如何
中国移动全球合作伙伴大会--和创未来,智连万物
外线不分光CO/CO2二合一分析仪如何选择
如何做一台能满足家庭多样化需求的机器人?
2017-2018年动力电池公司深度分析报告
华为视频内容管理平台的作用及应用分析
能源电力供应有望成为区块链的主要应用之一
可以用恒压恒流电源替代锂电池充电器吗?