18.进程间通信

codemagiciant / 2023-08-28 / 原文

18.进程间通信

1.学习目标

  • 熟练使用pipe进行父子进程间通信

  • 熟练使用pipe进行兄弟进程间通信

  • 熟练使用fifo进行无血缘关系的进程间通信

  • 使用mmap进行有血缘关系的进程间通信

  • 使用mmap进行无血缘关系的进程间通信

2.进程间通信相关概念

2.1 什么是进程间通信

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

2.2进程间通信的方式

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:

  • 管道 (使用最简单)

  • 信号(开销最小)

  • 共享映射区(无血缘关系)

  • 本地套接字(最稳定)

3.管道-pipe

3.1管道的概念

管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。

有如下特质:

  • 管道的本质是一块内核缓冲区

  • 由两个文件描述符引用,一个表示读端,一个表示写端。

  • 规定数据从管道的写端流入管道,从读端流出。

  • 当两个进程都终结的时候,管道也自动消失。

  • 管道的读端和写端默认都是阻塞的。

3.2管道的原理

  • 管道的实质是内核缓冲区,内部使用环形队列实现。

  • 默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。

  • 实际操作过程中缓冲区会根据数据压力做适当调整。

3.3管道的局限性

  • 数据一旦被读走,便不在管道中存在,不可反复读取。

  • 数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道

  • 只能在有血缘关系的进程间使用管道。

3.4创建管道-pipe函数

  • 函数作用:

创建一个管道

  • 函数原型:

int pipe(int fd[2]);

  • 函数参数:

若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端

  • 返回值:

 ▶成功返回0;

 ▶失败返回-1,并设置errno值。

函数调用成功返回读端和写端的文件描述符,其中fd[0]是读端,fd[1]是写端,向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区。

管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?

3.5父子进程使用管道通信

一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在血缘关系,这里的血缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。父子进程间具有相同的文件描述符,且指向同一个管道pipe,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信。

第一步:父进程创建管道

第二步:父进程fork出子进程

第三步:父进程关闭fd[0],子进程关闭fd[1]

创建步骤总结:

  • 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]和fd[1],分别指向管道的读端和写端。

  • 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管。

  • 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出,这样就实现了父子进程间通信。

3.6 管道练习

█一个进程能否使用管道完成读写操作呢? 可以但没必要。

█使用管道完成父子进程间通信?


read是没有数据读取时阻塞,write是空间写满了阻塞



子进程一定是先退出,父进程一定是后退出。因为wait函数是阻塞函数。

加sleep(5)阻塞了,阻塞在read行,子进程中"reader over"暂时输出不出来,5秒后输出

管道:

1.管道的本质是一块内核缓冲区,内部的实现是环形队列

2.管道有读写两端,读写两端是两个文件描述符

3.数据的流向是从管道的写端流到管道的读端(数据的流向是单向的)

4.数据被读走之后,在管道中就消失了

5.pipe只能用于有血缘关系的进程间通信

6.管道的读写两端是阻塞的

7.管道的大小默认是4K,但是会根据实际情况做适当调整

pipe用于父子间进程通信:

1.父进程创建pipe

2.父进程调用fork函数创建子进程

3.父进程关闭一端

4.子进程关闭一端

5.父进程和子进程分别执行read或者write操作

█父子进程间通信,实现ps aux | grep bash

使用execlp函数和dup2函数

ps aux | grep bash只关注包含bash的

/dev/tty:标准输出

ps aux:往标准输出写

ps aux 是一个在 Unix 和 Linux 操作系统中用于显示当前进程信息的命令组合。这条命令提供了关于系统上所有运行的进程的详细信息。让我们分解这个命令的每个部分:

  • ps:这是“process status”的缩写,用于显示系统进程的信息。

  • aux:这是传递给ps命令的选项组合,每个字母都有其特定的意义。

    • a:列出所有的进程,包括那些由其他用户启动的进程。

    • u:用户格式化输出,它会显示关于进程的更多信息,例如进程的所有者、CPU 使用率、启动时间等。

    • x:列出没有控制终端的进程。这通常用于显示那些后台或守护进程。

