进程间通信IPC

时间:2022-02-26 19:05:40

深刻理解Linux进程间通信(IPC)

一个大型的应用系统,往往需要众多进程协作,进程(Linux进程概念见附1)间通信的重要性显而易见。本系列文章阐述了Linux环境下的几种主要进程间通信手段,并针对每个通信手段关键技术环节给出详细实例。为达到阐明问题的目的,本文还对某些通信手段的内部实现机制进行了分析。

linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示:

进程间通信IPC

其中,最初Unix IPC包括:管道、FIFO、信号;System V IPC包括:System V消息队列、System V信号灯、System V共享内存区;Posix IPC包括: Posix消息队列、Posix信号灯、Posix共享内存区。有两点需要简单说明一下:1)由于Unix版本的多样性,电子电气工程协会(IEEE)开发了一个独立的Unix标准,这个新的ANSI Unix标准被称为计算机环境的可移植性操作系统界面(PSOIX)。现有大部分Unix和流行版本都是遵循POSIX标准的,而Linux从一开始就遵循POSIX标准;2)BSD并不是没有涉足单机内的进程间通信(socket本身就可以用于单机内的进程间通信)。事实上,很多Unix版本的单机IPC留有BSD的痕迹,如4.4BSD支持的匿名内存映射、4.3+BSD对可靠信号语义的实现等等。

图一给出了linux 所支持的各种IPC手段,在本文接下来的讨论中,为了避免概念上的混淆,在尽可能少提及Unix的各个版本的情况下,所有问题的讨论最终都会归结到Linux环境下的进程间通信上来。并且,对于Linux所支持通信手段的不同实现版本(如对于共享内存来说,有Posix共享内存区以及System V共享内存区两个实现版本),将主要介绍Posix API。

linux下进程间通信的几种主要手段简介:

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

下面将对上述通信机制做具体阐述。

附1:参考文献[2]中对linux环境下的进程进行了概括说明:

一般来说,linux下的进程包含以下几个关键要素:

  • 有一段可执行程序;
  • 有专用的系统堆栈空间;
  • 内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;
  • 具有独立的存储空间

进程和线程有时候并不完全区分,而往往根据上下文理解其含义。

原文地址:http://www.ibm.com/developerworks/cn/linux/l-ipc/

 

例1.1 PIPE

//pipe.c

1 #include <sys/wait.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 #include <string.h>
6 #include <assert.h>
7
8 int main(int argc, char* argv[])
9 {
10 assert(argc==2);
11
12 pid_t cpid;
13 int pfd[2];
14 char ch;
15
16 if (pipe(pfd) == -1)
17 {
18 perror("pipe");
19 exit(EXIT_FAILURE);
20 }
21
22 cpid = fork();
23 if (cpid == -1)
24 {
25 perror("fork");
26 exit(EXIT_FAILURE);
27 }
28
29 if (cpid == 0) /* child read from pipe */
30 {
31 close(pfd[1]);
32 while (read(pfd[0], &ch, 1) > 0)
33 write(STDOUT_FILENO, &ch, 1);
34 write(STDOUT_FILENO, "\n", 1);
35 close(pfd[0]);
36 exit(EXIT_SUCCESS);
37 }
38 else /* parent write to pipe */
39 {
40 close(pfd[0]);
41 write(pfd[1], argv[1], strlen(argv[1]));
42 close(pfd[1]);
43 wait(NULL);
44 exit(EXIT_SUCCESS);
45 }
46 exit(0);
47 }

例1.2 FIFO

//mkfifo.c

1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 #include <fcntl.h>
6 #include <string.h>
7
8 #define FIFOFILE "./my_fifo"
9
10 int main(int argc, char* argv[])
11 {
12 int res = mkfifo(FIFOFILE, 0777);
13 if (res < 0)
14 {
15 perror("mkfifo");
16 }
17
18 char buf[] = "write string";
19 int fd = open(FIFOFILE, O_WRONLY);
20 if (write(fd, buf, strlen(buf)) <= 0)
21 {
22 perror("write");
23 }
24 close(FIFOFILE);
25
26 return 0;
27
28 }


//readfifo.c

1 #include <sys/stat.h>
2 #include <fcntl.h>
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6
7 #define FIFOFILE "./my_fifo"
8
9 int main(int argc, char* argv[])
10 {
11 int fd = open(FIFOFILE, O_RDONLY);
12 if (fd == -1)
13 {
14 perror("open fifo error");
15 exit(EXIT_FAILURE);
16 }
17
18 char buf[64];
19 memset(buf, 0, sizeof(buf));
20 if (read(fd, buf, sizeof(buf)) == -1)
21 {
22 perror("read error");
23 }
24
25 printf("%s\n", buf);
26
27 return 0;
28 }

 

