netlink socket理解

http://home.51.com/yong253535551: 这篇文章讲述了netlink底层的实现, 通过这个可以清楚src 地址, dest地址等。

 

由于开发和维护内核的复杂性,只用最为关键同时对性能要求最高的代码才会放在内核中。其他的诸如GUI,管理和控制代码,通常放在用户空间运行。这种将实现分离在内核和用户空间的思想在Linux中非常常见。现在的问题是内核代码和用户代码如果彼此通信。

答案是内核空间和用户空间存在的各种IPC方法,例如系统调用,ioctl,proc文件系统和netlink socket。这篇文章讨论netlink socket和讨论其作为一种网络特征IPC的优势。

简介

Netlink socket是用于内核和用户空间之间交换信息的特殊的IPC机制。它提供了一种全复用的通信链路。和TCP/IP使用的地址族AF_INET相 比,Netlink socket使用地址族AF_NETLINK,每个的netlink socket特征定义协议类型在内核头文件中include/linux/netlink.h

以下是netlink socket当前支持的特征和他们的协议类型的子集:

  • NETLINK_ROUTE:用户空间路由damon,如BGP,OSPF,RIP和内核包转发模块的通信信道。用户空间路由damon通过此种netlink协议类型更新内核路由表
  • NETLINK_FIREWALL:接收IPv4防火墙代码发送的包
  • NETLINK_NFLOG:用户空间iptable管理工具和内核空间Netfilter模块的通信信道
  • NETLINK_ARPD:用户空间管理arp表

为 什么以上的特征使用netlink而不是系统调用,ioctl或者proc文件系统来完成通信?为新特性添加系统调用,ioctl和proc文件系统相对 而言是一项比较复杂的工作,我们冒着污染内核和损害系统稳定性的风险。netlink socket相对简单:只有一个常量,协议类型,需要加入到netlink.h中。然后,内核模块和用户程序可以通过socket类型的API进行通信。

和 其他socket API一样,Netlink是异步的,它提供了一个socket队列来平滑突发的信息。发送一个netlink消息的系统调用将消息排列到接受者的 netlink队列中然后调用接收者的接收处理函数。接收者,在接收处理函数的上下文中,可以决定是否立即处理该消息还是等待在另一个上下文中处理。不想 netlink,系统调用需要同步处理。因此,如果我们使用了一个系统来传递一条消息到内核,如果需要处理该条信息的时间很长,那么内核调度粒度可以会受 影响。

在内核中实现的系统调用代码在编译时被静态的链接到内核中,因此在一个可以动态加载的模块中包括系统调用代码是不合适的。在netlink socket中,内核中的netlink核心和在一个可加载的模块中没有编译时的相互依赖。

netlink socket支持多播,这也是其与其他交互手段相比较的优势之一。一个进程可以将一条消息广播到一个netlink组地址。任意多的进程可以监听那个组地址。这提供了一种从内核到用户空间进行事件分发接近完美的机制。

从 会话只能由用户空间应用发起的角度来看,系统调用和ioctl是单一的IPC。但是,如果一个内核模块有一个用户空间应用的紧急消息,没有一种直接的方法 来实现这些功能。通常,应用需要阶段性的轮询内核来获取状态变化,尽管密集的轮询会有很大的开销。netlink通过允许内核初始化一个对话来优雅的解决 这个问题。我们称之为netlink的复用特性。

最后,netlink提供了bsd socket风格的API,而这些API是被软件开发社区所熟知。因此,培训费用相较较小。

和BSD路由socket的关系

在BSC TCP/IP的栈实现中,有一种叫做路由套接字的特殊的socket。它有AF_ROUTE地址族,PF_ROUTE协议族和SOCK_RAWsocket类型。在bsd中,路由套接字用于在内核路由表中添加和删除路由。

在Linux中,路由套接字的实现通过netlink套接字的NETLINK_ROUTE协议类型来支持。netlink套接字提供了bsd路由套接字的功能的超集。

Netlink套接字API

