深入剖析命名管道FIFO对程序性能的影响

来源:互联网 发布:js中新建json对象 编辑:程序博客网 时间:2024/05/22 04:56

        命名管道FIFO是一种简单的跨进程IPC机制,相对比共享内存,消息队列等,FIFO支持基本的VFS操作,也支持poll事件处理。因此FIFO经常被作为进程之间快捷有效的消息通知管道。比如一个高性能服务器程序,往往会生成少数进程,这些进程往往可以分为两类,一类专门负责网络消息包收发处理,一类专门负责业务逻辑处理。而这两类进程之间往往通过共享内存作为消息包的环形缓冲区,同时使用FIFO作为这两类进程的消息通知机制(即通过write FIFOread FIFO来通知对方有消息包到达和消除这种通知,这样使得负责网络消息包收发处理的进程能够以epoll或者select的方式统一监测socketFIFO)。但是实际使用中,当处理的消息包量比较大的时候,会发现进程由于读写FIFO(已经设置FIFONON_BLOCK)造成了较大的阻塞延迟,导致消息包处理的最大处理延时难以严格把控。于是对此进行了深入分析。

        首先做几个简单的实验,验证一下猜想。测试机器内核是32linux2.6.16.60CPUXEON E5405 ,共4个核。写了两个程序fifo_readfifo_write,以NON_BLOCK方式操作同一个FIFO。其中fifo_read不停循环read FIFOfifo_write不停循环write FIFO,数据长度是1个字节。然后在read或者write操作加入延迟统计。

        测试1:只运行fifo_read进程

        从图上可见read fifo平均延迟大概在0.4us左右,而且最大延迟稳定小于0.1msfifo_read进程占用一个cpu核,利用率持续100%,可见这种情况下fifo_read是几乎无阻塞的,延迟也符合预期。

        测试2:只运行fifo_write进程

        同理,write fifo的延迟也在0.4us左右,比read fifo稍微大一些,最大延迟也稳定小于0.1ms,而且fifo_write进程占用一个cpu核,利用率持续100%,可见这种情况下fifo_write也是几乎无阻塞的,符合预期。

        测试3:同时运行fifo_readfifo_write进程

        这个测试过程发现了read FIFOwrite FIFO延迟都上升了不少,前者达到了4us以上,后者5-6us之间,而且超过1ms的延迟经常出现,最大延迟有时达到了几个ms以上。这两个进程分别占用了一个cpu核,但是cpu利用率却不是想象中的100%了,都在70%左右。这个也说明了延迟上升的原因,从进程有时处于D状态可以明显看出来进程有挂起现象。

        测试4同时运行fifo_readfifo_write进程,而且同时运行专吃cpu3个干扰进程eat_cpu

        上面两个图分别是fifo_readfifo_write进程的延迟统计,可见超过1ms的操作比例有时比较多,而且最大延迟有时相当大。而且这里要特别注意一点,这两个进程的平均延迟绝大部分情况下对比测试3都下降了,有时还比较接近测试1或者2的延迟。

        上面几个图是fifo_readfifo_write进程和3个干扰的eat_cpu进程的不同时间top截图。由于cpu只有4个核,其中3个被eat_cpu牢牢占住了,所以fifo_readfifo_write有时呈现出分享一个cpu核,有时呈现出独占一个cpu核,从上面延迟统计上也可以看出来,因为有时平均延迟很低(当fifo_read或者fifo_write独占cpu),有时又很高(fifo_readfifo_write有挂起,分享了cpu),这些都是内核进程调度导致的。

        测试5:仿照fifo_readfifo_write写了udp版本,即把程序里面读写FIFO换成为了收发UDP包,这样得到两个程序udp_readudp_write,这样同时运行 udp_readudp_write进程。

        从这个top图可见,这两个进程都持续跑满了各自的cpu核,跟测试3有明显的差别,测试3里面fifo_readfifo_write都不能充分利用各自的cpu核,有明显的进程挂起现象。

        从上面的测试基本可以推出,FIFO的读写即使是NON_BLOCK方式,也是有可能存在明显的阻塞(测试3里面同时运行fifo_readfifo_write,实际上如果是同时运行两个fifo_read或者两个fifo_write,效果是一样的,只不过有程度上有些差别),只要竞争情况充足,进程挂起毫秒级以上是很容易的。在用户态,应用程序能控制的很有限,所以这是内核实现的问题。下面对2.6.16.60内核下,FIFO实现代码的进行分析。关于FIFO的操作主要有三个方法openreadwrite比较关键。先看fs/fifo.c代码,看看fifo_open如下:

        这里最关键的还是看当FIFO文件open之后的file对象的f_op指针(第5584103行),(实际上FIFOpipe具有相同的inode的和几乎相同的实现,只是FIFO文件的inode是挂接在磁盘文件系统上的,而pipeinode是在虚拟的pipefs里面的,不过都很简单,第40行可以看到FIFOpipe都有一个很关键的pipe_inode_info结构依附在inode上面,这个pipe_inode_info则是管道实际的存储缓冲区,纯粹在内存里面),FIFO在只读、只写和读写三种情况下的file_operations分别是read_fifo_fopswrite_fifo_fopsrdwr_fifo_fops,这几个结构定义在fs/pipe.c里面:

        从代码上看,其实没太大差别,我们关注的readwrite方法,其实都是指向pipe_readpipe_writepipe_readpipe_write实际调用的是向量版本pipe_readvpipe_writev)。下面看看pipe_readvpipe_writev方法:

        可以看到,无论是在读还是写,第139行和第240行都加了互斥锁,而NON_BLOCK标志的判断是在第188行和326行的,NON_BLOCK是在pipe_inode_info(管理最多16buffer,每个buffer是一个page大小)里面操作缓冲区的时候进行判断(当无空闲空间写或者无内容可读),无论读还是写FIFO,由于pipe_indoe_info是唯一的,都会改变pipe_inode_info的内容,所以都进行了mutex_lock,而mutex_lock加锁失败时把进程设置为TASK_UNINTERRUPTIBLE状态(从top看进程是D状态)然后schedule主动让出cpu调度其他进程运行的。因此,可以知道FIFO的内核实现是不那么精致的,至少在锁的处理上来看,即使是一个读和一个写或者是多个同时读都需要互斥,就可能导致进程挂起,从而产生延迟大增的毛刺现象。而前面测试5中,如果换成了udp收发包则显然没有这种现象,因为socket通讯是基于一对socket,在sockfs中是两个socketinode,显然没有这样的互斥问题,但是socket的消息收发实现比FIFO的消息收发复杂很多。经过测试,FIFOwrite操作比udpwrite操作速度在没有竞争的情况下快近10倍,但是在有竞争的情况下,则FIFOwrite延迟会下降很多,几乎与udpwrite相近。

        因此,从应用程序的角度上看,要降低因为FIFO互斥导致的进程阻塞现象,最好就是合理降低对FIFO的读写频率,使得尽量降低冲突发生的现象。另外,对FIFO的内核实现应该是可以再优化的,起码读写锁或者是lockfree之类的设计是可以做的,不过2.6.22内核之后,已经提供了eventfd来替代pipeFIFO在这方面的应用了,eventfd的实现非常高效,不过也有点小缺点,在没有亲缘关系的进程之间需要事先用unix socket传递eventfd句柄。

 

原创粉丝点击