Linux netlink机制及示例

来源:互联网 发布:潭州学院java vip视频 编辑:程序博客网 时间:2024/05/18 11:45


原理

Netlink是一种在内核态和用户态可以进行双向数据传输的通信机制,也就是说,用户进程既可以作为服务器端又可以作为客户端,内核也是如此。用户进程和内核谁是服务器端谁是客户端,这个问题与谁先主动发起数据交互会话有关。

用户进程主动向内核发起会话在Linux内核中很常见,比如系统调用、对/proc的操作等。本文通过详解一个简单的实例程序来说明用户进程通过netlink机制如何主动向内核发起会话。

用户态使用标准的socket API就可以使用netlink提供的强大功能,内核态需要使用专门的内核API来使用netlink。

Netlink相对于系统调用,ioctl以及/proc文件系统而言具有以下优点:

1,为了使用 netlink,用户仅需要在include/linux/netlink.h中增加一个新类型的 netlink协议定义即可, #define NETLINK_MYTEST 17然后,内核和用户态应用就可以立即通过socket API使用该netlink协议类型进行数据交换。但系统调用需要增加新的系统调用,ioctl则需要增加设备或文件,那需要不少代码,proc文件系统则需要在/proc下添加新的文件或目录,那将使本来就混乱的/proc更加混乱。

2. netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息,但系统调用与ioctl则是同步通信机制,如果传递的数据太长,将影响调度粒度。

3.使用netlink的内核部分可以采用模块的方式实现,使用 netlink的应用部分和内核部分没有编译时依赖,但系统调用就有依赖,而且新的系统调用的实现必须静态地连接到内核中,它无法在模块中实现,使用新系统调用的应用在编译时需要依赖内核。

4netlink支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性,任何对内核事件感兴趣的应用都能收到该子系统发送的内核事件。

5.内核可以使用netlink首先发起会话,但系统调用和 ioctl只能由用户应用发起调用。


用户空间

用户态应用使用标准的socket与内核通讯,标准的socket API 的函数,socket(), bind(), sendmsg(), recvmsg() close()很容易地应用到netlink socket

为了创建一个netlink socket,用户需要使用如下参数调用 socket():

socket(AF_NETLINK,SOCK_RAW, netlink_type)

同样地,socket函数返回的套接字,可以交给bing等函数调用:

staticint skfd;

skfd =socket(PF_NETLINK, SOCK_RAW, NL_IMP2);

if(skfd< 0)

{

    printf("can not create a netlink socket\n");

    exit(0);

}

bind函数需要绑定协议地址,netlinksocket地址使用structsockaddr_nl结构描述:

structsockaddr_nl

{

  sa_family_t   nl_family;

  unsignedshort nl_pad;

  __u32         nl_pid;

  __u32         nl_groups;

};

成员nl_family为协议簇AF_NETLINK,成员nl_pad当前没有使用,因此要总是设置为 0,成员 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。成员nl_groups用于指定多播组,bind函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组:

structsockaddr_nl local;

memset(&local,0, sizeof(local));

local.nl_family= AF_NETLINK;

local.nl_pid= getpid();                /*设置pid为自己的pid*/

local.nl_groups= 0;


用户空间可以调用send函数簇向内核发送消息,如sendtosendmsg等,同样地,也可以使用structsockaddr_nl来描述一个对端地址,以待send函数来调用,与本地地址稍不同的是,因为对端为内核,所以nl_pid成员需要设置为0

structsockaddr_nl kpeer;

memset(&kpeer,0, sizeof(kpeer));

kpeer.nl_family= AF_NETLINK;

kpeer.nl_pid= 0;

kpeer.nl_groups= 0;

另一个问题就是发内核发送的消息的组成,使用我们发送一个IP网络数据包的话,则数据包结构为“IP包头+IP数据,同样地,netlink的消息结构是“netlink消息头部+数据Netlink消息头部使用structnlmsghdr结构来描述:

