Netlink 介绍说明

介绍:

        Netlink socket是一种特别的IPC,用于在内核和用户空间传输数据.它提供了标准socket的全双工的数据连接.对用户来说使用标准的socket APIs,内核模块使用另外一组特殊的kernel API. Netlink socket使用address family AF_NETLINK,就如同TCP/IP socket使用AF_INET.每种netlink socket特性在include/linux/netlink.h定义它自己的协议类型.

 

以下是这些属性的子集和目前netlink socket支持的协议类型:

            .NETLINK_ROUTE:在用户空间和routing damons例如: BGP, OSPF,RIP和内核packet forwarding模块间的通讯通道.用户空间的routing damon通过这个netlink协议类来更新内核的kernel routing table.

            . NETLINK_FIREWALL:接受被IPV4防火墙代码发送的packets

           . NETLINK_NFLOG:用户空间的iptable管理工具和内核Netfilter模块间的通讯通道.

          . NETLINK_ARPD:为了从用户空间管理arp table.

         为什么使用netlink去做以上的特性而不使用system call, ioctls或者proc文件系统进行用户和内核世界间的通讯? 加system calls, itocls或者proc文件系统并不是一件平常的事情,我们要冒着污染内核和毁坏系统的稳定性.Netlink socket特别简单,仅仅是需要添加一个常量(协议类型)到netlink.h.这样内核模块和application可以使用socket-styple APIs立即进行通讯.

        Netlink是一个异步方法,就像其它socket API,因为它提供了了一个socket队列,来平缓消息的爆发式传输.系统调用发送一个netlink消息队列中的消息到接受者的netlink消息队列,然后调用接受者的接受处理函数.接受者在接受函数的上下文中决定是否理解处理消息还是把消息放到队列中,在另外的上下文中以后处理.System call不像netlink它必须同步处理,因此如果我们使用一个system call从用户空间传送一个消息到内核,而这个内核处理消息的时间比较长,那么就可能影响到内核的调动力度.

       内核中执行一个system call的代码需要在编译内核静态的连接到内核,因此不合适在一个可以装载的模块中包含一个system call,而这种情况在驱动中是经常出现的.使用netlink socket,没有内核的netlink代码和用户netlink 应用模块间编译时的依赖关系.

        Netlink socket支持广播,这是另外比system call, ioctls 和 proc 文件系统的好处.一个进程可以广播一个消息到一个netlink组地址上,任何在监听这个组地址上的进程都能听到.这就提供了一个近似完美的从内核到用户空间的事件分发机制.

       System call和ioctl就对由用户空间应用程序引起的IPCs的任务来说时相当简单的,但是如果一个内核模块有一个紧急通知应用程序?就不能直接使用这些IPCs.通常,应用程序需要周期性的查询内核,得到状态改变,即使这样做是很浪费CPU. Netlink很优雅的解决了这个问题,允许内核启动一个通知任务.我们称它为netlink socket的双工特性.

      最后,netlink socket提供一个BSD socket风格的 API,很容易被软件开发社区理解.因此相对于复杂的system call和ioctl来说学习的代价是很小的.

Netlink Socket APIs
        标准的socket API: socket(), sendmsg(), recvmsg()和close(),可以在用户空间使用来访问netlink socket.这些API的详细使用请参考相应的man.这里我们只讨论netlink相关的内容.这些API应该对用TCP/IP socket 写过普通网络应用程序的人非常熟悉的.

用socket()生成一个socket:

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

         domain(address family)用AF_NETLINK,socket的类型是SOCK_RAW或者SOCK_DGRAM,因为netlink是一个面向数据包的服务.

        协议类型选择要根据需要的netlink socket特性.以下就是netlink协议预先定义好的类型: NETLINK_ROUTE, NETLINK_FIREWALL, NETLINK_ARPD, NETLINK_ROUTE6 和 NETLINK_IP6_FW.你也能容易的添加netlink protocol类型..       

对于每种netlink协议类型,最多有32个广播组可以使用.每个广播组用一个位掩码表示, 1<<i, (0<=i<=31).

 

bind()
如同TCP/IP socket, netlink bind() 把一个本地地址和一个打开的socket联系起来.

netlink address 结构:

struct sockaddr_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(), nl_pid用调用进程的pid赋值,这里nl_pid用作netlink socket的本地地址.应用程序负责用一个32bit的整数填充nl_pid

NL_PID Formula 1:  nl_pid = getpid();
 
公式1使用应用程序进程的ID作为nl_pid,这是很自然的选择,因为对于一个给定的netlink协议类型,只有这个netlink socket是这个进程需要的.

在同一个进程中不同的线程需要在同一个netlink协议中访问不同的netlink socket.那么公式2需要:

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

