支持多线程并发与消息异步处理的Linux Netlink通信机制研究
2017-11-02熊伟丁涵罗云锋
熊伟++丁涵++罗云锋
摘要:Netlink是Linux操作系统内核空间与用户空间最流行的进程间通信机制之一,但目前在多线程程序中的使用还存在一些问题。介绍了Netlink相对于Linux其它传统通信手段的优点,阐述了使用Netlink进行用户程序与内核模块通信的实现方法,分析了目前公开资料上Netlink线程并发支持机制存在的问题,并给出了支持多线程并发与消息异步处理的正确方法,最后在真实机器上进行了验证。结果显示,该方法能有效支持在多线程Linux应用中使用Netlink进行用户态与内核态通信。
关键词:Linux;nelink;进程间通信;多线程并发;异步处理
DOIDOI:10.11907/rjdk.172576
中图分类号:TP319文献标识码:A文章编号:16727800(2017)010009905
0引言
Linux是当今应用最广泛的操作系统之一,其兼容性好,能适应从嵌入式设备、个人用户终端到高性能服务器的不同硬件平台,具有多任务与多用户能力。Linux符合POSIX标准,在GNU公共许可权限下可免费获得其内核源代码,同时还具备完整的软件生态链,包含各种开发工具及第三方软件库,非常方便用户开发定制自己的应用。
Linux采用模块化单内核架构,支持内核模块动态加载与卸载,其系统地址空间结构如图1所示。Linux所有
图1Linux操作系统结构
内核代码可看作一个整体,运行在一个独立的地址空间中,通常被称为内核空间[1]。运行于内核空间的代码不受任何限制,能够自由地访问任何有效地址以及直接进行设备访问。而用户应用运行在内核之上,其不能随意占用系统资源与修改系统配置,从而确保系统安全性与稳定性。
在日常应用中,应用程序通常包括上层用户界面程序与底层内核驱动模块两部分,用户界面负责接收用户输入及显示最终处理结果,内核驱动则负责调用内核处理用户请求。因此内核空间与用户空间进行通信的方法非常重要。
目前,Linux常用的内核用户通信机制有以下几种[2]:
(1)设备驱动接口。设备节点位于/dev目录下。设备驱动接口允许用户访问设备节点[3],利用copy_from_user()与copy_to_user()函数,在用户态与内核态间拷贝数据。但是这两个函数只支持阻塞式调用,不能在中断中使用,而且只支持用户程序主动进行通信,通常用在硬件驱动中。
(2)Proc与sysfs文件系统。Proc与sysfs是虚拟文件系统,用于显示进程、处理器、内存、中断等信息[4,5]。用户可以通过读写这两种文件系统与内核进行通信。其最大缺点是不支持基于事件的信号机制,数据传输大小也不能超过一个内存页,可扩展性较差。
(3)内存映射。/dev/mem是Linux系统中一种特殊的字符设备文件[6],应用程序通过这个节点,可以在内核地址空间与用户地址空间进行映射,然后访问映射后的内存区域,实现用户空间与内核空间的通信。但是对内核地址的误操作将引起严重后果,导致系统崩溃。
(4)Netlink套接字。Netlink是一种面向数据报的消息系统,目前在Linux内核中有非常多应用可用于通信,包括路由、IPSEC、防火墙、netfilter日志等[710]。Netlink具有以下特点:消息具有较强的扩展能力,用户可以自定义消息格式,且提供了基于事件的信号机制,允许大数据传输;支持全双工传输,允许内核主动发起传输通信;支持单播与组播两种通信方式[11]。
如上所述,目前在Linux系统内核空间与用户空间通信方式中:设备节点适合于驱动程序开发,但只支持单工传输;Proc与sysfs文件使用方便,但不支持传输大数据;内存映射传输效率最高,但误操作时会对系统造成严重破坏;Netlink则使用很灵活,能满足大多数用户需求。
本文首先介绍Netlink套接字基本使用方法与通信流程,然后详细阐述使用Netlink如何实现多线程并发与消息异步处理通信,最后在真实硬件平台上对该机制进行验证。
1Netlink机制概述
Netlink机制包含用户态接口与内核态接口,其中用户态沿用标准的socket接口,内核态则提供了专用接口。
1.1用户态Netlink接口
Netlink用户态接口与BSD套接字接口基本一致,包括:socket()、bind()、sendmsg()、recvmsg()、close等常用接口。
1.1.1Netlink套接字創建
int socket(int domain, int type, int protocol)
其中,domain参数为AF_NETLINK或PF_NETLINK,表示使用Netlink协议,type参数是SOCK_RAW或SOCK_DGRAM,代表Netlink面向数据报,最后一个参数指定Netlink协议类型,除了内核中已经定义的类型,用户还可以定义自己的协议类型。
1.1.2套接字地址绑定
int bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl))
函数bind()用于Netlink套接字句柄与Netlink源地址绑定。第一个参数为创建Netlink套接字时获取的描述符,第二个为Netlink源地址结构指针,最后一个参数为Netlink源地址结构大小。
Netlink socket地址定义如下:
struct sockaddr_nl {
_kernel_sa_family_t nl_family;/*AF_NETLINK*/endprint
unsigned short nl_pad; /* zero */
_u32 nl_pid; /* port ID */
_u32 nl_groups; /* multicast groups mask */
};
其中:nl_family代表协议类型,设置为AF_NETLINK或者PF_NETLINK;字段nl_pad保留,默认设置为0;nl_pid代表Netlink socket的本地地址,为确保消息发送准确性,其设置非常关键,必须确保唯一性;字段nl_groups用于设置多播组,如果设置为0,表示进行单播。
1.1.3Netlink消息发送
int sendmsg(int sock, struct msghdr *msg, int flags)
Netlink发送消息前需要填充信息,信息由消息头与数据部分组成,其结构如图2所示。
图2Netlink消息头
消息长度代表Netlink消息的总长度,包括消息头长度与数据部分长度;应用内部定义消息类型,大部分情况下设置为0;标志用于设置消息标志,内核读取与修改这类标志,通常不需修改,默认为0,在一些高级应用(如路由daemon)中需要设置它进行特殊操作;字段序列号与消息端口号用于应用追踪消息来源,分别表示消息发送顺序号与来源端口号。
1.1.4Netlink消息接收
int recvmsg(int sock, struct msghdr *msg, int flags)
应用接收消息时,首先需要为消息头与数据部分分配足够空间,然后填充消息头。
1.1.5关闭Netlink套接字
Close函数用于关闭套接字,释放资源。
1.2内核态Netlink接口
Linux内核包含一套专门的接口函数,接收用户程序发送的数据以及将处理完数据发送回用户程序。
1.2.1内核态Netlink套接字创建
struct sock*netlink_kernel_create(struct net*net, int unit, struct netlink_kernel_cfg*cfg)
netlink_kernel_create函数在不同内核版本间变化非常大,在使用过程中,需要查找与内核匹配的函数定义。在3.0以上版本中,该函数包含3个参数:第一个参数指定网络名字空间,默认为init_net全局变量;第二个参数设置Netlink协议类型,需要与用户态定义一致;第三个参数用于指定内核态Netlink配置信息。其结构为:
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb);
struct mutex *cb_mutex;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
bool (*compare)(struct net *net, struct sock *sk); }
通常用户只需设置groups与input字段:Groups用于设置单播还是组播;input用于注册消息回调处理函数,当内核接收到用户发来的Netlink信息后会自动调用它。
1.2.2从内核态向用户态发送数据
Netlink支持单播与组播,因此内核态信息发送函数包括两个。
单播:
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock)
第一个参数为内核Netlink套接字句柄;第二个参数存放消息结构,数据字段为发送的Netlink消息;控制块则包含消息地址信息;第三个参数portid为接收对象的Netlink地址,最后一个用于设置阻塞属性。
组播:
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,__u32 group, gfp_t allocation)
前3个参數与单播一样,第四个参数用于指定接收消息的多播组,最后一个参数则为内核内存分配类型。
1.3Netlink内核态与用户态通信流程
用户程序通过Netlink机制与内核进行通信,流程如图3所示。
图3Netlink用户态与内核态交互流程
用户态Netlink使用流程与常用BSD Socket一样,首先使用socket函数创建套接字,然后使用bind函数绑定地址,封装并使用sendmsg向内核发送消息,接着使用recvmsg接收消息,最后通过close函数关闭套接字,释放资源。内核态处理过程类似,发送消息时还可以根据需要选择单播或者多播。
2机制设计
Netlink作为socket的一个变种,本身就支持并发与异步处理,但是需要针对线程中Netlink socket本地地址设置与消息接收处理松耦合进行特殊设计。
2.1线程Netlink本地地址生成方法
Netlink并发实现的关键点是套接字创建时地址设置的唯一性。Bind函数负责给Netlink套接字命名,将本地地址与其相关联。上文已经介绍了结构体sockaddr_nl中nl_pid字段用于代表32位本地地址,其在填充时必须保证唯一性才能确保收发消息的准确性。endprint
如果用户程序实现的是进程级并发,可以采用进程号作为nl_pid的值,进程号在系统中的唯一性确保了Netlink本地地址的唯一性。
但是如果进程中多个线程需要创建各自独立的Netlink socket,基于线程共享进程号的原因,进程号就不能用于区分线程创建的Netlink套接字地址。目前流传最广泛的线程Netlink中nl_pid创建方法为[12]:
pthread_self() 16 | getpid()
其中,nl_pid由线程自身ID后半部与其所属进程pid拼接而成,期望由pid在系统的唯一性与pthread_self在进程中的唯一性,保证nl_pid在全系统的唯一性。但在实际使用中,生成的值并不唯一。图4是在ubuntu 14.04中创建10个线程生成的pid、tid(pthread_self返回值)与nl_pid结果。如图4所示,10个样本中就出现了重复。
图4线程地址示例图
从图4中可以发现,pthread_self低16位有12位重复,pthread_self()16后只有高4位发生变化,由于pid一般小于65 536,因此进程中创建n个线程,使用pthread_self()16 | getpid()出现重复数据的概率为:
1-15!(16-n)!×16n-1(1 运行10次出现重复的概率约为85%,而创建17个以上线程nl_pid重复概率就已经为1。 分析pthread_self实现,可以发现其实际获取的是线程TCB地址相对于进程数据段的偏移,所以低地址一致,造成按上述方法生成nl_pid出现重复。 因此,为了保证线程中Netlink套接字正常使用,需要重新设计nl_pid生成公式。考察pthread_self的实现,它在进程内唯一而且后半部基本一致,因此可以考虑取其前半部与线程pid进行拼接,从而确保生成nl_pid在全系统的唯一性。新生成方法如下: pthread_self()16 | getpid()16 一个支持多线程并发的Netlink用户态示例用例如下,所有nl_pid设置都使用新方法: struct sockaddr_nl src_addr, dest_addr; struct nlmsghdr* nlh = NULL; struct iovec iov; struct msghdr msg; /**建立用戶空间netlink套接字*/ sock_nl=socket(AF_NETLINK, SOCK_RAW, LinuxV_NL_P_TYPE); /**填充SRC_ADDR并进行端口绑定*/ memset(&src_addr, 0, sizeof(struct sockaddr_nl)); src_addr.nl_family=AF_NETLINK; /**按照新公式定义本地地址*/ src_addr.nl_pid=pthread_self()16 | getpid()16; src_addr.nl_groups = 0; /**绑定netlink套接字,在nl_pid端口监听*/ bind(sock_nl, (struct sockaddr*)&src_addr, sizeof(struct sockaddr_nl); /**封装Netlink消息*/ dest_addr.nl_family=AF_NETLINK; dest_addr.nl_pid=0; /**to Linux kernel*/ dest_addr.nl_groups=0; /**填充netlink命令包头*/ nlh=(struct nlmsghdr*)malloc(NLMSG_LENGTH(MAX_PAYLOAD)); nlh->nlmsg_type=MY_TYPE_0; /**注意保持和初始化时nl_pid一致,用于确定消息来源*/ nlh->nlmsg_pid=pthread_self()16 | getpid()16; /**填充发送命令报内容*/ memcpy(NLMSG_DATA(nlh), buf, buflen); /**构造Netlink消息包*/ iov.iov_base=(void*)nlh; msg.msg_name=(void*)&dest_addr; msg.msg_iov=&iov; /**发送netlink包*/ sendmsg(sock_nl, &msg, 0); /**接收netlink包*/ recvmsg(sock_nl, &msg, 0); close(sock_nl) 2.2内核Netlink消息异步处理机制 Netlink是BSD套接字的一种,继承了其异步处理特性,用户程序发送消息并把消息保存到接收者的接收队列后,不需要一直等待内核处理完消息。 为了提高异步处理效率,在内核态可以将Netlink信息的接收与处理过程松耦合,这样内核收到用户发来的消息后只负责唤醒处理内核线程,然后就返回。所有的消息处理工作由处理线程完成,从而可以实现用户程序持续快速发送Netlink消息到内核,提高吞吐率。特别是如果应用程序发送信息处理流程不同,就可以创建多个内核线程进行并发处理。在内核3.12.11上内核态Netlink创建与消息异步收发实现代码如下:
struct netlink_kernel_cfg cfg={
.groups=0,
.input=receive_us_msg,
};
nl_sk=netlink_kernel_create(&init_net,LinuxV_NL_P_TYPE, &cfg);
/**创建netlink消息处理内核线程*/
nl_msg_thread=kthread_run(nlmsg_process_thread, NULL, "process");
void receive_us_msg(struct sk_buff* skb)
{
struct sk_buff* nl_skb=NULL;
nl_skb=skb_copy(skb,GFP_ATOMIC);
if(nl_skb)
skb_queue_tail(&(nl_sk->sk_receive_queue),nl_skb);
wake_up_interruptible(sk_sleep(nl_sk));
}
/*nlmsg_process_thread函数负责处理Netlink消息并回传用户程序*/
3实验平台与测试方法
为了验证本文Netlink多线程并发机制的稳定性与扩展性,测试将在32位与64位Linux系统上进行。测试机具体配置如表1、表2所示。
在测试中,检验使用上述方法进程能创建包含Netlink连接的线程数量。如果进程中不同线程生成的nl_pid一致,Netlink将创建失败。在32位系统上,进程可以使用的虚拟用户地址空间为3GB,其创建线程分配的线程空间大小总和不能超过这个限制,因此系统中进程能创建的线程数量取决于线程堆栈大小,在实验中通过ulimit命令设置不同的线程堆栈大小,然后记录进程能创建的最大线程数量,看其是否与理论值相符。在64位机器上,由于虚拟地址空间可以达到TB级,因此测试时首先设置系统最大可创建线程数,然后通过测试程序记录实际能创建的最大线程数量,看其是否与设置值相符。
4实验结果与分析
在32位操作系统上测试结果如表3所示。
试验显示,在32位操作系统上,线程堆栈为2M时,创建线程数量为1 449,这个值与理论最大值相符,因为在32位Linux操作系统下进程用户空间大小为3G(3 072M),用3 072M除以2M得1 536,但实际测试用例中代码段与数据段等占用大概1KB,这个值应该为1 400多。同理,内核堆栈为4M、8M、16M时,线程数量与理论最大值相符合。
在64位操作系统上测试结果如表4所示。
系统中最大线程数量值设置为32 768,测试结果显示,不同大小線程堆栈情况下创建的线程数量与系统线程最大值相差不大,符合预期,完全能够满足实际应用需求。
5结语
Netlink socket是Linux系统中用户程序与内核模块之间一种很灵活的通信方式,它使用方便,提供了全双工、缓冲I/O、多点传送及异步通讯等高级特性,应用极其广泛。但是,Netlink在内核不同版本中变化非常大,目前公开资料上提供的线程并发与消息接收处理松耦合方法存在错误,也不适应当前内核版本。本文分析了Netlink socket本地地址nl_pid流行计算公式的错误原因,设计了新的计算公式,并在真实机器上进行了验证,测试结果显示新计算公式能真正支持线程中Netlink的使用。同时,还针对当前内核版本,设计了切实可用的消息接收与处理松耦合流程,实现了上层应用程序的快速响应。
但是,由于Netlink是基于BSD socket实现的,其通信过程耗时非常大,传输效率不高,在今后工作中可以考虑结合其它传输机制,实现快捷高效的内核态与用户态数据通信。
参考文献参考文献:
[1]ROBERT L. Linux内核设计与实现[M].陈莉君,康华,张波,译.北京:机械工业出版社,2006.
[2]NEIRA-AYUSO P, GASCA R M, LEFEVRE L. Communicating between the kernel and userspace in Linux using Netlink sockets[J]. Software Practice & Experience,2013,40(9):797–810.
[3]CORBET J, RUBINI A, KROAHHARTMAN G. Linux device drivers[M]. Cambridge :O'Reilly Media, Inc.2005.
[4]郭松,谢维波.Linux视域下Proc文件系统的编程剖析[J].华侨大学学报:自然科学版,2010,31(5):515520.
[5]MOCHEL P. The sysfs filesystem[J]. Proceedings of Annual Linux Symposium,2005(1):313326.
[6]STEVENS W R,RAGO S A. UNIX环境高级编程[M].尤晋元,译.北京:人民邮电出版社,2009.
[7]SALIM J, KHOSRAVI H, KLEEN A, et al. Linux Netlink as an IP services protocol[J]. International Journal of Developmental Neuroscience,2003,28(8):94.
[8]KENT S, SEO K. RFC4301: security architecture for internet protocol (IPSec)[M]. Los Angeles: RFC Editor,2005.
[9]PURDY G N. Linux iptablespocket reference: firewalls, nat and accounting[M]. Sebastopol, CA: OReilly Media, Inc,2004.
[10]ROSEN. Netfilter[M]. Berkeley, CA: Linux Kernel Networking Apress,2014.
[11]刘斌,朱程荣.Linux内核与用户空间通信机制研究[J].电脑知识与技术,2012,8(16):38163817.
[12]HE K K. Why and how to use Netlink socket[J]. Linux Journal,2005,11(130):1419.
责任编辑(责任编辑:何丽)endprint