进程间通信
每个进程各有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到。
所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信 (IPC,InterProcess Communication)
如图:
1.进程间的通信方式
- 无名管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
- 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
- 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
管道(pipe)
管道是一种最基本的IPC 机制,由pipe 函数创建:
#include <unistd.h>
int pipe(int filedes[2]);
调用pipe 函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0] 指向管道的读端,filedes[1] 指向管道的写端(很好记,就像0是标准输入,1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。pipe 函数调用成功返回0, 调用失败返回-1 。
开辟了管道之后如何实现两个进程间的通信呢?如图可以按下列的步骤通信。
<1. 父进程调pipe 开辟管道,得到两个文件描述符指向管道的两端。
<2. 父进程调fork 创建子进程,那么该进程也有两个文件描述符指向同一管道。
<3. 子进程关闭管道读端,父进程关闭管道写端。子进程可以往管道写,父进程可以从管道读,管道是环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
1. 匿名管道:
#include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> int main() { int _pipe[2]; int ret = pipe(_pipe); if(ret == -1){ printf("create pipe error! errno code is : %d\n",errno); return 1; } pid_t id = fork(); if( id < 0 ){ printf("fork error!"); return 2; }else if( id == 0 ){ //child close(_pipe[0]); int i =0; char *_mesg_c=NULL; while(i<100){ _mesg_c="i am child!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); sleep(1); i++; } }else{ //father close(_pipe[1]); char _mesg[100]; int j = 0; while(j<100){ memset(_mesg, ‘\0‘, sizeof(_mesg)); read(_pipe[0], _mesg, sizeof(_mesg)); printf("%s\n",_mesg); j++; } } return 0; }
使用管道有些限制:
两个进程通过一个管道只能实现单向通信。如上的例子,子进程写父进程读,如果有时也需要父进程写给子进程读,则需要开辟一条新的管道。
思考,如果只开一个管道,但是子进程不关闭读端,父进程也不关闭写端,双都有读端和写端,为什么不能实现双向通信?
解答(个人意见):管道数据是面向字节流的环形结构(),父进程只需从管道读端读数据,子进程只从管道写端写数据,这样就保证了无论是读、写数据的完整性 和有序性。假设所有端口开启,父、子进程同时读写数据,那么管道中的信息就会参杂在一块,读出的数据必定不是完整的。有序的。这便违背了通信的初衷,因此不能这样。管道的单向通信,既是优点(简单,高效)也是缺点(无法实现相互通信)
管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那继承管道文件描述符。上面的例子是父进程把文件描述符传给子进程之后父、子进程之间通信,也可以父进程fork 两次,把文件描述符传给两个子进程,然后两个子进程之间通信, 总之需要通过fork 传递文件描述符使两个进程都能访问同一管道,它们才能通信。 也就是说,管道通信是需要进程之间有关系。
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O 操作,没有设置O_NONBLOCK标志):
<1. 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0), 仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read 会返回0,就像读到文件末尾一样。
代码如下:
#include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/wait.h> int main() { int _pipe[2]; int ret = pipe(_pipe); if(ret == -1){ printf("create pipe error! errno code is : %d\n",errno); return 1; } pid_t id = fork(); if( id < 0 ){ printf("fork error!"); return 2; }else if( id == 0 ){ //child close(_pipe[0]); int i =0; char *_mesg_c=NULL; while(i<10){ _mesg_c="i am child!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); sleep(1); i++; } close(_pipe[1]); }else{ //father close(_pipe[1]); char _mesg[100]; int j = 0; while(j<100){ memset(_mesg, ‘\0‘, sizeof(_mesg)); int ret = read(_pipe[0], _mesg, sizeof(_mesg)); printf("%s : code is : %d\n",_mesg, ret); j++; } if ( waitpid(id, NULL, 0)< 0) { return 3; } } return 0; }
<2.如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0), 持有管道写 端的 进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read 会阻塞,直到管道中有数据可读了才读取数据并返回。
代码如下:
#include <unistd.h> #include <errno.h> #include <string.h> #include <sys/wait.h> int main() { int _pipe[2]; int ret = pipe(_pipe); if(ret == -1){ printf("create pipe error! errno code is : %d\n",errno); return 1; } pid_t id = fork(); if( id < 0 ){ printf("fork error!"); return 2; }else if( id == 0 ){ //child close(_pipe[0]); int i =0; char *_mesg_c=NULL; while(i<20){ if( i < 10 ){ _mesg_c="i am child!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); } sleep(1); i++; } close(_pipe[1]); }else{ //father close(_pipe[1]); char _mesg[100]; int j = 0; while(j<20){ memset(_mesg, ‘\0‘, sizeof(_mesg)); int ret = read(_pipe[0], _mesg, sizeof(_mesg)); printf("%s : code is : %d\n",_mesg, ret); j++; } if ( waitpid(id, NULL, 0)< 0) { return 3; } } return 0; }
<3. 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0), 这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
#include <stdio.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/wait.h> int main() { int _pipe[2]; int ret = pipe(_pipe); if(ret == -1){ printf("create pipe error! errno code is : %d\n",errno); return 1; } pid_t id = fork(); if( id < 0 ){ printf("fork error!"); return 2; }else if( id == 0 ){ //child close(_pipe[0]); int i =0; char *_mesg_c=NULL; while(i<20){ if( i < 10 ){ _mesg_c="i am child!"; write(_pipe[1], _mesg_c, strlen(_mesg_c)+1); } sleep(1); i++; } }else{ //father close(_pipe[1]); char _mesg[100]; int j = 0; while(j<3){ memset(_mesg, ‘\0‘, sizeof(_mesg)); int ret = read(_pipe[0], _mesg, sizeof(_mesg)); printf("%s : code is : %d\n",_mesg, ret); j++; } close(_pipe[0]); sleep(10); if ( waitpid(id, NULL, 0)< 0) { return 3; } } return 0; }
<4. 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。(代码类似上)
2.命名管道 (FIFO)
<1.概念
管道的一个不足之处是没有名字,因此,只能用于具有亲缘关系的进程间通信,在命名管道(named pipe 或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。命名管道是一个设备文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。值得注意的是,FIFO(first input first output)总是按照先进先出的原则操作(保留匿名管道的特性),第一个被写入的数据将先从管道中读出。
<2.命名管道的创建与读写
Linux 下有两种方式创建命名管道。一是在Shell 下交互地建一个命名管道,二是在程序中使用系统函数建命名管道。Shell 方式下可使用mknod 或mkfifo命令,下面命令使 mknod 创建了一个命名管道:
mknod namedpipe
创建命名管道的系统函数有两个:mknod 和mkfifo。两个函数均定义在头件sys/stat.h ,
函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char *path,mode_t mod,dev_t dev);
int mkfifo(const char *path,mode_t mode);
函数mknod 参数中path 为创建的命名管道的全路径名;mod 为创建的命名管道的模式,指明其存取权限;dev为设备值,该值取决于件创建的种类,它只在创建设备件时才会用到。这两个函数调用成功都返回0,失败都返回-1。下面使用 mknod 函数创建了个命名管道:
umask(0);
if (mknod("/tmp/fifo",S_IFIFO | 0666) == -1)
{
perror("mkfifo error");
exit(1);
}
函数mkfifo前两个参数的含义和mknod 相同。下面是使用 mkfifo的代例代码:
umask(0);
if (mkfifo("/tmp/fifo",S_IFIFO|0666) == -1)
{
perror("mkfifo error!");
exit(1);
}
“S_IFIFO|0666”指明创建一个命名管道且存取权限为0666,即创建者、与创建者同组的用户、其他用户对该命名管道的访问权限都是可读可写(这里要注意umask对生成的
管道文件权限的影响)。
命名管道创建后就可以使用了,命名管道和管道的使用方法基本是相同的。只是使用命名管道时,必须先调open()将其打开。因为命名管道是一个存在于硬盘上的文件,匿名管道是存在于内存中的特殊文件。
需要注意的是,调用open()打开命名管道的进程可能会被阻塞。但如果同时用读、写方式(O_RDWR)打开,则一定不会导致阻塞;如果以只读方式(O_RDONLY)打开,则调open()函数的进程将会被阻塞,直到有写方式打开管道;同样只以写方(O_WRONLY)打开也会阻塞直到有读方式打开管道。
<3.总结
文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的路径名来标识一个IPC 通道。
命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。
由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使。
mknod mkfifo这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,
filename指定了文件名,mode则指定了文件的读写权限。mknod 是较老的函数。 使用mkfifo函数更加简单和规范,所以建议在可能的情况下,尽量使用mkfifo不是mknod 。
mkfifo函数的作用是在文件系统中创建一个文件,该文件用于提供FIFO功能,即命名管道。
前边讲的那些管道都没有名字,因此它们被称为匿名管道,或简称管道。对文件系统来说,匿名管道是不可见的,它的作用仅限于在父进程和子进程两个进程间进行通信。
命名管道是个可见的文件,因此,它可以用于任何两个进程之间的通信,不管这两个进程是不是父子进程,也不管这两个进程之间有没有关系。
程序实例:
read端:
int main() { int fd = open(_PATH_, O_RDONLY); if(fd < 0){ printf("open file error!\n"); return 1; } char buf[_SIZE_]; memset(buf, ‘\0‘, sizeof(buf)); while(1){ int ret = read(fd, buf, sizeof(buf)); if (ret <= 0)//error or end of file { printf("read end or error!\n"); break; } printf("%s\n", buf); if( strncmp(buf, "quit", 4) == 0 ){ break; } } close(fd); return 0; }
write 端:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define _PATH_ "/tmp/file.tmp" #define _SIZE_ 100 int main() { int ret = mkfifo(_PATH_,0666|S_IFIFO); if(ret == -1){ printf("mkfifo error\n"); return 1; } int fd = open(_PATH_, O_WRONLY); if(fd < 0){ printf("open error\n"); } char buf[_SIZE_]; memset(buf, ‘\0‘, sizeof(buf)); while(1){ scanf("%s", buf); int ret = write(fd, buf, strlen(buf)+1); if(ret < 0){ printf("write error\n"); break; } if( strncmp(buf, "quit", 4) == 0 ){ break; } } close(fd); return 0; }
XSI IPC(消息队列,信号量,共享内存)
简介:
XSI IPC 包括消息队列、信号量以及共享内存,他们都依托标识符和键来实现的,这就像是管道靠文件描述符来实现一样
IPC 主题一:消息队列
一、什么是消息队列
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类 型值。我们可以通过发送消息来避免命名管道的同步和阻塞问题。消息队列与 管道不同的是,消息队列是基于消息的,管道是基于字节流的,且消息队列的 读取不一定是先进先出。消息队列与 命名管道有一样的不足,就是每个消 息的最大长度是有上限的(MSGMAX), 每个消息队列的总的字节数是有上 限的(MSGMNB),系统上消息队列的总数 也有一个上限(MSGMNI)。
二、IPC 对象数据结构
内核为每个IPC 对象维护个数据结构(/usr/include/linux/ipc.h)
struct ipc_perm {
key_t __key; /* Key supplied to xxxget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
消息队列,共享内存和信号量都有这样一个共同的数据结构。
三、消息队列结构(/usr/include/linux/msg.h )
可以看到第一个条目就是IPC 结构体,即是共有的,后面的都是消息队列所私有的成员。
消息队列是用链表实现的。
四、函数
1. 创建新消息队列或取得已存在消息队列
原型:int msgget(key_t key, int msgflg);
参数:
key:可以认为是一个端口号,也可以由函数ftok 生成。
msgflg:
IPC_CREAT: 如果IPC 不存在,则创建一个IPC 资源,否则
打开操作。
IPC_EXCL :只有在共享内存不存在的时候,新的共享内存
才建立,否则就产生错误。
注意:如果单独使IPC_CREAT ,msgget() 函数要么返回一个已经 存在的共享内存的操作符,要么返回一个新建的共享内存的标识符。
如果将IPC_CREAT 和IPC_EXCL 标志一起使用,msgget() 将返 回一个新建的IPC 标识符;如果该IPC 资源已存在,或者返回-1 。
IPC_EXEL标志本并没有太大的意义,但是和IPC_CREAT 标志一
起使用可以用来保证所得的对象是新建的,不是打开已有的对象。
2. 向队列读/写消息
原型:
msgrcv 从队列中取出消息:ssize_t msgrcv(int msqid, void
*msgp,size_t msgsz, long msgtyp, int msgflg);
msgsnd 将数据放到消息队列中:int msgsnd(int msqid, const
void *msgp, size_t msgsz, int msgflg);
参数:
msqid:消息队列的标识码
msgp:指向消息缓冲区的指针,此位置用来暂时存储发送和接收的消 息,是一个用户可定义的通用结构体,形态如下:
struct msgstru{
long mtype; //大于0
char mtext[ 户指定定大小];
};
msgsz:消息的大小。
msgtyp:从消息队列内读取的消息形态。如果值为零,则表示消息队 列中的所有消息都会被读取。
msgflg:来指明核芯程序在队列没有数据的情况下所应采取的行动。 如果msgflg和常数IPC_NOWAIT合,则在msgsnd()执行时若是消息 队列已满,则msgsnd()将不会阻塞,而会立即返回-1 ,如果执 的是msgrcv(),则在消息队列呈空时,不做等待马上返回-1 ,并 设定错误码为ENOMSG。当msgflg为 0时,msgsnd()及msgrcv()在 队列呈满或呈空的情形时,采取阻塞等待的处理模式。
3. 设置消息队列属性
原型:int msgctl ( int msgqid, int cmd,
struct msqid_ds *buf );
参数:msgctl :系统调用对 msgqid 标识的消息队列执cmd 操作, 系统定义了 3 种 cmd 操作: IPC_STAT , IPC_SET , IPC_RMID IPC_STAT : 该命令用来获取消息队列对应的 msqid_ds数据结 构,并将其保存到 buf 指定的地址空间。
IPC_SET : 该命令用来设置消息队列的属性,要设置的属性存储 在buf中。
IPC_RMID : 从内核中删除 msqid 标识的消息队列。
五. key_t键
System V IPC 使用 key_t值作为它们的名字,在Redhat linux(后续验 证默认都在该平台下)下key_t被定义为int类型,追溯如下:
/usr/include/sys/ipc.h
#ifndef __key_t_defined
typedef __key_t key_t;
#define __key_t_defined
#endif
/usr/include/bits/types.h
typedef __DADDR_T_TYPE __daddr_t; /* The type of a disk address. */
typedef __SWBLK_T_TYPE __swblk_t; /* Type of a swap block maybe? */
typedef __KEY_T_TYPE __key_t; /*type of an ipc key*/
/usr/include/bits/typesizes.h
#define __KEY_T_TYPE __S32_TYPE
/usr/include/bits/types.h
#define __S32_TYPE int
ftok 函数
函数ftok 把一个已存在的路径名和一个整数标识得转换成一个key_t值,称为IPC 键:
# include <sys/types.h>
# include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
DESCRIPTION
The ftok function uses the identity of the file named by the given pathname (which must refer to an existing, accessible file) and the least significant 8
bits of proj_id (which must be nonzero) to generate a key_t type System V IPC key 。
//该函数把从pathname导出的信息与id 的低序8位组合成一个整数IPC 键。
ftok 的典型实现调用stat 函数,然后组合以下三个值:
1.pathname 所在的文件系统的信息(stat 结构的st_dev 成员)
2.该文件在本文件系统内的索引节点号(stat 结构的st_ino 成员)
3. proj_id 的低序8位(不能为0)
从程序运行的结果可以看出,ftok 调用返回的整数IPC 键由proj_id 的 低序8位,st_dev 成员的低序8位,st_info 的低序16位组合而成。
//不能保证两个不同的路径名与同一个proj_id 的组合产生不同的键,因为 上面所列三个条目(文件系统标识符、索引节点、proj_id)中的信息位数可 能用于一个整数的信息位数。
思考:为何要有key_t?
解释(个人见解):如果没有key,n个进程仅通过 FILEPATH 访问消息队列
,假设这些进程;两两通信,则每次写入数据和读取数据的位置是不确定的,即每次通过路径访问则很难定位到某一条消息,因此才引入key。下图是个人猜想的key的作用。
代码实例:
comm.h
#ifndef __COMM__ #define __COMM__ #include <stdio.h> #include <stdlib.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/types.h> #include <string.h> #include <time.h> #define __MSG_SIZE__ 1024 #define FILEPATH "/tmp/.msg" #define ID 0 extern const int g_ser_send_type;//server extern const int g_cli_send_type;//client typedef struct _msginfo{ long mtype; char mtext[__MSG_SIZE__]; }msginfo; void print_log(char *); #endif
comm.c
#include "comm.h" const int g_ser_send_type=1;//server const int g_cli_send_type=2;//client void print_log(char *msg) { printf("%s[%d] : %s\n", __FUNCTION__,__LINE__,msg); }
msg_server.h
#ifndef _MSG_SERVER_ #define _MSG_SERVER_ #include"comm.h" int msg_server_start(); int msg_server_end(int); #endif
msg_server.c
#include "msg_server.h" int _msg_id = -1; int msg_server_start() { key_t _key = ftok (FILEPATH,ID);// 创建键值 if( _key < 0 ){ print_log("get key id error"); return 1; } msginfo _ser_info; _msg_id = msgget(_key, IPC_CREAT); // 获取信号队列ID if( _msg_id < 0 ){ print_log("msg_server get key id failed\n"); return 1; } while(1){ // sleep(10); if( msgrcv(_msg_id, &_ser_info, sizeof(_ser_info) , g_cli_send_type, 0) == -1 ){ print_log("msg rcv error"); return 1; } printf("client :> %s\n",_ser_info.mtext); printf("server :>"); memset(_ser_info.mtext, ‘\0‘, sizeof(_ser_info.mtext)); fgets(_ser_info.mtext, __MSG_SIZE__, stdin); if(strncasecmp(_ser_info.mtext, "quit", 4) == 0){ printf("server bye!\n"); break; } _ser_info.mtype = g_ser_send_type; if( msgsnd(_msg_id, &_ser_info, __MSG_SIZE__, 0) == -1 ){ printf("server send msg error\n"); exit(0); } } return 0; } int msg_server_end(int id) { if( msgctl(id, IPC_RMID, NULL) == -1){ printf("delete msg kernel info error\n"); return 1; } return 0; } static void delete_msg(void) { if( _msg_id != -1 ){ msg_server_end(_msg_id); } printf("delete msg queue end\n"); } int main(int argc, char *argv[]) { atexit(delete_msg); if(msg_server_start() == 0){ print_log("msg_server start success\n"); }else{ print_log("msg_server start failed\n"); } return 0; }
msg_client.h
msg_client.h #ifndef _MSG_CLIENT_ #define _MSG_CLIENT_ #include"comm.h" int msg_client_start(); int msg_client_end(int); #endif
msg_client.c
#include "msg_client.h" int _msg_id = -1; int msg_client_start() { key_t _key = ftok(FILEPATH,ID);// 创建键值 if( _key < 0 ){ print_log("client get key id error"); return 1; } msginfo _cli_info; _msg_id = msgget(_key, 0); //获取信号队列ID if( _msg_id < 0 ){ print_log("msg_server get key id failed\n"); return 1; } while(1){ printf("client :>"); fgets(_cli_info.mtext, sizeof(_cli_info.mtext),stdin); if(strncasecmp(_cli_info.mtext, "quit", 4) == 0){ printf("client bye!\n"); break; } _cli_info.mtype = g_cli_send_type; if( msgsnd(_msg_id, &_cli_info,sizeof(_cli_info) , 0) == -1 ){ printf("client send msg error\n"); exit(0); } memset(_cli_info.mtext, ‘\0‘, sizeof(_cli_info.mtext)); if( msgrcv(_msg_id, &_cli_info, __MSG_SIZE__, g_ser_send_type, 0) == -1 ){ print_log("client recive msg error"); return 1; } printf("server :>%s\n",_cli_info.mtext); } return 0; } int msg_client_end(int id) { if(msgctl(id, IPC_RMID, NULL) == -1){ return 1; } return 0; } static void delete_msg(void) { if( _msg_id != -1 ){ msg_client_end(_msg_id); } printf("delete msg queue end\n"); } int main(int argc, char *argv[]) { atexit(delete_msg); if(msg_client_start() == 0){ print_log("msg_server start success\n"); }else{ print_log("msg_server start failed\n"); } return 0; }