Unix/Linux编程:Netlink机制
什么是Netlink通信机制Netlink是linux提供的用于内核和用户态进程之间的通信方式。但是注意虽然Netlink主要用于用户空间和内核空间的通信,但是也能用于用户空间的两个进程通信。只是进程间通信有其他很多方式,一般不用Netlink。除非需要用到Netlink的广播特性时。NetLink机制是一种特殊的socket,它是Linux特有的,由于传送的消息是暂存在socket接收缓存中,并
什么是Netlink通信机制
- Netlink是linux提供的用于内核和用户态进程之间的通信方式。
- 但是注意虽然Netlink主要用于用户空间和内核空间的通信,但是也能用于用户空间的两个进程通信。
- 只是进程间通信有其他很多方式,一般不用Netlink。除非需要用到Netlink的广播特性时。
- NetLink机制是一种特殊的socket,它是Linux特有的,由于传送的消息是暂存在socket接收缓存中,并不为接受者立即处理,所以netlink是一种异步通信机制。系统调用和ioctl是同步通信机制
- 用户空间进程可以通过标准socket API来实现消息的发送、接收。在Linux中,很多用户空间和内核空间的交互都是通过Netlink机制完成的。在Linux3.0的内核版本中定义了下面的21个用于Netlink通信的宏,其中默认的最大值为32
#define NETLINK_ROUTE 0 /* Routing/device hook */
#define NETLINK_UNUSED 1 /* Unused number */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Firewalling hook */
#define NETLINK_INET_DIAG 4 /* INET socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#define NETLINK_GENERIC 16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
#define NETLINK_ECRYPTFS 19
#define NETLINK_RDMA 20
#define MAX_LINKS 32
目前在Linux 内核中使用netlink 进行应用与内核通信的应用很多,包括:
- 路由 daemon(NETLINK_ROUTE)
- 用户态 socket 协议(NETLINK_USERSOCK)
- 防火墙(NETLINK_FIREWALL)
- netfilter 子系统(NETLINK_NETFILTER)
- 内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)
- 通用 netlink(NETLINK_GENERIC)。NETLINK_GENERIC是一个通用的协议类型,它是专门为用户使用的,因此,用户可以直接使用它,而不必添加新的协议类型。
Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。
- 用户态可以使用标准的socket APIs,socket()、bind()、sendmsg()、recvmsg()和close()等函数就能够很容易的使用netlink socket,我们在用户空间可以直接通过socket函数来使用netlink通信,比如可以通过下面的方式:
sock = socket (AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
那么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接口
- 支持多播(因此支持“总线”式通信,可实现消息订阅)
- 在内核端可用于进程上下文与中断上下文
对于每一个netlink协议类型,可以使用多播的概念,最多可以有32个多播组,每一个多播组用一个位表示,netlink的多播特性是的发送消息给同一个组仅需要一次系统调用,因而对于需要多播消息的应用而言,大大降低了系统调用的次数。
Netlink 相对于系统调用,ioctl 以及 /proc文件系统而言具有以下优点:
- netlink使用简单,只需要在include/linux/netlink.h中增加一个新类型的 netlink 协议定义即可,(如 #define NETLINK_TEST 20 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换);
- netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息;
- 使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖;
- netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性;
- 内核可以使用 netlink 首先发起会话;
常用宏
#define NLMSG_ALIGNTO 4U
/* 宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/* Netlink 头部长度 */
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/* 计算消息数据len的真实消息长度(消息体 + 消息头)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/* 宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址, 同时len 变为剩余消息的长度 */
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
/* 判断消息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len <= (len))
/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的长度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
用户态数据结构
首先看一下几个重要的数据结构的关系:
struct msghdr
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
/* iov_base: iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,
以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff)
*/
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
/* msg_name: 数据的目的地址,网络包指向sockaddr_in, netlink则指向sockaddr_nl;
msg_namelen: msg_name 所代表的地址长度
msg_iov: 指向的是缓冲区数组
msg_iovlen: 缓冲区数组长度
msg_control: 辅助数据,控制信息(发送任何的控制信息)
msg_controllen: 辅助信息长度
msg_flags: 消息标识
*/
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特有的功能。
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 */
};
说明:
sa_family_t nl_family
; //一般为AF_NETLINK,unsigned short nl_pad;
//字段 nl_pad 当前没有使用,因此要总是设置为 0nl_pid
:- 在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。
- 通常情况下nl_pid都设置为当前进程的进程号。前面我们也说过,Netlink不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般指内核。
__u32 nl_groups;
- 如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码
- 如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于Netlink协议域的协议,最多可支持32个多播组(因为nl_groups的长度为32比特),每个多播组用一个比特来表示。
NETLINK_ROUTE的多播组定义位于retnetlink.h,RTMGRP_*格式,这里列出常用的几个:
- RTMGRP_LINK - 当网卡变动时会触发这个多播组,例如插拔网线、增减网卡设备、启用禁用接口等
- RTMGRP_IPV4_IFADDR - 当ipv4地址变动时会触发这个多播组,例如修改IP
- RTMGRP_IPV4_ROUTE - 当ipv4路由变动时会触发这个多播组
- RTMGRP_IPV6_IFADDR - 当ipv6地址变动时会触发这个多播组,例如修改IP
- RTMGRP_IPV6_ROUTE - 当ipv6路由变动时会触发这个多播组
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 */
};
消息头中各成员属性的解释及说明:
nlmsg_len
:整个消息的长度,按字节计算。包括了Netlink消息头本身。
-nlmsg_type
:消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)Netlink仅支持四种类型的控制消息,如下:- NLMSG_NOOP:不执行任何动作,必须将该消息丢弃;
- NLMSG_ERROR:指明该消息中包含一个错误;
- NLMSG_DONE:标识分组消息的末尾;----如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。
- NLMSG_OVERRUN:缓冲区溢出,表示某些消息已经丢失。
- NLMSG_MIN_TYPEK:预留
nlmsg_flags
:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。- nlmsg_seq:消息序列号,用以将消息排队,有些类似TCP协议中的序号(不完全一样),但是netlink的这个字段是可选的,不强制使用;
- nlmsg_pid:发送端口的ID号,对于内核来说该值就是0,对于用户进程来说就是其socket所绑定的ID号
用户空间Netlink socket API
1. 创建套接字
skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
- 第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink,
- 第二个参数必须是SOCK_RAW或SOCK_DGRAM, 因为netlink是一个面向数据报的服务;
- 第三个参数指定netlink协议类型,它可以是一个自定义的类型,也可以使用内核预定义的类型
可以非常容易的添加自己的netlink协议。
为每一个协议类型最多可以定义32个多播组。
每一个多播组用一个bitmask来表示,1<<i(0<=i<= 31),这在一组进程和内核进程协同完成一项任务时非常有用。发送多播netlink消息可以减少系统调用的数量,同时减少用来维护多播组成员信息的负担。
2. 绑定套接字
bind(skfd, (struct sockaddr*)&local, sizeof(local));
local为netlink的socket地址,即上面提到的:
struct sockaddr_nl
{
sa_family_t nl_family; // 成员 nl_family为协议簇 AF_NETLINK
unsigned short nl_pad; //成员 nl_pad 当前没有使用,因此要总是设置为 0
__u32 nl_pid; // 成员 nl_pid 为接收或发送消息的进程的 ID
__u32 nl_groups;
};
例子
struct sockaddr_nl local;
memset(&local, 0, sizeof(local));
local.nl_family = AF_NETLINK;
local.nl_pid = getpid(); /*设置pid为自己的pid值*/
local.nl_groups = 0;
3.发送netlink消息
用户空间调用send函数(如sendto、sendmsg等)向内核发送数据,使用同样的socket地址来描述内核,不过需要注意,由于对端是内核,nl_pid必须设置为0。
内核的socket地址
struct sockaddr_nl kpeer;
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;
为了发送一条netlink消息到内核或者其他的用户空间进程,另外一个struct sockaddr_nl nladdr需要作为目的地址,这和使用sendmsg()发送一个UDP包是一样的。
- 如果该消息是发送至内核的,那么nl_pid和nl_groups都置为0.
- 如果消息是发送给另一个进程的单播消息,nl_pid是另外一个进程的pid值而nl_groups为零。
- 如果消息是发送给一个或多个多播组的多播消息,所有的目的多播组必须bitmask必须or起来从而形成nl_groups域。
用户进程想内核发送的数据包格式为:“netlink消息头 + 数据”,消息头描述为:
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
自定义消息首部,它仅包含了netlink的消息首部
struct msg_to_kernel
{
struct nlmsghdr hdr;
};
填充首部信息
struct msg_to_kernel message;
memset(&message, 0, sizeof(message));
message.hdr.nlmsg_len = NLMSG_LENGTH(0); /*没有数据,所以长度为0.*/
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = IMP2_U_PID;
message.hdr.nlmsg_pid = local.nl_pid;
向内核发送消息
sendto(skfd, &message, message.hdr.nlmsg_len, 0,(struct sockaddr*)&kpeer, sizeof(kpeer));
4.接收netlink消息
当发送完请求后,就可以调用recv函数簇从内核接收数据了,接收的数据包含了netlink消息首部和自定义数据结构。
自定义的数据结构
一个接收程序必须分配一个足够大的内存用于保存netlink消息头和消息负载。然后其填充struct msghdr msg,再使用标准的recvmsg()函数来接收netlink消息。
struct u_packet_info
{
struct nlmsghdr hdr; /*netlink消息头*/
struct packet_info icmp_info;
};
struct u_packet_info info;
接受和处理从内核接受到的信息
while(1)
{
kpeerlen = sizeof(struct sockaddr_nl);
/*接收内核空间返回的数据*/
rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),0, (struct sockaddr*)&kpeer, &kpeerlen);
/*处理接收到的数据*/
……
}
5. 关闭socket
函数close用于关闭打开的netlink socket。程序中,因为程序一直循环接收处理内核的消息,需要收到用户的关闭信号才会退出
♦ 关闭套接字的工作放在了自定义的信号函数sig_int中处理
static void sig_int(int signo)
{
struct sockaddr_nl kpeer; /*内核的socket地址*/
struct msg_to_kernel message; /*自定义netlink消息首部*/
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;
memset(&message, 0, sizeof(message));
message.hdr.nlmsg_len = NLMSG_LENGTH(0);
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = IMP2_CLOSE;
message.hdr.nlmsg_pid = getpid();
/*向内核发送一个消息,由nlmsg_type表明,应用程序将关闭*/
sendto(skfd, &message, message.hdr.nlmsg_len, 0,(struct sockaddr *)(&kpeer),sizeof(kpeer));
close(skfd);
exit(0);
}
内核空间Netlink socket API
当 netlink 套接字用于内核空间与用户空间的通信时,在用户空间的创建方法和一般套接字使用类似,但内核空间的创建方法则不同,下图是 netlink 套接字实现此类通信时创建的过程:
1.创建netlink套接字
通过netlink_kernel_create创建一个netlink套接字,同时,注册一个回调函数,用于接收处理用户空间的消息。
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);
参数说明:
- net:是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用 init_net这个全局变量。
- unit:表示netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX。
- groups:多播地址。
- input:为内核模块定义的netlink消息处理函数,当有消 息到达这个netlink socket时,该input函数指针就会被引用,且只有此函数返回时,调用者的sendmsg才能返回。
- cb_mutex:为访问数据时的互斥信号量。
- module: 一般为THIS_MODULE。
2.发送单播消息 netlink_unicast
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)
参数说明:
- ssk:为函数 netlink_kernel_create()返回的socket。
- skb:存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,宏NETLINK_CB(skb)就用于方便设置该控制块。
- pid:为接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为 0。
- nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;而如果为0,该函数在没有接收缓存可利用定时睡眠。
static int send_to_user(struct packet_info *info)
{
int ret;
int size;
unsigned char *old_tail;
struct sk_buff *skb;
struct nlmsghdr *nlh;
struct packet_info *packet;
/*计算消息总长:消息首部加上数据加度*/
size = NLMSG_SPACE(sizeof(*info));
/*分配一个新的套接字缓存*/
skb = alloc_skb(size, GFP_ATOMIC);
old_tail = skb->tail;
/*初始化一个netlink消息首部,NLMSG_PUT(skb, pid, seq, type, len)*/
nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
/*跳过消息首部,指向数据区*/
packet = NLMSG_DATA(nlh);
/*初始化数据区*/
memset(packet, 0, sizeof(struct packet_info));
/*填充待发送的数据*/
packet->src = info->src;
packet->dest = info->dest;
/*计算skb两次长度之差,即netlink的长度总和*/
nlh->nlmsg_len = skb->tail - old_tail;
/*设置控制字段*/
NETLINK_CB(skb).dst_groups = 0; /*如果它目标为某一进程或内核,dst_group 应当设置为 0。*/
/*发送数据*/
read_lock_bh(&user_proc.lock);
ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
read_unlock_bh(&user_proc.lock);
}
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. 接收处理用户空间发送的数据(kernel_receive 函数)
用户空间向内核发送了两种自定义消息类型:IMP2_U_PID和IMP2_CLOSE,分别是请求和关闭。kernel_receive 函数分别处理这两种消息:
DECLARE_MUTEX(receive_sem); /*初始化信号量*/
static void kernel_receive(struct sock *sk, int len)
{
do
{
struct sk_buff *skb;
if(down_trylock(&receive_sem)) /*获取信号量*/
return;
/*从sk接收队列中取得skb,然后进行一些基本的长度的合法性校验*/
while((skb = skb_dequeue(&sk->receive_queue)) != NULL)
{
{
struct nlmsghdr *nlh = NULL;
if(skb->len >= sizeof(struct nlmsghdr))
{
/*获取数据中的nlmsghdr 结构的报头*/
nlh = (struct nlmsghdr *)skb->data;
if((nlh->nlmsg_len >= sizeof(struct nlmsghdr))&& (skb->len >= nlh->nlmsg_len))
{
/*长度的全法性校验完成后,处理应用程序自定义消息类型,主要是对用户PID的保存,即为内核保存“把消息发送给谁”*/
if(nlh->nlmsg_type == IMP2_U_PID)/*请求*/
{
write_lock_bh(&user_proc.pid);
user_proc.pid = nlh->nlmsg_pid;
write_unlock_bh(&user_proc.pid);
}
else if(nlh->nlmsg_type == IMP2_CLOSE)/*应用程序关闭*/
{
write_lock_bh(&user_proc.pid);
if(nlh->nlmsg_pid == user_proc.pid)
user_proc.pid = 0;
write_unlock_bh(&user_proc.pid);
}
}
}
}
kfree_skb(skb);
}
up(&receive_sem); /*返回信号量*/
}while(nlfd && nlfd->receive_queue.qlen);
}
实例
http://blog.chinaunix.net/uid-20788636-id-2980152.html
https://www.cnblogs.com/wenqiang/p/6306727.html
更多推荐
所有评论(0)