这样, 不同的线程就可以用同一个netlink协议中访问不同的netlink socket. 事实上,即使在同一个线程中也可以出现同样的情况,但是我们目前不考虑这个问题.

如果应用程序需要接收某种协议类型netlink message,而这类消息只是为特定广播组的,那么需要在socketaddr_nl对应bitmask置位,否则 nl_groups应该被填零,这样应用程序只能接受对应于这个程序的消息了.当完成填写nladdr,做如下的bind动作:

bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));
Sending a Netlink Message
为了发送一个netlink消息到内核或者其它应用程序,另外的一个struct sockaddr_nl nladdr需要作为目的地址去填充,就像用sendmsg()一样发送一个UPD包.如果这个消息目的是kernel,那么nl_pid和nl_groups都应该为0.

如果这个消息仅仅为另外一个进程的,那么nl_pid需要填写另外一个进程的pid,nl_groups填0,就如同公式1中的.

如果这个消息是一个广播消息,目的是一个或者多个广播组,那么对应的广播组对应于nl_groups的bit位应该被置位.然后我们就可以使用这个netlink地址,到sendmsg() API中:

     struct msghdr msg;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
netlink socket也需要它自己的消息头.这样为所有协议类型的netlink消息提供了一个统一的背景.

因为Linux kernel的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 core所必须的. nlmsg_type 通常用来给出另外的附加控制. nlmsg_seq 和 nlmsg_pid被应用程序来跟踪这个消息,这个对netlink core来说是透明的.

一个netlink消息因此由 nlmsghdr 和消息的 payload组成.一旦一个消息被收到,它被一个nlh指针指向.我们也能发送一个消息使用struct 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 message:

 
sendmsg(fd, &msg, 0);
 
 

Receiving Netlink Messages
一个接受的应用程序需要分配一个大的内存去存放netlink message和它的payloads.然后如下的填写struct msghdr msg,调用recvmsg()去接受netlink消息:

 
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和multicast groups,并且_DATA(nlh), 定义在 netlink.h, 返回一个指向这个netlink消息的payload. Close(fd)关闭这个netlink socket.

Kernel-Space Netlink APIs
内核空间的netlink API在net/core/af_netlink.c中被支持.对内核来言,这个API不同于用户空间的API.这个API可以被内核模块使用来和用户空间通讯.你也可添加你自己的netlink协议类型.例如,我们添加一个测试目的的netlink协议类型在netlink.h中:

#define NETLINK_TEST  17
 
之后,你可在内核的任何位置中使用.

在用户空间,我们调用socket()去创建一个netlink socket,但是在内核空间,我们需要调用下面的API:

 
struct sock *
netlink_kernel_create(int unit,
           void (*input)(struct sock *sk, int len));
 
参数unit,事实上是netlink协议类型,例如NETLINK_TEST.函数指针,input是一个callback函数,当这个netlink socket收到一个消息的时候被调用.

当内核已经生成一个NETLINK_TEST类型的netlink socket,任何时候一个NETLINK_TEST类型的消息发送到内核,那个回调函数就会被调用.一下是这个例子:

 
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),然后处理被skb->data指向的netlink消息

这个内核线程在没有netlink消息的时候,处于休眠状态,因此我们在input()回调函数中唤醒这个kernel thread:

 
void input (struct sock *sk, int len)
{
  wake_up_interruptible(sk->sleep);
}
 
这是更加灵活的内核和用户空间通讯的方式.同时也增强的内核上下文切换的力度.

Sending Netlink Messages from the Kernel
如同在用户空间,源netlink地址和目的netlink地址在发送一个netlink消息的时候都要设定.假定socket buffer中保存要发送的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;
 

这些信息没有存放在skb->data中,它被保存在socket buffer(skb)的netlink控制块中.

发送一个单播消息:

 
int
netlink_unicast(struct sock *ssk, struct sk_buff
                *skb, u32 pid, int nonblock);
 
这个ssk是netlink_kernel_create()返回的netlink socket.skb->data指向要发送的netlink消息,pid是接受消息的进程,noblock暗示这当接受buffer没有的时候等待或者马上返回一个错误.

你也可以发送一个广播消息.下面的API发送这样的消息:

 
void
netlink_broadcast(struct sock *ssk, struct sk_buff
         *skb, u32 pid, u32 group, int allocation);
 
Closing a Netlink Socket from the Kernel
我们调用下面的内核API来官兵netlink socket:

 
sock_release(nl_sk->socket);
 
到此,我们列出了netlink编程的最小代码框架.下面我们看看如何使用NETLINK_TEST netlink协议,假定它已经被加入内核.

Unicast Communication between Kernel and Application
在和责怪例子中,一个用户空间进程发送一个netlink消息给内核模块,内核模块应答一个消息给发送进程,这里是用户空间的代码:

 
#include <sys/socket.h>
#include <linux/netlink.h>
#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);
}   
 
 

