转自:http://blog.csdn.net/haomcu/article/details/7371835linux
Linux中的进程间通讯机制源自于Unix平台上的进程通讯机制。Unix的两大分支AT&T Unix和BSD Unix在进程通讯实现机制上的各有所不一样,前者造成了运行在单个计算机上的System V IPC,后者则实现了基于socket的进程间通讯机制。同时Linux也遵循IEEE制定的Posix IPC标准,在三者的基础之上实现了如下几种主要的IPC机制:管道(Pipe)及命名管道(Named Pipe),信号(Signal),消息队列(Message queue),共享内存(Shared Memory),信号量(Semaphore),套接字(Socket)。经过这些IPC机制,用户空间进程之间能够完成互相通讯。为了完成内核空间与用户空间通讯,Linux提供了基于socket的Netlink通讯机制,能够实现内核与用户空间数据的及时交换。缓存
本文第2节概述相关研究工做,第3节与其余IPC机制对比,详细介绍Netlink机制及其关键技术,第4节使用KGDB+GDB组合调试,经过一个示例程序演示Netlink通讯过程。第5节作总结并指出Netlink通讯机制的不足之处。安全
到目前Linux提供了9种机制完成内核与用户空间的数据交换,分别是内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,其中模块参数与sysfs、procfs、debugfs、relayfs是基于文件系统的通讯机制,用于内核空间向用户控件输出信息;sysctl、系统调用是由用户空间发起的通讯机制。因而可知,以上均为单工通讯机制,在内核空间与用户空间的双向互动数据交换上略显不足。Netlink是基于socket的通讯机制,因为socket自己的双共性、突发性、不阻塞特色,所以可以很好的知足内核与用户空间小量数据的及时交互,所以在Linux 2.6内核中普遍使用,例如SELinux,Linux系统的防火墙分为内核态的netfilter和用户态的iptables,netfilter与iptables的数据交换就是经过Netlink机制完成。 网络
Linux操做系统中当CPU处于内核状态时,能够分为有用户上下文的状态和执行硬件、软件中断两种。其中当处于有用户上下文时,因为内核态和用户态的内存映射机制不一样,不可直接将本地变量传给用户态的内存区;处于硬件、软件中断时,没法直接向用户内存区传递数据,代码执行不可中断。针对传统的进程间通讯机制,他们均没法直接在内核态和用户态之间使用,缘由以下表:数据结构
通讯方法dom |
没法介于内核态与用户态的缘由异步 |
管道(不包括命名管道)socket |
局限于父子进程间的通讯。函数 |
消息队列post |
在硬、软中断中没法无阻塞地接收数据。 |
信号量 |
没法介于内核态和用户态使用。 |
内存共享 |
须要信号量辅助,而信号量又没法使用。 |
套接字 |
在硬、软中断中没法无阻塞地接收数据。 |
1*(引自 参考文献5)
解决内核态和用户态通讯机制可分为两类:
2.1 能够经过Linux内核提供的spinlock自旋锁实现内核线程与中断过程的同步,因为内核线程运行在有上下文的进程中,所以能够在内核线程中使用套接字或消息队列来取得用户空间的数据,而后再将数据经过临界区传递给中断过程.
2.2 经过Netlink机制实现。Netlink 套接字的通讯依据是一个对应于进程的标识,通常定为该进程的 ID。Netlink通讯最大的特色是对对中断过程的支持,它在内核空间接收用户空间数据时再也不须要用户自行启动一个内核线程,而是经过另外一个软中断调用用户事先指定的接收函数。经过软中断而不是自行启动内核线程保证了数据传输的及时性。
Netlink相对于其余的通讯机制具备如下优势:
在内核源码有关Netlink协议的头文件中包含了内核预约义的协议类型,以下所示:
上述这些协议已经为不一样的系统应用所使用,每种不一样的应用都有特有的传输数据的格式,所以若是用户不使用这些协议,须要加入本身定义的协议号。对于每个Netlink协议类型,能够有多达 32多播组,每个多播组用一个位表示,Netlink 的多播特性使得发送消息给同一个组仅须要一次系统调用,于是对于须要多拨消息的应用而言,大大地下降了系统调用的次数。
创建Netlink会话过程以下:
内核使用与标准socket API相似的一套API完成通讯过程。首先经过netlink_kernel_create()建立套接字,该函数的原型以下:
其中net参数是网络设备命名空间指针,input函数是netlink socket在接受到消息时调用的回调函数指针,module默认为THIS_MODULE.
而后用户空间进程使用标准Socket API来建立套接字,将进程ID发送至内核空间,用户空间建立使用socket()建立套接字,该函数的原型以下:
int socket(int domain, int type, int protocol);
其中domain值为PF_NETLINK,即Netlink使用协议族。protocol为Netlink提供的协议或者是用户自定义的协议,Netlink提供的协议包括NETLINK_ROUTE, NETLINK_FIREWALL, NETLINK_ARPD, NETLINK_ROUTE6和 NETLINK_IP6_FW。
接着使用bind函数绑定。Netlink的bind()函数把一个本地socket地址(源socket地址)与一个打开的socket进行关联。完成绑定,内核空间接收到用户进程ID以后即可以进行通信。
用户空间进程发送数据使用标准socket API中sendmsg()函数完成,使用时需添加struct msghdr消息和nlmsghdr消息头。一个netlink消息体由nlmsghdr和消息的payload部分组成,输入消息后,内核会进入nlmsghdr指向的缓冲区。
内核空间发送数据使用独立建立的sk_buff缓冲区,Linux定义了以下宏方便对于缓冲区地址的设置,以下所示:
#define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))
在对缓冲区设置完成消息地址以后,可使用netlink_unicast()来发布单播消息,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用于非原子上下文。
接收数据时程序须要申请足够大的空间来存储netlink消息头和消息的payload部分。而后使用标准函数接口recvmsg()来接收netlink消息
调试平台:Vmware 5.5 + Fedora Core 10(两台,一台做为host机,一台做为target机)。
调试程序:分为内核模块和用户空间程序两部分,当内核模块被加载后,运行用户空间程序,由用户空间发起Netlink会话,和内核模块进行数据交换。
被加载的内核模块没法经过外加的调试器进行调试,KGDB提供了一种内核源码级别的调试机制。Linux内核自2.6.26版本以后在内核中内置了KGDB选项,编译内核时须要选择与之相关的选项,调试时host端需使用带有符号表的vmlinz内核,target端使用gdb调试用户空间的程序。
用户空间程序关键代码以下:
内核模块须要防止资源抢占,保证Netlink资源互斥占有,内核模块部分关键代码以下:
5.其余相关说明
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是一个通用的协议类型,它是专门为用户使用的,所以,用户能够直接使用它,而没必要再添加新的协议类型。内核预约义的协议类型有:
对于每个netlink协议类型,能够有多达 32多播组,每个多播组用一个位表示,netlink 的多播特性使得发送消息给同一个组仅须要一次系统调用,于是对于须要多拨消息的应用而言,大大地下降了系统调用的次数。
函数 bind() 用于把一个打开的 netlink socket 与 netlink 源 socket 地址绑定在一块儿。netlink socket 的地址结构以下:
字段 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 需以下设置:
其中 nladdr 为消息接收者的 netlink 地址。
struct nlmsghdr 为 netlink socket 本身的消息头,这用于多路复用和多路分解 netlink 定义的全部协议类型以及其它一些控制,netlink 的内核实现将利用这个消息头来多路复用和多路分解已经其它的一些控制,所以它也被称为netlink 控制块。所以,应用在发送 netlink 消息时必须提供该消息头。
字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,字段 nlmsg_type 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,所以大部分状况下设置为 0,字段 nlmsg_flags 用于设置消息标志,可用的标志包括:
标志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。下面是一个示例:
结构 struct iovec 用于把多个消息经过一次系统调用来发送,下面是该结构使用示例:
在完成以上步骤后,消息就能够经过下面语句直接发送:
sendmsg(fd, &msg, 0); |
应用接收消息时须要首先分配一个足够大的缓存来保存消息头以及消息的数据部分,而后填充消息头,添完后就能够直接调用函数 recvmsg() 来接收。
注意: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的内核实如今.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) { nlh = (struct nlmsghdr *)skb->data; data = NLMSG_DATA(nlh); } } |
函数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()的返回值。
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 看到所有的内容,同时内核也把该消息发送给用户态接收程序,用户态接收程序将把接收到的内容输出到屏幕上。