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

来源:互联网 发布:360网络攻防实验室 编辑:程序博客网 时间:2024/05/03 10:55

文章出处:http://qa.baidu.com/blog/?p=48


【摘要】在本文中,作者剖析了IPC通信机制,通过对系统函数源码分析,指出了其中存在的1/65535几率可能出现的隐患,并结合实际的案例给出了解决方案。在本文中,我们可以了解到针对IPC通信常见问题。在我们之后的测试工作中,可以有选择的针对这些注意事项和易错点设计测试case,让bug无处藏身。由于作者能力有限,文中如果有一些不够清晰不够全面的地方,欢迎指正。

【关键词】IPC,共享内存


1  IPC通信概述

    IPC(InterProcess Communication)进程间通信,通常在同一台主机各个进程间的IPC主要有:管道,FIFO,消息队列,信号量,以及共享内存,而不同主机上各个进程间IPC通信可以通过套接字和stream。

(1)管道(pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。

(2)命令管道(named pipe):命名管道克服了管道没有名字的限制,因此,除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。

(3)信号(signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数signal外,还支持语义符合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: 消息队列,信号量以及共享内存

    由于篇幅关系,这里着重讨论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_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  不同通信方式大比拼

    XSI IPC的主要问题是:IPC结构是在系统范围内起作用,没有访问计数。例如:如果进程创建了一个消息队列,在该队列中放入了几则消息,然后终止,但是该消息队列及其内容不会被删除。它们留在系统中直至出现下述情况:由某个进程调用msgrcv或msgctl读消息或删除消息队列;或某个进程执行ipcrm命令删除该消息队列;或是由正在启动的系统删除消息队列。与管道相比,当最后一个访问管道的进程终止时,管道就被完全删除了。对于FIFO而言,当最后一个引用FIFO的进程终止时,其名字仍保留在系统中,直至显示地删除它,但是留在FIFO中的数据却在此时全部被删除了。

    XSI IPC的另一问题:这些IPC结构在文件系统中没有名字,不得不增加新的命令ipcs和ipcrm。

    因为这些XSI IPC不使用文件描述符,所以不能对它们使用多路转接I/O函数:select 和poll。这使得难于一次使用多个IPC结构,以及文件或设备I/O中使用IPC结构。例如,没有某种形式的忙-等待循环,就不能使一个服务器进程等待将要放在两个消息队列任一个中的消息。


4  1/65535的陷阱

    刚才提到,系统建立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一定要在系统中存在并且进程能够访问的;

2)proj_id是一个1-255之间的整数值,典型的值是一个ASCII值。

一切看上去都是那么合理。


在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号可到key_t的返回值。

让我们看一下ftok代码到底如何何如key的呢,果然很撮

// 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_ino的低序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/65535的概率,真实的中奖了。


6 解决方法的思考

    那么这种陷阱有没有办法避免呢?在了解了以上原理之后,相信聪明的读者已经想到了解决方法:

1.  最直接的方法是改proj_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这个字段就表示,连接在关联的共享内存段的进程数。通过这个来判断下,是否符合预期,像刚刚描述的血案,很容易通过实际建立的共享内存数量和nattach字段观察出异常。


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需要对这方面使用得更加熟练、了解得更加透彻,才能更好地发现隐藏在代码和实现细节中的问题。欢迎同学们就文章中的内容与我进一步交流,谢谢!


 

0 0
原创粉丝点击