标 准的套接字API,socket(),sendmsg(),recvmsg()和close(),可以被用户态程序使用。可以通过查询man手册页来看这 些函数的具体定义。这儿,我们讨论在netlink上下文中为这些API选择参数。对于写过TCP/IP套接字程序的人对这些API都应该非常熟悉。

创建一个套接字,

int socket(int domain,int type, int protocol)

domain指代地址族,AF_NETLINK,套接字类型不是SOCK_RAW就是SOCK_DGRAM,因为netlink是一个面向数据报的服务。

protocol选择该套接字使用那种netlink特征。以下是几种预定义的协议类型:NETLINK_ROUTE,NETLINK_FIREWALL,NETLINK_APRD,NETLINK_ROUTE6_FW。你也可以非常容易的添加自己的netlink协议。

为 每一个协议类型最多可以定义32个多播组。每一个多播组用一个bit mask来表示,1<

bind()

和TCP/IP套接字一样,netlink bind()API用来将一个本地socket地址和一个打开的socket关联。一个netlink地址结构如下所示:

   1:  struct sockaddr_nl
   2:  {
   3:    sa_family_t    nl_family;  /* AF_NETLINK   */
   4:    unsigned short nl_pad;     /* zero         */
   5:    __u32          nl_pid;     /* process pid */
   6:    __u32          nl_groups;  /* mcast groups mask */
   7:  } nladdr;

当使用bind()调用的时候,nl_pid域可以被赋值为调用进程的pid。nl_pid在这儿被当做该netlink套接字的本地地址。程序负责找一个独一无二的32位整数在填充该域。一种常见的做法是:

   1:  NL_PID Formula 1:  nl_pid = getpid();

公式一使用进程ID号作为nl_pid的值,如果说该进程只需要一个netlink套接字,这是一个自然的选择。当一个进程中的不同线程需要同一个netlink协议多个netlink套接字。公式而可以用来产生nl_pid号:

   1:  NL_PID Formula 2: pthread_self() << 16 | getpid();

通过这种方法,同一个进程中的不同线程可以有同一种netlink协议类型的netlink套接字。事实上,即使在同一个线程中,在可能使用同一种协议类型的多个套接字。开发者必须要更有创造力来产生一个唯一的nl_pid。

如果应用想接受发送给特定多播组的netlink消息,所有感兴趣的多播组bit应该or在一起,并填充到nl_groups域。否则, nl_groups应该被显式至零,说明该应用只接受到该应用的消息,填充完上述域, 使用如下方式进行绑定:

   1:  bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));

发送一条netlink消息

为了发送一条netlink消息到内核或者其他的用户空间进程,另外一个struct sockaddr_nl nladdr需要作为目的地址,这和使用sendmsg()发送一个UDP包是一样的。如果该消息是发送至内核的,那么nl_pid和nl_groups 都置为0.

如果说消息时发送给另一个进程的单播消息,nl_pid是另外一个进程的pid值而nl_groups为零。

如果消息是发送给一个或多个多播组的多播消息,所有的目的多播组必须bitmask必须or起来从而形成nl_groups域。当我们填充struct msghdr结构用于sendmsg时,使用如下:

   1:  struct msghdr msg;
   2:  msg.msg_name = (void *)&(nladdr);
   3:  msg.msg_namelen = sizeof(nladdr);

netlink套接字也需要它自己本身的消息头,这是为了给所有协议类型的netlink消息提供一个统一的平台。

因为Linux内核netlink核心假设每个netlink消息中存在着以下的头,所有应用也必须在其发送的消息中提供这些头信息:

   1:  struct nlmsghdr
   2:  {
   3:    __u32 nlmsg_len;   /* Length of message */
   4:    __u16 nlmsg_type;  /* Message type*/
   5:    __u16 nlmsg_flags; /* Additional flags */
   6:    __u32 nlmsg_seq;   /* Sequence number */
   7:    __u32 nlmsg_pid;   /* Sending process PID */
   8:  };

