进程间通信
不同进程有不同的用户地址空间,进程之间的数据都是相互独立的,即使是父子进程,也就是说在父进程中的全局变量,即使在子进程中对其进行了修改,也不会影响到父进程中该变量的值,两个进程的地址空间不同。
进程之间如需交换数据,必须通过内核。在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)
管道
管道是个只存在于内存中的文件,对这个文件的操作要通过两个已经打开的文件进行,它们分别代表管道的两端。管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,有其自己的数据结构。
管道分为无名管道和有名管道,分别进行解析。
一、无名管道
主要用于父进程与子进程之间,或者两个兄弟进程之间。在linux系统中可以通过系统调用建立起一个单向的通信管道,且这种关系只能由父进程来建立。一般情况下,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信。
当需要双向通信时就需要建立起两个管道。管道两端的进程均将该管道看做一个文件,一个进程负责往管道中写内容,而另一个从管道中读取。这种传输遵循“先入先出”(FIFO)的规则。
向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。
1、使用
- 头文件:#include <unistd.h>
- 函数原型:int pipe(pipefd[2])
- 若执行成功,pipefd内将存储两个文件描述符,指向管道的两端
- pipefd[0]指向读端,pipefd[1]指向写端
- 若执行失败,返回值为-1。
2、演示程序
2.1 父进程向子进程发送数据
写测试程序test-1,再fork子进程,父子进程建立无名通道,父进程向子进程发送数据
//test-1
#include <stdio.h>
#include <unistd.h> //pipe通信所需头文件
#include <stdlib.h>
#include <string.h>//memset函数所需
#define BUFSIZE 20
int main()
{
int fd[2];
pid_t pid;
char readBuf[BUFSIZE];
char writeBuf[BUFSIZE]="jisuanjiwangluo";
memset(readBuf,0,BUFSIZE);//接受数据的数组清零
if(pipe(fd)==-1)//pipe建立失败
{
perror("create pipe fail!");
exit(-1);
}
if((pid=fork())==-1)//fork子进程失败
{
perror("create child fail!");
exit(-1);
}
else if(pid==0)//子进程
{
//子进程读管道数据
printf("Sub-process read data from pipe:");
if((read(fd[0],readBuf,BUFSIZE))==-1)//子进程读管道数据失败
{
perror("Sub-process read fail!");
exit(-1);
}
printf("%s\n",readBuf);
exit(0);
}
else if(pid>0)//父进程
{
//父进程写数据到管道
printf("Father-process write date to pipe:");
if((write(fd[1],writeBuf,BUFSIZE))==-1)
{
perror("Father-process write fail!");
//exit(-1);
}
else
{
printf("%s\n",writeBuf);
}
sleep(2);//等待子进程结束
}
return 1;
}
该测试需要注意的是父进程需要等待子进程。简单为了测试,随手在父进程函数块的最后添加了sleep(2)语句,默认子进程在2s内能结束进程并退出。但正确的做法应该是调用wait函数,捕捉子进程退出的信号后父进程再向后执行。
运行结果如下
2.2 子进程向父进程发送数据
测试程序test-2,再fork子进程,父子进程建立无名通道,子进程向父进程发送数据
//test-2
#include <stdio.h>
#include <unistd.h> //pipe通信所需头文件
#include <stdlib.h>
#include <string.h>//memset函数所需
#define BUFSIZE 20
int main()
{
int fd[2];
pid_t pid;
char readBuf[BUFSIZE];
char writeBuf[BUFSIZE]="jisuanjiwangluo";
memset(readBuf,0,BUFSIZE);//接受数据的数组清零
if(pipe(fd)==-1)//pipe建立失败
{
perror("create pipe fail!");
exit(-1);
}
if((pid=fork())==-1)//fork子进程失败
{
perror("create child fail!");
exit(-1);
}
else if(pid==0)//子进程
{
//子进程写管道数据
printf("Sub-process write data to pipe:");
if((write(fd[1],writeBuf,BUFSIZE))==-1)//子进程写管道数据失败
{
perror("Sub-process write fail!");
exit(-1);
}
printf("%s\n",writeBuf);
exit(0);
}
else if(pid>0)//父进程
{
//sleep(2);//等待子进程写管道
//父进程读管道的数据
printf("Father-process read date from pipe:");
if((read(fd[0],reatBuf,BUFSIZE))==-1)
{
perror("Father-process read fail!");
//exit(-1);
}
else
{
printf("%s\n",readBuf);
}
sleep(2);//等待子进程结束
}
return 1;
}
运行结果:
2.3 双向通信
若要实现双向传递,则需要建立两条无名通道fd1,fd2,分别实现子进程向父进程传递数据,父进程向子进程传递数据。
//test-1
#include <stdio.h>
#include <unistd.h> //pipe通信所需头文件
#include <stdlib.h>
#include <string.h>//memset函数所需
#define BUFSIZE 20
int main()
{
int fd1[2],fd2[2];//建立起两条管道
pid_t pid;
char readBuf[BUFSIZE];//如果通信成功,该数字组应是"i am your son"
char writeBuf[BUFSIZE]="i am your father";
char subReadBuf[BUFSIZE];//如果通信成功,该数组应是“i am your father”
char subWriteBuf[BUFSIZE]="i am your son";
memset(readBuf,0,BUFSIZE);//接受数据的数组清零
if(pipe(fd1)==-1 | pipe(fd2)==-1)//pipe建立失败
{
perror("create pipe fail!");
exit(-1);
}
if((pid=fork())==-1)//fork子进程失败
{
perror("create child fail!");
exit(-1);
}
else if(pid==0)//子进程
{
//子进程先写管道数据1,再读管道2
printf("Sub-process write data to pipe:");
if((write(fd1[1],subWriteBuf,BUFSIZE))==-1)//子进程写管道数据失败
{
perror("Sub-process write fail!");
exit(-1);
}
printf("%s\n\n",subWriteBuf);
//close(fd1[1]);//关闭管道1的写端
printf("Sub-process read data from pipe:");
if((read(fd2[0],subReadBuf,BUFSIZE))==-1)
{
perror("Sub-process read fail!");
exit(-1);
}
else
{
printf("%s\n\n",subReadBuf);
}
exit(0);
}
else if(pid>0)//父进程
{
//父进程先读管道1的数据,再写管道2
printf("Father-process read date from pipe:");
if((read(fd1[0],readBuf,BUFSIZE))==-1)
{
perror("Father-process read fail!");
//exit(-1);
}
else
{
printf("%s\n\n",readBuf);
}
//close(fd1[0]);
printf("Father-process write data to pipe:");
if((write(fd2[1],writeBuf,BUFSIZE))==-1)
{
perror("Father-process write fail!");
}
else
{
printf("%s\n\n",writeBuf);
}
sleep(2);//等待子进程结束
}
return 1;
}
运行结果:
二、有名管道
有名管道(命名管道)是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件。任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。因此,实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。
强调,与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。
值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。
1、使用
- 头文件:#include <unistd.h>
- 函数原型:int mkfifo(const char *pathname,mode_t mode);
其中,pathname是FIFO文件名称;mode是文件访问权限;成功则返回0,失败返回-1,并设置erron。 - 创建删除:用命令mkfifo创建,不能重复创建,用命令unlink删除
2、演示程序
2.1 父子进程间通信—父进程向子进程发送数据
测试程序test2-1,在fork子进程,父子进程间建立有名管道,父进程向子进程发送数据
//test2-1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>//用于文件读写
#include <errno.h>
#define BUFSIZE 20
#define PATH_NAME "fifo_file"
int main()
{
pid_t pid;
char writeBuf[BUFSIZE]="I am your father";
char readBuf[BUFSIZE];
memset(readBuf,0,BUFSIZE);
int fd;
if(mkfifo(PATH_NAME,0666)==-1 && erron!=EEXIST)
{
perror("Create fifo fail!");
exit(-1);
}
if((pid=fork())==-1)//创建子进程失败
{
perror("Create sub-process fail!");
exit(-1);
}
else if(pid==0)//子进程读取管道数据
{
fd=open(PATH_NAME,O_RDONLY,0666);
if(fd<0)//打开失败
{
perror("open FIFO_FILE fail!");
exit(-1);
}
if((read(fd,readBuf,BUFSIZE))<0)
{
perror("sub-process read fail!");
exit(-1);
}
printf("[Sub-process read data from Fifo] : %s\n",readBuf);
close(fd);
}
else if(pid>0)//父进程向管道写数据
{
fd=open(PATH_NAME,O_WRONLY,0666);
if(fd<0)//打开失败
{
perror("open FIFO_FILE fail!");
exit(-1);
}
if((write(fd,writeBuf,BUFSIZE))<0)
{
perror("father-process write fail!");
exit(-1);
}
printf("[Father-process write data to Fifo] : %s\n",writeBuf);
close(fd);
sleep(2);
}
return 1;
}
2.2 父子进程间双向通信
由于管道是单向通信,无法直接完成双向通信。但可以通过其他方法来实现:
- 方法一:使用一对FIFO或管道,一个方向使用一个
- 方法二:先关闭在重新打开FIFO,改变数据流的方向
使用时通常先是创建一个管道,再调用fork()函数创建一个子进程,该子进程会继承父进程所创建的管道,这时,父子进程管道的文件描述符对应关系如下图
父子进程分别拥有自己的读写通道,为了实现父子进程之间的读写,只需把无关的读端或写端的文件描述符关闭即可
2.3 不相关进程间通信
测试程序test2-3-1/test2-3-2,两个进程间建立有名管道,test2-3-1向test2-3-2发送数据
//test2-3-1
//向test2-3-2发送数据
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>//用于文件读写
#include <errno.h>
#define BUFSIZE 20
#define PATH_NAME "fifo_file"
int main()
{
char writeBuf[BUFSIZE]="Hello! Test2-3-2!";
int fd;
if(mkfifo(PATH_NAME,0666)==-1 && errno!=EEXIST)//如果不是已存在文件而创建失败,则异常退出
{
perror("Create fifo fail!");
exit(-1);
}
fd=open(PATH_NAME,O_WRONLY,0666);
if(fd<0)//打开失败
{
perror("open FIFO_FILE fail!");
exit(-1);
}
if((write(fd,writeBuf,BUFSIZE))<0)
{
perror("test2-3-1 write fail!");
exit(-1);
}
printf("[test2-3-1 write:] : %s\n",writeBuf);
close(fd);
return 1;
}
//test2-3-2
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>//用于文件读写
#include <errno.h>
#define BUFSIZE 20
#define PATH_NAME "fifo_file"
int main()
{
char readBuf[BUFSIZE];
memset(readBuf,0,BUFSIZE);
int fd;
if(mkfifo(PATH_NAME,0666)==-1 && errno!=EEXIST)
{
perror("Create fifo fail!");
exit(-1);
}
fd=open(PATH_NAME,O_RDONLY,0666);
if(fd<0)//打开失败
{
perror("open FIFO_FILE fail!");
exit(-1);
}
if((read(fd,readBuf,BUFSIZE))<0)
{
perror("test2-3-2 read fail!");
exit(-1);
}
printf("test2-3-2 read] : %s\n",readBuf);
close(fd);
unlink(PATH_NAME);
return 1;
}
2.4 不相关进程间双向通信
在不相关进程之间的双向通信,使用一对FIFO或管道,一个方向使用一个。
//test-2-4-1
#include <stdio.h>
#include <unistd.h> //pipe通信所需头文件
#include <stdlib.h>
#include <string.h>//memset函数所需
#include <fcntl.h>//文件读写
#include <errno.h>//ERRNO
#define BUFSIZE 20
#define PATH_NAME_1 "fifo_file1"
#define PATH_NAME_2 "fifo_file2"
int main()
{
int fd1,fd2;
char readBuf[BUFSIZE];//如果通信成功,该数字组应是"i am your son"
char writeBuf[BUFSIZE]="i am your test2-4-1";
memset(readBuf,0,BUFSIZE);//接受数据的数组清零
if((mkfifo(PATH_NAME_1,0666)==-1 | mkfifo(PATH_NAME_2,0666)==-1)&&errno!=EEXIST)//不是因为文件存在而建立失败,则异常退出
{
perror("create pipe fail!");
exit(-1);
}
//先写再读
printf("test2-4-1 write :");
if((fd1=open(PATH_NAME_1,O_WRONLY,0666))==-1)
{
perror("fifo_file1 open fail!");
exit(-1);
}
if(write(fd1,writeBuf,BUFSIZE)==-1)
{
perror("test2-4-1 write fail!");
exit(-1);
}
printf("%s\n\n",writeBuf);
printf("test2-4-1 read :");
if((fd2=open(PATH_NAME_2,O_RDONLY,0666))==-1)
{
perror("fifo_file2 open fail!");
exit(-1);
}
if((read(fd2,readBuf,BUFSIZE))==-1)
{
perror("test2-4-1 read fail!");
exit(-1);
}
else
{
printf("%s\n\n",readBuf);
}
close(fd1);
close(fd2);
return 1;
}
//test2-4-2
#include <stdio.h>
#include <unistd.h> //pipe通信所需头文件
#include <stdlib.h>
#include <string.h>//memset函数所需
#include <fcntl.h>//文件读写
#include <errno.h>//ERRNO
#define BUFSIZE 20
#define PATH_NAME_1 "fifo_file1"
#define PATH_NAME_2 "fifo_file2"
int main()
{
int fd1,fd2;
int flag=1;
char readBuf[BUFSIZE];//如果通信成功,该数字组应是"i am your son"
char writeBuf[BUFSIZE]="i am your test2-4-2";
memset(readBuf,0,BUFSIZE);//接受数据的数组清零
if((mkfifo(PATH_NAME_1,0666)==-1 | mkfifo(PATH_NAME_2,0666)==-1)&&errno!=EEXIST)//不是因为文件存在而建立失败,则异常退出
{
perror("create pipe fail!");
exit(-1);
}
//进程先写管道数据1,再读管道2
printf("test2-4-2 read :");
if((fd1=open(PATH_NAME_1,O_RDONLY,0666))==-1)
{
perror("fifo_file1 open fail!");
exit(-1);
}
if((read(fd1,readBuf,BUFSIZE))==-1)
{
perror("test2-4-2 read fail!");
exit(-1);
}
else
{
printf("%s\n\n",readBuf);
}
printf("test2-4-2 write :");
if((fd2=open(PATH_NAME_2,O_WRONLY,0666))==-1)
{
perror("fifo_file2 open fail!");
exit(-1);
}
if(write(fd2,writeBuf,BUFSIZE)==-1)
{
perror("test2-4-2 write fail!");
exit(-1);
}
printf("%s\n\n",writeBuf);
close(fd1);
close(fd2);
unlink(PATH_NAME_1);
unlink(PATH_NAME_2);
return 1;
}
全局补充
不可否认,管道通信的速度是比较快的,但是也有诸多限制与不便。
使用管道时需要注意以下几点:
1、管道写端有效的前提是读端有效。也就是说,如果读端关闭了,写端会收到信号SIGPIPE,通常直接导致进程终止。
2、管道有容量大小限制,当缓冲区满时写端阻塞,容量大小根据系统默认值为4k,即一页的大小,可以用ulimit –p查看
3、管道传送的数据类型都是无格式字节流,只需管道的两端事先约定好数据格式即可