1 引言
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通信机制的不足之处。
2 相关研究
到目前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机制完成。
3 Netlink机制及其关键技术
3.1 Netlink机制
Linux操作系统中当CPU处于内核状态时,可以分为有用户上下文的状态和执行硬件、软件中断两种。其中当处于有用户上下文时,由于内核态和用户态的内存映射机制不同,不可直接将本地变量传给用户态的内存区;处于硬件、软件中断时,无法直接向用户内存区传递数据,代码执行不可中断。针对传统的进程间通信机制,他们均无法直接在内核态和用户态之间使用,原因如下表:
通信方法
无法介于内核态与用户态的原因
管道(不包括命名管道)
局限于父子进程间的通信。
消息队列
在硬、软中断中无法无阻塞地接收数据。
信号量
无法介于内核态和用户态使用。
内存共享
需要信号量辅助,而信号量又无法使用。
套接字
在硬、软中断中无法无阻塞地接收数据。
1*(引自 参考文献5)
解决内核态和用户态通信机制可分为两类:
1. 处于有用户上下文时,可以使用Linux提供的copy_from_user()和copy_to_user()函数完成,但由于这两个函数可能阻塞,因此不能在硬件、软件的中断过程中使用。
2. 处于硬、软件中断时。
2.1 可以通过Linux内核提供的spinlock自旋锁实现内核线程与中断过程的同步,由于内核线程运行在有上下文的进程中,因此可以在内核线程中使用套接字或消息队列来取得用户空间的数据,然后再将数据通过临界区传递给中断过程.
2.2 通过Netlink机制实现。Netlink
套接字的通信依据是一个对应于进程的标识,一般定为该进程的
ID。Netlink通信最大的特点是对对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数。通过软中断而不是自行启动内核线程保证了数据传输的及时性。
3.2 Netlink优点
Netlink相对于其他的通信机制具有以下优点:
1. 使用Netlink通过自定义一种新的协议并加入协议族即可通过socket
API使用Netlink协议完成数据交换,而ioctl和proc文件系统均需要通过程序加入相应的设备或文件。
2. Netlink使用socket缓存队列,是一种异步通信机制,而ioctl是同步通信机制,如果传输的数据量较大,会影响系统性能。
3. Netlink支持多播,属于一个Netlink组的模块和进程都能获得该多播消息。
4. Netlink允许内核发起会话,而ioctl和系统调用只能由用户空间进程发起。
在内核源码有关Netlink协议的头文件中包含了内核预定义的协议类型,如下所示:
#define
NETLINK_ROUTE 0
#define
NETLINK_W1 1
#define
NETLINK_USERSOCK 2
#define
NETLINK_FIREWALL 3
#define NETLINK_INET_DIAG
4
#define
NETLINK_NFLOG 5
#define
NETLINK_XFRM 6
#define NETLINK_SELINUX 7
#define
NETLINK_ISCSI 8
#define
NETLINK_AUDIT 9
#define
NETLINK_FIB_LOOKUP 10
#define
NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER
12
#define
NETLINK_IP6_FW 13
#define
NETLINK_DNRTMSG 14
#define NETLINK_KOBJECT_UEVENT
15
#define
NETLINK_GENERIC 16
上述这些协议已经为不同的系统应用所使用,每种不同的应用都有特有的传输数据的格式,因此如果用户不使用这些协议,需要加入自己定义的协议号。对于每一个Netlink协议类型,可以有多达
32多播组,每一个多播组用一个位表示,Netlink
的多播特性使得发送消息给同一个组仅需要一次系统调用,因而对于需要多拨消息的应用而言,大大地降低了系统调用的次数。
建立Netlink会话过程如下:
内核使用与标准socket
API类似的一套API完成通信过程。首先通过netlink_kernel_create()创建套接字,该函数的原型如下:
struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,
struct module *module);
其中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消息