structnlmsghdr

{

 __u32nlmsg_len;   /* Length of message */

 __u16nlmsg_type;  /* Message type*/

 __u16nlmsg_flags; /* Additional flags */

 __u32nlmsg_seq;   /* Sequence number */

__u32nlmsg_pid;   /* Sending process PID */

};

字段nlmsg_len指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,一般地,我们使用netlink提供的宏NLMSG_LENGTH来计算这个长度,仅需向NLMSG_LENGTH宏提供要发送的数据的长度,它会自动计算对齐后的总长度:

/*计算包含报头的数据报长度*/

#defineNLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))

/*字节对齐*/

#defineNLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

后面还可以看到很多netlink提供的宏,这些宏可以为我们编写netlink宏提供很大的方便。

字段nlmsg_type用于应用内部定义消息的类型,它对netlink内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_flags 用于设置消息标志,对于一般的使用,用户把它设置为 0就可以,只是一些高级应用(如netfilter和路由daemon需要它进行一些复杂的操作),字段nlmsg_seqnlmsg_pid用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID


内核空间

与应用程序内核,内核空间也主要完成三件工作:

1.创建netlink套接字

2.接收处理用户空间发送的数据

3. 发送数据至用户空间

API函数netlink_kernel_create用于创建一个netlinksocket,同时,注册一个回调函数,用于接收处理用户空间的消息:

        static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)

        static struct netlink_kernel_cfg cfg = {
             .input = kernel_receive,
        };

参数unit表示netlink协议类型;sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个structsock结构来表示。


向用户空间进程发送的消息包含三个部份:netlink消息头部、数据部份和控制字段,控制字段包含了内核发送netlink消息时,需要设置的目标地址与源地址,内核中消息是通过sk_buff来管理的,linux/netlink.h中定义了NETLINK_CB宏来方便消息的地址设置:

#defineNETLINK_CB(skb)         (*(structnetlink_skb_parms*)&((skb)->cb))

例如:

NETLINK_CB(skb).pid= 0;

NETLINK_CB(skb).dst_pid= 0;

NETLINK_CB(skb).dst_group= 1;

字段pid表示消息发送者进程ID,也即源地址,对于内核,它为 0 dst_pid表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则dst_group表示目标组地址,如果它目标为某一进程或内核,dst_group应当设置为 0


send_to_user用于将数据发送给用户空间进程,发送调用的是API函数netlink_unicast完成的:

intnetlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);

参数sk为函数netlink_kernel_create()返回的套接字,参数skb存放待发送的消息,它的data字段指向要发送的netlink消息结构,而skb的控制块保存了消息的地址信息,参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠。


staticint send_to_user(struct packet_info *info)

{

intret;

intsize;

unsignedchar *old_tail;

structsk_buff *skb;

structnlmsghdr *nlh;

structpacket_info *packet;

/*计算消息总长:消息首部加上数据加度*/

size =NLMSG_SPACE(sizeof(*info));

/*分配一个新的套接字缓存*/

skb =alloc_skb(size, GFP_ATOMIC);

old_tail= skb->tail;

/*初始化一个netlink消息首部*/

nlh =NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));

/*跳过消息首部,指向数据区*/

packet= NLMSG_DATA(nlh);

/*初始化数据区*/

memset(packet,0, sizeof(struct packet_info));

/*填充待发送的数据*/

packet->src= info->src;

packet->dest= info->dest;

/*计算skb两次长度之差,即netlink的长度总和*/

nlh->nlmsg_len= skb->tail - old_tail;

/*设置控制字段*/

NETLINK_CB(skb).dst_groups= 0;

/*发送数据*/

ret =netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);

}

函数初始化netlink消息首部,填充数据区,然后设置控制字段,这三部份都包含在skb_buff中,最后调用netlink_unicast函数把数据发送出去。



用户空间示例:






内核空间示例:




运行结果:

可以看到用户空间接收到内核发送的信息!




















0 0