nlmsg_len指整个netlink消息的长度,包括头信息,这也是netlink核心所必须的。nlmsg_type用于应用但是对于 netlink核心而言其是透明的。nlmsg_flags用于给定附加的控制信息,其被netlink核心读取和更新。nlmsg_seq和 mlmsg_pid,应用用来跟踪消息,这些对于netlink核心也是透明的。

所以一个netlink消息由消息头和消息负载组成。一旦一个消息被加入,它就加入到一个通过nlh指针指向的缓冲区。我们也可以将消息发送到struct msghdr msg:

   1:  struct iovec iov;
   2:   
   3:  iov.iov_base = (void *)nlh;
   4:  iov.iov_len = nlh->nlmsg_len;
   5:   
   6:  msg.msg_iov = &iov;
   7:  msg.msg_iovlen = 1;

经过以上步骤,调用sendmsg()函数来发送netlink消息:

   1:  sendmsg(fd, &msg, 0);
 
接收netlink消息

一个接收程序必须分配一个足够大的内存用于保存netlink消息头和消息负载。然后其填充struct msghdr msg,然后使用标准的recvmsg()函数来接收netlink消息,假设缓存通过nlh指针指向:

   1:  struct sockaddr_nl nladdr;
   2:  struct msghdr msg;
   3:  struct iovec iov;
   4:   
   5:  iov.iov_base = (void *)nlh;
   6:  iov.iov_len = MAX_NL_MSG_LEN;
   7:  msg.msg_name = (void *)&(nladdr);
   8:  msg.msg_namelen = sizeof(nladdr);
   9:   
  10:  msg.msg_iov = &iov;
  11:  msg.msg_iovlen = 1;
  12:  recvmsg(fd, &msg, 0);

当消息被正确的接收之后,nlh应该指向刚刚接收到的netlink消息的头。nladdr应该包含接收消息的目的地址,其中包括了消息发送者的 pid和多播组。同时,宏NLMSG_DATA(nlh),定义在netlink.h中,返回一个指向netlink消息的负载的指针。 close(fd)调用关闭fd描述符所标识的socket。

内核空间netlink API

内核空间的netlinkAPI在内核中被netlink核心支持,即net/core/af_netlink.c。从内核角度看,这些API不同 于用户空间的API。这些API可以被内核模块使用从而存取netlink套接字与用户空间程序通信。除非你使用现存的netlink套接字协议类型,否 则你必须通过在netlink.h中定义一个常量来添加你自己的协议类型。例如,我们需要添加一个netlink协议类型用于测试,则在 netlink.h中加入下面的语句:

   1:  #define NETLINK_TEST  17

之后,亦可以在linux内核中的任何地方引用添加的协议类型。

在用户空间,我们使用socket()来创建一个netlink套接字,但是在内核空间,我们使用如下的API:

   1:  truct sock *
   2:  netlink_kernel_create(int unit,
   3:             void (*input)(struct sock *sk, int len));

参数unit,即为netlink协议类型,如NETLINK_TEST,回调函数会在消息到达netlink套接字时调用。当用户态程序发送一个NETLINK_TEST协议类型的消息给内核时,input()函数被调用。下面是一个实现回调函数的例子:

   1:  void input (struct sock *sk, int len)
   2:  {
   3:   struct sk_buff *skb;
   4:   struct nlmsghdr *nlh = NULL;
   5:   u8 *payload = NULL;
   6:   
   7:   while ((skb = skb_dequeue(&sk->receive_queue))
   8:         != NULL) {
   9:   /* process netlink message pointed by skb->data */
  10:   nlh = (struct nlmsghdr *)skb->data;
  11:   payload = NLMSG_DATA(nlh);
  12:   /* process netlink message with header pointed by
  13:    * nlh    and payload pointed by payload
  14:    */
  15:   }
  16:  }

