Linux内核通信之netlink机制

Posted 逝去的浪花

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核通信之netlink机制相关的知识,希望对你有一定的参考价值。

前言:

       开发和维护内核是一件很繁杂的工作,因此,只有那些最重要或者与系统性能息息相关的代码才将其安排在内核中。其它程序,比如GUI,管理以及控制部分的代码,一般都会作为用户态程序。用户态和内核态的通讯机制IPC(interprocess   communication  )机制:比如系统调用,ioctl接口,proc文件系统以及netlink socket。

介绍:
        netlink socekt是一种用于在内核态和用户态进程之间进行数据传输的特殊的IPC。它通过为内核模块提供一组特殊的API,并为用户程序提供了一组标准的socket 接口的方式,实现了一种全双工的通讯连接。类似于TCP/IP中使用AF_INET地址族一样,netlink socket使用地址族AF_NETLINK。每一个netlink socket在内核头文件
include/linux/netlink.h
中定义自己的协议类型。

                      

        Netlink提供了一种异步通讯方式,与其他socket API一样,它提供了一个socket队列来缓冲或者平滑瞬时的消息高峰。发送netlink消息的系统调用在把消息加入到接收者的消息对列后,会触发接收者的接收处理函数。接收者在接收处理函数上下文中,可以决定立即处理消息还是把消息放在队列中,在以后其它上下文去处理它(因为我们希望接收处理函数执行的尽可能快)。系统调用与netlink不同,它需要一个同步的处理,因此,当我们使用一个系统调用来从用户态传递消息到内核时,如果处理这个消息的时间很长的话,内核调度的力度就会受到影响。

    内核中实现系统调用的代码都是在编译时静态链接到内核的,因此,在动态加载模块中去包含一个系统调用的做法是不合适的,那是大多数设备驱动的做法。使用netlink socket时,动态加载模块中的netlink程序不会和linux内核中的netlink部分产生任何编译时依赖关系。
Netlink优于系统调用,ioctls和proc文件系统的另外一个特点就是它支持多点传送。一个进程可以把消息传输给一个netlink组地址,然后任意多个进程都可以监听那个组地址(并且接收消息)。这种机制为内核到用户态的事件分发提供了一种近乎完美的解决方案。

    系统调用和ioctl都属于单工方式的IPC,也就是说,这种IPC会话的发起者只能是用户态程序。但是,如果内核有一个紧急的消息想要通知给用户态程序时,该怎么办呢?如果直接使用这些IPC的话,是没办法做到这点的。通常情况下,应用程序会周期性的轮询内核以获取状态的改变,然而,高频度的轮询势必会增加系统的负载。Netlink 通过允许内核初始化会话的方式完美的解决了此问题,我们称之为netlink socket的双工特性。

Netlink Socket 的API

标准的socket API函数-socket(), sendmsg(), recvmsg()和close();

1、使用socket()函数创建一个socket,输入:int socket(int domain, int type, int protocol);

2、跟TCP/IP中的socket一样,netlink的bind()函数把一个本地socket地址(源socket地址)与一个打开的socket进行关联,netlink地址结构体如下:

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;

3、另外一个结构体 struct sockaddr_nl nladdr作为目的地址。如果这个netlink消息是发往内核的话,nl_pid属性和nl_groups属性都应该设置为0。

如果这个消息是发往另外一个进程的单点传输消息,nl_pid应该设置为接收者进程的PID,nl_groups应该设置为0。netlink消息同样也需要它自身的消息头,这样做是为了给所有协议类型的netlink消息提供一个通用的背景。

4、由于linux内核的netlink部分总是认为在每个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核心需要的信息。mlmsg_type可以被应用程序所用,它对于netlink核心来说是一个透明的值。nsmsg_flags 用来该对消息体进行另外的控制,会被netlink核心代码读取并更新。Nlmsg_seq和nlmsg_pid同样对于netlink核心部分来说是透明的,应用程序用它们来跟踪消息。

5因此,一个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消息发送出去:
sendmsg(sock_fd, &msg, 0);


接收netlink消息:
接收程序需要申请足够大的空间来存储netlink消息头和消息的payload部分。它会用如下的方式填充结构体 struct 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应该指向刚刚接收到的netlink消息的头部分。Nladdr应该包含接收到消息体的目的地信息,这个目的地信息由pid和消息将要发往的多播组的值组成。Netlink.h中的宏定义NLMSG_DATA(nlh)返回指向netlink消息体的payload的指针。调用