本文为一部分:管道相关内容。
前言
Linux环境编程中进程间通信的重要性和方法:进程间通信是通过传输数据或共享信息来实现的,包括数据交换、共享资源、进程同步和消息传递。
Linux系统提供了四大类进程间通信方法:管道、SystemVIPC、POSIXIPC和套接字。其中管道适用于具有亲缘关系的进程,SystemVIPC有消息队列、信号量和共享内存,POSIXIPC通过文件实现消息队列、信号量和共享内存。
本质就是不用的进程可以访问相同的内存区域。
总共四大类方式如下:
一、无名管道
因为管道数据是通过队列来维护的,我们先来分析一个管道中数据的特点:
管道对应的内核缓冲区大小是固定的,默认为4k(也就是队列最大能存储4k数据)
管道分为两部分:读端和写端(队列的两端),数据从写端进入管道,从读端流出管道。
管道中的数据只能读一次,做一次读操作之后数据也就没有了(读数据相当于出队列)。
管道是单工的:数据只能单向流动, 数据从写端流向读端。
对管道的操作(读、写)默认是阻塞的
读管道:管道中没有数据,读操作被阻塞,当管道中有数据之后阻塞才能解除
写管道:管道被写满了,写数据的操作被阻塞,当管道变为不满的状态,写阻塞解除
管道在内核中, 不能直接对其进行操作,我们通过什么方式去读写管道呢?
其实管道操作就是文件IO操作,内核中管道的两端分别对应两个文件描述符,通过写端的文件描述符把数据写入到管道中,通过读端的文件描述符将数据从管道中读出来。读写管道的函数就是Linux中的文件IO函数
1.无名管道原理
无名管道(pipe)是一种特殊的管道,用于在具有亲缘关系的进程之间进行单向通信无名管道特点:
半双工的通信方式,数据只能单向流动。
以字节流方式通信,数据格式由用户自行定义。
无名管道多用于父子进程间通信,也可用于其他亲缘关系进程间通信。
父进程调用pipe函数会创建两个文件(读管道文件,写管道文件),这两个文件对应的文件节点为pipe inode,pipeinode为无名管道具体实现。
父进程调用fork函数创建子进程,子进程拷贝父进程的文件表,由于父子进程文件表内容相同,指向的file相同,所以最终父子进程操作的pipe管道相同。父子进程都能看到pipe管道内存空间,所以父子进程能正常通信。
父进程调用fork函数成功后,父子进程不能同时保留读写文件描述符,需要关闭读或者写文件描述符。防止父子进程同时读写引发数据错误。
调用一次pipe函数只能产生一个pipe管道(一个缓冲区),所以无名管道是半双工模式。要是实现全双工模式必须有两个pipe管道,且对应两套读写文件。
无亲缘关系进程文件表不能访问相同文件,进程间无法访问相同的pipe管道,所以不能通过无名管道进程通信。
2.无名管道代码
int pipe(int pipefd[2]);
int pipe2(int pipefd[2], int flags),
功能:pipe或pipe2函数用于创建一个无名管道(pipe),用于进程间通信。
参数:
pipefd:整型数组,用于存储管道的文件描述符。
pipefd[0]:读文件描述符。
pipefd[1]:写文件描述符。
flags:用于设置管道的标志位的参数,可以是0或者以下常量的按位或:
O NONBLOCK:设置非阻塞模式,即读取和写入管道时不会阻塞进程。
O_CLOEXEC:设置在execve系统调用执行时关闭管道。
返回值:
成功:返回0。
失败:返回-1,并设置errno。
创建无名管道代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#define BUFFER_SIZE 100
#define TEST_STRING "Hello, Pipe!\n"
#define SIZE_OF_TEST_STRING sizeof(TEST_STRING) - 1
int main() {
int fd[2];
pid_t pid;
// 步骤1: 创建pipe
if (pipe(fd) == -1) {
perror("Pipe creation failed");
return 1;
}
// 步骤2: 创建子进程
pid = fork();
if (pid < 0) {
perror("Fork failed");
return 1;
} else if (pid == 0) { // 子进程
// 步骤3: 子进程关闭写端
close(fd[1]);
while(1) {
char rbuf[BUFFER_SIZE] = {0};
ssize_t bytes_read = read(fd[0], rbuf, BUFFER_SIZE - 1);
if (bytes_read > 0) {
rbuf[bytes_read] = '\0'; // 添加字符串结束符
printf("Child received: %s", rbuf);
} else if (bytes_read == 0) {
printf("Pipe closed.\n");
break; // 读取到EOF,退出循环
} else {
perror("Read error");
break;
}
}
// 在适当的时候关闭读端,这里可以在循环后或使用信号处理来优雅关闭
close(fd[0]);
} else { // 父进程
// 步骤3: 父进程关闭读端
close(fd[0]);
while(1) {
// 写数据到pipe
ssize_t bytes_written = write(fd[1], TEST_STRING, SIZE_OF_TEST_STRING);
if (bytes_written != SIZE_OF_TEST_STRING) {
perror("Write error");
break;
}
printf("Parent wrote data.\n");
sleep(1); // 暂停一秒
// 实际应用中可能不需要每次都关闭写端,但这里为了示例清晰每次写完都关闭
close(fd[1]);
}
}
return 0;
}
创建管道(pipe(fd)): 使用pipe()系统调用创建一个管道。
创建子进程fork()): 调用fork()生成一个子进程。
子进程分支:
关闭管道的写端(close(fd[1]))。
在无限循环中从管道读取数据(read(fd[0], …))并处理。
(注:原循环中关闭读端的操作应在循环外,以避免重复关闭。)
父进程分支:
关闭管道的读端(close(fd[0]))。
在无限循环中向管道写入数据(write(fd[1], …)),每次写入后短暂休眠。
写操作后关闭写端,虽然实际应用中通常不在每次写后关闭,这里为了示例清晰进行了关闭。
二、命名管道(FIFO)
1.命名管道原理
FIFO文件(也称为命名管道)是一种特殊类型的文件,在Linux中用于进程间通信。FIFO文件允许不相关的进程通过读取和写入相同的文件来进行通信。FIFO文件特点:
FIFO文件位于文件系统中,可以像其他文件一样进行访问和管理。
FIFO文件可以通过名称进行识别和引用,而不仅仅依赖于文件描述符。
FIFO文件可以在不同的进程之间进行双向通信,允许同时进行读取和写入操作。
有名管道拥有管道的所有特性,之所以称之为有名是因为管道在磁盘上有实体文件, 文件类型为p ,有名管道文件大小永远为0,因为有名管道也是将数据存储到内存的缓冲区中,打开这个磁盘上的管道文件就可以得到操作有名管道的文件描述符,通过文件描述符读写管道存储在内核中的数据。
进程通过调用mkfifo函数创建一个FIFO文件,FIFO文件对应一个fifoinode,fifoinode和pipe inode底层实现相同。
命名管道使用文件名创建管道,所以称为命名管道。
命名管道和无名管道底层实现原理相同。
命名管道通过mkfifo创建完后,进程调用open打开FIFO文件,由于每个文件都对应唯一的inode节点所以多个进程都是打开相同的inode节点,成功打开FIFO文件后,进程能正常读写管道完成进程间通信。
打开FIFO文件需要知道FIFO文件路径,进程间只要知道FIFO文件路径,就能建立连接,完成通信。
进程之间都能看到管道内存空间,所以进程间能正常通信。
调用一次mkfifo函数只能产生一个管道(一个缓冲区),所以命名管道是半双工模式。多个进程同时读写一个命名管道可能会出现数据异常,所以进程调用open函数时得指定打开标志为O RDONLY或O WRONLY.
命名管道也需要创建多个命名管道实现全双工通信。
2.命名管道代码
int mkfifo(const char *pathname, mode t mode);
功能:mkfifo函数是Linux下的一个系统调用函数,用于一个命名管道(FIFO文件)。
参数:
pathname:命名管道的路径名。
mode:创建的命名管道的权限,可参考open函数。
返回值:
成功:返回0。
失败:返回-1,并设置errno。
创建命名管道代码(写端)
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#define FIFO_PATH "/tmp/test_fifo"
int main(int argc, char *argv[]) {
int fd = 0;
// 创建FIFO(命名管道)
if (mkfifo(FIFO_PATH, 0644) == -1) {
perror("Failed to create FIFO");
return EXIT_FAILURE;
}
// 以只读方式打开FIFO
fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) {
perror("Failed to open FIFO");
unlink(FIFO_PATH); // 如果打开失败,尝试删除已创建的FIFO
return EXIT_FAILURE;
}
printf("Reading from FIFO...\n");
while(1) {
char rbuf[100];
ssize_t bytes_read = read(fd, rbuf, 100);
// 读是阻塞的, 如果管道中没有数据, read自动阻塞
// 有数据解除阻塞, 继续读数据
if (bytes_read > 0) {
rbuf[bytes_read] = '\0'; // 确保字符串被正确终止
printf("rbuf: %s\n", rbuf);
} else if (bytes_read == 0) {
printf("End of FIFO reached.\n");
break; // 读取到EOF,退出循环
} else if (errno != EAGAIN) { // EAGAIN 表示非阻塞模式下的无数据可读,通常应该继续读取
perror("Read error");
break;
}
}
// 关闭文件描述符
close(fd);
// 可选:如果程序结束时不再需要FIFO,可以考虑删除它
unlink(FIFO_PATH);
return 0;
}
总结
关于管道不管是有名的还是匿名的,在进行读写的时候,它们表现出的行为是一致的,下面是对其读写行为的总结:
读管道,需要根据写端的状态进行分析:
写端没有关闭 (操作管道写端的文件描述符没有被关闭)
如果管道中没有数据 ==> 读阻塞, 如果管道中被写入了数据, 阻塞解除
如果管道中有数据 ==> 不阻塞,管道中的数据被读完了, 再继续读管道还会阻塞
写端已经关闭了 (没有可用的文件描述符可以写管道了)
管道中没有数据 ==> 读端解除阻塞, read函数返回0
管道中有数据 ==> read先将数据读出, 数据读完之后返回0, 不会阻塞了
写管道,需要根据读端的状态进行分析:
读端没有关闭
如果管道有存储的空间, 一直写数据
如果管道写满了, 写操作就阻塞, 当读端将管道数据读走了, 解除阻塞继续写
读端关闭了,管道破裂(异常), 进程直接退出