input()函数在发送进程的sendmsg()系统调用上下文执行。如果在input中处理netlink消息非常快,那是没有问题的。如果处 理netlink消息需要很长的时间,我们希望在input()外面处理消息来避免阻塞其他系统调用进入内核。事实上,我们可以使用一个指定的内核线程来 来不断执行以下的步骤。使用skb=skb_recv_datagram(nl_sk),其中nl_sk是 netlink_kernel_create()返回的netlink套接字。然后,处理由skb->data指向的netlink消息。

以下的内核线程在没有netlink消息在nl_sk中时睡眠,在回调函数input中,我们只要唤醒睡眠的内核线程,如下所示:

   1:  void input (struct sock *sk, int len)
   2:  {
   3:    wake_up_interruptible(sk->sleep);
   4:  }

这是一个更具有扩展性的用户和内核通信的模型。其也提高了上下文交换的粒度。

在内核中发送netlink消息

真如在用户空间中一样,在发送一个netlink消息时需要设置源和目的netlink消息地址。假设socket缓存中包含了将要发送的netlink消息,本地地址可以通过以下方式设置:

   1:  NETLINK_CB(skb).groups = local_groups;
   2:  NETLINK_CB(skb).pid = 0;   /* from kernel */

目的地址可以如下设置:

   1:  NETLINK_CB(skb).dst_groups = dst_groups;
   2:  NETLINK_CB(skb).dst_pid = dst_pid;

这些信息不是存储在skb->data,而是存储在skb中的netlink控制块中。发送一个消息,使用:

   1:  int
   2:  netlink_unicast(struct sock *ssk, struct sk_buff
   3:                  *skb, u32 pid, int nonblock);

其中ssk是netlink_kernel_create返回的netlink套接字,skb->data指向netlink将要发送的消息而pid是接受该消息的用户程序id。nonblock用于标识在接收缓存不可用时,API是阻塞还是立即返回失败。

你也可以发送一个多播消息。以下的API用于将消息传送到指定的进程,同时多播至指定的多播组。

   1:  void
   2:  netlink_broadcast(struct sock *ssk, struct sk_buff
   3:           *skb, u32 pid, u32 group, int allocation);

group是所有接收多播组的bitmask。allocation是内核内存分配的类型。通常,GFP_ATOMIC用于中断上下文而在其他情况下是用GFP_KERNEL.只是因为API可能需要分配一个或者多个套接字缓存来克隆多播消息。

在内核中关闭一个netlink套接字

给定了netlink_kernel_create()函数返回的struct sock *nl_sk,我们可以通过调用以下的API关闭netlink套接字。

   1:  sock_release(nl_sk->socket);

