IPC通讯陷阱之六万五千分之一

 

 

【摘要】在本文中,做者剖析了IPC通讯机制,经过对系统函数源码分析,指出了其中存在的1/65535概率可能出现的隐患, 并结合实际的案例给出了解决方案.在本文中,咱们能够了解到针对IPC通讯常见问题 。在咱们以后的测试工做中,能够有选择针对这些注意事项和易错点设计测试case,让bug无处藏身。因为做者能力有限,文中若是有一些不够清晰不够全面的地方,欢迎指正。
【关键词】IPC,共享内存html

1 IPC通讯概述

IPC(InterProcess Communication) 进程间通讯,一般在同一台主机各个进程间的PIC主要有:管道,FIFO,消息队列,信号量,以及共享内存,而不一样主机上各个进程间IPC通讯能够经过套接字和stream.node

(1)管道(Pipe):管道可用于具备亲缘关系进程间的通讯,容许一个进程和另外一个与它有共同祖先的进程之间进行通讯。
(2)命名管道(named pipe):命名管道克服了管道没有名字的限制,所以,除具备管道所具备的功能外,它还容许无亲缘关系进程间的通讯。命名管道在文件系统中有对应的文件名。命名管道经过命令mkfifo或系统调用mkfifo来建立。
(3)信号(Signal):信号是比较复杂的通讯方式,用于通知接受进程有某种事件发生,除了用于进程间通讯外,进程还能够发送信号给进程自己;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又可以统一对外接口,用sigaction函数从新实现了signal函数)。
(4)消息(Message)队列:消息队列是消息的连接表,包括Posix消息队列system V消息队列。有足够权限的进程能够向队列中添加消息,被赋予读权限的进程则能够读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
(5)共享内存:使得多个进程能够访问同一块内存空间,是最快的可用IPC形式。是针对其余通讯机制运行效率较低而设计的。每每与其它通讯机制,如信号量结合使用,来达到进程间的同步及互斥。
(6)内存映射(mapped memory):内存映射容许任何多个进程间通讯,每个使用该机制的进程经过把一个共享的文件映射到本身的进程地址空间来实现它。
(7)信号量(semaphore):主要做为进程间以及同一进程不一样线程之间的同步手段。
(8)套接口(Socket):更为通常的进程间通讯机制,可用于不一样机器之间的进程间通讯。起初是由Unix系统的BSD分支开发出来的,但如今通常能够移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

2 XSI IPC: 消息队列,信号量以及共享内存linux


每因为篇幅关系,在这里着重讨论 XSI IPC,消息队列,信号量以及共享存储器,他们之间有不少共同之处
一个内核中的IPC结构(消息队列,信号量和共享存储器)都用一个非负整数的标识符(identifier)加以引用。例如,为了对一个消息队列发 送消息或取消息,只需知道其队列标识符。当一个IPC结构被建立,之后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大值,而后又 回到0。
标识符是IPC对象的内部名。键(key)是IPC对象的外部名。不管什么时候建立IPC结构(调用msgget,semget或shmget),都应 该指定一个键。键的数据类型是基本系统数据类型key_t,一般在<sys/types.h>中被定义为长整型。键由内核变成标识符。
客户进程和服务器进程认同一个路径名和项目ID(项目ID是0~255之间的字符值),接着调用ftok将这两个值变成为一个键。而后用该键建立一个新的IPC结构或获得一个的IPC结构。ftok提供的惟一服务就是由一个路径名和项目ID产生一个键。
#include <sys/ipc.h>
key_t ftok(const char *path, int id);
path参数必须引用一个现存文件。当产生键时,只使用id参数的低八位。
三个get函数(msgget,semget和shmget)都有两个相似的参数:一个key和一个整型flag。若是知足如下两个条件之一,则建立一个新的IPC结构:
1). key是IPC_PRIVATE;
2). key当前未与特定类型的IPC结果相结合,而且flag中指定IPC_CREAT位。
为访问现存的队列(一般是客户进程),key必须等于建立该队列时所指定的键,而且不该该指定IPC_CREAT。
若是但愿建立一个新的IPC结构,并且要确保不是引用具备同一标识符的一个现行IPC结构,则必须在flag中同时指定IPC_CREAT和IPC_EXCL位。这样作,若是IPC结构已经存在就会出错,返回EEXIST。
XSI IPC为每一个IPC结构设置了一个ipc_perm结构。该结构规定了权限和全部者。
struct ipc_perm{
uid_t uid; // owner's effective user id
gid_t gid; // owner's effective group id
uid_t cuid; // creator's effective user id
gid_t cgid; // creator's effective group id
mode_t mode; // access modes
};
在建立IPC结构时,对全部字段都附初值。调用msgctl、semctl或shmctl修改uid、gid和mode字段。为了改变这些值,调用进程必须是IPC结构的建立者或超级用户。更改这些字段相似于文件调用chown和chmod。
字段mode的值以下所示的值,可是对于任何IPC结构都不存在执行权限。另外,消息队列和共享内存使用术语读(read)和写(write),而信号量则用术语读(rend)和更改(alter)。服务器