当你运行ps aux命令时,你会看到一个关于所有进程的详细列表,包括其PID(进程ID)、用户、CPU和内存使用率、进程状态、开始时间、所用时间以及命令名称/路径等信息。

这个命令在系统管理和故障排查中是非常有用的,因为它可以帮助管理员或用户查看系统上正在运行的进程和它们的状态。

ps aux | grep bash先往管道写入

dup2(fd[1],STDOUT_FILENO);//写到了管道的写端

grep bash原来从标准输入读,现在从管道读

两次重定向

dup2(fd[0],STDIN_FILENO);//将标准输入重定向到管道读端

实现:


wait(NULL);可以不写:父进程先退出,子进程后退出,子进程会被1号进程领养


效果一样

标红了

3.7 管道的读写行为

  • 读操作

 ▶有数据

  read正常读,返回读出的字节数

 ▶无数据

  ▷写端全部关闭

   read解除阻塞,返回0,相当于读文件读到了尾部

  ▷没有全部关闭

   read阻塞

  • 写操作

 ▶读端全部关闭

  管道破裂,进程终止, 内核给当前进程发SIGPIPE信号

 ▶读端没全部关闭

  ▷缓冲区写满了

   write阻塞

  ▷缓冲区没有满

   继续write

无数据、写端全部关闭->read解除阻塞,返回0,相当于读文件读到了尾部

无数据、没有全部关闭(意味着可能还要写数据)->read阻塞


阻塞了,没有关闭写端又想读

写操作,读端没全部关闭,缓冲区写满了、write阻塞;缓冲区没有满、继续write。


3.8 如何设置管道为非阻塞

默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参

考下列三个步骤进行:

第1步: int flags = fcntl(fd[0], F_GETFL, 0);

第2步: flag |= O_NONBLOCK;

第3步: fcntl(fd[0], F_SETFL, flags);

若是读端设置为非阻塞:

  ▶写端没有关闭,管道中没有数据可读,则read返回-1;

 ▶写端没有关闭,管道中有数据可读,则read返回实际读到的字节数

 ▶写端已经关闭,管道中有数据可读,则read返回实际读到的字节数

 ▶写端已经关闭,管道中没有数据可读,则read返回0



立刻退出。没有数据可读read返回-1,有数据可读将其读出来立刻返回

读到了

写端已经关闭,管道中有数据可读,则read返回实际读到的字节数11

3.9 如何查看管道缓冲区大小

  • 命令

ulimit -a

  • 函数

long fpathconf(int fd, int name);

