8.1.6 常用进程间通信的方法
在支持多进程的操作系统里,用户可以创建多个进程,分别处理不同的功能。多进程机制为处理不同的数据带来好处。但是,在实际处理过程中,经常需要在不同的进程之间传递数据。如有两个进程,一个读取不同用户的配置文件并且解析配置文件,另一个进程需要把每个用户的配置发送到远程的服务器,这样的两个进程需要数据的传递,这个时候就会用到进程间通信。
Linux提供了多种进程间通信的方法,常见的包括管道、FIFO、消息队列、信号量、共享存储以及通过socket也可以实现不同进程间的通信。本节将简述管道和共享内存这两种进程间的通信方法。
1.管道
管道是最常用的进程间的通信方法,也是最古老的一种进程间的通信方法。所有的UNIX系统都支持管道。管道的概念比较好理解,如图8-2所示,管道就好像日常的水管一样,在两个进程之间,用来传送数据。与日常生活中的水管不同的是,进程间的管道有两个限制:一个是管道是半双工的,也就是说,一个管道只能在一个方向上传送数据;另一个是管道只能在有共同父进程的进程间使用。通常,管道由一个进程创建,之后进程调用fork()创建新的进程,父进程和子进程之间就可以使用管道通信了。
图8-2 进程间通信机制——管道示意图
从图8-2中可以看出,两个进程间通过管道传递数据的时候是单向的,在同一时刻,进程1只能向进程2写入数据,或者从进程2读出数据。
使用管道的方法很简单,需要通过pipe()函数创建一个管道就可以使用了。pipe()函数的定义如下:
#include <unistd.h>
int pipe(int filedes[2]);
参数fieldes返回两个文件描述符,filedes[0]为读打开,filedes[1]为写打开。当创建管道成功时pipe()返回0,如果创建失败会返回–1。下面的实例演示创建管道后父进程和子进程之间通过管道传递数据。
实例8-5 进程间通过管道通信
1 #include <sys/types.h>
2 #include <unistd.h>
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6
7 int main()
8 {
9 int fd[2];
10 pid_t pid;
11 char buf[64] = "I'm parent process!/n"; // 父进程要写入管道的信息
12 char line[64];
13
14 if (0!=pipe(fd)) { // 创建管道并检查结果
15 fprintf(stderr, "Fail to create pipe!/n");
16 return 0;
17 }
18
19 pid = fork(); // 创建进程
20 if (pid<0) {
21 fprintf(stderr, "Fail to create process!/n");
22 return 0;
23 } else if (0==pid) { // 父进程
24 close(fd[0]); // 关闭读管道,使得父进程只能向管道写入数据
25 write(fd[1], buf, strlen(buf)); // 写数据到管道
26 close(fd[1]); // 关闭写管道
27 } else { // 子进程
28 close(fd[1]); // 关闭写管道,使得子进程只能从管道读取数据
29 read(fd[0], line, 64); // 从管道读取数据
30 printf("DATA From Parent: %s", line);
31 close(fd[0]); // 关闭读管道
32 }
33
34 return 0;
35 }
36
程序给出了一个操作管道的例子,由于管道的单向特性,在同一时刻,管道的数据只能单方向流动,所以在程序的第24行,父进程关闭了读管道;程序的第28行,子进程关闭了写管道,这样,管道变成了一个从父进程到子进程单向传递数据的通道。程序的运行结果验证了这个特性如下:
DATA From Parent: I'm parent process!
子进程打印出了父进程通过管道发送来的字符串。管道的操作比较简单,虽然有一定的局限性,但是对于父子进程间传递数据还是很方便的,因此有广泛的应用。还有一种称做有名管道的通信机制,突破了传统管道的限制,可以在不同的进程间传递数据,有兴趣的读者可以通过其他资料或者Linux的在线手册man了解一下。
2.共享内存
共享内存是在内存中开辟一段空间,供不同的进程访问。与管道相比,共享内存不仅能在多个不同进程(非父子进程)间共享数据,而且可以比管道传送更大量的数据。图8-3展示了共享内存的结构。
图8-3 共享内存示意图
从图8-3可以看出,进程1和进程2可以访问一块共同的内存区域,并且在同一时刻可以读写共享的内存区域。
共享内存在使用之前需要先创建,之后获得共享内存的入口地址就可以对共享内存操作了。如果不需要使用共享内存的时候,还可以在程序中分离共享内存。Linux为操作共享内存提供了几个函数如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
shmget()函数用来创建共享内存,参数key是由ftok()函数生成的一个系统唯一的关键字,用来在系统中标示一块内存,size参数制定需要的共享内存字节数,shmflg参数是内存的操作方式,有读或者写两种。如果成功创建共享内存,函数会返回一个共享内存的ID。
shmat()函数是获得一个共享内存ID对应的内存起始地址;参数shmid是共享内存ID,shmaddr参数指定了共享内存的地址,如果参数值为0,表示需要让系统决定共享内存地址,如果获取内存地址成功,则函数返回对应的共享内存地址。
shmdt()函数从程序中分离一块共享内存。参数shmaddr标识了要分离的共享内存地址。
实例8-6给出两个文件:shm_write.c文件的代码创建共享内存,之后向共享内存写入一个字符串;shm_read.c文件的代码获得已经创建的共享内存,打印共享内存中的数据。
实例8-6 共享内存操作实例
写共享内存操作代码如下:
1 // shm_write.c --> gcc -o w shm_write.c
2 #include <sys/ipc.h>
3 #include <sys/shm.h>
4 #include <sys/types.h>
5 #include <unistd.h>
6
7 int main()
8 {
9 int shmid; // 定义共享内存ID
10 char *ptr;
11 char *shm_str = "string in a share memory";
12
13 shmid = shmget(0x90, 1024, SHM_W|SHM_R|IPC_CREAT|IPC_EXCL);
// 创建共享内存
14 if (-1==shmid)
15 perror("create share memory");
16
17 ptr = (char*)shmat(shmid, 0, 0); // 通过共享内存ID获得共享内存地址
18 if ((void*)-1==ptr)
19 perror("get share memory");
20
21 strcpy(ptr, shm_str); // 把字符串写入共享内存
22 shmdt(ptr);
23
24 return 0;
25 }
26
读共享内存操作代码如下:
1 // shm_read.c --> gcc -o r shm_read.c
2 #include <sys/ipc.h>
3 #include <sys/shm.h>
4 #include <sys/types.h>
5 #include <unistd.h>
6
7 int main()
8 {
9 int shmid; // 定义共享内存ID
10 char *ptr;
11
12 shmid = shmget(0x90, 1024, SHM_W|SHM_R|IPC_EXCL);
// 根据key获得共享内存ID
13 if (-1==shmid)
14 perror("create share memory");
15
16 ptr = shmat(shmid, 0, 0); // 通过共享内存ID获得共享内存地址
17 if ((void*)-1==ptr)
18 perror("get share memory");
19
20 printf("string in share memory: %s/n", ptr);
// 打印共享内存中的内容
21
22 shmdt(ptr);
23 return 0;
24 }
25
实例中代码没有使用ftok()函数创建共享内存使用的key,因为只要保证共享内存的key是系统中唯一的即可,如果系统没有很多的共享内存程序,指定一个key即可,需要注意的是两个程序需要使用相同的key才能访问到相同的共享内存。
shm_write.c的第13行是创建一块1024字节大小的共享内存,指定了共享内存可以读写。第17行获得共享内存的地址,之后检查共享内存地址是否合法,shmat()函数返回的地址–1表示地址不合法,而不是NULL。第21行写入一个字符串到共享内存,随后用shmdt()函数断开和共享内存的连接。
shm_read.c第12行通过key值得到已经创建好的共享内存地址,和shm_write.c第13行不同是的,没有指定IPC_CREAT属性,因为这里只是得到共享内存ID。第16行获取共享内存地址,如果地址是合法的,则在第20行打印共享内存的内容,最后调用shmdt()函数断开共享内存连接。
两个文件按照注释中的方法编译,运行w程序,如果创建共享内存成功,则没有任何提示。执行r程序,获得共享内存,打印共享内存内容,程序运行结果如下:
string in share memory: string in a share memory
程序打印出了共享内存的内容。细心的读者可能会发现,当再一次运行程序w的时候,会提示:
create share memory: File exists
get share memory: Invalid argument
段错误
出错的原因是已经有相同key值的共享内存了,实例中的两个程序退出的时候都没有删除共享,导致出错,使用命令ipcs可以查看目前的共享资源情况:
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000090 360450 gonglei 600 1024 0
------ Semaphore Arrays --------
key semid owner perms nsems
------ Message Queues --------
key msqid owner perms used-bytes messages
命令输出结果有3部分,分别打印出了共享内存、信号量和消息队列的使用情况。在本例中,系统只创建了一个共享内存,并且没有释放,使用命令ipcrm可以释放指定的共享内存:
ipcrm -m 360450
程序没有提示表示删除共享内存成功,再次运行ipcs命令查看,共享内存已经被删除。