Chinaunix首页 | 论坛 | 博客
  • 博客访问: 807546
  • 博文数量: 142
  • 博客积分: 3505
  • 博客等级: 中校
  • 技术积分: 1501
  • 用 户 组: 普通用户
  • 注册时间: 2011-07-30 19:30
文章分类

全部博文(142)

文章存档

2012年(33)

2011年(109)

分类: C/C++

2011-09-21 21:27:06

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

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

(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: 消息队列,信号量以及共享内存


       每由于篇幅关系,在这里着重讨论 XSI IPC,消息队列,信号量以及共享存储器,他们之间有很多共同之处
       一个内核中的IPC结构(消息队列,信号量和共享存储器)都用一个非负整数的标识符(identifier)加以引用。例如,为了对一个消息队列发 送消息或取消息,只需知道其队列标识符。当一个IPC结构被创建,以后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大值,然后又 回到0。
       标识符是IPC对象的内部名。键(key)是IPC对象的外部名。无论何时创建IPC结构(调用msgget,semget或shmget),都应 该指定一个键。键的数据类型是基本系统数据类型key_t,通常在中被定义为长整型。键由内核变成标识符。
       客户进程和服务器进程认同一个路径名和项目ID(项目ID是0~255之间的字符值),接着调用ftok将这两个值变成为一个键。然后用该键创建一个新的IPC结构或得到一个的IPC结构。ftok提供的唯一服务就是由一个路径名和项目ID产生一个键。
       #include
       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结构。例如,没有某种形式的忙-等待循环,就不能使一个服务器进程等待将要放在两个消息队列任一个中的消息。

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 )
           * 头文件:
           * 返回值: 成功则返回key, 出错则返回(key_t)-1.
           * 参数: fname参数必须引用一个现存文件. fname就时你指定的文件名,id是指定的值。
       Keys:
1)pathname一定要在系统中存在并且进程能够访问的
3)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_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 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需要对这方面使用得更加熟练、了解得更加透彻,才能更好地发现隐藏在代码和实现细节中的问题。欢迎同学们就文章中的内容与我进一步交流,谢谢!
阅读(1421) | 评论(2) | 转发(1) |
给主人留下些什么吧!~~

platinaluo2011-09-22 18:02:55

GFree_Wind: 当project_id相同时, 及时inode不同,但是只要inode的低序16相同时,就会映射到同一个key中,而如果恰巧这个key中也有IPC访问权限,那么这会导致程序可能访问了本不.....
Thanks! :)