3 不一样IPC通讯方式大比拼
XSI IPC的主要问题是:IPC结构是在系统范围内起做用,没有访问计数。例如:若是进程建立了一个消息队列,在该队列中放入了几则消息,而后终止,可是该消 息队列及其内容并不会被删除。它们留在系统中直至出现下述状况:由某个进程调用msgrcv或msgctl读消息或删除消息队列;或某个进程执行 ipcrm命令删除该消息队列;或是由正在启动的系统删除消息队列。与管道相比,当最后一个访问管道的进程终止时,管道就被彻底删除了。对于FIFO而 言,当最后一个引用FIFO的进程终止时,其名字仍保留在系统中,直至显示地删除它,可是留在FIFO中的数据却在此时所有被删除了。
XSI IPC的另外一问题:这些IPC结构在文件系统中没有名字,不得不增长新的命令ipcs和ipcrm。
由于这些XSI IPC不使用文件描述符,因此不能对它们使用多路转接I/O函数:select和poll。这使得难于一次使用多个IPC结构,以及文件或设备I/O中使 用IPC结构。例如,没有某种形式的忙-等待循环,就不能使一个服务器进程等待将要放在两个消息队列任一个中的消息。oracle

4 1/65553的陷阱
刚才提到,系统创建IPC通信(如消息队列、共享内存时)必须指定一个ID值。一般状况下,该id值经过ftok函数获得。好比: if (-1 == (conf.shm_key = ftok(conf.chnl_shm_path, 'l')));
看一下ftok函数
* 原型: key_t ftok( char * fname, int id )
* 头文件: <sys/ipc.h>
* 返回值: 成功则返回key, 出错则返回(key_t)-1.
* 参数: fname参数必须引用一个现存文件. fname就时你指定的文件名,id是指定的值。
Keys:
1)pathname必定要在系统中存在而且进程可以访问的
3)proj_id是一个1-255之间的一个整数值,典型的值是一个ASCII值。
一切看上去都是那么合理。app

在通常的UNIX实现中,是将文件的索引节点号取出,前面加上子序号获得key_t的返回值。
让咱们在看下ftok代码到底如何获取key的呢,果真很撮ide

// ftok库实现为
// key_t ftok(const char* path, int project_id)
// {
// struct stat st;
// if ( lstat(path, &st) < 0 )
// return -1;
// return (key_t)( (st.st_ino & 0xffff) | ((st.st_dev & 0xff) << 16) | ((id & 255) << 24) );
// }
st.st_ino 是inode节点
st.st_dev是文件系统设备号
id是程序指定的0-255值
ftok调用返回的整数IPC键由proj_id的低序8位,st_dev成员的低序8位,st_info的低序16位组合而成。函数

可就会出现如下的风险:
当project_id相同时, 及时inode不一样,可是只要inode的低序16相同时,就会映射到同一个key中,而若是恰巧这个key中也有IPC访问权限,那么这会致使程序可能访问了本不该该访问的key,即访问了本不应通讯的区域获取错误的信息,那么这种事情发生的几率是多少呢?答案是1/65535如何得来?低16bit最多能够表示65536个数。因此65537个文件里面必定是有两个文件的inode号的最低两位相同。 请读者朋友们想想是否是酱紫的.源码分析

