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处于内核状态时,可以分为有用户上下文的状态和执行硬件、软件中断两种。其中当处于有用户上下文时,由于内核态和用户态的内存映射机制不同,不可直接将本地变量传给用户态的内存区;处于硬件、软件中断时,无法直接向用户内存区传递数据,代码执行不可中断。针对传统的进程间通信机制,他们均无法直接在内核态和用户态之间使用,原因如下表:
解决内核态和用户态通信机制可分为两类: 处于有用户上下文时,可以使用Linux提供的copy_from_user()和copy_to_user()函数完成,但由于这两个函数可能阻塞,因此不能在硬件、软件的中断过程中使用。 处于硬、软件中断时。
Netlink相对于其他的通信机制具有以下优点:
在内核源码有关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消息
调试平台:Vmware 5.5 + Fedora Core 10(两台,一台作为host机,一台作为target机)。 调试程序:分为内核模块和用户空间程序两部分,当内核模块被加载后,运行用户空间程序,由用户空间发起Netlink会话,和内核模块进行数据交换。 被加载的内核模块无法通过外加的调试器进行调试,KGDB提供了一种内核源码级别的调试机制。Linux内核自2.6.26版本之后在内核中内置了KGDB选项,编译内核时需要选择与之相关的选项,调试时host端需使用带有符号表的vmlinz内核,target端使用gdb调试用户空间的程序。 用户空间程序关键代码如下:
int send_pck_to_kern(u8 op, const u8 *data, u16 data_len)
{
struct user_data_ *pck;
int ret;
pck = (struct user_data_*)calloc(1, sizeof(*pck) + data_len);
if(!pck) {
printf("calloc in %s failed!!!\n", __FUNCTION__);
return -1;
}
pck->magic_num = MAGIC_NUM_RNQ;
pck->op = op;
pck->data_len = data_len;
memcpy(pck->data, data, data_len);
ret = send_to_kern((const u8*)pck, sizeof(*pck) + data_len);
if(ret)
printf("send_to_kern in %s failed!!!\n", __FUNCTION__);
free(pck);
return ret ? -1 : 0;
}
static void recv_from_nl()
{
char buf[1000];
int len;
struct iovec iov = {buf, sizeof(buf)};
struct sockaddr_nl sa;
struct msghdr msg;
struct nlmsghdr *nh;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&sa;
msg.msg_namelen = sizeof(sa);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
len = recvmsg(nl_sock, &msg, 0);
for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len); nh = NLMSG_NEXT (nh, len)) {
// The end of multipart message.
if (nh->nlmsg_type == NLMSG_DONE) {
puts("nh->nlmsg_type == NLMSG_DONE");
return;
}
if (nh->nlmsg_type == NLMSG_ERROR) {
// Do some error handling.
puts("nh->nlmsg_type == NLMSG_ERROR");
return;
}
#if 1
puts("Data received from kernel:");
hex_dump((u8*)NLMSG_DATA(nh), NLMSG_PAYLOAD(nh, 0));
#endif
}
}
内核模块需要防止资源抢占,保证Netlink资源互斥占有,内核模块部分关键代码如下:
static void nl_rcv(struct sk_buff *skb)
{
mutex_lock(&nl_mtx);
netlink_rcv_skb(skb, &nl_rcv_msg);
mutex_unlock(&nl_mtx);
}
static int nl_send_msg(const u8 *data, int data_len)
{
struct nlmsghdr *rep;
u8 *res;
struct sk_buff *skb;
if(g_pid < 0 || g_nl_sk == NULL) {
printk("Invalid parameter, g_pid = %d, g_nl_sk = %p\n", g_pid, g_nl_sk);
return -1;
}
skb = nlmsg_new(data_len, GFP_KERNEL);
if(!skb) {
printk("nlmsg_new failed!!!\n");
return -1;
}
if(g_debug_level > 0) {
printk("Data to be send to user space:\n");
hex_dump((void*)data, data_len);
}
rep = __nlmsg_put(skb, g_pid, 0, NLMSG_NOOP, data_len, 0);
res = nlmsg_data(rep);
memcpy(res, data, data_len);
netlink_unicast(g_nl_sk, skb, g_pid, MSG_DONTWAIT);
return 0;
}
static int nl_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
const u8 res_data[] = "Hello, user";
size_t data_len;
u8 *buf;
struct user_data_ *pck;
struct user_req *req, *match = NULL;
g_pid = NETLINK_CB(skb).pid;
buf = (u8*)NLMSG_DATA(nlh);
data_len = nlmsg_len(nlh);
if(data_len < sizeof(struct user_data_)) {
printk("Too short data from user space!!!\n");
return -1;
}
pck = (struct user_data_ *)buf;
if(pck->magic_num != MAGIC_NUM_RNQ) {
printk("Magic number not matched!!!\n");
return -1;
}
if(g_debug_level > 0) {
printk("Data from user space:\n");
hex_dump(buf, data_len);
}
req = user_reqs;
while(req->op) {
if(req->op == pck->op) {
match = req;
break;
}
req++;
}
if(match) {
match->handler(buf, data_len);
}
nl_send_msg(res_data, sizeof(res_data));
return 0;
}