linux下从内核空间到用户空间的异步通信可以使用到netLink,像设备的路由等信息都是通过该机制实现。
一 netlink的了解
Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。
netlink协议基于BSD socket和AF_NETLINK地址簇(address family),使用32位的端口号寻址(以前称作PID),每个netlink协议(或称作总线,man手册中则称之为netlink family),通常与一个或一组内核服务/组件相关联,如NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送通知等。
Netlink 是一个面向数据包的服务。 SOCK_RAW 和 SOCK_DGRAM 都是 socket_type 的有效值。然而 netlink 协议对数据包 datagram 和原套接字(raw sockets)并不作区分。
netlink具有以下特点:
① 支持全双工、异步通信(当然同步也支持)
② 用户空间可使用标准的BSD socket接口(但netlink并没有屏蔽掉协议包的构造与解析过程,推荐使用libnl等第三方库)
③ 在内核空间使用专用的内核API接口
③ 支持多播(因此支持“总线”式通信,可实现消息订阅)
④ 在内核端可用于进程上下文与中断上下文
一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、Netlink
。而前两种都是单向的,而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 首先发起会话
二、为AF_NETLINK地址簇添加新协议的方法
要添加的netlink”协议“的说法,要在内核中使用netlink,需要为自己的内核服务添加新的netlink或则在现有的上面修改,现在有两种方法:
① 最开始的方法,使用<net/netlink.h>中的接口,即直接基于nlmsghdr。这是在Linux加入netlink机制之初添加协议的方法。该方法有个限制就是协议的数量不能超过32个,Linux3.10内核已经使用了22个。
② 使用netlink generic。netlink generic是基于第一种方法实现的,协议号为NETLINK_GENERIC。它在NETLINK_GENERIC协议之上提供了多路复用,在其之上添加的新协议称之为Generic Netlink 协议,在没有歧义的情况下,也称作Generic协议或netlink协议。
基本入门了解,而且本文写的是第一种的。
三、NetLink协议基础
netlink的协议头如图:
netlink协议是面向消息的,要定义自己的协议,需要基于netlink提供的协议头,即struct nlmsghdr。自定义协议按照协议头格式填充协议头内容,并定义自己的playload,通常自定义的协议体包含自定义协议头与额外的属性,netlink提供了一系列的标准方法用于对消息进行打包与拆包。详细见下面的结构体。
二 使用的结构体
用户态应用使用标准的 socket API有sendto(),recvfrom(), sendmsg(), recvmsg()。
1. netlink消息类型
已经有许多内核模块使用netlink 机制,其中驱动模型中使用的uevent 就是基于netlink 实现。目前 netlink 协议族支持32种协议类型,它们定义在 include/uapi/linux/netlink.h 中:
#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 /* Unused number, formerly ip_queue */
#define NETLINK_SOCK_DIAG 4 /* 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 NETLINK_CRYPTO 21 /* Crypto layer */
#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG
#define MAX_LINKS 32
2. struct sockaddr_nl结构
Netlink通信跟常用UDP Socket通信类似,struct sockaddr_nl
是netlink通信地址,跟普通socket struct sockaddr_in
类似。
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* AF_NETLINK (跟AF_INET对应)*/
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* port ID (通信端口号)*/
__u32 nl_groups; /* multicast groups mask */
};
3. struct nlmsghdr 结构:
/* struct nlmsghd 是netlink消息头*/
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 port ID */
};
- ① Total Length (32bit) 协议头与payload的总长度(包含中间对齐和payload尾部对齐的空间)
② Message Type (16bit)。除了预定义的几个类型外,新协议可以自由的加入自己的消息类型。类型对netlink核心透明
③ Message Flags (16bit)。用于描述协议的行为,对netlink核心不透明
④ Sequence Number (32bit)。可选,用于标志已发送的消息,如错误消息可以引用一个已发送消息。
⑤ Port Number (32bit)。目的端口。若未指定,则会发送给内核,内核时此值为 0 - nlmsg_type:消息状态,内核在
include/uapi/linux/netlink.h
中定义了以下4种通用的消息类型 -
nlmsg_type决定这次要执行的操作,如查询当前路由表信息,所使用的就是RTM_GETROUTE。根据采用的nlmsg_type不同,还要选取不同的数据结构来填充到nlmsghdr后面
-
status = recvmsg (sock, &msg, 0);
//nlmsg_type 类型枚举
//标准nlmsg_type
#define NLMSG_NOOP 0x1 /* Nothing. */
#define NLMSG_ERROR 0x2 /* Error */
#define NLMSG_DONE 0x3 /* End of a dump */
#define NLMSG_OVERRUN 0x4 /* Data lost */
#define NLMSG_MIN_TYPE 0x10 /* < 0x10: reserved control messages */
//查询当前路由表信息等
RTM_NEWLINK ifinfomsg
RTM_DELLINK
RTM_GETLINK
RTM_NEWADDR ifaddrmsg
RTM_DELADDR
RTM_GETADDR
RTM_NEWROUTE rtmsg
RTM_DELROUTE
RTM_GETROUTE
RTM_NEWNEIGH ndmsg/nda_chcheinfo
RTM_DELNEIGH
RTM_GETNEIGH
RTM_NEWRULE rtmsg
RTM_DELRULE
RTM_GETRULE
RTM_NEWQDISC tcmsg
RTM_DELQDISC
RTM_GETQDISC
RTM_NEWTCLASS tcmsg
RTM_DELTCLASS
RTM_GETTCLASS
RTM_NEWTFILTER tcmsg
RTM_DELTFILTER
- nlmsg_flags:消息标记,它们用以表示消息的类型,如下
/* Flags values */
#define NLM_F_REQUEST 1 /* It is request message. */
#define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK 4 /* Reply with ack, with zero or error code */
#define NLM_F_ECHO 8 /* Echo this request */
#define NLM_F_DUMP_INTR 16 /* Dump was inconsistent due to sequence change */
/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
#define NLM_F_MATCH 0x200 /* return all matching */
#define NLM_F_ATOMIC 0x400 /* atomic GET */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing */
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist */
#define NLM_F_APPEND 0x800 /* Add to end of list */
标志NLM_F_REQUEST用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。
标志NLM_F_MULTI 用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得。
宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来。
标志NLM_F_ECHO表示该消息是相关的一个包的回传。
标志NLM_F_ROOT 被许多 netlink 协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个地返回。有该标志的请求通常导致响应消息设置 NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段nlmsg_type 中指定协议类型。
标志 NLM_F_MATCH 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。
标志 NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。
标志 NLM_F_DUMP 未实现。
标志 NLM_F_REPLACE 用于取代在数据表中的现有条目。
标志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败。
标志 NLM_F_CREATE 指示应当在指定的表中创建一个条目。
标志 NLM_F_APPEND 指示在表末尾添加新的条目。
内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些设置)
4. struct msghdr 结构体
此结构又与sockaddr_nl,iovec和nlmsghdr三个结构相关,因此必须依次对这些数据结构进行初始化。目的端nl_pid必须为为0,表示接收方为内核
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 */
};
如果我们需要返回一个 ACK 消息,可以对 flags 标志进行设置如下:
/* Request an acknowledgement by setting NLM_F_ACK */
n->nlmsg_flags |= NLM_F_ACK;
5.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)))
三 使用的例子
在应用层侧:
我们准要需要做一下的动作:
1.创建一个使用的AF_NETLINK簇的socket,并定义使用的哪个协议
2.本地端点的信息的填充以及绑定
3.目的端点的信息的填充
4.构造发送 的消息,也就是填充struct msghdr 结构体。里面包括目的端点信息,消息头,负载数据等。主要是几个关键的结构的变量填充
5.数据发送 ,需要使用netlink中的宏做一些处理
6.数据接收 ,需要使用netlink中定义的宏方便提取出数据
在linux 内核驱动侧:
1.通过netlink_kernel_create函数申请服务器端的套接字nl_sk,netlink协议类型NETLINK_TEST
2.设置一个nl_data_handler是一个钩子函数,收到消息就被回调对用户数据进行处理
3.使用NLMSG_DATA宏获取用户进程发送过来的数据。
4.重新申请一个套接字缓冲区
5.nlmsg_put 函数将填充 netlink 数据报头
6.netlink_unicast 发送数据包到用户进程
下面是附上的一个demo:
仅设计一个具有echo功能的协议。不在标准nlmsghdr之后的payload中定义自己的协议头,payload即为echo文本串,不使用netlink提供的消息标志。定义两种消息类型:NLMSG_GETECHO(用于echo请求包)和NLMSG_SETECHO(用于echo响应包。
应用层:
//应用层代码
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/netlink.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#define NETLINK_TEST 31 // 自定义的协议号
/** 消息类型 **/
#define NLMSG_SETECHO 0x11
#define NLMSG_GETECHO 0x12
/** 最大协议负荷(固定) **/
#define MAX_PAYLOAD 101
struct sockaddr_nl src_addr, dst_addr;
struct iovec iov;
int sockfd;
struct nlmsghdr *nlh = NULL;
struct msghdr msg;
int main( int argc, char **argv)
{
if (argc != 2) {
printf("usage: ./a.out <str>\n");
exit(-1);
}
sockfd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_TEST); // 创建NETLINK_TEST协议的socket
/* 设置本地端点并绑定,用于侦听 */
bzero(&src_addr, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid();
src_addr.nl_groups = 0; //未加入多播组
bind(sockfd, (struct sockaddr*)&src_addr, sizeof(src_addr));
/* 构造目的端点,用于发送 */
bzero(&dst_addr, sizeof(dst_addr));
dst_addr.nl_family = AF_NETLINK;
dst_addr.nl_pid = 0; // 表示内核
dst_addr.nl_groups = 0; //未指定接收多播组
/* 构造发送消息 */
nlh = (struct nlmsghdr *) malloc(NLMSG_SPACE(MAX_PAYLOAD));
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); //保证对齐
nlh->nlmsg_pid = getpid(); /* self pid */
nlh->nlmsg_flags = 0;
nlh->nlmsg_type = NLMSG_GETECHO;
strcpy((char *)NLMSG_DATA(nlh), argv[1]);
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_name = (void *)&dst_addr;
msg.msg_namelen = sizeof(dst_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
sendmsg(sockfd, &msg, 0); // 发送
/* 接收消息并打印 */
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
recvmsg(sockfd, &msg, 0);
printf(" Received message payload: %s\n",
(char *) NLMSG_DATA(nlh));
printf(" Received message payload: %s %lx %lx\n",
(char *) NLMSG_DATA(nlh) ,msg.msg_iov->iov_len
,(unsigned long)((struct nlmsghdr *)msg.msg_iov->iov_base)->nlmsg_len);
}
linux内核端:
//内核端代码,实现数据的收,然后加上"[from kernel]:"后原样返回给用户层
#include <linux/module.h>
#include <linux/netlink.h>
#include <net/netlink.h>
#include <net/net_namespace.h>
#define NETLINK_TEST 31
#define NLMSG_SETECHO 0x11
#define NLMSG_GETECHO 0x12
static struct sock *sk; //内核端socket
static void nl_custom_data_ready(struct sk_buff *skb); //接收消息回调函数
int __init nl_custom_init(void)
{
struct netlink_kernel_cfg nlcfg = {
.input = nl_custom_data_ready,
};
sk = netlink_kernel_create(&init_net, NETLINK_TEST, &nlcfg);
printk(KERN_INFO "initialed ok!\n");
if (!sk) {
printk(KERN_INFO "netlink create error!\n");
}
return 0;
}
void __exit nl_custom_exit(void)
{
printk(KERN_INFO "existing...\n");
netlink_kernel_release(sk);
}
static void nl_custom_data_ready(struct sk_buff *skb)
{
struct nlmsghdr *nlh;
void *payload;
struct sk_buff *out_skb;
void *out_payload;
struct nlmsghdr *out_nlh;
int payload_len; // with padding, but ok for echo
nlh = nlmsg_hdr(skb);
switch(nlh->nlmsg_type)
{
case NLMSG_SETECHO:
break;
case NLMSG_GETECHO:
payload = nlmsg_data(nlh);
payload_len = nlmsg_len(nlh);
printk(KERN_INFO "payload_len = %d\n", payload_len);
printk(KERN_INFO "Recievid: %s, From: %d\n", (char *)payload, nlh->nlmsg_pid);
out_skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL); //分配足以存放默认大小的sk_buff
if (!out_skb) goto failure;
out_nlh = nlmsg_put(out_skb, 0, 0, NLMSG_SETECHO, payload_len, 0); //填充协议头数据
if (!out_nlh) goto failure;
out_payload = nlmsg_data(out_nlh);
strcpy(out_payload, "[from kernel]:"); // 在响应中加入字符串,以示区别
strcat(out_payload, payload);
nlmsg_unicast(sk, out_skb, nlh->nlmsg_pid);
break;
default:
printk(KERN_INFO "Unknow msgtype recieved!\n");
}
return;
failure:
printk(KERN_INFO " failed in fun dataready!\n");
}
module_init(nl_custom_init);
module_exit(nl_custom_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("a simple example for custom netlink protocal family");
MODULE_AUTHOR("RSLjdkt");
四 内核中使用的实例
内核hotplug事件---利用Netlink处理hotplug实现热插拔监控_硬族科技的博客-CSDN博客_hotplug
其实在常用的路由器的cli 管理系统中,就有着这么一套通过netlink来管控路由的策略,以及实现的接口。可以去看看zebra源码。
在openwrt的那一套中,接口的管理换成了netifd去管理接口了。但实际添加路由之类的应该还是用的ip route 等这一类的工具,这些有的是使用的netlink,有的是使用ioctl进行控制的。
参考资料
linux下netlink的使用简介 - 专注it - 博客园
Linux netlink之添加一个简单协议_weixin_41666796的博客-CSDN博客
netlink实例_NeiborGirl的博客-CSDN博客
AF_NetLink结构体及例程_Nerazzur的博客-CSDN博客_af_netlink
Netlink机制详解_xinyuan0214的博客-CSDN博客_netlink_route
https://blog.csdn.net/luckyapple1028/article/details/50839395