目录
一,引言
在进程控制中讲到进程之间有一个非常重要的性质:独立性;也就是说两个进程之间是相互独立的,一个进程的改变会触发写时拷贝,并不会影响另一个进程。那么一个进程想要讲一份资源给另一个进程要怎么办呢?也就是说需要将两种相关或者不相关的进程建立联系--也就是进程间通信,讲解进程间通信主要包括三个方面:匿名管道;命名管道。进程间通信的首要条件是不同进程看见同一份资源。这个资源由操作系统提供。
二,匿名管道
1,常见用法:
匿名管道就像名字一样是没有名字的管道,由于该管道没有名字所以依靠继承等等方式使两个带有血缘关系的进程(一般指父子进程)来使用。使用方法如下:
cpp
int pipe(int fd[2])
函数原型如上,通过传入一个数组该数组有两个元素,其中0表示读端,1表示写端。
看如下例子:
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
int fd[2];
pid_t pid;
char buf[1024];
// 1. 创建管道
if (pipe(fd) == -1) {
perror("pipe failed");
exit(EXIT_FAILURE);
}
// 2. fork 创建子进程(子进程继承管道描述符)
pid = fork();
if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
}
// 3. 子进程:读数据(关闭写端)
if (pid == 0) {
// 关闭不需要的写端(必须关,否则读端会一直等待)
close(fd[1]);
// 从读端读取数据(阻塞直到有数据/写端关闭)
ssize_t n = read(fd[0], buf, sizeof(buf)-1);
if (n == -1) {
perror("read failed");
exit(EXIT_FAILURE);
}
// 处理读取的数据
buf[n] = '\0'; // 补字符串结束符
printf("子进程(PID:%d)读取到数据:%s\n", getpid(), buf);
// 关闭读端
close(fd[0]);
exit(EXIT_SUCCESS);
}
// 4. 父进程:写数据(关闭读端)
else {
// 关闭不需要的读端
close(fd[0]);
// 向写端写入数据
const char *msg = "Hello 子进程!我是父进程";
ssize_t n = write(fd[1], msg, strlen(msg));
if (n == -1) {
perror("write failed");
exit(EXIT_FAILURE);
}
printf("父进程(PID:%d)写入数据:%s\n", getpid(), msg);
// 关闭写端(否则子进程的 read 会一直阻塞)
close(fd[1]);
// 等待子进程退出
wait(NULL);
printf("子进程已退出,父进程结束\n");
}
return 0;
}
通过如上例子:初步理解管道的用法。在进程的管道通信中,管道还有如下属性。
2,管道属性
在实现进程间同时时讲到,想要实现进程间通信首先需要让不同的进程看见同一份资源,恰好文件就满足这个特点。不同的进程可以打开同一份文件 ;在正常的文件操作时,当进程修改文件的数据时会触发写时拷贝,并且操作系统会不定时的刷新缓冲区。因此管道借助文件的缓冲区,但是又不连接文件系统并且不进行刷新缓冲区。
首先进程创建 管道文件,这个文件经过操作系统特殊处理,会有两个文件描述符(上图中fd数组)。在进行文件学习时,进程通过文件描述符可以找到对应的文件。而管道文件,进程通过创建管道文件会同时链接该文件的读写两端。之后创建子进程,子进程会继承父进程的文件描述符表,因此子进程也可以找到该管道文件的读写两端。此时对于父子进程,通过关闭对于不需要的文件描述符,来实现进程的通信--本质上就是一个进程向管道写数据,另一个向管道读数据的操作。
在了解了管道基本通信过程之后,管道有如下五个性质:
1,匿名管道只能进行具有血缘关系 之间的进程进行通信--本质上通过继承相同的文件描述符来实现找到同一份资源,因此必须具有血缘关系的进程。
2,管道文件自带同步机制--在进行通信时会有以及四种情况:
|-----------|----------------------------|
| 读快,写慢 | 读端就要阻塞进程,也就是等待写段写入 |
| 读慢,写快 | 当写端将进程写满时,写端就要阻塞等待读端读取 |
| 读端关闭,写端继续 | 操作系统就会发现写端没有意义,会发送信号关闭写端进程 |
| 读端继续,写端关闭 | 当读到文件结束,读端关闭 |
3,管道是面向字节流的
4,管道是单向通信
5,管道的生命周期是随进程
但是对于没有血缘关系的进程进行通信就无法使用匿名管道,因此引入命名管道的概念。
三,命名管道
命名管道和匿名管道实现管道通信的逻辑几乎一致,第一步还是要找到同一份资源,当进程创建命名管道文件时,会指明这个管道路径和名称。在进行文件学习时,指明文件的路径和文件名就可以确定这个唯一文件。创建命名管道函数原型如下:
cpp
int mkfifo(const char *filename,mode_t mode)
通过两个进程打开相同的文件(管道文件),就可以实现进程之间的通信。如下例子:
服务器端口:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
// 定义命名管道的路径(约定好的通信通道)
#define FIFO_PATH "/tmp/my_named_pipe"
#define BUFFER_SIZE 1024
int main() {
int fd; // 管道文件描述符
char buffer[BUFFER_SIZE];
ssize_t bytes_read;
// 1. 创建命名管道,权限为 0666(所有用户可读可写)
if (mkfifo(FIFO_PATH, 0666) == -1) {
// 如果管道已存在,忽略该错误;其他错误则退出
if (errno != EEXIST) {
perror("mkfifo failed");
exit(EXIT_FAILURE);
}
}
printf("Server: 命名管道创建成功,等待客户端连接...\n");
// 2. 以只读方式打开管道(会阻塞,直到有客户端以写方式打开)
fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) {
perror("open fifo failed");
exit(EXIT_FAILURE);
}
printf("Server: 客户端已连接,开始接收数据...\n");
// 3. 循环读取客户端发送的数据
while (1) {
// 清空缓冲区
memset(buffer, 0, BUFFER_SIZE);
// 读取管道数据(阻塞等待)
bytes_read = read(fd, buffer, BUFFER_SIZE - 1);
if (bytes_read == -1) {
perror("read failed");
break;
} else if (bytes_read == 0) {
// 客户端关闭写端,管道读端返回 0
printf("Server: 客户端断开连接\n");
break;
}
// 打印接收到的数据
printf("Server 收到: %s\n", buffer);
// 如果收到 "exit",服务器退出
if (strcmp(buffer, "exit") == 0) {
printf("Server: 收到退出指令,关闭服务器\n");
break;
}
}
// 4. 关闭管道并删除(可选,也可保留供下次使用)
close(fd);
unlink(FIFO_PATH);
return 0;
}
客户端:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
// 必须和服务器端的管道路径一致
#define FIFO_PATH "/tmp/my_named_pipe"
#define BUFFER_SIZE 1024
int main() {
int fd; // 管道文件描述符
char buffer[BUFFER_SIZE];
// 1. 以只写方式打开命名管道(如果管道不存在,会报错)
fd = open(FIFO_PATH, O_WRONLY);
if (fd == -1) {
perror("open fifo failed (请先启动服务器)");
exit(EXIT_FAILURE);
}
printf("Client: 已连接到服务器,输入消息发送(输入 exit 退出):\n");
// 2. 循环读取用户输入并发送到管道
while (1) {
// 读取用户输入
printf("Client 发送: ");
fgets(buffer, BUFFER_SIZE, stdin);
// 去掉 fgets 读取的换行符(避免发送多余的换行)
buffer[strcspn(buffer, "\n")] = '\0';
// 向管道写入数据
if (write(fd, buffer, strlen(buffer)) == -1) {
perror("write failed");
break;
}
// 如果输入 "exit",客户端退出
if (strcmp(buffer, "exit") == 0) {
printf("Client: 退出客户端\n");
break;
}
}
// 3. 关闭管道
close(fd);
return 0;
}
注意 :当两个进程处于同一路径下可以使用相对路径的方式创建命名管道文件,若不在同一路径创建命名管道时一定要指明路径名称,一保证两个进程访问同一个管道文件。