现在的位置: 首页 > 综合 > 正文

NETLINK usage

2013年09月01日 ⁄ 综合 ⁄ 共 24921字 ⁄ 字号 评论关闭

 Netlink 是一种特殊的 socket,它是 Linux 所特有的,类似于 BSD 中的AF_ROUTE
但又远比它的功能强大,目前在最新的 Linux 内核(2.6.14)中使用netlink 进行应用与内核通信的应用很多,包括:路由
daemon(NETLINK_ROUTE),1-wire 子系统(NETLINK_W1),用户态 socket
协议(NETLINK_USERSOCK),防火墙(NETLINK_FIREWALL),socket
监视(NETLINK_INET_DIAG),netfilter 日志(NETLINK_NFLOG),ipsec
安全策略(NETLINK_XFRM),SELinux 事件通知(NETLINK_SELINUX),iSCSI
子系统(NETLINK_ISCSI),进程审计(NETLINK_AUDIT),转发信息表查询
(NETLINK_FIB_LOOKUP),netlink connector(NETLINK_CONNECTOR),netfilter
子系统(NETLINK_NETFILTER),IPv6 防火墙(NETLINK_IP6_FW),DECnet
路由信息(NETLINK_DNRTMSG),内核事件向用户态通知(NETLINK_KOBJECT_UEVENT),通用
netlink(NETLINK_GENERIC)。

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 的应用部分和内核部分没有编译时依赖,但系统调用就有依赖,而且新的系统调用的实现必须静态地连接到内核中,它无法在模块中实现,使用新系统调用的应用在编译时需要依赖内核。

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

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

6.netlink 使用标准的 socket API,因此很容易使用,但系统调用和 ioctl则需要专门的培训才能使用。

用户态使用 netlink

用户态应用使用标准的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和
close() 就能很容易地使用 netlink socket,查询手册页可以了解这些函数的使用细节,本文只是讲解使用 netlink
的用户应该如何使用这些函数。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket
需要的头文件也必不可少,sys/socket.h。

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

socket(AF_NETLINK, SOCK_RAW, netlink_type)

第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux
中,它们俩实际为一个东西,它表示要使用netlink,第二个参数必须是SOCK_RAW或SOCK_DGRAM,
第三个参数指定netlink协议类型,如前面讲的用户自定义协议类型NETLINK_MYTEST,
NETLINK_GENERIC是一个通用的协议类型,它是专门为用户使用的,因此,用户可以直接使用它,而不必再添加新的协议类型。内核预定义的协议类
型有:

#define NETLINK_ROUTE           0       /* Routing/device hook                          */
#define NETLINK_W1              1       /* 1-wire subsystem                             */
#define NETLINK_USERSOCK        2       /* Reserved for user mode socket protocols      */
#define NETLINK_FIREWALL        3       /* Firewalling hook                             */
#define NETLINK_INET_DIAG       4       /* INET socket monitoring                       */
#define NETLINK_NFLOG           5       /* netfilter/iptables ULOG */
#define NETLINK_XFRM            6       /* ipsec */
#define NETLINK_SELINUX         7       /* SELinux event notifications */
#define NETLINK_ISCSI           8       /* Open-iSCSI */
#define NETLINK_AUDIT           9       /* auditing */
#define NETLINK_FIB_LOOKUP      10
#define NETLINK_CONNECTOR       11
#define NETLINK_NETFILTER       12      /* netfilter subsystem */
#define NETLINK_IP6_FW          13
#define NETLINK_DNRTMSG         14      /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT  15      /* Kernel messages to userspace */
#define NETLINK_GENERIC         16


对于每一个netlink协议类型,可以有多达 32多播组,每一个多播组用一个位表示,netlink 的多播特性使得发送消息给同一个组仅需要一次系统调用,因而对于需要多拨消息的应用而言,大大地降低了系统调用的次数。

函数 bind() 用于把一个打开的 netlink socket 与 netlink 源 socket 地址绑定在一起。netlink socket 的地址结构如下:

struct sockaddr_nl
{
  sa_family_t    nl_family;
  unsigned short nl_pad;
  __u32          nl_pid;
  __u32          nl_groups;
};

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

传递给 bind 函数的地址的 nl_pid 字段应当设置为本进程的进程 ID,这相当于 netlink socket 的本地地址。但是,对于一个进程的多个线程使用 netlink socket 的情况,字段 nl_pid 则可以设置为其它的值,如:

pthread_self() << 16 | getpid();

因此字段 nl_pid 实际上未必是进程 ID,它只是用于区分不同的接收者或发送者的一个标识,用户可以根据自己需要设置该字段。函数 bind 的调用方式如下:

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

