Netlink   

Linux中的进程间通信机制源自于Unix平台上的进程通信机制。Unix的两大分支AT&T Unix和BSD Unix在进程通信实现机制上的各有所不同,前者形成了运行在单个计算机上的System V IPC,后者则实现了基于socket的进程间通信机制。同时Linux也遵循IEEE制定的Posix IPC标准,在三者的基础之上实现了以下几种主要的IPC机制:管道(Pipe)及命名管道(Named Pipe),信号(Signal),消息队列(Message queue),共享内存(Shared Memory),信号量(Semaphore),套接字(Socket)。通过这些IPC机制,用户空间进程之间可以完成互相通信。为了完成内核空间与用户空间通信,Linux提供了基于socket的Netlink通信机制,可以实现内核与用户空间数据的及时交换. 本文第2节概述相关研究工作,第3节与其他IPC机制对比,详细介绍Netlink机制及其关键技术,第4节使用KGDB+GDB组合调试,通过一个示例程序演示Netlink通信过程。第5节做总结并指出Netlink通信机制的不足之处。

1. 介绍

到目前Linux提供了9种机制完成内核与用户空间的数据交换,分别是内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,其中模块参数与sysfs、procfs、debugfs、relayfs是基于文件系统的通信机制,用于内核空间向用户控件输出信息;sysctl、系统调用是由用户空间发起的通信机制。由此可见,以上均为单工通信机制,在内核空间与用户空间的双向互动数据交换上略显不足。Netlink是基于socket的通信机制,由于socket本身的双共性、突发性、不阻塞特点,因此能够很好的满足内核与用户空间小量数据的及时交互,因此在Linux 2.6内核中广泛使用,例如SELinux,Linux系统的防火墙分为内核态的netfilter和用户态的iptables,netfilter与iptables的数据交换就是通过Netlink机制完成。

1.1 Netlink机制及其关键技术

Linux操作系统中当CPU处于内核状态时,可以分为有用户上下文的状态和执行硬件、软件中断两种。其中当处于有用户上下文时,由于内核态和用户态的内存映射机制不同,不可直接将本地变量传给用户态的内存区;处于硬件、软件中断时,无法直接向用户内存区传递数据,代码执行不可中断。针对传统的进程间通信机制,他们均无法直接在内核态和用户态之间使用,原因如下表:

解决内核态和用户态通信机制可分为两类: 处于有用户上下文时,可以使用Linux提供的copy_from_user()和copy_to_user()函数完成,但由于这两个函数可能阻塞,因此不能在硬件、软件的中断过程中使用。 处于硬、软件中断时。

1.2 Netlink优点

Netlink相对于其他的通信机制具有以下优点:

在内核源码有关Netlink协议的头文件中包含了内核预定义的协议类型,如下所示:

#define NETLINK_ROUTE         0     
#define NETLINK_W1             1      
#define NETLINK_USERSOCK     2        
#define NETLINK_FIREWALL      3       
#define NETLINK_INET_DIAG     4       
#define NETLINK_NFLOG         5        
#define NETLINK_XFRM          6        
#define NETLINK_SELINUX       7        
#define NETLINK_ISCSI           8        
#define NETLINK_AUDIT          9        
#define NETLINK_FIB_LOOKUP    10  
#define NETLINK_CONNECTOR    11  
#define NETLINK_NETFILTER      12       
#define NETLINK_IP6_FW          13  
#define NETLINK_DNRTMSG       14       
#define NETLINK_KOBJECT_UEVENT 15         
#define NETLINK_GENERIC        16  

上述这些协议已经为不同的系统应用所使用,每种不同的应用都有特有的传输数据的格式,因此如果用户不使用这些协议,需要加入自己定义的协议号。对于每一个Netlink协议类型,可以有多达 32多播组,每一个多播组用一个位表示,Netlink 的多播特性使得发送消息给同一个组仅需要一次系统调用,因而对于需要多拨消息的应用而言,大大地降低了系统调用的次数。

建立Netlink会话过程如下:

内核使用与标准socket API类似的一套API完成通信过程。首先通过netlink_kernel_create()创建套接字,该函数的原型如下:

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参数是网络设备命名空间指针,input函数是netlink socket在接受到消息时调用的回调函数指针,module默认为THIS_MODULE. 然后用户空间进程使用标准Socket API来创建套接字,将进程ID发送至内核空间,用户空间创建使用socket()创建套接字,该函数的原型如下: int socket(int domain, int type, int protocol); 其中domain值为PF_NETLINK,即Netlink使用协议族。protocol为Netlink提供的协议或者是用户自定义的协议,Netlink提供的协议包括NETLINK_ROUTE, NETLINK_FIREWALL, NETLINK_ARPD, NETLINK_ROUTE6和 NETLINK_IP6_FW。 接着使用bind函数绑定。Netlink的bind()函数把一个本地socket地址(源socket地址)与一个打开的socket进行关联。完成绑定,内核空间接收到用户进程ID之后便可以进行通讯。 用户空间进程发送数据使用标准socket API中sendmsg()函数完成,使用时需添加struct msghdr消息和nlmsghdr消息头。一个netlink消息体由nlmsghdr和消息的payload部分组成,输入消息后,内核会进入nlmsghdr指向的缓冲区。 内核空间发送数据使用独立创建的sk_buff缓冲区,Linux定义了如下宏方便对于缓冲区地址的设置,如下所示:

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

