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

何为进程间通信

进程间通信(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]);12

由参数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]);
}
1234567891011121314151617181920212223

输出结果是: 


例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;
}123456789101112131415161718192021222324

输出结果如下: 


例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);
}
12345678910111213141516171819202122232425262728293031323334353637383940414243

注意: 
(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);1234

使用方式: 
1)使用open函数打开管道文件 
如果一个进程以只读(只写)打开,那么这个进程会被阻塞到open,直到另一个进程以只写(只读)或者读写。 
2)使用read函数读取内容 
read读取普通文件,read不会阻塞。而read读取管道文件,read会阻塞运行,直到管道中有数据或者所有的写端关闭。 
3)使用write函数发送内容,使用close函数关闭打开的文件。 
举例: 
首先通过命令创建管道文件:fifo

mkfifo fifo1

 
例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);
}1234567891011121314151617181920212223242526272829303132333435363738

读端:

#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);
}1234567891011121314151617181920212223242526272829303132333435

输出结果: 
(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;
}123456789101112131415161718

读端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;
}
1234567891011121314151617181920212223

读端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;
}
1234567891011121314151617181920212223

输出结果如下:

其中test1.c和test3.c是读文件,test2.c是写文件,需要关注的是写端和读端的关闭时间,只有写端开着,读端才能读到数据,而且如果有一个读端关的早,写端如果再写入数据会产生异常,系统会按默认处理异常信号,然后关闭,这样的话另一个读端也不能读取数据。

总结: 
(1)两个进程运行时,写端彻底关闭,则读端read返回0,也会关闭。 
(2)如果管道的读端关闭,继续写入数据会发生异常,写端收到未捕获的信号SIGPIPE会关闭写端。当然也可以修改默认的信号响应方式,比如增加信号处理函数。 
(3)写端写入数据以后,读端不从里面读取内容:数据保持在管道中存在一段时间。管道文件的大小是0。 
(4)管道通讯发送的数据若没有指定进程接收,任何一个进程只要打开的是同一个管道文件,都有可能读到数据。 
(5)read读取管道中的数据,只要读过的数据就会被清空    。

有名管道和无名管道的异同点

1、相同点 
open打开管道文件以后,在内存中开辟了一块空间,管道的内容在内存中存放,有两个指针—-头指针(指向写的位置)和尾指针(指向读的位置)指向它。读写数据都是在给内存的操作,并且都是半双工通讯。 
2、区别 
有名在任意进程之间使用,无名在父子进程之间使用。

拓展: 
全双工、半双工、单工通讯的区别: 
单工:方向是固定的,只有一个方向可以写,例如广播。 
半双工:方向不固定,但在某一刻只能有一个方向进行写,例如对讲机。 
全双工:两个方向都可以同时写,例如打电话。

发布了9 篇原创文章 · 获赞 5 · 访问量 2206

猜你喜欢

转载自blog.csdn.net/CSDN_liu_sir/article/details/102623002