netlink套接字的使用

使用这个双向、多用的方法来解决内核空间――用户空间的数据传递问题。

由于内核的不断发展和维护的复杂性,只有大部分的基础性的、临界执行的代码植入内核中,而其他的,例如GUI、管理和控制模块都是在用户空间执行的。Linux中,内核空间和用户空间的数据交换是相当频繁的,其主要问题是如何让内核代码和用户空间代码进行相互通信。

众所周知,解决这个问题的方发,是利用各种不同的内核-用户进程间通信方法。例如系统调用、ioctl、proc文件系统和Netlink套接字。本文介绍了netlink套接字并且展示了这一具有友好特型的进程间通信套接字的优点。
简介:
Netlink套接字是一种特殊的应用于那和空间和用户空间进行进程间数据传输的进程间通信方法,进程间通信方法在内核空间和用户空间提供了一种全双工通信方式,具体方法是在用户空间使用标准API,在内核空间使用特殊API来实现的。Netlink套接字使用的协议族是AF_NETLINK,其功能就像TCP/IP协议族中的AF_INET一样。在include/linux/netlink.h头文件中,定义了各种不同风格的netlink套接字。
下面是一些netlink套接字所支持的协议类型和特征。
? NETLINK_ROUTE:用户空间路由信息交流渠道。例如BGP、OSPF、RIP以及内核包中的推进模块。内核空间通过本Netlink协议类型对内核空间的路由表进行更新。
? NETLINK_FIREWALL:接收从IPv4防火墙代码发送来的数据包。
? NETLINK_NFLOG:是用户空间iptable工具和内核Netfilter模块之间的通信渠道。
? NETLINK_ARPD:从用户空间来维护ARP表。
在实现内核-用户空间信息通信时,为何使用以上提到的netlink套接字来代替传统的系统调用、ioctl和proc文件系统呢?这是因为使用系统调用、ioctl和proc文件来实现某些功能并不是一件简单的事情,而且我们使用他们的时候,是冒着影响内核正常工作和破坏系统的风险来做的。而Netlink套接字是简单易用的,其实:我们只需要在netlink.h中添加一个我们所需要的协议类型,一个固定的数值(通常是17—32之间的整数),然后,内核空间和用户空间就可以马上来实现通信了。简单吧?
Netlink是一种异步模式,和其他套接字API相比,它提供了一个套接字队列用来缓冲消息的迅速膨胀。发送一个Netlink消息的系统调用将消息放入接收者的netlink队列中,然后调用接收者的接收句柄。接收方,结合接收句柄,可以判断出是应该立即对其作出处理还是将其闲置于队列中等待其他进程上下文对其处理。系统调用不同于netlink,它是一种同步模式。因此,如果我们使用系统调用从用户空间发送一个消息到内核空间的话,如果处理该消息的时间较长,则会影响到内核进程安排的处理粒度。
当一段代码在内核中执行一次系统调用时,这段代码在编辑的时候就已经连接到内核中去了;因此,在一个可加载模块中,使用系统调用是不合适的,例如某些设备的驱动程序模块。使用Netlink套接字就有所不同,并没有什么编辑时依赖于Linux内核中的netlink核心,并且netlink套接字可以在可加载模块中处于活动状态。
Netlink套接字是支持多播的,这使其相对于系统调用、ioctl、proc的又一个优势。一个进程可以将消息一多播的形式发到一个netlink地址组中,并且其他的任何进程可以对这个netlink地址组进行监听。这中方式为内核发送消息到用户提供了一种几乎是完美的机制。
系统调用和ioctl是一种简单的进程间通信,他们只能有用户空间的应用发起会话,这种方法不适合于当内核进程有紧急事件信息需要发送到用户空间的情况。通常,应用程序是通过周期性的对内核进行轮讯来取得状态的转换的,而太强的轮讯的消费是相当昂贵的。Netlink套接字解决了这一难题,它可以让内核空间主动发起会话。我们将这种方法称为netlink套接字的全双工特性。
总之,Netlink套接字提供了一个BSD套接字类型的API,这种类型是软件发展团体中的通用模式,是一种容易理解的类型。因此,相对于系统调用和ioctl的API的熟悉过程也较为短暂,训练花销较小。

涉及到BSD路由套接字
在BSDTCP/IP协议栈执行过程中,有一个特殊的套接字我们称之为路由套接字,其地址族定义为AF_ROUTE、协议族为PF_ROUTE、套接字类型为SOCK_RAW。BSD系统中的路由套接字用于对内核路由表内容进行添加或者删除路由信息。
在linux系统中,与上面提到的路由套接字有异曲同工之用的是netlink套接字提供的协议类型NETLINK_ROUTE。这是Netlink套接字提供的一个BSD路由套接字的父类。