fd为前面的 socket 调用返回的文件描述符,参数 nladdr 为 struct sockaddr_nl 类型的地址。为了发送一个
netlink 消息给内核或其他用户态应用,需要填充目标 netlink socket 地址,此时,字段 nl_pid 和 nl_groups
分别表示接收消息者的进程 ID 与多播组。如果字段 nl_pid 设置为 0,表示消息接收者为内核或多播组,如果 nl_groups为
0,表示该消息为单播消息,否则表示多播消息。使用函数 sendmsg 发送 netlink 消息时还需要引用结构 struct
msghdr、struct nlmsghdr 和 struct iovec,结构 struct msghdr 需如下设置:

struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);

其中 nladdr 为消息接收者的 netlink 地址。

struct nlmsghdr 为 netlink socket 自己的消息头,这用于多路复用和多路分解 netlink
定义的所有协议类型以及其它一些控制,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 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,字段 nlmsg_type
用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_flags
用于设置消息标志,可用的标志包括:

/* Flags values */

#define NLM_F_REQUEST           1       /* It is request message.       */
#define NLM_F_MULTI             2       /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK               4       /* Reply with ack, with zero or error code */
#define NLM_F_ECHO              8       /* Echo this request            */

/* Modifiers to GET request */
#define NLM_F_ROOT      0x100   /* specify tree root    */
#define NLM_F_MATCH     0x200   /* return all matching  */
#define NLM_F_ATOMIC    0x400   /* atomic GET           */
#define NLM_F_DUMP      (NLM_F_ROOT|NLM_F_MATCH)

/* Modifiers to NEW request */
#define NLM_F_REPLACE   0x100   /* Override existing            */
#define NLM_F_EXCL      0x200   /* Do not touch, if it exists   */
#define NLM_F_CREATE    0x400   /* Create, if it does not exist */
#define NLM_F_APPEND    0x800   /* Add to end of list           */

标志NLM_F_REQUEST用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。

标志NLM_F_MULTI 用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得。

宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来。

标志NLM_F_ECHO表示该消息是相关的一个包的回传。

标志NLM_F_ROOT 被许多 netlink
协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置
NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型。

标志 NLM_F_MATCH 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。

标志 NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。

标志 NLM_F_DUMP 未实现。

标志 NLM_F_REPLACE 用于取代在数据表中的现有条目。

标志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败。

标志 NLM_F_CREATE 指示应当在指定的表中创建一个条目。

标志 NLM_F_APPEND 指示在表末尾添加新的条目。

内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon
需要它进行一些复杂的操作),字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程
ID。下面是一个示例:

#define MAX_MSGSIZE 1024
char buffer[] = "An example message";
struct nlmsghdr nlhdr;
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA(nlhdr),buffer);
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
nlhdr->nlmsg_pid = getpid();  /* self pid */
nlhdr->nlmsg_flags = 0;

结构 struct iovec 用于把多个消息通过一次系统调用来发送,下面是该结构使用示例:

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

在完成以上步骤后,消息就可以通过下面语句直接发送:

sendmsg(fd, &msg, 0);

应用接收消息时需要首先分配一个足够大的缓存来保存消息头以及消息的数据部分,然后填充消息头,添完后就可以直接调用函数 recvmsg() 来接收。

#define MAX_NL_MSG_LEN 1024
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
struct nlmsghdr * nlhdr;
nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);
iov.iov_base = (void *)nlhdr;
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);

注意:

fd为socket调用打开的netlink socket描述符。

在消息接收后,nlhdr指向接收到的消息的消息头,nladdr保存了接收到的消息的目标地址,宏NLMSG_DATA(nlhdr)返回指向消息的数据部分的指针。

在linux/netlink.h中定义了一些方便对消息进行处理的宏,这些宏包括:

#define NLMSG_ALIGNTO   4
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值。

#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))

宏NLMSG_LENGTH(len)用于计算数据部分长度为len时实际的消息长度。它一般用于分配消息缓存。

#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存。

#define NLMSG_DATA(nlh)  ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))

宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏。

#define NLMSG_NEXT(nlh,len)      ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len),
                      (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))

宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址,同时len也减少为剩余消息的总长度,该宏一般在一个消息被分成几个部分发送或接收时使用。

#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) &&
                           (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) &&
                           (nlh)->nlmsg_len <= (len))

宏NLMSG_OK(nlh,len)用于判断消息是否有len这么长。

#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))

宏NLMSG_PAYLOAD(nlh,len)用于返回payload的长度。

函数close用于关闭打开的netlink socket。

netlink内核API

netlink的内核实现在.c文件net/core/af_netlink.c中,内核模块要想使用netlink,也必须包含头文件linux
/netlink.h。内核使用netlink需要专门的API,这完全不同于用户态应用对netlink的使用。如果用户需要增加新的netlink协
议类型,必须通过修改linux/netlink.h来实现,当然,目前的netlink实现已经包含了一个通用的协议类型
NETLINK_GENERIC以方便用户使用,用户可以直接使用它而不必增加新的协议类型。前面讲到,为了增加新的netlink协议类型,用户仅需增
加如下定义到linux/netlink.h就可以:

#define NETLINK_MYTEST  17

只要增加这个定义之后,用户就可以在内核的任何地方引用该协议。