printf("pipe size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));

printf("pipe size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));

4.FIFO

4.1 FIFO介绍

FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO,不相关的进程也能交换数据

FIFO是Linux基础文件类型中的一种(文件类型为p,可通过ls -l查看文件类型)。但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信。

4.2 创建管道

  • 方式1-使用命令 mkfifo

命令格式: mkfifo 管道名

例如:mkfifo myfifo

  • 方式2-使用函数

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

参数说明和返回值可以查看man 3 mkfifo

当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可用于FIFO。如:close、read、write、unlink等。

FIFO严格遵循先进先出(first in first out),对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

例子:

fifo_read.c

fifo_write.c

我make之后,又./了输出mkfifo error: File exists为什么?

这个错误提示是因为在你执行程序时,名为"myfifo"的FIFO文件已经存在。每次当你尝试运行第一个程序,它都会尝试创建一个名为"myfifo"的FIFO。如果该文件已经存在,mkfifo函数会失败,并返回-1。

为了解决这个问题,你有几个选择:

  1. 检查文件是否存在再创建:在调用mkfifo之前,可以先检查"./myfifo"是否已经存在。如果不存在,再去创建。

    struct stat st;
    if (stat("./myfifo", &st) == -1) {
        int ret = mkfifo("./myfifo", 0777);
        if (ret < 0) {
            perror("mkfifo error");
            return -1;
        }
    }
    
  2. 忽略"File exists"错误:如果mkfifo返回错误,并且这个错误是"File exists",那么可以选择简单地忽略这个错误,因为你知道FIFO已经存在,可以继续执行后面的代码。

    int ret = mkfifo("./myfifo", 0777);
    if (ret < 0 && errno != EEXIST) {
        perror("mkfifo error");
        return -1;
    }
    

    这里,errno是一个全局变量,当系统调用返回一个错误时,它会被设置为一个特定的值。EEXIST是一个错误码,表示文件已经存在。

  3. 每次运行前删除FIFO:你可以选择在每次执行程序前手动删除"./myfifo",或者在程序开始执行时自动删除它。

    unlink("./myfifo");
    int ret = mkfifo("./myfifo", 0777);
    ...
    

    请注意,unlink函数会删除指定的文件,所以只有当你确定要删除这个文件时才使用它。

你可以根据你的具体需求和使用场景选择最适合的方法。

File exists为什么会输出出来?

当你调用mkfifo试图创建一个已经存在的文件时,mkfifo会返回-1,并设置全局变量errnoEEXIST,表示文件已经存在。

你的程序中使用了perror("mkfifo error");来输出错误信息。perror函数的工作原理是:它会打印你提供的字符串,然后根据当前的errno值输出相应的系统错误描述。由于在这种情况下errno被设置为EEXISTperror将输出"File exists"来描述这个错误。

因此,你看到的错误信息"mkfifo error: File exists"是由以下两部分组成的:

  1. "mkfifo error":这是你传递给perror的字符串。
  2. "File exists":这是perror根据errno的值生成的系统错误描述。

如果你希望不显示这个错误,可以按照我之前的回答中提到的方法进行处理。

char buf[64];
memset(buf, 0x00, sizeof(buf));

这段代码的作用是:

  1. 定义一个字符数组buf,其大小为64字节。
  2. 使用memset函数将buf中的所有字节设置为0x00

简单地说,这段代码创建了一个64字节的字符数组并将其所有字节初始化为0。

这两段代码是一个简单的示例,展示了Linux下FIFO(有时也叫做命名管道)的基本使用。FIFO允许不相关的进程进行双向通信。

让我们分析一下这两段代码:

第一个程序(写入FIFO):

  1. 使用mkfifo函数创建一个FIFO文件。这是一个特殊类型的文件,用于进程间通信。文件名为“myfifo”,权限设置为0777。
  2. 使用open函数打开FIFO以进行读写。
  3. 使用write函数向FIFO写入字符串"hello world"。
  4. 使用sleep函数让进程休眠10秒,这可能是为了模拟一些处理延迟,或者等待第二个进程读取数据。
  5. 使用close函数关闭FIFO。

第二个程序(从FIFO中读取):

  1. 使用open函数打开先前创建的FIFO。
  2. 准备一个字符数组buf,用于从FIFO读取数据。
  3. 使用read函数从FIFO读取数据并存储到buf中。
  4. 打印从FIFO读取的字节数和数据内容。
  5. 使用close函数关闭FIFO。

执行顺序和注意事项:

  1. 首先,你需要运行第一个程序来创建FIFO并写入数据。

  2. 在第一个程序休眠的时候(即等待10秒的期间),你应该在另一个终端或进程中运行第二个程序来读取FIFO中的数据。这样,第二个程序会打印出从FIFO读取的"hello world"。

  3. 注意,在实际使用中,当一个FIFO被打开以供读取时,直到有其他进程打开该FIFO以供写入,否则该读取进程会被阻塞。同样,当一个FIFO被打开以供写入时,直到有其他进程打开该FIFO以供读取,否则写入进程会被阻塞。

  4. 如果你在第一个程序中不使用sleep函数,那么你需要确保第二个程序在第一个程序结束之前开始运行,否则第一个程序可能会在第二个程序开始运行之前结束,这可能会导致第二个程序无法读取到数据。

总体来说,这是一个非常基础的FIFO使用示例。在实际应用中,你可能需要考虑更多的同步和并发控制来确保数据的完整性和通信的正确性。

循环读循环写


循环读

4.3FIFO

1.何为命名管道

管道(pipe)只能用于“有血缘关系”的进程间。

pipe与FIFO之间最大的区别就是FIFO提供一个路径名与之关联,在文件系统中有一个索引块,以文件路径的形式存在(在磁盘中没有数据块,所有数据都存放在内核),而这个文件路径是FIFO被称为命名管道的重要原因。

FIFO与管道类似,其本质上也是内核的一块缓冲区,FIFO也有一个写入端和读取端,FIFO中的数据读写顺序和管道PIPE中是一样的。进程就像打开普通文件一样,调用open函数打开FIFO文件进行读写操作,实现任意两个没有关系的进程通信。

2.使用mkfifo函数创建FIFO

mkfifo函数就是用于创建一个FIFO文件

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname,  mode_t mode);  

返回值说明:成功返回0; 失败返回-1,设置errno

参数pathname:要创建的FIFO文件名且该文件必须不存在

参数mode:指定FIFO文件的权限(8进制数,如0664)

一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。

调用mkfifo函数创建FIFO进行进程间通信的大概过程:

如图所示,当调用mkfifo函数创建一个名为myfifo的FIFO文件时,任何有权限的进程都能打开myfifo这个文件。

使用FIFO进行进程间通信的时候,通常会设置一个写入进程和读取进程,例如A进程通过open函数以只写方式打开FIFO文件,并通过文件描述符把数据写入FIFO内核缓冲区,B进程也通过open函数以只读方式打开FIFO文件,通过文件描述符从FIFO的缓冲区中读取数据。

还使用命令创建管道方式,例如:mkfifo test,其实mkfifo命令本质上就是调用了mkfifo函数来创建FIFO文件。

我们可以看到创建的FIFO文件test,另外在文件权限位的最前面的p就表示文件类型,即管道文件。

3.FIFO的打开规则

1)如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
2)如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。