Netlink套接字API
标准的套接字API有:socket(),sendmsg(),recvmsg()和close()。这些都可以用在用户空间来进入netlink套接字中。读者可用通过linux中的手册来对这些函数进行进一步的了解。这里我们只讨论在Netlink套接字环境中如何为这些函数选择参数。对于大多数从事过linux套接字编程的人们来说,这些函数并不陌生。

建立一个套接字。Socket()
int socket(ini domain, int type, int protocol)

这里套接字domain是指地址族,我们在Netlink中使用AF_NETLINK,套接字类型我们使用SOCK_RAW或者SOCK_DGRAM,因为netlink是一个基于数据包传输的套接字。
协议类型我们用netlink所提供的几种特型,例如NETLINK_ROUTE,NETLINK_FIREWALL,NETLINK_ARPD,NETLINK_ROUTE6,NETLINK_IP6_FW。我们在使用中可以根据需要选择其中的一个。
Netlink支持多点传送,最多可以定义32个不同的传送组。每一个组有一个比特掩码来表示。1<
绑定:bind()
和其他TCP/IP套接字相同,netlink套接字也需要使用bind()函数来进行原地址和套接字的绑定工作。Netlink套接字的地址类型如下:

代码:

struct sodkaddr_nl
   {
      sa_family_t      nl_family;     /*AF_NETLINK*/
      unsigned short    nl_pad;       /*zero*/
      __u32         nl_pid;      /*process pid*/
      __u32         nl_groups;   /*mcast groups mask*/
}nladdr;

当使用bind()时,在sockaddr_nl地址结构中的nl_pid可以用调用进程自己的pid号来填充。此处nl_pid的作用就像此netlink套接字的本地地址一样。应用程序负责取回一个唯一的32位整形来填充nl_pid:
NL_PID Formula 1: nl_pid = getpid();
公式1:程序的进程ID号作为nl_pid,在一个给定的netlink套接字类型中,如果这个进行只需要一个套接字,那么这个是默认选择的。
在某些情况中,当一个进程的不同线程需要在相同的netlink协议中创建不同的netlink套接字时,我们使用公式二:
NL_PID Formula 2:pthread_self() << 16 | getpid();
在这种方法中,现同进程的不同线程都可以拥有自己的在同一netlink协议类型下的netlink套接字。事实上,在单线程中,也极有可能为同一个netlink协议类型创建多个netlink套接字。开发人员需要不断的创新,在产生一个唯一的nl_pid时,我们并不需要认为这个时一个正常的使用类型。
如果某一程序想要接收到netlink协议所组播的netlink信息的话,所有的组播组都要有一个统一的sodkaddr_nl结构中nl_groups组号用来接收。否则,我们将nl_groups置0,这样每一个程序只能够收到netlink发送出来的单播信息。当将nladdr结构项初始化结束后,使用bind()函数:
bind(fd, (strut sockaddr *)&nladdr, sizeof(nladdr));


发送一个Netlink消息
为了向内核空间或者其他用户空间发送一个Netlink消息,这里需要有另外一个sockaddr_nl结构的nladdr地址,我们将他定为目的地址,就像用sendmsg()函数发送一个UDP协议一样。如果该消息是发送到内核空间的,那么nladdr地址结构中的nl_pid,nl_groups成员都要设置为0。
如果消息是以单播形式发送到其他进程的,那么nl_pid成员是接收者进程的pid,nl_groups置0。此处使用nlpid公式1。
如果消息是以多播形式向一个或多个接收组,所有的目标接收组的序号都要结合在一起来形成一个组域。我们可以将netlink地址中的数据用来填充msghdr结构中的信息,然后使用sendmsg()函数将其发送出去。
Struct msghdr msg;
Msg.msg_name = (void*)&nladdr;
Msg.msg_namelen = sizeof(nladdr);
我们也需要对netlink套接字本身的头信息进行初始化。这是为netlink套接字发送必须完成的工作,各个协议类型都是一样的。

因为linux内核中的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用来表示整个netlink套接字的长度,包括netlink套接字的头部长度。这项是必须填充的。Nlmsg_type可以被应用程序所使用,并且对于netlink核心来说是不透明的。Nlmsg_flags对一个消息进行操作的附加标志,netlink内核对其读取和更新。Nlmsg_sql和nlmsg_pid是程序用来捕获数据时所用的,他们对于netlink内核来说也时不透明的。
一个netlink消息有消息头和消息体组成。一旦一个消息被接受到之后,由nlh指针指向其存放的缓冲区。我们也可以将本消息发送到一个msghdr msg 结构。
代码:

