###1. netlink
linxu选择将关键并且性能要求较高的代码放置在内核空间, 其它的代码, 例如GUI等, 放置在用户空间中运行, 因此, 很多时候需要kernel与userspace之间的通信
kernel与userspace之间存在着多种IPC方法, 例如系统调用, ioctl, proc文件系统以及netlink socket
Netlink socket是用于内核和用户空间之间交换信息的特殊的IPC机制。它提供了一种全复用的通信链路。和TCP/IP使用的地址族AF_INET相 比,Netlink socket使用地址族AF_NETLINK,每个的netlink socket特征定义协议类型在内核头文件中include/linux/netlink.h
###2. netlink的优势
- 为了linux的稳定性,不建议在轻易为不常用的功能来添加系统调用,而使用ioctl和proc文件系统相对也比较复杂, 而添加一个新的netlink协议类型较简单
- 同socket API一样,Netlink是异步的,它提供了一个socket队列来平滑突发的信息, netlink消息将排列在接受者的netlink队列中, 然后调用接收者的处理函数
- netlink socket的一大优势是支持多播, 一个进程可以将一条消息广播到一个netlink组地址。任意多的进程可以监听那个组地址。这提供了一种从内核到用户空间进行事件分发接近完美的机制
- netlink socket的另一优势是双向IPC, userspace和kernel可以互向对方发送消息, 而系统调用和ioctl是单一的IPC 5. netlink socket 使用标准的sockt API, 容易使用
###3. netlink 和r enetlink
在BSC TCP/IP的栈实现中,有一种叫做路由套接字的特殊的socket。它有AF_ROUTE地址族,PF_ROUTE协议族和SOCK_RAWsocket类型。在bsd中,路由套接字用于在内核路由表中添加和删除路由。
在Linux中,路由套接字的实现通过netlink套接字的NETLINK_ROUTE协议类型来支持。netlink套接字提供了bsd路由套接字的功能的超集。
###4. netlink消息的结构
在netlink的消息中, 有多个成对的消息头(nlmsghdr)和消息负载(payload), 以及必要的填充以保证对齐
####4.1 nlmsghdr
struct nlmsghdr 为 netlink socket 自己的消息头,这用于多路复用和多路分解 netlink 定义的所有协议类型以及其它一些控制,netlink 的内核实现将利用这个消息头来多路复用和多路分解已经其它的一些控制,因此它也被称为netlink 控制块。Linux内核netlink核心假设每个netlink消息中存在着该消息头, 因此,应用在发送 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 */
};
- nlmsg_len: 消息长度, 包括数据和该消息头
- nlmsg_type: 消息类型, 对netlink的内核实现透明, 大部分时候可以为0, 可取的值如下所示
- nlmsg_flags: 附加的控制信息
- nlmsg_seq: 消息编号, 私有数据, 由netlink的来使用和维护, 用户不应访问
- nlmsg_pid: 消息的来源ID, 私有数据, 由netlink的来使用和维护, 用户不应访问
nlmsg_type 的可取值如下:
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 指示在表末尾添加新的条目
对于一般的使用,nlmsg_type设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作),字段 nlmsg_seq 和 nlmsg_pid为
####4.2 attr
netlink的消息负载由若干的 attr 构成, 而一个attr都是一个TLV(type-length-value)结构, 其中, type和length保存在nlattr结构体中
struct nlattr {
__u16 nla_len;
__u16 nla_type;
};
- nla_len : attr的长度, 包括nlattr, data, 以及pading
- nla_type :
宏 NLA_HDRLEN 代表 nlattr 结构体的长度 宏 NLA_ALIGN(payload) 代表data和其后的pading的长度
需要注意的是, payload中, 并非强制使用TLV结构, 如果是使用自定义的netlink子协议, 只要自己能够解析附带的消息, 可以使用任何结构, 但是对于通用的netlink子协议, 例如generic netlink, 应该严格使用TLV结构, 因为generic netlink并非私用, 如果胡乱使用数据结构, 将导致解析出错
###5. userspace使用netlink
用户态使用标准的socket api:
- int socket(int domain, int type, int protocol);
- bind()
- sendmsg()
- recvmsg()
- close()
####5.1 创建netlink socket
int socket(int domain, int type, int protocol);
先看domain参数, 用于指定地址族, 几个常用的选项有
- AF_UNIX 本地socket通信用
- AF_LOCAL 同AF_UNIX一致
- AF_INT IPv4因特网
- AF_INET6 IPv6因特网
- AF_NETLINK netlink
- AF_PACKET 用于直接获取数据链路层的数据
当然, 还有几个很少用到的地址族, 这里不涉及, 可以通过man socket
来查看, 对于netlink, 应该选择 AF_NETLINK
之所以划分地址族, 是因为, 不同的套接字种类拥有不同的通信和寻址方式, 其实现并不相同, 只是上层提供了统一的BSD套接字接口
再看type参数
- SOCK_STREAM 提供双向连续且可信赖的数据流, 即TCP
- SOCK_DGRAM 无连接的数据, 即UDP
- SOCK_SEQPACKET 提供连续可信赖的数据包连接
- SOCK_RAW 提供原始网络协议存取
- SOCK_RDM 提供可信赖的数据包连接
- SOCK_PACKET 提供和网络驱动程序直接通信, 已经被废弃, 请参考
man 7 packet
netlink是一个面向数据报的服务, 因此, 应该选择 SOCK_RAW 或者 SOCK_DGRAM
再看protocol参数,对于AF_INT,常见的有
- IPPROTO_TCP
- PPTOTO_UDP
- IPPROTO_SCTP
- IPPROTO_TIPC
protocol和type并不能随意组合, 通常, 一种地址族里面, 一般只会有一种protocol来支持一种type, 这时, 将protocol参数置0即可, 若有多种protocol, 则需指定
kernel 中定义好的协议类型有(include/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_SOCKEV 22 /* Socket Administrative Events */
#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG
用户可以选择自己定义新的 netlink 协议类型, 或者使用专为用户准备的通用协议类型 NETLINK_GENERIC
####5.2 绑定socket地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
套接字并不一定需要绑定地址, 比如socketpair()就生成了一对相互连接但是没有地址的套接口,这就是所谓的无名套接口
有时候也会有这样的情况,在相互连接的两个太接口中有一个套接口不需要地址,例如当连接到一个远程的套接口的时候,虽然必须确定远程套接口的地址,但是发出调用的本地套接口却可以是匿名的
有时候, 需要为套接字绑定一个固定的地址, 这个时候, 就绪要用到bind()
上一小节提到了地址族(doamin), 不同的地址族的地址标识方式并不相同
通用的套接字的地址表示为:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
其中sa_family_t是一个无符号短整数,2个字节,整个数据结构的长度为16个字节
本地套接字(AF_LOCAL和AF_UNIX)的地址表示为:
struct sockaddr_un
{
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108];
};
sun_path必须为一个合法的文件路径,此时, 会自动生成一个文件, 在close套接字时, 需要手工删除该套接字文件, 为防止影响文件系统, 可以在路径开头添加一个’\0’字符, 这样就不会生成对应的文件系统对象
最常用的AF_INET域的
struct sockaddr_in
{
sa_family_t sin_family; /* AF_INET */
uint16_t sin_port; /* port */
struct in_addr sin_addr; /* addr */
unsigned char sin_zero[8]; /* reserved */
};
而AF_NETLINK的地址表示为
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_pad 成员置0不使用
- nl_pid 唯一的ID, 可以指定为本进程ID
- nl_groups bitmap, 指定多播组, 一个协议类型最多支持32个多播组
需要注意的时, 在bind地址时, nl_pid应指定为本进程的ID
在发送消息时, nl_pid为接受进程的id或者为0(发给内核或者多播)
在发送消息时, nl_groups为0表示单播
####5.3 发送netlink消息
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
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: 应指定为netlink的地址(sockaddr_nl结构体)的指针
- msg_namelen: netlink的地址(sockaddr_nl结构体)的长度
上一节中讲解过netlink消息的结构, 一个netlink消息由若干个成对的消息头和消息负载组成, 结构 struct iovec用于把多个消息对组织起来, 通过一次系统调用来发送, 当有多个成对的消息头和消息负载时, 使用一个struct iovec的数组, 每一个数组的成员指向一个消息头, 最后, msghdr.msg_iov指向struct iovec的数组头
设置一个struct msghdr的实例
#define MAX_MSGSIZE 1024
char buffer[] = "An example message";
struct nlmsghdr nlhdr;
struct msghdr msg;
struct iovec iov;
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA(nlhdr),buffer);
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
nlhdr->nlmsg_pid = getpid(); /* self pid */
nlhdr->nlmsg_flags = 0;
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
在完成以上步骤后,消息就可以通过下面语句直接发送:
sendmsg(fd, &msg, 0);
####5.4 接受netlink消息
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
一个接收程序必须分配一个足够大的内存用于保存netlink消息头和消息负载。然后其填充struct msghdr msg,然后使用标准的recvmsg()函数来接收netlink消息,假设缓存通过nlh指针指向, 则:
#define MAX_NL_MSG_LEN 1024
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
struct nlmsghdr * nlhdr;
nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);
iov.iov_base = (void *)nlhdr;
iov.iov_len = MAX_NL_MSG_LEN;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
recvmsg(fd, &msg, 0);
###6. netlink内核空间API
内核空间的netlinkAPI在内核中被netlink核心支持,即net/core/af_netlink.c。从内核角度看,这些API不同 于用户空间的API。这些API可以被内核模块使用从而存取netlink套接字与用户空间程序通信。
除非你使用现存的netlink套接字协议类型,否 则你必须通过在netlink.h中定义一个常量来添加你自己的协议类型。例如,我们需要添加一个netlink协议类型用于测试,则在 netlink.h中加入下面的语句:
#define NETLINK_TEST 17
之后,亦可以在linux内核中的任何地方引用添加的协议类型
需要注意的是, netlink最多支持32种协议类型, 协议号通常分配给一些子系统使用
####6.1 内核中创建socket套接字
struct sock *netlink_kernel_create(int unit, void (*input)(struct sock *sk, int len));
- unit: netlink的协议类型
- input : input则为内核模块定义的netlink消息处理函数,当有消息到达这个netlink socket时,该函数就会被回调
- sk : input函数的sk参数即为创建netlink套接字时返回的sock, 代表一个套接字
一个input函数的实例
void input (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *data = NULL;
while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) {
/* process netlink message pointed by skb->data */
nlh = (struct nlmsghdr *)skb->data;
data = NLMSG_DATA(nlh);
/* process netlink message with header pointed by
* nlh and data pointed by data
*/
}
}
####6.2 内核中发送netlink消息
当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过struct sk_buff来管理的, linux/netlink.h中定义了一个宏用于获取sk_buff的控制块:
#define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))
例如
NETLINK_CB(skb).pid = 0;
NETLINK_CB(skb).dst_pid = 0;
NETLINK_CB(skb).dst_group = 1;
- pid : 表示消息发送者进程ID,也即源地址,对于内核,它为 0
- dst_pid : 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0
- dst_group : 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0
发送单播消息
int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);
- sk : 函数netlink_kernel_create()返回的socket
- skb : 存放消息,它的data字段指向要发送的 netlink消息结构,而skb的控制块保存了消息的地址信息
- pid : 为接收消息进程的pid
- nonblock : 表示该函数是否为非阻塞,如为1,在没有接收缓存可利用时立即返回,如为0,该函 在没有接收缓存可利用时睡眠
发送多播消息
void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group, int allocation);
前三个参数和发送单播消息时相同
- group : 为接收消息的多播组,该参数的每一个代表一个多播组,如果发送给多个多播组,将对应的bit置位
- allocation : 为内核内存分配类型,一般地为GFP_ATOMIC(用于原子的上下文)或GFP_KERNEL(用于非原子上下文)
###6.3 内核中释放netlink socket
void sock_release(struct socket * sock);
###7. 用户空间和内核netlink通信示例
####7.1 单播通信示例
用户空间代码
#include <sys/socket.h>
#include <linux/netlink.h>
#define MAX_PAYLOAD 1024 /* maximum payload size*/
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;
int main() {
sock_fd = socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST);
memset(&src_addr, 0, sizeof(src_addr));
src__addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); /* self pid */
src_addr.nl_groups = 0; /* not in mcast groups */
bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; /* For Linux Kernel */
dest_addr.nl_groups = 0; /* unicast */
nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
/* Fill the netlink message header */
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid(); /* self pid */
nlh->nlmsg_flags = 0;
/* Fill in the netlink message payload */
strcpy(NLMSG_DATA(nlh), "Hello you!");
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
sendmsg(fd, &msg, 0);
/* Read message from kernel */
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
recvmsg(fd, &msg, 0);
printf(" Received message payload: %s\n", NLMSG_DATA(nlh));
/* Close Netlink Socket */
close(sock_fd);
return 0;
}
内核代码
struct sock *nl_sk = NULL;
void nl_data_ready (struct sock *sk, int len)
{
wake_up_interruptible(sk->sleep);
}
void netlink_test() {
struct sk_buff *skb = NULL;
struct nlmsghdr *nlh = NULL;
int err;
u32 pid;
nl_sk = netlink_kernel_create(NETLINK_TEST, nl_data_ready);
/* wait for message coming down from user-space */
skb = skb_recv_datagram(nl_sk, 0, 0, &err);
nlh = (struct nlmsghdr *)skb->data;
printk("%s: received netlink message payload:%s\n",
__FUNCTION__, NLMSG_DATA(nlh));
pid = nlh->nlmsg_pid; /*pid of sending process */
NETLINK_CB(skb).groups = 0; /* not in mcast group */
NETLINK_CB(skb).pid = 0; /* from kernel */
NETLINK_CB(skb).dst_pid = pid;
NETLINK_CB(skb).dst_groups = 0; /* unicast */
netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
sock_release(nl_sk->socket);
12}
####7.2 组播通信示例
用户空间代码
#include <sys/socket.h>
#include <linux/netlink.h>
#define MAX_PAYLOAD 1024 /* maximum payload size*/
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;
int main() {
sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
memset(&src_addr, 0, sizeof(local_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); /* self pid */
/* interested in group 1<<0 */
src_addr.nl_groups = 1;
bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
memset(&dest_addr, 0, sizeof(dest_addr));
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
printf("Waiting for message from kernel\n");
/* Read message from kernel */
recvmsg(fd, &msg, 0);
printf(" Received message payload: %s\n", NLMSG_DATA(nlh));
close(sock_fd);
return 0;
}
内核空间代码
#define MAX_PAYLOAD 1024
struct sock *nl_sk = NULL;
void netlink_test() {
sturct sk_buff *skb = NULL;
struct nlmsghdr *nlh;
int err;
nl_sk = netlink_kernel_create(NETLINK_TEST, nl_data_ready);
skb=alloc_skb(NLMSG_SPACE(MAX_PAYLOAD),GFP_KERNEL);
nlh = (struct nlmsghdr *)skb->data;
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = 0; /* from kernel */
nlh->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlh), "Greeting from kernel!");
/* sender is in group 1<<0 */
NETLINK_CB(skb).groups = 1;
NETLINK_CB(skb).pid = 0; /* from kernel */
NETLINK_CB(skb).dst_pid = 0; /* multicast */
/* to mcast group 1<<0 */
NETLINK_CB(skb).dst_groups = 1;
/*multicast the message to all listening processes*/
netlink_broadcast(nl_sk, skb, 0, 1, GFP_KERNEL);
sock_release(nl_sk->socket);
}
###8. 在用户空间使用libnl
虽然在用户空间可以使用原生的socket API来使用netlink, 但是, 需要自己去处理许多繁琐的细节, 为此,开源社区提供了 libnl 用于简化用户空间的netlink开发
libnl 包含4个主要的模块
- libnl : libnl的核心库
- libnl-route : 工作在NETLINK_ROUTE子协议之上, 提供配置网络接口, 路由信息的API
- libnl-genl : 工作在NETLINK_GENERIC子协议之上, 提供扩展以通用
- libnl-nf : 提供netfilter相关的API
libnl官网链接 可以下载源码和文档
###9. 使用generic
netlink最多能够定义32种子协议,并且每一种子协议都是特殊用途, 其它用户不能够再使用, 为次, 在NETLINK_GENERIC子协议之上做出扩展, 定义了generic netlink总线, 最多支持1023个(第0个被generic netlink用于管理)子family,每一个使用者注册一个generic netlink family及operation即可使用
关于generic netlink见“generic netlink”这一章节
###10. netlink 丢失消息
基于netlink的通信中,有两种可能的情形会导致消息丢失:
- 内存耗尽,没有足够多的内存分配给消息
- 缓存复写,接收队列中没有空间存储消息,这在内核空间和用户空间之间通信时可能会发生
而缓存复写在以下情况很可能会发生:
- 内核子系统以一个恒定的速度发送netlink消息,但是用户态监听者处理过慢
- 用户存储消息的空间过小