总之,一旦设置了阻塞标志,调用mkfifo建立好之后,那么管道的两端读写必须分别打开,有任何一方未打开,则在调用open的时候就阻塞。对管道或者FIFO调用lseek,返回ESPIPE错误

4.使用FIFO进行进程通信

一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo,如:close、read、write、unlink等,但不能使用lseek函数。

实验:fifo1程序创建FIFO文件test,然后打开test文件写数据,fifo2程序打开test文件读数据

fifo1写数据

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

int main(){
        int fd = 0;
        int len = 0;
        int ret;
        char buf[1024] = {0};

        //创建FIFO文件
        ret = mkfifo("test" , 0664);
        if(ret != 0){
                perror("mkfifo error:");
                exit(-1);
        }

        //只写方式打开test
        fd = open("test", O_WRONLY);
        if(fd < 0){
                perror("open error");
        }
        puts("open fifo write");
        //向FIFO写入数据
        while((len = read(STDIN_FILENO, buf, sizeof(buf))) > 0){
                write(fd, buf, len);
        }
        close(fd);
        return 0;
}

fifo2读数据

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

int main(){
        int fd = 0;
        int len = 0;
        char buf[1024] = {0};

        //只读打开test
        fd = open("test", O_RDONLY);
        if(fd < 0){
                perror("open error:");
        }
        puts("open fifo read");

        //从管道中读取数据
        while((len = read(fd, buf, sizeof(buf))) > 0){
                write(STDOUT_FILENO, buf, len);
        }
        
        //如果read返回0,说明读到文件末尾或对端已关闭
        if(len == 0){
                puts("peer is close or file end");
        }else{
                puts("read fifo");
        }

        close(fd);
        return 0;
}

程序执行结果:
1 . 当执行./fifo1时会出现阻塞,因为./fifo2没有打开

2 . 接着再执行./fifo2后,程序执行结果如图所示:

程序的执行结果来看,fifo1进程打印open fifo write,fifo2进程打印了open fifo read,然后fifo1写入的数据会马上被fifo2读走。

当我们在fifo1处按下Ctrl+c时,该进程的写端会被关闭掉,对应的在fifo2的进程的读端的read就会读到0(对端已关闭)。我们再次查看test管道文件时,发现文件内容为0,这说明FIFO是一个管道,具有管道的特性(管道中的数据只能读取一次,一旦读完就不存在了)。

由此也证明了test文件在文件系统中只是以一个文件路径的形式而存在,在磁盘中并没有数据,所有的数据都在内核缓冲区中,当进程结束时,数据就会释放掉,但是test文件则会在文件系统中保存着,之后进程再次使用FIFO进行通信时,只需直接打开test文件就行了,然后通过test文件读写内核缓冲区的数据(可以把test文件理解为C语言中的指针,通过指针去操作这块内存,虽然这么理解有些不严谨),所以这才是FIFO与管道(PIPE)的真正区别。

5.使用FIFO通信的注意事项

1.当一个进程调用open打开FIFO文件读取数据时会阻塞等待,直到另一进程打开FIFO文件写入数据为止。也就是说,FIFO文件必须读和写同时打开才行,如果FIFO的读写两端都已打开,那么open调用会立即返回成功,否则一个进程单独打开写或者读都会引发阻塞(上一小节已经证明了这一点)。

2.如果一个进程调用open函数指定O_RDWR选项来打开FIFO时不会发生阻塞,open会立即返回,不会出错,但是大多数unix实现(包括linux)对于这样的行为是未知的,这会导致进程无法正确使用管道进行通信,因为这种做法破坏了FIFO文件的I/O模型(其实就是违背了管道的通信方式)。

换句话说,此时调用进程使用open函数返回的文件描述符读取数据时,read永远都不会读到文件末尾(至于详细原因请参考上一篇的第五小节:为何要关闭未使用的管道文件描述符)。

现在对fifo2程序做以下修改:

//以读写方式打开test
fd = open("test", O_RDWR);

程序的执行结果为:

当在fifo1出按下Ctrl+c时,fifo2进程并没有终止,而是一直在阻塞。

原因在于:fifo2进程因为是以O_RDWR(读写方式)打开test文件的,掌握着FIFO的读写两端,系统内核发现FIFO的写端还没有完全关闭,所以啥也不会做,于是fifo2就会阻塞在FIFO的读端处,等待着数据到来,但此时只有fifo2掌握着FIFO的写端,那么fifo2将会永远阻塞在读端。

3.如果在打开FIFO文件不希望阻塞时,在调用open函数可以指定O_NONBLOCK。

6.使用非阻塞I/O

上一小节中说过一个进程在打开FIFO不希望阻塞时,可以在调用open函数时指定O_NONBLOCK来实现非阻塞。但是O_NONBLOCK标志在不同情况下可能会产生不同的影响,甚至会导致程序出现一些不可预料的错误。

例如在FIFO的读端没有被打开的情况下,如果当前进程以写方式打开FIFO,那么open会调用失败,并将errno设置为ENXIO。

我们对fifo1程序做以下修改:

//以只写和非阻塞方式打开test
fd = open("test",  O_WRONLY|O_NONBLOCK);
if(fd < 0){
        //判断是否为对端没打开导致ENXIO错误
       if(errno == ENXIO){
               perror("open error");
       }
       exit(1);
}

程序执行结果:

No such device or address错误的大意就是没有这样的设备或地址,出错原因在于,当你打开FIFO写入数据,但是对方没打开读端就会出现这样的错误。

7.避免打开FIFO引发的死锁问题

通常,进程间通信都是有两个或两个以上进程的,假设这么一种情况,当每个进程都在等待对方完成某个动作而阻塞时,这可能会产生死锁问题,下图中就展示了两个进程产生死锁的情况:

在上图中的两个进程都因等待打开一个FIFO文件读取数据而阻塞。

fifo1进程

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