struct iovec iov;
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;

在以上步骤都结束后,我们调用sendmsg()函数,将这个netlink消息发送出去:
sendmsg(fd, &msg, 0);

接收netlink消息
一个接收函数必须为接收到的数据分配一个足够大的空间。然后填充msghdr msg结构后,使用recvmsg()来接收netlink消息,此处认为nlh指向其缓冲区。
代码:

struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
iov.iov_base = (void *)nlh;
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);

当消息被正确接受后,指针nlh指向刚刚接收到的消息的首地址。Nladdr包含了接受到数据包的目的地址,也包含了pid和多播组号。在netlink.h中定义了NLMSG_DATA(nlh),它返回一个指向netlink消息体的指针。使用cloas(fd)来关闭我们刚刚建立的netlink套接字。

内核空间Netlink API
内核空间netlinkAPI由内核中的netlink核心支持,在net/core/af_netlink.c中所定义。在内核中,netlinkAPI与用户空间有所不同。内核API可以由内核模块使用并进入netlink套接字和用户空间的应用程序进行交流。除非你使用netlink中已经存在的那些套接字协议类型,否则你要到netlink.h中定义自已所要用到的协议类型。例如,我们可以在netlink.h中添加一行来定义下面的协议类型:
引用:

#define NETLINK_TEST 17

然后,我们可以在程序中引用我们所定义的这个协议类型。
在用户空间,我们使用socket()函数来定义一个netlink 套接字,但是在内核空间我们是下面的函数:
代码:

struct sock *
netlink_kernel_create(int unit,
           void (*input)(struct sock *sk, int len));

上面的参数unit是一个netlink套接字协议论类型,例如我们上面定义过的NETLINK_TEST。那个函数指针-input指向一个回调函数,这个函数将会在一个消息到达本netlink套接字后被调用。
在内核空间建立了一个新的NETLINK_TEST类型的netlink套接字后,当用户空间使用NETLINK_TEST像内核空间发送netlink消息时,这个回调函数将会被调用,input()函数是在netlink_kernel_create()中注册的。下面是这个函数的一个例子:
代码:

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()发送消息时统调用上下文中被调用。如果在input()中处理netlink消息很快的话,那么对于这个进程函数来说是有益的。但是,如果这个函数时间太长,我们不使用input()函数,以免堵塞其他内核进程。进而,我们使用一个专用的内核线程来解决这个问题。使用skb =skb_recv_datagram(nl_sk),其中nl_sk是由netlink_kernel_create()的创建的套接字。然后,netlink套接字的消息由skb->data来指向。

这个内核线程在没有netlink消息到达nl_sk时候,处于睡眠状态。因此,在回调函数input()中,我们需要使用下面的方法,将其激活:
代码:

void input (struct sock *sk, int len)
{
  wake_up_interruptible(sk->sleep);
}

这是一个可升级的内核-用户空间信息交互模块。也可以用来提高上下文交换的粒度。

从内核空间向用户空间发送消息
和用户空间相同,当内核空间发送消息时,也需要配置源netlink地址和目的地址。假设我们准备发送的netlink信息是一个sk_buff *skb结构,那么,本地地址需要做如下设置:
代码:

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;

这些信息并不是保存在skb->data中的。而是保存在skb的一个netlink控制块中。

要发送一个单播信息,使用下面的函数:

代码:

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

其中,ssk是由前面提到的netlink_kernel_create()创建的netlink套接字,skb->data指向要被发送的netlink信息,pid是接收进程的pid号。这里使用NLPID公式1。Nonblock表示这个API在接收缓冲不可用或者立即返回失败时是不是被阻塞。

我们也可以使用多播来发送信息。下面的函数将发送一个消息到一个配置了特殊pid和多播组的进程。

代码:
void
netlink_broadcast(struct sock *ssk, struct sk_buff
         *skb, u32 pid, u32 group, int allocation);

其中group是所有接收的多播组的标志号。Allocation是内核内存分配类型。如果是再中断上下文环境,有GFP_ATOMIC,其他的有GFP_KERNEL。这取决于内核需要分配一个内存块还是分配多个缓冲用来克隆这个多播信息。

从内核关闭一个netlink套接字

这里使用下面的函数,将netlink_kernel_create()创建的新的套接字结构nl_sk释放。
代码:
sock_release(nl_sk->socket);

至此,我们简单的介绍了netlink套接字编程的概念。我们这里举一个例子,使用NETLINK_TESTnetlink协议类型,这里我们已经修改了netlink.h文件。这里内核模块在监听用户空间发现发送一个消息过来。