在内核和用户态使用单播通信

   1:  #include 
   2:  #include 
   3:   
   4:  #define MAX_PAYLOAD 1024  /* maximum payload size*/
   5:  struct sockaddr_nl src_addr, dest_addr;
   6:  struct nlmsghdr *nlh = NULL;
   7:  struct iovec iov;
   8:  int sock_fd;
   9:   
  10:  void main() {
  11:   sock_fd = socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST);
  12:   
  13:   memset(&src_addr, 0, sizeof(src_addr));
  14:   src__addr.nl_family = AF_NETLINK;
  15:   src_addr.nl_pid = getpid();  /* self pid */
  16:   src_addr.nl_groups = 0;  /* not in mcast groups */
  17:   bind(sock_fd, (struct sockaddr*)&src_addr,
  18:        sizeof(src_addr));
  19:   
  20:   memset(&dest_addr, 0, sizeof(dest_addr));
  21:   dest_addr.nl_family = AF_NETLINK;
  22:   dest_addr.nl_pid = 0;   /* For Linux Kernel */
  23:   dest_addr.nl_groups = 0; /* unicast */
  24:   
  25:   nlh=(struct nlmsghdr *)malloc(
  26:                   NLMSG_SPACE(MAX_PAYLOAD));
  27:   /* Fill the netlink message header */
  28:   nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  29:   nlh->nlmsg_pid = getpid();  /* self pid */
  30:   nlh->nlmsg_flags = 0;
  31:   /* Fill in the netlink message payload */
  32:   strcpy(NLMSG_DATA(nlh), "Hello you!");
  33:   
  34:   iov.iov_base = (void *)nlh;
  35:   iov.iov_len = nlh->nlmsg_len;
  36:   msg.msg_name = (void *)&dest_addr;
  37:   msg.msg_namelen = sizeof(dest_addr);
  38:   msg.msg_iov = &iov;
  39:   msg.msg_iovlen = 1;
  40:   
  41:   sendmsg(fd, &msg, 0);
  42:   
  43:   /* Read message from kernel */
  44:   memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
  45:   recvmsg(fd, &msg, 0);
  46:   printf(" Received message payload: %s\n",
  47:      NLMSG_DATA(nlh));
  48:   
  49:   /* Close Netlink Socket */
  50:   close(sock_fd);
  51:  }
   1:  struct sock *nl_sk = NULL;
   2:   
   3:  void nl_data_ready (struct sock *sk, int len)
   4:  {
   5:    wake_up_interruptible(sk->sleep);
   6:  }
   7:   
   8:  void netlink_test() {
   9:   struct sk_buff *skb = NULL;
  10:   struct nlmsghdr *nlh = NULL;
  11:   int err;
  12:   u32 pid;
  13:   
  14:   nl_sk = netlink_kernel_create(NETLINK_TEST,
  15:                                     nl_data_ready);
  16:   /* wait for message coming down from user-space */
  17:   skb = skb_recv_datagram(nl_sk, 0, 0, &err);
  18:   
  19:   nlh = (struct nlmsghdr *)skb->data;
  20:   printk("%s: received netlink message payload:%s\n",
  21:          __FUNCTION__, NLMSG_DATA(nlh));
  22:   
  23:   pid = nlh->nlmsg_pid; /*pid of sending process */
  24:   NETLINK_CB(skb).groups = 0; /* not in mcast group */
  25:   NETLINK_CB(skb).pid = 0;      /* from kernel */
  26:   NETLINK_CB(skb).dst_pid = pid;
  27:   NETLINK_CB(skb).dst_groups = 0;  /* unicast */
  28:   netlink_unicast(nl_sk, skb, pid, MSG_DONTWAIT);
  29:   sock_release(nl_sk->socket);
  30:  }

在内核和用户态使用单播通信

   1:  #include 
   2:  #include 
   3:   
   4:  #define MAX_PAYLOAD 1024  /* maximum payload size*/
   5:  struct sockaddr_nl src_addr, dest_addr;
   6:  struct nlmsghdr *nlh = NULL;
   7:  struct iovec iov;
   8:  int sock_fd;
   9:   
  10:  void main() {
  11:   sock_fd=socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
  12:   
  13:   memset(&src_addr, 0, sizeof(local_addr));
  14:   src_addr.nl_family = AF_NETLINK;
  15:   src_addr.nl_pid = getpid();  /* self pid */
  16:   /* interested in group 1<<0 */
  17:   src_addr.nl_groups = 1;
  18:   bind(sock_fd, (struct sockaddr*)&src_addr,
  19:        sizeof(src_addr));
  20:   
  21:   memset(&dest_addr, 0, sizeof(dest_addr));
  22:   
  23:   nlh = (struct nlmsghdr *)malloc(
  24:                            NLMSG_SPACE(MAX_PAYLOAD));
  25:   memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
  26:   
  27:   iov.iov_base = (void *)nlh;
  28:   iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
  29:   msg.msg_name = (void *)&dest_addr;
  30:   msg.msg_namelen = sizeof(dest_addr);
  31:   msg.msg_iov = &iov;
  32:   msg.msg_iovlen = 1;
  33:   
  34:   printf("Waiting for message from kernel\n");
  35:   
  36:   /* Read message from kernel */
  37:   recvmsg(fd, &msg, 0);
  38:   printf(" Received message payload: %s\n",
  39:          NLMSG_DATA(nlh));
  40:   close(sock_fd);
 
    

41: }

 

   1:  #define MAX_PAYLOAD 1024
   2:  struct sock *nl_sk = NULL;
   3:   
   4:  void netlink_test() {
   5:   sturct sk_buff *skb = NULL;
   6:   struct nlmsghdr *nlh;
   7:   int err;
   8:   
   9:   nl_sk = netlink_kernel_create(NETLINK_TEST,
  10:                                 nl_data_ready);
  11:   skb=alloc_skb(NLMSG_SPACE(MAX_PAYLOAD),GFP_KERNEL);
  12:   nlh = (struct nlmsghdr *)skb->data;
  13:   nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  14:   nlh->nlmsg_pid = 0;  /* from kernel */
  15:   nlh->nlmsg_flags = 0;
  16:   strcpy(NLMSG_DATA(nlh), "Greeting from kernel!");
  17:   /* sender is in group 1<<0 */
  18:   NETLINK_CB(skb).groups = 1;
  19:   NETLINK_CB(skb).pid = 0;  /* from kernel */
  20:   NETLINK_CB(skb).dst_pid = 0;  /* multicast */
  21:   /* to mcast group 1<<0 */
  22:   NETLINK_CB(skb).dst_groups = 1;
  23:   
  24:   /*multicast the message to all listening processes*/
  25:   netlink_broadcast(nl_sk, skb, 0, 1, GFP_KERNEL);
  26:   sock_release(nl_sk->socket);
  27:  }

