深入剖析命名管道FIFO对程序性能的影响
来源:互联网 发布:js中新建json对象 编辑:程序博客网 时间:2024/05/22 04:56
命名管道FIFO是一种简单的跨进程IPC机制,相对比共享内存,消息队列等,FIFO支持基本的VFS操作,也支持poll事件处理。因此FIFO经常被作为进程之间快捷有效的消息通知管道。比如一个高性能服务器程序,往往会生成少数进程,这些进程往往可以分为两类,一类专门负责网络消息包收发处理,一类专门负责业务逻辑处理。而这两类进程之间往往通过共享内存作为消息包的环形缓冲区,同时使用FIFO作为这两类进程的消息通知机制(即通过write FIFO和read FIFO来通知对方有消息包到达和消除这种通知,这样使得负责网络消息包收发处理的进程能够以epoll或者select的方式统一监测socket和FIFO)。但是实际使用中,当处理的消息包量比较大的时候,会发现进程由于读写FIFO(已经设置FIFO为NON_BLOCK)造成了较大的阻塞延迟,导致消息包处理的最大处理延时难以严格把控。于是对此进行了深入分析。
首先做几个简单的实验,验证一下猜想。测试机器内核是32位linux2.6.16.60,CPU是XEON E5405 ,共4个核。写了两个程序fifo_read和fifo_write,以NON_BLOCK方式操作同一个FIFO。其中fifo_read不停循环read FIFO,fifo_write不停循环write FIFO,数据长度是1个字节。然后在read或者write操作加入延迟统计。
测试1:只运行fifo_read进程
从图上可见read fifo平均延迟大概在0.4us左右,而且最大延迟稳定小于0.1ms,fifo_read进程占用一个cpu核,利用率持续100%,可见这种情况下fifo_read是几乎无阻塞的,延迟也符合预期。
测试2:只运行fifo_write进程
同理,write fifo的延迟也在0.4us左右,比read fifo稍微大一些,最大延迟也稳定小于0.1ms,而且fifo_write进程占用一个cpu核,利用率持续100%,可见这种情况下fifo_write也是几乎无阻塞的,符合预期。
测试3:同时运行fifo_read和fifo_write进程
这个测试过程发现了read FIFO和write FIFO延迟都上升了不少,前者达到了4us以上,后者5-6us之间,而且超过1ms的延迟经常出现,最大延迟有时达到了几个ms以上。这两个进程分别占用了一个cpu核,但是cpu利用率却不是想象中的100%了,都在70%左右。这个也说明了延迟上升的原因,从进程有时处于D状态可以明显看出来进程有挂起现象。
测试4:同时运行fifo_read和fifo_write进程,而且同时运行专吃cpu的3个干扰进程eat_cpu
上面两个图分别是fifo_read和fifo_write进程的延迟统计,可见超过1ms的操作比例有时比较多,而且最大延迟有时相当大。而且这里要特别注意一点,这两个进程的平均延迟绝大部分情况下对比测试3都下降了,有时还比较接近测试1或者2的延迟。
上面几个图是fifo_read,fifo_write进程和3个干扰的eat_cpu进程的不同时间top截图。由于cpu只有4个核,其中3个被eat_cpu牢牢占住了,所以fifo_read和fifo_write有时呈现出分享一个cpu核,有时呈现出独占一个cpu核,从上面延迟统计上也可以看出来,因为有时平均延迟很低(当fifo_read或者fifo_write独占cpu),有时又很高(fifo_read或fifo_write有挂起,分享了cpu),这些都是内核进程调度导致的。
测试5:仿照fifo_read和fifo_write写了udp版本,即把程序里面读写FIFO换成为了收发UDP包,这样得到两个程序udp_read和udp_write,这样同时运行 udp_read和udp_write进程。
从这个top图可见,这两个进程都持续跑满了各自的cpu核,跟测试3有明显的差别,测试3里面fifo_read和fifo_write都不能充分利用各自的cpu核,有明显的进程挂起现象。
从上面的测试基本可以推出,FIFO的读写即使是NON_BLOCK方式,也是有可能存在明显的阻塞(测试3里面同时运行fifo_read和fifo_write,实际上如果是同时运行两个fifo_read或者两个fifo_write,效果是一样的,只不过有程度上有些差别),只要竞争情况充足,进程挂起毫秒级以上是很容易的。在用户态,应用程序能控制的很有限,所以这是内核实现的问题。下面对2.6.16.60内核下,FIFO实现代码的进行分析。关于FIFO的操作主要有三个方法open,read,write比较关键。先看fs/fifo.c代码,看看fifo_open如下:
这里最关键的还是看当FIFO文件open之后的file对象的f_op指针(第55,84,103行),(实际上FIFO与pipe具有相同的inode的和几乎相同的实现,只是FIFO文件的inode是挂接在磁盘文件系统上的,而pipe的inode是在虚拟的pipefs里面的,不过都很简单,第40行可以看到FIFO和pipe都有一个很关键的pipe_inode_info结构依附在inode上面,这个pipe_inode_info则是管道实际的存储缓冲区,纯粹在内存里面),FIFO在只读、只写和读写三种情况下的file_operations分别是read_fifo_fops、write_fifo_fops和rdwr_fifo_fops,这几个结构定义在fs/pipe.c里面:
从代码上看,其实没太大差别,我们关注的read和write方法,其实都是指向pipe_read和pipe_write(pipe_read和pipe_write实际调用的是向量版本pipe_readv和pipe_writev)。下面看看pipe_readv和pipe_writev方法:
可以看到,无论是在读还是写,第139行和第240行都加了互斥锁,而NON_BLOCK标志的判断是在第188行和326行的,NON_BLOCK是在pipe_inode_info(管理最多16个buffer,每个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中是两个socket的inode,显然没有这样的互斥问题,但是socket的消息收发实现比FIFO的消息收发复杂很多。经过测试,FIFO的write操作比udp的write操作速度在没有竞争的情况下快近10倍,但是在有竞争的情况下,则FIFO的write延迟会下降很多,几乎与udp的write相近。
因此,从应用程序的角度上看,要降低因为FIFO互斥导致的进程阻塞现象,最好就是合理降低对FIFO的读写频率,使得尽量降低冲突发生的现象。另外,对FIFO的内核实现应该是可以再优化的,起码读写锁或者是lockfree之类的设计是可以做的,不过2.6.22内核之后,已经提供了eventfd来替代pipe和FIFO在这方面的应用了,eventfd的实现非常高效,不过也有点小缺点,在没有亲缘关系的进程之间需要事先用unix socket传递eventfd句柄。
- 深入剖析命名管道FIFO对程序性能的影响
- 一个简单的聊天程序--命名管道FIFO
- 命名管道(FIFO)聊天程序
- 匿名管道 与 命名管道/FIFO管道 的特点
- Linux命名管道FIFO的读写规则
- Linux命名管道FIFO的读写规则
- 关于命名管道FIFO不错的总结
- Linux命名管道FIFO的读写规则
- Linux命名管道FIFO的读写规则
- 命名管道(FIFO)的实现
- Linux命名管道FIFO
- UNIX命名管道FIFO
- 命名管道(FIFO)
- 命名管道(FIFO)
- 【Linux】命名管道FIFO
- 命名管道(FIFO)
- Linux 命名管道FIFO
- 命名管道(FIFO)
- quotactl() function prototype missing in sys/quota.h
- 设计模式:基于javabean的 MVC
- Algorithms 第四版 习题 4.1.16- 4.1.18
- Code for fun (1)
- shell shift
- 深入剖析命名管道FIFO对程序性能的影响
- 月初伤感日志巨献:爱流逝了,我依旧等你
- Android的版本(Version)和API-level的对应关系
- C++中头文件(.h)和源文件(.cpp)都应该写些什么
- 漫谈中国各省未来发展趋势(2011版)
- 10.26事件
- 转载过来的反调试方法
- ZOJ_1067_Color Me Less
- android 视频播放器