单播模式代码:
这里,用户空间发送一个netlink套接字消息到内核空间,然后内核空间接受到这一消息后,将其输出。
用户空间代码如下:
代码:

#i nclude
#i nclude
#define MAX_PAYLOAD 1024  /* maximum payload size*/
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;
void 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);
}   

内核空间代码:
代码:

#i nclude
#i nclude
#i nclude
#i nclude
#i nclude
#i nclude
#i nclude

#i nclude
#i nclude
#i nclude


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);
}


static int my_module_init(void){
        printk(KERN_INFO "initializing Netlink Socket!\n");
        netlink_test();
        return 0;
}
static void netlink_clear(void)
{
        printk(KERN_INFO"GOod Bye!\n");
}

module_init(my_module_init);
module_exit(netlink_clear);

将内核空间代码编译通过后,将其插入到内核模块中,然后在用户空间执行已经编译过的文件,我们将会得到如下结果:
引用:

Received message payload: Hello you!

我们使用dmesg命令可以看到如下结果:
引用:

netlink_test: received netlink message payload:
Hello you!

多播方式:
这个例子中,我们使用两个用户空间进程,等待内核模块发送netlink消息过来。
这里是用户空间代码:
代码:

#i nclude
#i nclude
#i nclude


#define MAX_PAYLOAD 1024  /* maximum payload size*/
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct msghdr msg;
struct iovec iov;
int sock_fd;
int ret = 1;
void main() {
 sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
 if (sock_fd == -1){
    perror("socket error");
        exit(1);
  }
 memset(&src_addr, 0, sizeof(src_addr));

 src_addr.nl_family = AF_NETLINK;
 src_addr.nl_pid = getpid();  /*self pid */
 /* interested in group 1<<0 */
 src_addr.nl_groups = 35;

if(bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr))== -1)
  {
   perror("bind error");
        exit(1);
   }

 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("\nWaiting for message from kernel\n");

 /* Read message from kernel */
 ret = recvmsg(sock_fd, &msg, 0);
 printf("%d\n",ret);
 printf("Received message payload: %s\n",NLMSG_DATA(nlh));

 close(sock_fd);
}

内核代码如下:
代码:

#ifndef __KERNEL__
   #define __KERNEL__
#endif

#ifndef MODULE
   #define MODULE
#endif

#i nclude
#i nclude
#i nclude

#i nclude
#i nclude
#i nclude
#i nclude

#i nclude
#i nclude
#i nclude

#i nclude
#i nclude


#define MAX_PAYLOAD 1024

struct sock *nl_sk = NULL;
int bc;
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;
 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 = (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*/

 bc = netlink_broadcast(nl_sk, skb, 0, 1, GFP_KERNEL);
 /*  bc = netlink_unicast(nl_sk,skb,0,MSG_DONTWAIT); */

        printk(KERN_INFO"netlinkbroadast :%d\n",bc);

 /*sock_release(nl_sk->socket);*/
}

static int my_module_init(void){
        printk(KERN_INFO "initializing Netlink Socket!\n");
        netlink_test();
        return 0;
}

static void netlink_exit(void)
{
        sock_release(nl_sk->socket);
        printk(KERN_INFO "Good Bye Netlink!\n");
}

module_init(my_module_init);
module_exit(netlink_exit);
                               
MODULE_LICENSE("GPL");


在用户空间运行两个接收进程,比如我们编译后的可执行文件为nl_recv
引用:

./nl_recv &
Waiting for message from kernel
./nl_recv &
Waiting for message from kernel

之后,我们将要把内核模块插入到内核中来。会得到如下结果:
引用:

Received message payload: Greeting from kernel!
Received message payload: Greeting from kernel!


结论:
netlink套接字是一个较为灵活的内核空间和用户空间信息交互的接口,它为内核和用户空间都提供简单易用的套接字函数。它提供了高级信息交互特性,例如全双工模式,I/O缓冲,多播和异步等交互模式,这些都是其他进程间通信中所没有的特征。

关于作者:
KevinKaichuan ([email protected]),Solustek公司的软件设计工程主要负责人。目前主要从事嵌入式系统、设备驱动和网络协议等项目。他曾参加过思科系统的高级软件设计,和研究助理、Purdue大学等工作。在其业余时间,喜欢数码摄影、PS2游戏和文学。

翻译后记:
这是第一次这么认真的翻译外文文档。首先,自己正在做这方面的研究,对自己的研究有帮助;其次,自己觉得这个较为简单,锻炼一下自己的翻译能力。第三,一直在督促自己将其翻译完毕。因为以前总是半途而废。

你可能感兴趣的:(linux,netlink,linux)