您的位置:首页 > 运维架构 > Linux

Linux进程间通信(一)之无名管道(PIPE)和有名管道(FIFO)

2017-10-21 09:43 866 查看

何为进程间通信

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。简单说就是进程之间可以相互发送数据。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。Socket用在网络编程中。

管道

管道通常指无名管道,是 UNIX 系统IPC最古老的形式。

特点有:

1、它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

2、它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间),实现依赖父子进程文件共享。

3、它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

函数原型

#include <unistd.h>
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 <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>

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 <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>

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 <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *filename,mode_t mode);


使用方式:

1)使用open函数打开管道文件

如果一个进程以只读(只写)打开,那么这个进程会被阻塞到open,直到另一个进程以只写(只读)或者读写。

2)使用read函数读取内容

read读取普通文件,read不会阻塞。而read读取管道文件,read会阻塞运行,直到管道中有数据或者所有的写端关闭。

3)使用write函数发送内容,使用close函数关闭打开的文件。

举例:

首先通过命令创建管道文件:fifo

mkfifo fifo




例1:一个进程写,一个进程读。

写端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <signal.h>

//信号处理函数。
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>

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、区别

有名在任意进程之间使用,无名在父子进程之间使用。

拓展:

全双工、半双工、单工通讯的区别:

单工:方向是固定的,只有一个方向可以写,例如广播。

半双工:方向不固定,但在某一刻只能有一个方向进行写,例如对讲机。

全双工:两个方向都可以同时写,例如打电话。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: