一 概述
Linux提供了多种机制来完成内核空间与用户空间之间的数据交换,分别有内核启动参数、模块参数、sysfs、sysctl、系统调用、procfs、seq_file、debugfs、relayfs。其中,模块参数、sysfs、sysctl、procfs、seq_file、debugfs、relayfs是基于文件系统的通信机制,用于内核空间向用户空间输出信息;sysctl、系统调用是由用户空间发起的通信机制。以上均为单工通信机制,在内核空间与用户空间的双向数据交换上略显不足,由此引入了netlink机制。
netlink是一种实现内核空间和用户空间通信的进程间通信机制IPC,可以理解为一种特殊的socket,具有双向全双工异步传输的特点,能够很好的满足内核空间和用户空间交互。它支持由内核态主动发起通信,内核为Netlink通信提供了一组特殊的API接口,用户态则基于socket API,内核发送的数据会保存在接收进程socket 的接收缓存中,由接收进程处理。
netlink相对于其他的通信机制具有以下优点:
- 使用netlink通过自定义一种新的协议并加入协议族,即可通过socket API使用netlink协议完成数据交换,而ioctl和proc文件系统均需要通过程序加入相应的设备或文件。
- netlink使用socket缓冲队列,是一种异步通信机制,而ioctl是同步通信,如果传输数据量较大会影响系统性能。
- netlink支持多播,属于一个netlink组的模块和进程都能获得该多播消息。 即内核态可以将消息发送给多个接收进程,这样就不用每个进程单独来查询了。
- netlink允许内核发起会话,而ioctl和系统调用只能由用户空间发起。
- 双向全双工异步传输,支持由内核主动发起传输通信,如此用户空间在等待内核某种触发条件满足时就无需不断轮询,而异步接收内核消息即可。
netlink架构框图:
二 使用
1 用户空间
用户态使用标准的socket API如socket,bind,sendmsg,recvmsg和close等接口就能很容易地使用netlink socket。
1)、创建netlink socket
sock_fd =
socket(AF_NETLINK/PF_NETLINK, int type, int protocol)
其中,type可以取SOCK_RAW或者SOCK_DGRAM。
protocol指定netlink协议类型。当前支持的协议类型定义如下:
目前 netlink 协议族支持32种协议类型,内核使用了21中,定义在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,用于uevent消息通信
#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</span>
2)、将netlink socket和进程绑定
bind(sock_fd, (struct sockaddr*)&nl_addr, sizeof(&nl_addr));
函数bind用于把一个打开的netlink socket与进程进行绑定,需要进行绑定的netlink socket地址结构如下:
//netlink socket地址结构体
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* 固定取AF_NETLINK或者PF_NETLINK,无差别 */
unsigned short nl_pad; /* 填充字段,取zero */
__u32 nl_pid; /* 当前进程port ID,一般通过getpid获取 */
__u32 nl_groups; //multicast groups mask ,用于指定多播组,每一个bit对应一个多播组,如果设置为0,表示不加入任何多播组
};
3)、发送netlink消息
为了能够把netlink消息发送给内核或者别的用户进程,需要使用另外一个结构体struct sockaddr_nl作为目的地址。如果消息是发往内核的话,nl_pid和nl_groups都应该设置为0,;如果消息是发往另一个进 程,nl_pid应该设置为接受者进程的PID,nl_groups用于设置需要发往的多播组。
填充好了目的地址后,就可以将netlink地址应用到结构体struct msghdr中,供函数sendmsg来调用:
struct msghdr msg;
msg.msg_name = (void *)&nladdr;
msg.msg_namelen = sizeof(nladdr);
由于linux内核的netlink部分总是认为每个netlink消息体中已经包含了下面的消息头,所以每个应用程序在发送netlink消息之前需要提供这个头信息:
//netlink message header
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 */
};
填充完消息头后,在消息头后面就可以填充消息体的内容了,填充完消息体,使用struct iovec结构体,使iov_base指向包含netlink消息的缓冲区,即可调用sendmsg接口发送netlink消息。struct iovec结构定义如下:
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
发送netlink消息的代码如下:
struct iovec iov;
iov.iov_base = (void *)msg_buffer;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
sendmsg(sock_fd, &msg, 0);
4)、接收消息
接收程序需要申请足够大的空间来存储netlink消息头和消息的payload部分。用如下的方式填充结构体struct msghdr,然后调用recvmsg接口来接收netlink消息:
char msg_buffer[MSX_NL_MSG_LEN];
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
iov.iov_base = (void *)msg_buffer;
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(sock_fd, &msg, 0);
当消息正确接收后,msg_buffer里保存包含netlink消息头的整个netlink消息,nladdr包含接收到的消息体的目的地址信息。
5)、关闭netlink socket
使用完前面创建的netlink socket后,就可以使用close接口关闭netlink socket,释放socket资源。关闭netlink socket的代码与关闭其他socket一致,代码如下:
close(sock_fd);
2 内核态使用netlink socket
内核空间的netlink API接口是由内核中的netlink核心代码支持的,在net/core/af_netlink.c中实现。内核模块通过这些API访问netlink socket并与用户空间的程序进行通信。
1) 添加自定义协议类型
内核已经支持的协议类型在linux/netlink.h中定义,如果要使用自定义的协议类型,需要在此文件中新增一个协议类型,然后就可以在linux内核模块中使用这个协议类型了。
2) 在内核创建netlink socket
在内核空间,通过如下接口创建netlink socket:
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为网络设备名称,一般传入&init_net即可;unit为协议类型,传入自定义的协议类型;groups指定netlink 消息有多少个组,一般情况下传入0即可;input是用于netlink socket在收到消息时调用的处理消息的函数指针;cb_mutex是用于内核处理netlink socket消息时使用的互斥锁,一般情况下传入NULL即可;module指定创建的netlink socket所属的内核模块,一般情况下传入THIS_MODULE。
在内核创建了netlink socket后,当用户程序发送一个netlink消息到内核时,函数input都会被调用。下面是一个实现了消息处理函数input的例子:
void input (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *payload = NULL;
while ((skb = skb_dequeue(&sk->receive_queue))!= NULL)
{
/* process netlink message pointed by skb->data */
nlh = (struct nlmsghdr *)skb->data;
payload = NLMSG_DATA(nlh);
/* process netlink message with header pointed by
* nlh and payload pointed by payload
*/
}
}
回调函数input是在用户进程调用sendmsg系统调用时被调用的。因此该函数中的处理时间不能太长,否则会导致其他系统调用被阻塞。比较好的做法是在该函数中创建一个新的内核线程来处理netlink socket消息。
3) 在内核中发送netlink消息
在内核空间发送netlink消息时有两个接口可以使用,netlink_unicast接口用来发送一个单播消息,其定义如下:
int netlink_unicast(struct sock *ssk, struct sk_buff *skb,
u32 pid, int nonblock)
ssk是调用netlink_kernel_create接口所创建的socket控制块,skb中的data指向需要发送的netlink消息体,pid为要发往的用户进程的PID,nonblock指示当发送缓冲区不可用时尝试发送的超时时间。
netlink消息的源地址和目的地址可以通过如下代码进行设置:
NETLINK_CB(skb).groups = local_groups;
NETLINK_CB(skb).pid = 0; /* from kernel */
NETLINK_CB(skb).dst_groups = dst_groups;
NETLINK_CB(skb).dst_pid = dst_pid;
使用netlink_broadcast接口可以发送一个多播消息,其定义如下:
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid,
u32 group, gfp_t allocation)
其中group是接收消息的各个组的比特位进行与运算的结果。allocation是内核申请内存的类型,通常情况下在中断上下文中使用GFP_ATOMIC,否则使用GFP_KERNEL。
4)在内核中关闭netlink socket
假设netlink_kernel_create函数返回的netlink socket为struct sock *nl_sk,我们可以通过如下API来关闭这个netlink socket:
sock_release(nl_sk->socket);
二 netlink子系统初始化
内核Netlink的初始化在系统启动阶段完成,初始化代码在af_netlink.c的netlink_proto_init()函数中,整个初始化流程如下:
netlink_table结构体:
struct netlink_table {
struct rhashtable hash; //用来索引同种协议类型的不同netlink套接字实例
struct hlist_head mc_list;//多播使用的sock散列表
struct listeners __rcu *listeners; //监听者掩码
unsigned int flags;
unsigned int groups; //协议支持最大多播组数目
struct mutex *cb_mutex;
struct module *module;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
bool (*compare)(struct net *net, struct sock *sock);
int registered;
};
netlink子系统初始化:
static int __init netlink_proto_init(void)
{
int i;
int err = proto_register(&netlink_proto, 0); //向内核注册netlink协议
if (err != 0)
goto out;
BUILD_BUG_ON(sizeof(struct netlink_skb_parms) > FIELD_SIZEOF(struct sk_buff, cb));
nl_table = kcalloc(MAX_LINKS, sizeof(*nl_table), GFP_KERNEL);//创建并初始化netlink_table表数组nl_table,每种协议占nl_table表数组的一项
if (!nl_table)
goto panic;
for (i = 0; i < MAX_LINKS; i++) {
if (rhashtable_init(&nl_table[i].hash,
&netlink_rhashtable_params) < 0) {
while (--i > 0)
rhashtable_destroy(&nl_table[i].hash);
kfree(nl_table);
goto panic;
}
}
INIT_LIST_HEAD(&netlink_tap_all);
netlink_add_usersock_entry();//初始化应用层使用的NETLINK_USERSOCK协议类型的netlink(用于应用层进程间通信)
sock_register(&netlink_family_ops);//向内核注册协议处理函数,即将netlink的socket创建处理函数注册到内核中,如此以后应用层创建netlink类型的socket时将会调用该协议处理函数,
register_pernet_subsys(&netlink_net_ops);//向内核所有的网络命名空间注册”子系统“的初始化和去初始化函数,这里的"子系统”并非指的是netlink子系统,而是一种通用的处理方式,在网络命名空间创建和注销时会调用这里注册的初始化和去初始化函数(当然对于已经存在的网络命名空间,在注册的过程中也会调用其初始化函数),后文中创建各种协议类型的netlink也是通过这种方式实现的。
/* The netlink device handler may be needed early. */
rtnetlink_init();//创建NETLINK_ROUTE协议类型的netlink
out:
return err;
panic:
panic("netlink_init: Cannot allocate nl_table\n");
}
core_initcall(netlink_proto_init);