这么巧的事情真的会发生么?答案很悲剧的是的,这个世界就是无巧不成书的,这种状况在复杂的线上系统上会无情的发生的。测试

5 血案实例
背景:
1.线上A程序须要和同时和B,C两个程序经过两段不一样的共享内存进行IPC通讯
2.BC之间没有关系,但B和A,C和A之间发生关系:B,C须要写各自的内存,A去读,从中获取以便进行进行后续处理;
3.B和C和A的通讯机制彻底一致,区别仅仅在于共享内存指向的路径不一样,因此是用的一段代码不一样配置项

结果:
11台机器部署彻底一致的程序,可是只有一台机器上观察共享内存处理逻辑是混乱滴,而且在线下死活是不能复现滴,
缘由:
仔细观察了B,C的共享内存分别指向路径:path_B,path_c,没错,灰长正常,可是再定睛看他们的inode节点,以下:
31866881 --> 1E64001
32260097 ---> 1EC4001
发现了什么: 天杀的低16位彻底相同4001,只有高位不一样,回忆下,返回的整数IPC键由proj_id的低序8位,st_dev成员的低序8位,st_info的低序16位组合而成, proj_id(相同代码,因此指定的id相同),设备号相同的状况下,inode的低16位又相同,因而B,C同时写到了一块共享内存中,疯掉了,这是 1/65536的几率,真实的中奖了。


6 解决方法的思考
那么这种陷阱有没有办法避免呢?在了解了以上的原理后,相信聪明的读者已经想到了解决方法.
1.最直接的方法:改project_id,可是这样就须要升级程序,又要从新的回归,测试,劳民伤财
2.本身写一个代替ftok的,保证不冲突的,缺点也同1
3.在共享内存中加一个标识,B,C只认属于本身的标识,缺点同上
4.最后来个最简单的吧,诸如共享内存之类的方式,部署后借助系统命令查看,发现不对,当即重启撒
经过ipcs查看
$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x6c03806a 32769 work 666 33554016 1

nattch字段就表示,链接在关联的共享内存段的进程数。经过这个来判断下,是否符合预期,像刚刚描述的血案,很容易经过实际创建的共享内存数量和natch字段这个观察出异常.

7 其余tips

1.取得ipc信息:
ipcs [-m|-q|-s]
-m 输出有关共享内存(shared memory)的信息
-q 输出有关信息队列(message queue)的信息
-s 输出有关“遮断器”(semaphore)的信息
# ipcs -m
IPC status from <running system> as of 2007年04月10日 星期二 18时32分18秒 CST
T ID KEY MODE OWNER GROUP
Shared Memory:
m 0 0x50000d43 --rw-r--r-- root root
m 501 0x1e90c97c --rw-r----- oracle dba
#ipcs |grep oracle|awk '{print $2}
501

2.删除ipc(清除共享内存信息)
ipcrm -m|-q|-s shm_id
%ipcrm -m 501
for i in `ipcs |grep oracle|awk '{print $2}'`
do
ipcrm -m $i
ipcrm -s $i
done

ps -ef|egrep "ora_|asm_"|grep -v grep |grep -v crs|awk '{print $2}' |xargs kill -9
helgrind死锁,或者由于线程问题致使valgrind崩溃的状况。
还有不少其余的经验,在你们的使用过程当中将会继续发现的,咱们也会持续更新这个列表,让你们有所参考。


8 结束语
在Linux环境中进行IPC测试是一件颇有挑战性的事情,由于不少问题每每不容易设计case进行覆盖,每每只能通原理层面分析和经验积累来发现隐患;从这个角度而言,笔者给咱们提供了一些经验, 帮助咱们更快更好地发现问题;而另外一方面,因为IPC也是程序中的易错点,因此咱们QA须要对这方面使用得更加熟练、了解得更加透彻,才能更好地发现隐藏在代码和实现细节中的问题。欢迎同窗们就文章中的内容与我进一步交流,谢谢!

(做者:qijiayin、liuchao)
 

相关文章
相关标签/搜索