在内核中,为了创建一个netlink socket用户需要调用如下函数:

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

参数unit表示netlink协议类型,如NETLINK_MYTEST,参数input则为内核模块定义的netlink消息处理函数,当有消
息到达这个netlink
socket时,该input函数指针就会被引用。函数指针input的参数sk实际上就是函数netlink_kernel_create返回的
struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct
sock结构来表示。下面是一个input函数的示例:

void input (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *data = NULL;
while ((skb = skb_dequeue(&sk->receive_queue))
       != NULL) {
/* process netlink message pointed by skb->data */
nlh = (struct nlmsghdr *)skb->data;
data = NLMSG_DATA(nlh);
/* process netlink message with header pointed by
  * nlh and data pointed by data
  */
}  
}

函数input()会在发送进程执行sendmsg()时被调用,这样处理消息比较及时,但是,如果消息特别长时,这样处理将增加系统调用
sendmsg()的执行时间,对于这种情况,可以定义一个内核线程专门负责消息接收,而函数input的工作只是唤醒该内核线程,这样sendmsg将
很快返回。

函数skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收队列上的消息,返回为一个struct sk_buff的结构,skb->data指向实际的netlink消息。

函数skb_recv_datagram(nl_sk)也用于在netlink socket
nl_sk上接收消息,与skb_dequeue的不同指出是,如果socket的接收队列上没有消息,它将导致调用进程睡眠在等待队列
nl_sk->sk_sleep,因此它必须在进程上下文使用,刚才讲的内核线程就可以采用这种方式来接收消息。

下面的函数input就是这种使用的示例:

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

当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过struct sk_buff来管理的, linux/netlink.h中定义了一个宏:

#define NETLINK_CB(skb)         (*(struct netlink_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。

在内核中,模块调用函数 netlink_unicast 来发送单播消息:

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

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

内核模块或子系统也可以使用函数netlink_broadcast来发送广播消息:

void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group, int allocation);

前面的三个参数与netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个代表一个多播组,因此如果发送给多个多播
组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或
GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。

在内核中使用函数sock_release来释放函数netlink_kernel_create()创建的netlink socket:

void sock_release(struct socket * sock);

注意函数netlink_kernel_create()返回的类型为struct sock,因此函数sock_release应该这种调用:

sock_release(sk->sk_socket);

sk为函数netlink_kernel_create()的返回值。

源代码包
给出了一个使用 netlink 的示例,它包括一个内核模块 netlink-exam-kern.c 和两个应用程序
netlink-exam-user-recv.c,
netlink-exam-user-send.c。内核模块必须先插入到内核,然后在一个终端上运行用户态接收程序,在另一个终端上运行用户态发送程
序,发送程序读取参数指定的文本文件并把它作为 netlink
消息的内容发送给内核模块,内核模块接受该消息保存到内核缓存中,它也通过proc接口出口到 procfs,因此用户也能够通过
/proc/netlink_exam_buffer
看到全部的内容,同时内核也把该消息发送给用户态接收程序,用户态接收程序将把接收到的内容输出到屏幕上。

 

 

 

 

 

 


netlink套接字的使用

作者: Kevin He on wed,2005-01-05 02:00
原文: Kernel Korner – Why and How to Use Netlink Socket
翻译: Ricky Goo (http://cn.iventor.org/people.html#ricky)
网址: http://cn.iventor.org/forum/viewtopic.php?t=715

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

由于内核的不断发展和维护的复杂性,只有大部分的基础性的、临界执行的代码植入内核中,而其他的,例如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<<i,0<=i<=31。当一组进程和内核
进程正在并行执行相同的工作时,比如发送组播netlink信息,使用这种方法,可以减少系统调用使用次数、减轻维护组播成员应用程序的负担。

绑定: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
内核空间netlink
API由内核中的netlink核心支持,在net/core/af_netlink.c中所定义。在内核中,netlink
API与用户空间有所不同。内核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套接字消息到内核空间,然后内核空间接受到这一消息后,将其输出。
用户空间代码如下:

代码:
#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);
}   


内核空间代码:

代码:
#include <linux/config.h>
#include <linux/module.h>
#include <linux/version.h>
#include <linux/skbuff.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <net/sock.h>

#include <linux/types.h>
#include <linux/socket.h>
#include <linux/netlink.h>

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消息过来。
这里是用户空间代码:

代码:
#include <sys/socket.h>
#include <linux/types.h>
#include <linux/netlink.h>

#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

#include <linux/string.h>
#include <linux/stat.h>
#include <net/sock.h>

#include <linux/module.h>
#include <linux/version.h>
#include <linux/socket.h>
#include <linux/types.h>

#include <linux/capability.h>
#include <linux/errno.h>
#include <linux/stat.h>

#include <linux/skbuff.h>
#include <linux/netlink.h>

#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缓冲,多播和异步等交互模式,这些都是其他进程间通信中所没有的特征。

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

抱歉!评论已关闭.