进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。简单说就是进程之间可以相互发送数据。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。Socket用在网络编程中。
管道通常指无名管道,是 UNIX 系统IPC最古老的形式。
特点有:
1、它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
2、它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间),实现依赖父子进程文件共享。
3、它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
函数原型
#include
int pipe(int filedes[2]);
由参数filedes返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。
单个进程中的管道几乎没有任何用处。通常,调用pipe的进程接着调用fork,这样就创建了从父进程到子进程(或反向)的IPC通道。
父子进程都有读端和写端,子进程的是从父进程复制过来的。
进程复制的时候复制了PCB、文件结构体,不止拷贝了文件描述符。
调用fork之后做什么取决于我们想要有的数据流的方向。对于从父进程到子进程的管道,父进程关闭管道的读端(fd[0]),子进程则关闭写端(fd[1])。
举例:
例1:只有一个进程,读写管道文件
#include
#include
#include
#include
#include
int main()
{
int fd[2];
pipe(fd);
write(fd[1],"hello world",12);
sleep(2);
char buff[128];
int n = read(fd[0],buff,127);
printf("read:%s\n",buff);
close(fd[0]);
close(fd[1]);
}
例2:创建由父进程到子进程的管道,父进程写入helloworld,子进程读取数据到buf,然后打印输出。
#include
#include
#include
#include
#include
int main()
{
int fd[2];
pipe(fd);//fd[0]是读,fd[1]是写
if(fork() != 0)
{
close(fd[0]);
write(fd[1],"helloworld",10);
}
else
{
close(fd[1]);
char buf[128] = {0};
read(fd[0],buf,127);
printf("%s\n",buf);
}
return 0;
}
例3:使用管道文件,创建由父进程到子进程的管道,父进程循环输入数据,子进程循环读取数据,当写端输入end时,父子线程都结束。
#include
#include
#include
#include
#include
int main()
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid == 0)
{
close(fd[1]);
char buff[128] = {0};
int n = 0;
while((n = read(fd[0],buff,127)) > 0)
{
printf("child read:%s\n",buff);
}
close(fd[0]);
}
else
{
close(fd[0]);
while(1)
{
char buff[128] = {0};
printf("input:\n");
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
write(fd[1],buff,127);
}
close(fd[1]);
}
exit(0);
}
注意:
(1)当读一个写端已被关闭的管道是,在所有数据都被读取后,read返回0,以指示达到文件结束处。管道的写端彻底关闭(父子进程的写端都得关闭,否则会有进程处于未关闭状态,还在等待写),读端返回一个0,父子进程都得关闭。
(2)如果写一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从处理程序返回,则write返回-1,errno设置为EPIPE。
有名管道也叫命名管道,在文件系统目录中存在一个管道文件。
管道文件仅仅是文件系统中的标示,并不在磁盘上占据空间。在使用时,在内存上开辟空间,作为两个进程数据交互的通道。
管道文件的创建:
1) 在shell中使用mkfifo 命令
mkfifo filename
2) mkfifo 函数 (在代码中使用其创建管道文件)
函数原型:
#include
#include
int mkfifo(const char *filename,mode_t mode);
使用方式:
1)使用open函数打开管道文件
如果一个进程以只读(只写)打开,那么这个进程会被阻塞到open,直到另一个进程以只写(只读)或者读写。
2)使用read函数读取内容
read读取普通文件,read不会阻塞。而read读取管道文件,read会阻塞运行,直到管道中有数据或者所有的写端关闭。
3)使用write函数发送内容,使用close函数关闭打开的文件。
举例:
首先通过命令创建管道文件:fifo
mkfifo fifo
#include
#include
#include
#include
#include
#include
#include
//信号处理函数。
void fun(int sig)
{
printf("sig == %d\n",sig);
}
int main()
{
signal(SIGPIPE,fun);
int fd = open("fifo",O_WRONLY);
// int fd = open("fifo",O_RDWR);
assert(fd != -1);
printf("fd = %d\n",fd);
char buff[128] = {0};
while(1)
{
printf("input:\n");
fgets(buff,128,stdin);
write(fd,buff,strlen(buff));
if(strncmp(buff,"end",3)==0)
{
break;
}
}
close(fd);
exit(0);
}
读端:
#include
#include
#include
#include
#include
#include
int main()
{
int fd = open("fifo",O_RDONLY);
assert(fd != -1);
printf("fd = %d\n",fd);
char buff[128] = {0};
// while(1)
// {
// int n = read(fd,buff,127);//127是期望要读的字符个数
// if(strncmp(buff,"end",3)==0)
// {
// break;
// }
// printf("read:%s\n",buff);
// printf("n = %d\n",n);
// }
int n = 0;
while((n = read(fd,buff,127))>0)
{
printf("read:(n = %d)%s\n",n,buff);
//将buff中的数据清空
memset(buff,0,128);
}
close(fd);
exit(0);
}
输出结果:
(1)正常写入和读取,输入end结束读写。
(2)写端彻底关闭、读端read返回0,也会关闭。
(3)管道的读端关闭,当写端继续写入数据时,会产生SIGPIPE信号,修改默认响应方式,故信号被捕获之后执行信号处理函数fun。
例2:有三个进程分别是进程C(写端)和进程A、B (读端),写端写入数据“Hello World”,A读端读取数据,B再去读取。
写端C:
#include
#include
#include
#include
#include
#include
int main()
{
int fw = open("myfifo",O_WRONLY);
assert(fw != -1);
write(fw,"helloworld",10);
// sleep(2);
// write(fw,"how are you?",12);//继续写入数据
sleep(15);
// close(fw);
return 0;
}
读端A
#include
#include
#include
#include
#include
#include
int main()
{
int fr = open("myfifo",O_RDONLY);
assert(fr != -1);
char buf[128] = {0};
int len = read(fr,buf,5);
if(len != -1)
{
printf("buf:%s\n",buf);
}
sleep(10);
// close(fr);
return 0;
}
读端B
#include
#include
#include
#include
#include
#include
int main()
{
int fr = open("myfifo",O_RDONLY);
assert(fr != -1);
char buf[128] = {0};
int len = read(fr,buf,5);
if(len != -1)
{
printf("buf:%s\n",buf);
}
sleep(10);
// close(fr);
return 0;
}
输出结果如下:
其中test1.c和test3.c是读文件,test2.c是写文件,需要关注的是写端和读端的关闭时间,只有写端开着,读端才能读到数据,而且如果有一个读端关的早,写端如果再写入数据会产生异常,系统会按默认处理异常信号,然后关闭,这样的话另一个读端也不能读取数据。
总结:
(1)两个进程运行时,写端彻底关闭,则读端read返回0,也会关闭。
(2)如果管道的读端关闭,继续写入数据会发生异常,写端收到未捕获的信号SIGPIPE会关闭写端。当然也可以修改默认的信号响应方式,比如增加信号处理函数。
(3)写端写入数据以后,读端不从里面读取内容:数据保持在管道中存在一段时间。管道文件的大小是0。
(4)管道通讯发送的数据若没有指定进程接收,任何一个进程只要打开的是同一个管道文件,都有可能读到数据。
(5)read读取管道中的数据,只要读过的数据就会被清空 。
1、相同点
open打开管道文件以后,在内存中开辟了一块空间,管道的内容在内存中存放,有两个指针—-头指针(指向写的位置)和尾指针(指向读的位置)指向它。读写数据都是在给内存的操作,并且都是半双工通讯。
2、区别
有名在任意进程之间使用,无名在父子进程之间使用。
拓展:
全双工、半双工、单工通讯的区别:
单工:方向是固定的,只有一个方向可以写,例如广播。
半双工:方向不固定,但在某一刻只能有一个方向进行写,例如对讲机。
全双工:两个方向都可以同时写,例如打电话。