例3 msg

//msgrcv.c

1 #include <unistd.h>
2 #include <stdlib.h>
3 #include <stdio.h>
4 #include <string.h>
5 #include <errno.h>
6 #include <sys/msg.h>
7
8 struct msg_st
9 {
10 long int msg_type;
11 char text[BUFSIZ];
12 };
13
14 int main()
15 {
16 int running = 1;
17 int msgid = -1;
18 struct msg_st data;
19 long int msgtype = 0; //注意1
20
21 //建立消息队列
22 char* msgpath = "./my_msgqueue";
23 key_t key = ftok(msgpath, 'a');
24 int gflags = IPC_CREAT | IPC_EXCL;
25 msgid = msgget(key, gflags | 0666);
26 if(msgid == -1)
27 {
28 fprintf(stderr, "msgget failed with error: %d\n", errno);
29 exit(EXIT_FAILURE);
30 }
31 //从队列中获取消息,直到遇到end消息为止
32 while(running)
33 {
34 if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)
35 {
36 fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
37 exit(EXIT_FAILURE);
38 }
39 printf("You wrote: %s\n",data.text);
40 //遇到end结束
41 if(strncmp(data.text, "end", 3) == 0)
42 running = 0;
43 }
44 //删除消息队列
45 if(msgctl(msgid, IPC_RMID, 0) == -1)
46 {
47 fprintf(stderr, "msgctl(IPC_RMID) failed\n");
48 exit(EXIT_FAILURE);

49 }
50 exit(EXIT_SUCCESS);
51 }

 

//msgsnd.c

1 #include <unistd.h>
2 #include <stdlib.h>
3 #include <stdio.h>
4 #include <string.h>
5 #include <sys/msg.h>
6 #include <errno.h>
7
8 #define MAX_TEXT 512
9 struct msg_st
10 {
11 long int msg_type;
12 char text[MAX_TEXT];
13 };
14
15 int main()
16 {
17 int running = 1;
18 struct msg_st data;
19 char buffer[BUFSIZ];
20 int msgid = -1;
21
22 //建立消息队列
23 char* msgpath = "./my_msgqueue";
24 key_t key = ftok(msgpath, 'a');
25 int gflags = IPC_CREAT;
26 msgid = msgget(key, gflags | 0666);
27 if(msgid == -1)
28 {
29 fprintf(stderr, "msgget failed with error: %d\n", errno);
30 exit(EXIT_FAILURE);
31 }
32
33 //向消息队列中写消息,直到写入end
34 while(running)
35 {
36 //输入数据
37 printf("Enter some text: ");
38 fgets(buffer, BUFSIZ, stdin);
39 data.msg_type = 1; //注意2
40 strcpy(data.text, buffer);
41 //向队列发送数据
42 if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)
43 {
44 fprintf(stderr, "msgsnd failed\n");
45 exit(EXIT_FAILURE);
46 }
47 //输入end结束输入
48 if(strncmp(buffer, "end", 3) == 0)

49 running = 0;
50 sleep(1);
51 }
52 exit(EXIT_SUCCESS);
53 }

运行结果如下:进程间通信IPC 进程间通信IPC >>例子分析——消息类型 这里主要说明一下消息类型是怎么一回事,注意msgreceive.c文件main函数中定义的变量msgtype(注释为注意1),它作为msgrcv函数的接收信息类型参数的值,其值为0,表示获取队列中第一个可用的消息。再来看看msgsend.c文件中while循环中的语句data.msg_type = 1(注释为注意2),它用来设置发送的信息的信息类型,即其发送的信息的类型为1。所以程序msgreceive能够接收到程序msgsend发送的信息。 如果把注意1,即msgreceive.c文件main函数中的语句由long int msgtype = 0;改变为long int msgtype = 2;会发生什么情况,msgreceive将不能接收到程序msgsend发送的信息。因为在调用msgrcv函数时,如果msgtype(第四个参数)大于零,则将只获取具有相同消息类型的第一个消息,修改后获取的消息类型为2,而msgsend发送的消息类型为1,所以不能被msgreceive程序接收。重新编译msgreceive.c文件并再次执行,其结果如下: 进程间通信IPC进程间通信IPC 我们可以看到,msgreceive并没有接收到信息和输出,而且当msgsend输入end结束后,msgreceive也没有结束,通过jobs命令我们可以看到它还在后台运行着。 >>消息队列与命名管道的比较 消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。 与命名管道相比,消息队列的优势在于,1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。