netlink

Posted by Vector Blog on September 12, 2017

1. Netlink 简介

什么是Netlink?Netlink是linux提供的用于内核和用户态进程之间的通信方式。但是注意虽然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接口
  • 支持多播(因此支持“总线”式通信,可实现消息订阅)
  • 在内核端可用于进程上下文与中断上下文

2. 用户态数据结构

netlink 和 UDP 比较类似都是面向数据报的无连接传输, 和 UDP 一样Netlink也支持 sendto 和 sendmsg , sendmsg 需要用户空间自己构造 msghdr 头, 而 sendto 会由系统调用 sys_sendto 来添加 msghdr . 与 UDP 不同的是会多一个 nlmsghdr 头。

先来看几个重要的数据结构的关系:

2.1 struct msghdr

msghdr这个结构在socket变成中就会用到,并不算Netlink专有的 , 其中 msg_iov指定数据缓冲区数组,而msg_iovlen指明了该数组的元素个数。

2.2 struct sockaddr_nl

Struct sockaddr_nl 为Netlink 的地址,和我们通常socket编程中的sockaddr_in作用一样 , 字段 nl_family 为 netlink 套接字地址类型,一般 nl_family 默认为 AF_NETLINK 。nl_pad 为填充字段,一般填充0 . 字段 nl_pid 为端口号。nl_groups 为多播地址掩码。

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_pid:在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。通常情况 nl_pid都设置为当前进程的进程号。对于一个进程的多个线程同时使用netlink socket的情况,nl_pid的设置一般采用如下这个样子来实现:

pthread_self() << 16 | getpid();

2.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仅支持四种类型的控制消息,如下:

NLMSG_NOOP 空消息,什么也不做
NLMSG_ERROR 指明该消息中包含一个错误
NLMSG_DONE 当内核通过Netlink队列返回了多个消息,队列的最后一条消息的类型为NLMSG_DONE,其余消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效
NLMSG_OVERRUN 暂时没用到

(3) nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。
(4) nlmsg_seq:消息序列号。因为Netlink是面向数据报的,所以存在丢失数据的风险,但是Netlink提供了如何确保消息不丢失的机制,让程序开发人员根据其实际需求而实现。消息序列号一般和NLM_F_ACK类型的消息联合使用,如果用户的应用程序需要保证其发送的每条消息都成功被内核收到的话,那么它发送消息时需要用户程序自己设置序号,内核收到该消息后对提取其中的序列号,然后在发送给用户程序回应消息里设置同样的序列号。有点类似于TCP的响应和确认机制。 当内核主动向用户空间发送广播消息时,消息中的该字段总是为0。

2.4 用户空间示例

#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <errno.h>

#define MAX_PAYLOAD 1024 // maximum payload size
#define NETLINK_TEST 25 //自定义的协议
int main(int argc, char* argv[])
{
    int state;
    struct sockaddr_nl src_addr, dest_addr;
    struct nlmsghdr *nlh = NULL; 	//Netlink数据包头
    struct iovec iov;
    struct msghdr msg;
    int sock_fd, retval;
    int state_smg = 0;
    
    // Create a socket
    sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);

    // To prepare binding
    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid(); 	 //A:设置源端端口号
    src_addr.nl_groups = 0;
    
    //Bind
    retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));

    // To orepare create mssage
    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    memset(&dest_addr,0,sizeof(dest_addr));
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid =getpid(); 		//C:设置源端口
    nlh->nlmsg_flags = 0;
    strcpy(NLMSG_DATA(nlh),"Hello you!"); 	//设置消息体
    
    iov.iov_base = (void *)nlh;
    iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
    
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0; 		//B:设置目的端口号
    dest_addr.nl_groups = 0;
    
    //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
    state_smg = sendmsg(sock_fd,&msg,0);
    
    memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
    
    //receive message
    while(1){
        state = recvmsg(sock_fd, &msg, 0);
        if(state<0)
        {
            printf("state<1");
        }
        printf("Received message: %s\n",(char *) NLMSG_DATA(nlh));
    }
    close(sock_fd);
    return 0;
}

以上有3个设置端口号的位置, A处 bind socket 时为注册地址 ; B处将包含端口号的地址加入msghdr , 代表收信人的地址, 0代表内核 ; C处为数据中包含的源地址, 通常情况和 A 一样。

#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 */

3. 内核Netlink

static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)

(1) net:是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用 init_net这个全局变量。 (2) unit:表示netlink协议类型,如NETLINK_TEST、NETLINK_SELINUX。 (3) cfg: 包含多播地址和消息接收函数等netlink 的相关参数 , 3.6.0之前的版本这些参数直接是 netlink_kernel_create 的参数