在对缓冲区设置完成消息地址之后,可以使用netlink_unicast()来发布单播消息,netlink_unicast()原型如下: int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock); 参数sk为函数netlink_kernel_create()返回的socket,参数skb存放消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块,参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。 内核模块或子系统也可以使用函数netlink_broadcast来发送广播消息: void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group, int allocation); 前面的三个参数与netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个代表一个多播组,因此如果发送给多个多播组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。 接收数据时程序需要申请足够大的空间来存储netlink消息头和消息的payload部分。然后使用标准函数接口recvmsg()来接收netlink消息

2. Netlink 通信过程

调试平台:Vmware 5.5 + Fedora Core 10(两台,一台作为host机,一台作为target机)。 调试程序:分为内核模块和用户空间程序两部分,当内核模块被加载后,运行用户空间程序,由用户空间发起Netlink会话,和内核模块进行数据交换。 被加载的内核模块无法通过外加的调试器进行调试,KGDB提供了一种内核源码级别的调试机制。Linux内核自2.6.26版本之后在内核中内置了KGDB选项,编译内核时需要选择与之相关的选项,调试时host端需使用带有符号表的vmlinz内核,target端使用gdb调试用户空间的程序。 用户空间程序关键代码如下:

int send_pck_to_kern(u8 op, const u8 *data, u16 data_len)  
{  
    struct user_data_ *pck;  
    int ret;  
  
    pck = (struct user_data_*)calloc(1, sizeof(*pck) + data_len);  
    if(!pck) {  
       printf("calloc in %s failed!!!\n", __FUNCTION__);  
       return -1;  
    }  
  
    pck->magic_num = MAGIC_NUM_RNQ;  
    pck->op = op;  
    pck->data_len = data_len;  
  
    memcpy(pck->data, data, data_len);  
    ret = send_to_kern((const u8*)pck, sizeof(*pck) + data_len);  
    if(ret)  
       printf("send_to_kern in %s failed!!!\n", __FUNCTION__);  
  
    free(pck);  
  
    return ret ? -1 : 0;  
}  
  
static void recv_from_nl()  
{  
    char buf[1000];  
    int len;  
    struct iovec iov = {buf, sizeof(buf)};  
    struct sockaddr_nl sa;  
    struct msghdr msg;  
    struct nlmsghdr *nh;  
  
    memset(&msg, 0, sizeof(msg));  
    msg.msg_name = (void *)&sa;  
    msg.msg_namelen = sizeof(sa);  
    msg.msg_iov = &iov;  
    msg.msg_iovlen = 1;  
  
    len = recvmsg(nl_sock, &msg, 0);  	  
    for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len);  nh = NLMSG_NEXT (nh, len)) {  
       // The end of multipart message.  
       if (nh->nlmsg_type == NLMSG_DONE) {  
           puts("nh->nlmsg_type == NLMSG_DONE");  
           return;  
       }  
  
       if (nh->nlmsg_type == NLMSG_ERROR) {  
           // Do some error handling.  
           puts("nh->nlmsg_type == NLMSG_ERROR");  
           return;  
       }  
  
#if 1 
       puts("Data received from kernel:");  
       hex_dump((u8*)NLMSG_DATA(nh), NLMSG_PAYLOAD(nh, 0));  
#endif  
  
   }  
  
}  

内核模块需要防止资源抢占,保证Netlink资源互斥占有,内核模块部分关键代码如下:

static void nl_rcv(struct sk_buff *skb)  
{  
    mutex_lock(&nl_mtx);  
    netlink_rcv_skb(skb, &nl_rcv_msg);  
    mutex_unlock(&nl_mtx);  
}  
  
static int nl_send_msg(const u8 *data, int data_len)  
{  
    struct nlmsghdr *rep;  
    u8 *res;  
    struct sk_buff *skb;  
  
    if(g_pid < 0 || g_nl_sk == NULL) {  
       printk("Invalid parameter, g_pid = %d, g_nl_sk = %p\n", g_pid, g_nl_sk);  
       return -1;  
    }  
  
    skb = nlmsg_new(data_len, GFP_KERNEL);  
    if(!skb) {  
       printk("nlmsg_new failed!!!\n");  
       return -1;  
    }  
   
    if(g_debug_level > 0) {  
       printk("Data to be send to user space:\n");  
       hex_dump((void*)data, data_len);  
    }  
  
    rep = __nlmsg_put(skb, g_pid, 0, NLMSG_NOOP, data_len, 0);  
    res = nlmsg_data(rep);  
    memcpy(res, data, data_len);  
    netlink_unicast(g_nl_sk, skb, g_pid, MSG_DONTWAIT);  
  
    return 0;  
}    
  
static int nl_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)  
{  
    const u8 res_data[] = "Hello, user";  
    size_t data_len;  
    u8 *buf;  
    struct user_data_ *pck;  
    struct user_req *req, *match = NULL;  
  
    g_pid = NETLINK_CB(skb).pid;  
    buf = (u8*)NLMSG_DATA(nlh);  
    data_len = nlmsg_len(nlh);  

    if(data_len < sizeof(struct user_data_)) {  
       printk("Too short data from user space!!!\n");  
       return -1;  
    }  
  
    pck = (struct user_data_ *)buf;  
    if(pck->magic_num != MAGIC_NUM_RNQ) {  
       printk("Magic number not matched!!!\n");  
       return -1;  
    }  
  
    if(g_debug_level > 0) {  
       printk("Data from user space:\n");  
       hex_dump(buf, data_len);  
    }  
  
    req = user_reqs;  
    while(req->op) {  
       if(req->op == pck->op) {  
           match = req;  
           break;  
       }  
       req++;  
    }  
  
    if(match) {  
       match->handler(buf, data_len);  
    }  
  
    nl_send_msg(res_data, sizeof(res_data));  
  
    return 0;  
}