进程间通信–管道
两个或者多个进程实现数据层面的交互
因为进程的独立性存在,导致进程间的通信成本较高
为什么要通信?
基本数据
发送命令
进程间通信的本质:必须让不同的进程看到同一份资源
“资源”?特定形式的内存空间
匿名管道
匿名管道的原理:
它允许一个进程向另一个进程发送数据,从而实现进程之间的数据交换。
-
创建管道:在进行管道通信之前,操作系统会创建一个管道,它是一个单向的通信通道,通常由两个文件描述符表示。一个文件描述符用于读取数据,另一个文件描述符用于写入数据。 管道的两个文件描述符指向同一个管道对象。管道是一种特殊的文件类型,它在文件系统中没有对应的物理文件,而是存在于内核中。当一个进程创建管道后,操作系统会在内核中创建一个管道对象,并为其分配两个文件描述符,一个用于读取数据,另一个用于写入数据。
这两个文件描述符都指向同一个管道对象,但是它们是不同的,具有不同的权限。一个文件描述符用于向管道写入数据,另一个文件描述符用于从管道读取数据。因此,一个进程可以通过一个文件描述符向管道写入数据,另一个进程可以通过另一个文件描述符从管道读取数据,实现进程间的通信。
-
建立通信:通常,一个进程(父进程)创建了管道后,会调用系统调用
fork()
来创建一个子进程。这样,父进程和子进程之间就共享了同一个管道。父进程可以向管道写入数据,子进程则可以从管道读取数据。 -
读写数据:一旦管道建立通信后,进程就可以通过文件描述符向管道写入数据或从管道读取数据。写入数据的进程会将数据写入管道的写入端,而读取数据的进程会从管道的读取端读取数据。管道中的数据遵循先进先出(FIFO)的原则。
-
关闭管道:当不再需要进行通信时,进程应该关闭管道。关闭管道后,写入端被关闭的进程如果继续写入数据,会收到管道关闭的信号(SIGPIPE),读取端被关闭的进程如果继续读取数据,会读取到文件结束(EOF)标志。
系统接口:
open()接口是用来打开磁盘级的文件,管道文件是内存级的文件,没有inode等。
创建一个匿名管道文件:
#include <unistd.h>
int pipe(int pipefd[2]);
return: On success, zero is returned. On error, -1 is returned, and errno is set appropriately
pipe的参数是输出型参数,
pipefd[0]:读下标
pipefd[1]:写下标
匿名管道的特征:
管道只能单向通信
具有血缘关系的进程进行进程间的通信
父子进程是会协同的,同步与互斥,保护管道文件的数据安全。
管道是面向字节流的
管道是基于文件的,而文件的生命周期是随进程的
匿名管道的四种情况:
读写端正常,管道如果为空,读端就要阻塞
读写端正常,管道如果被写满,写端就要阻塞
读端正常读,写端关闭,读端就会读到0,表明读到了文件pipe的结尾,不会被阻塞
写端是正常写,读端关闭了。操作系统就要杀掉正在写入的进程。通过信号将写入的进程干掉。
匿名管道能够通信的前提:
当父进程创建子进程时,无论是使用 fork
函数还是 vfork
函数,子进程通常都会继承父进程的文件描述符。所谓的继承,就是子进程可以使用相同的文件描述符,与父进程操作同一个文件对象。这种继承可能会造成权限安全隐患。为了解决这个问题,我们可以采取以下两种方法:
- 不做任何处理:这是最简单的方法,但也是最不安全的。开发人员需要自行注意父子进程之间共享文件对象的方式可能存在危险。
- 立即关闭继承而来的文件描述符:在刚创建完子进程后,立即关闭它继承而来的文件描述符。关闭文件描述符通常使用
close
函数。要知道进程打开了哪些文件描述符,可以遍历/proc/PID/fd
目录,其中PID
是进程的 ID。在该目录中,每个文件描述符对应一个符号链接文件,其名称即为文件描述符的数值。通过遍历该目录,我们可以获取当前进程打开的所有文件描述符,并为其调用close
函数
总之,为了确保子进程不会意外地访问到父进程的文件描述符,我们需要在创建子进程后采取适当的措施来处理继承的文件描述符
命名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
RETURN VALUE
On success mkfifo() returns 0. In the case of an error, -1 is returned (in which case, errno is set
appropriately).
命名管道(也称为有名管道或FIFO)是一种用于进程间通信的特殊类型的文件。与无名管道(匿名管道)不同,命名管道允许无亲缘关系的进程之间进行通信。
命名管道的特点:
- 命名管道允许多个进程通过一个共享的管道进行通信。
- 所有实例共享相同的管道名称,但每个实例都有自己的缓冲区和句柄,为客户端/服务器通信提供单独的管道。