struct netlink_kernel_cfg {
	unsigned int	groups;		//多播地址
	unsigned int	flags;
	/* input 为内核模块定义的netlink消息处理函数,当有消 息到达这个netlink socket时,
	该input函数指针就会被引用,且只有此函数返回时,调用者的sendmsg才能返回。 */
	void		(*input)(struct sk_buff *skb);
	struct mutex	*cb_mutex;		//访问数据时的互斥信号量
	int		(*bind)(int group);
	void		(*unbind)(int group);
	bool		(*compare)(struct net *net, struct sock *sk);
};

3.2 发送消息

当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过struct sk_buff来管理的, linux/netlink.h中定义了一个宏用于获取sk_buff的控制块:

#define NETLINK_CB(skb)         (*(struct netlink_skb_parms*)&((skb)->cb))

//示例
NETLINK_CB(skb).portid = 0;	//发送者pid
NETLINK_CB(skb).dst_group = 1;	//目标组地址 

发送单播消息netlink_unicast :

int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)

(1) ssk:为函数 netlink_kernel_create()返回的socket。
(2) skb:存放消息,它的data字段指向要发送的netlink消息结构,而 skb的控制块保存了消息的地址信息,宏NETLINK_CB(skb)就用于方便设置该控制块。
(3) pid:为接收此消息进程的pid,即目标地址,如果目标为组或内核,它设置为 0。
(4) nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回;而如果为0,该函数在没有接收缓存可利用定时睡眠。

发送广播消息netlink_broadcast : 最多允许设置32个多播组,每个多播组用1个比特表示,所以不同的多播组不可能出现重复。用户可以根据自己的实际需求,决定哪个多播组是用来做什么的。用户空间的进程如果对某个多播组感兴趣,那么它就加入到该组中,当内核空间的进程往该组发送多播消息时,所有已经加入到该多播组的用户进程都会收到该消息。

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用于非原子上下文。

void netlink_kernel_release(struct sock *sk)

3.4 内核程序示例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/time.h>
#include <linux/types.h>
#include <net/sock.h>
#include <net/netlink.h>

#define NETLINK_TEST 25
#define MAX_MSGSIZE 1024

int stringlength(char *s);
int err;
struct sock *nl_sk = NULL;

//向用户态进程回发消息
void sendnlmsg(char *message, int pid)
{
    struct sk_buff *skb_1;
    struct nlmsghdr *nlh;
    int len = NLMSG_SPACE(MAX_MSGSIZE);
    int slen = 0;

    printk(KERN_ERR "pid:%d\n",pid);
    skb_1 = alloc_skb(len,GFP_KERNEL);

    slen = stringlength(message);
    nlh = nlmsg_put(skb_1,0,0,0,MAX_MSGSIZE,0);
    NETLINK_CB(skb_1).portid = 0;
    NETLINK_CB(skb_1).dst_group = 0;
    message[slen]= '\0';
    memcpy(NLMSG_DATA(nlh),message,slen+1);
    
    netlink_unicast(nl_sk,skb_1,pid,MSG_DONTWAIT);
}
int stringlength(char *s)
{
    int slen = 0;
    for(; *s; s++)
    {
        slen++;
    }
    return slen;
}

//接收用户态发来的消息
void nl_data_ready(struct sk_buff *__skb)
 {
     struct sk_buff *skb;
     struct nlmsghdr *nlh;
     char str[100];
     struct completion cmpl;
     int i=10;
     int pid;
     
     skb = skb_get (__skb);
     if(skb->len >= NLMSG_SPACE(0))
     {
         nlh = nlmsg_hdr(skb);
         memcpy(str, NLMSG_DATA(nlh), sizeof(str));
         printk("Message received:%s\n",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);
        }
         kfree_skb(skb);
    }
 }
 
// Initialize netlink
int netlink_init(void)
{
   struct netlink_kernel_cfg cfg = {
      .groups = 1,
      .input = nl_data_ready
   };
    nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
    return 0;
}

static void netlink_exit(void)
{
    if(nl_sk != NULL){
        sock_release(nl_sk->sk_socket);
    }
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_AUTHOR("yilong");
MODULE_LICENSE("GPL");

值得注意的是 : 因为在 nl_data_ready 中会每次延时3s 多次用户进程发送消息, 所以该函数不会立刻返回 ,这样造成用户态的 sendmsg 不会立刻返回, 等到 nl_data_ready return 才会返回, 所以用户程序会看到的现象是发送很久之后才一次性收到kernel 发回的数据。 进程使用Netlink向内核发数据是同步,内核向进程发数据是异步