这里是内核代码:

 
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);
}   
 
在内核模块被加载到内核,当我们运行用户程序,我们将看到下面的信息:

Received message payload: Hello you!
 
 

然后用dmesg我们可以看到内核输出:

netlink_test: received netlink message payload:
Hello you!
 
 

Multicast Communication between Kernel and Applications
这个例子中,两个应用程序在监听同一个netlink广播组.内核模块发送一个netlink消息给这个广播组,所用的应用程序都收到它,如下是用户程序代码:

 
#include <sys/socket.h>
#include <linux/netlink.h>
#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(local_addr));
 src_addr.nl_family = AF_NETLINK;      
 src_addr.nl_pid = getpid();  /* self pid */
 /* interested in group 1<<0 */ 
 src_addr.nl_groups = 1;
 bind(sock_fd, (struct sockaddr*)&src_addr,
      sizeof(src_addr));
 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("Waiting for message from kernel\n");
 /* Read message from kernel */
 recvmsg(fd, &msg, 0);
 printf(" Received message payload: %s\n",
        NLMSG_DATA(nlh));
 close(sock_fd);
}   
 
 

内核代码:

 
#define MAX_PAYLOAD 1024
struct sock *nl_sk = NULL;
void netlink_test() {
 sturct sk_buff *skb = NULL;
 struct nlmsghdr *nlh;
 int err;
 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->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*/
 netlink_broadcast(nl_sk, skb, 0, 1, GFP_KERNEL);
 sock_release(nl_sk->socket);
}   
 
我们允许用户程序:

 
./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!
 
 

Conclusion
Netlink socket是一个可塑性的用户空间和内核空间的通讯接口.它提供医用的socket API给用户程序和内核.并提供先进的通讯机制:双工,缓冲I/O,广播,和异步通讯,这些都是其它kernel/user-space IPCs缺少的.

Kevin Kaichuan He (hek_u5@yahoo.com [1]) is a principal software engineer at Solustek Corp. He currently is working on embedded system, device driver and networking protocols projects. His previous work experience includes senior software engineer at Cisco Systems and research assistant at CS, Purdue University. In his spare time, he enjoys digital photography, PS2 games and literature.

Bookmark/Search this post with:

<!--[if !vml]--><!--[endif]-->digg [2] | <!--[if !vml]--><!--[endif]-->reddit [3]

 

Links
[1] http://www.linuxjournal.com/mailto:hek_u5@yahoo.com
[2] http://digg.com/submit?phase=2&url=http://www.linuxjournal.com/article/7356
[3] http://reddit.com/submit?url=http://www.linuxjournal.com/article/7356&title=Kernel+Korner+-+Why+and+How+to+Use+Netlink+Socket

 

Source URL: http://www.linuxjournal.com/article/7356


一下是一个简单的测试内核事件的应用程序。

#define MAX_PAYLOAD        1024
struct sockaddr_nl    src_addr, dest_addr;
char *KernelMsg = NULL;
struct iovec iov;
int sock_fd;
struct msghdr msg;
int msglen;

#define DEVICE_ADD            "add"
#define DEVICE_REMOVE        "remove"
#define DEVICE_NAME            "event0"
#define DEVICE_NAMELEN    6
void * DeviceManagement(void *arg)
{
    sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
    int msgle;
   
    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = pthread_self() << 16 | getpid();
    src_addr.nl_groups = 1;
    bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr));
   
    memset(&dest_addr, 0, sizeof(dest_addr));
    KernelMsg = (struct nlmsghdr *)malloc(MAX_PAYLOAD);
    memset(KernelMsg, 0,     MAX_PAYLOAD);
   
    iov.iov_base = (void *)KernelMsg;
    iov.iov_len = MAX_PAYLOAD;
    msg.msg_name = (void *)&dest_addr;
    msg.msg_namelen = sizeof(dest_addr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
   
    while(1) {
        //printf("Waiting for message from kernel\n");
        recvmsg(sock_fd, &msg, 0);
        //printf("Receved message payload: %s\n", KernelMsg);
        msglen = strlen(KernelMsg);
        //printf("Device: %s\n", KernelMsg+msglen-DEVICE_NAMELEN);
        if(!strncmp(DEVICE_NAME, KernelMsg+msglen-DEVICE_NAMELEN, DEVICE_NAMELEN)) {
            if(!strncmp(DEVICE_ADD, KernelMsg, strlen(DEVICE_ADD)))
            {
                printf("Add event0 device\n");
                USBKeyboardReady = 1;
            }
            else if(!strncmp(DEVICE_REMOVE, KernelMsg, strlen(DEVICE_REMOVE))) {
                printf("Remove event0 device\n");
                USBKeyboardReady = 0;
            }
        }
    }
    close(sock_fd);
   
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值