int main(){
        int fd1 = 0;
        int fd2 = 0;
        int ret = 0;

        //创建A_FIFO
        ret = mkfifo("A_FIFO" , 0664);
        if(ret != 0){
                perror("mkfifo A_FIFO error:");
                exit(-1);
        }

        //创建B_FIFO
        ret = mkfifo("B_FIFO" , 0664);
        if(ret != 0){
                perror("mkfifo B_FIFO error:");
        }


        //以只读方式打开A_FIFO
        fd1 = open("A_FIFO",  O_RDONLY);
        if(fd1 < 0){
                perror("open A_FIFO error:");
                exit(1);
        }

        puts("open A_FIFO read");


        //以只写方式打开B_FIFO
        fd2 = open("B_FIFO" , O_WRONLY);
        if(fd2 < 0){
                perror("open B_FIFO error");
                exit(1);
        }
        puts("open B_FIFO write");
        
        close(fd1);
        close(fd2);
        return 0;
}

fifo2进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

int main(){
        int fd1 = 0;
        int fd2 = 0;

        //以读方式打开B_FIFO
        fd1 = open("B_FIFO", O_RDONLY);
        if(fd1 < 0){
                perror("open B_FIFO error:");
                exit(1);
        }
        puts("open B_FIFO read");

        //以写方式打开A_FIFO
        fd2 = open("A_FIFO" , O_WRONLY);
        if(fd2 < 0){
                perror("open A_FIFO error:");
                exit(1);
        }
        puts("open A_FIFO write");
        
        //关闭管道
        close(fd1);
        close(fd2);
        return 0;
}

程序执行结果:

分析死锁的原因:
因为fifo1进程在打开A_FIFO读数据之前,fifo2进程并没有打开A_FIFO的写端,所以fifo1进程会阻塞等待在open调用处,对于fifo2进程来说也是如此,双方进程都在等待对方打开FIFO的另一端,如果不采取有效措施,双方将会一直死等下去。

为了避免这个问题,我们可以让其中一个进程或两个线程在打开FIFO时都指定O_NONBLOCK选项以非阻塞方式解决这个问题。

5.1 存储映射区介绍

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。

使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

5.2 mmap函数

  • 函数作用:

建立存储映射区

  • 函数原型

    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

  • 函数返回值:

  ▶成功:返回创建的映射区首地址;

  ▶失败:MAP_FAILED宏

  • 参数:

  ▶addr: 指定映射的起始地址, 通常设为NULL,由系统指定

  ▶length:映射到内存的文件长度

  ▶prot:映射区的保护方式, 最常用的:

    ◆读:PROT_READ

    ◆写:PROT_WRITE

    ◆读写:PROT_READ | PROT_WRITE

  ▶flags: 映射区的特性, 可以是

    ◆MAP_SHARED: 写入映射区的数据会写回文件,且允许其他映射该文件的进程共享。

    ◆MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write),对此区域所做的修改不会写回原文件。

  ▶fd:由open返回的文件描述符, 代表要映射的文件。

  ▶offset:以文件开始处的偏移量,必须是4k的整数倍,通常为0,表示从文件头开始映射。

addr:一班传NULL,表示让内核去指定一个内存起始地址

length:文件大小

    lseek(打开了用这个方便)或者stat函数(获取文件大小)

prot:PROT_READ PROT_WRITE PROT_READ | PROT_WRITE

flags:

  MAP_SHARED:对映射区的修改会反映到文件中(可以对文件进行修改) 通过内存

  MAP_PRIVATE:对映射区的修改不会对文件产生影响

fd:打开的文件描述符

  fd = open();

offset:从文件的哪个位置开始映射,一般传0

mmap1.c

O_RDWR是一个打开文件的模式,表示以读写方式打开文件。

上面是直接覆盖文件,文件改回去,用MAP_PRIVATE

不能反应到文件当中去,无法写

使用mmap完成毫无血缘关系的进程间的通信

mmap_read.c

mmap_write.c

参考资料:

34-进程间通信——FIFO(命名管道)