Netlink可靠性机制

在基于netlink的通信中,有两种可能的情形会导致消息丢失:

  1. 内存耗尽,没有足够多的内存分配给消息
  2. 缓存复写,接收队列中没有空间存储消息,这在内核空间和用户空间之间通信时可能会发生

缓存复写在以下情况很可能会发生:

  1. 内核子系统以一个恒定的速度发送netlink消息,但是用户态监听者处理过慢
  2. 用户存储消息的空间过小

   如果netlink传送消息失败,那么recvmsg()函数会返回No  buffer space available(ENOBUFS)错误。那么,用户空间进程知道它丢失了信息,如果内核子系统支持dump操作,它可以重新同步来获取最新的消息。在 dump操作中,netlink通过在每次调用recvmsg()函数时传输一个包的流控机制来防止接收队列的复写。改包消耗一个内存页,其包含了几个多 部分netlink消息。图6中的序列图显示了在一个重新同步的过程中所使用的dump操作。

 

netlink socket理解_第1张图片

另一方面,缓存复写不会发生在用户和内核空间的通信中,因为sendmsg()同步的将netlink消息发送到内核子系统。如果使用的是阻塞套接字,那么netlink在从用户空间到内核空间的通信时完全可靠的,因为内存分配可以等待,所以没有内存耗尽的可能。

netlink也可以提供应答机制。所以如果用户空间进程发送了一个设置了NLM_F_ACK标志的请求,netlink会在netlink错误消息中报告给用户空间刚才请求操作的结果。

从用户空间的角度来看,Netlink套接字在通用的BSD套接字接口之上实现。因此,netlink套接字编程与通用的TCP/IP编程类似。但是,我们也应该考虑几个与netlink相关的特殊问题:

  1. netlink套接字没有像其他协议一样对用户空间隐藏协议细节。事实上,netlink传递的是整个消息,包括netlink头和其他信 息。因此,这就导致了数据处理函数与通用的TCP/IP套接字不同,所以用户态程序必须根据其格式解析和构建netlink信息。然而,没有标准的工具来 完成这些工作,所以你必须实现自己的函数或者使用一些现成的库。
  2. 来自netlink和内核子系统的错误不是通过recvmsg()函数返回的整数值来表现的。事实上,错误信息时被包装在netlink错误 消息中的。唯一的例外是(ENOBUFS)错误,该错误不是包装在netlink消息中,因为报告该错误的原因就是我们没有足够的空间来缓存新的 netlink消息。标准的通用套接字错误,如(EAGAIN),通常和其他轮询原语,例如poll()和select(),也是通过recvmsg() 返回整数值。

你可能感兴趣的:(netlink socket理解)