匿名管道通信
- 匿名管道是一种特殊的文件,这种文件只存在于内存中。其实是一个在内核内存中维护的缓冲器
- 匿名管道只能用于父子进程或兄弟进程之间,必须用于具有亲缘关系的进程间的通信
这是因为管道实际上是内存的一个缓冲区
管道的写入端是一个文件描述符
fd[1]
,对应的是文件x1
管道的读取端也是一个文件描述符
fd[0]
,对应的文件是x2
在进程之间通过管道通信,实际上是通过缓冲区+两个文件通信。如果两个进程没有关系(父子或者兄弟)则文件描述符对应的文件不是同一个文件,便无法通信
匿名管道只能由一端向另一端发送数据,是半双工方式,如果双方需要同时收发数据需要两个管道
#include <unistd.h>
int pipe(int pipefd[2]);
- pipefd[2]: 传出参数
- pipefd[0]: 对应管道的读端
- pipefd[1]: 对应管道的写端
管道默认是阻塞的,没有数据read
阻塞,管道满了write
阻塞
通信双方的进程中写数据的一方需要把fd[0]
先close
掉,读的一方需要先把fd[1]
给close
掉
单向通信
子进程写,父进程读
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
pid_t pid = fork();
if (pid > 0) { // 父进程
printf("I am parent, pid: %d\n", getpid());
close(pipefd[1]); // 关闭写端
char buf[1024] = {0};
while (1) {
int len = read(pipefd[0], buf, sizeof(buf)); // 从管道读取端读取数据
printf("parent rev: %s, pid: %d\n", buf, getpid());
}
}
else if (pid == 0) { // 子进程
printf("I am child, pid: %d\n", getpid());
close(pipefd[0]); // 关闭读端
char buf[1024] = {0};
while (1) {
char *str = "hello, I am child";
write(pipefd[1], str, strlen(str)); // 往写端写数据
}
}
return 0;
}
双向通信
正常不会这么用
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1) {
perror("pipe");
exit(0);
}
pid_t pid = fork();
if (pid > 0) {
printf("I am parent, pid: %d\n", getpid());
// 从管道读取端读取数据
char buf[1024] = {0};
while (1) {
int len = read(pipefd[0], buf, sizeof(buf)); // 读数据
printf("parent rev: %s, pid: %d\n", buf, getpid());
char *str = "hello, I am parent";
write(pipefd[1], str, strlen(str)); // 写数据
sleep(1);
}
}
else if (pid == 0) {
printf("I am child, pid: %d\n", getpid());
char buf[1024] = {0};
while (1) {
char *str = "hello, I am child";
write(pipefd[1], str, strlen(str)); // 写数据
sleep(1);
int len = read(pipefd[0], buf, sizeof(buf)); // 读数据
printf("chlid rev: %s, pid: %d\n", buf, getpid());
}
}
return 0;
}
I am parent, pid: 26544
I am child, pid: 26545
parent rev: hello, I am child, pid: 26544
chlid rev: hello, I am parent, pid: 26545
管道大小
int main() {
int pipefd[2];
int ret = pipe(pipefd);
long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
printf("pipe size: %ld\n", size);
return 0;
}
4096
管道应用
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
int main() {
int fd[2];
int ret = pipe(fd);
pid_t pid = fork();
if (pid > 0) { // 父进程
close(fd[1]); // 关闭写端
char buf[1024] = {0};
int len = -1;
while ((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {
printf("%s", buf);
memset(buf, 0, 1024);
}
wait(NULL);
}
else if (pid == 0) { // 子进程
close(fd[0]); // 关闭读端
dup2(fd[1], STDOUT_FILENO); // 输出重定向到管道的写端
execlp("ps", "ps", "aux", NULL); // 执行 ps aux
perror("execlp");
exit(0);
}
else {
perror("fork");
exit(0);
}
return 0;
}
管道的读写特点
使用管道时,应该注意一下几种特殊情况(假设都是阻塞I/O操作)
- 所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从读端读数据,数据被读完后,再次调用
read
返回0 - 如果有指向管道写端的文件描述符没有关闭,但没有写数据,数据被读完后,再有进程调用
read
会阻塞,直到有数据写入 - 如果所有指向管道读端的文件描述符都关闭了,这时如果有进程往管道中写数据,该进程会收到一个信号
SIGPIPE
,通常会导致进程异常终止 - 如果有指向管道读端的文件描述符没有关闭,而持有管道读端的进程也没有读数据,这时有进程往管道中写数据,管道被写满后会再次
write
会阻塞,直到管道中有位置才能再次写入数据
读管道:
- 管道中有数据,
read
返回实际读到的字节数- 管道中无数据
- 写端被全部关闭,
read
返回0- 写端没有完全关闭,
read
阻塞等待
写管道:
- 管道读端全部关闭,进程异常终止(收到
SIGPIPE
信号)- 管道读端没有全部关闭
- 管道已满,
write
阻塞- 管道没有满,
write
将数据写入,并返回实际写入的字节数
管道设置为非阻塞
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main() {
int pipefd[2]; // 在fork之前创建管道
int ret = pipe(pipefd);
pid_t pid = fork();
if (pid > 0) { // 父进程
printf("I am parent, pid: %d\n", getpid());
close(pipefd[1]); // 关闭写端
char buf[1024] = {0};
int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag
fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag
while (1) {
int len = read(pipefd[0], buf, sizeof(buf)); // 从管道读取端读取数据
printf("len: %d\n", len);
printf("parent rev: %s, pid: %d\n", buf, getpid());
memset(buf, 0, 1024);
sleep(1);
}
}
else if (pid == 0) { // 子进程
printf("I am child, pid: %d\n", getpid());
close(pipefd[0]); // 关闭读端
char buf[1024] = {0};
while (1) {
char *str = "hello, I am child";
write(pipefd[1], str, strlen(str)); // 往写端写数据
sleep(5);
}
}
return 0;
}
I am parent, pid: 29120
len: -1 // 管道里没数据
parent rev: , pid: 29120
I am child, pid: 29121
len: 17 // 子进程往管道里写了数据,父进程读到了
parent rev: hello, I am child, pid: 29120
len: -1 // 子进程休眠,没数据
parent rev: , pid: 29120
len: -1
parent rev: , pid: 29120
len: -1
parent rev: , pid: 29120
len: 17 // 子进程又写了,父进程就读到了
parent rev: hello, I am child, pid: 29120
有名管道
也叫命名管道,FIFO
文件,以文件的形式存在于文件系统中
有名管道(FIFO)与匿名管道(pipe)的不同
- FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容存放于内存中
- 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用
- FIFO 有名字,不相关的进程可以通过打开FIFO文件进行通信
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- pathname: 管道的路径
- mode: 文件权限,跟open的mode是一样的
- 返回值: 成功返回0,失败返回-1
写端
sprintf函数打印到字符串中,要注意字符串的长度要足够容纳打印的内容,否则会出现内存溢出
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 1. 判断管道文件是否存在
int ret = access("fifo1", F_OK);
if (ret == -1) {
printf("make fifo\n");
// 2. 没有就创建管道文件
int ret = mkfifo("fifo1", 0664);
}
// 3. 打开管道文件
int fd = open("fifo1", O_WRONLY);
for (int i = 0; i < 10; i++) {
char buf[1024];
sprintf(buf, "hello, %d\n", i); // 把要写的数据先输入到buf里面
write(fd, buf, strlen(buf)); // 再用buf往FIFO文件里面写数据
sleep(1);
}
close(fd);
return 0;
}
读端
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("fifo1", O_RDONLY); // 打开管道文件
while (1) {
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf)); // 读取数据
if (len == 0) {
printf("write end close...\n");
break;
}
printf("recv buf: %s\n", buf);
}
close(fd);
return 0;
}
两个程序都运行才会有输出
$ ./write
data: 0
data: 1
data: 2
data: 3
data: 4
data: 5
$ ./read
recv buf: hello, 0
recv buf: hello, 1
recv buf: hello, 2
recv buf: hello, 3
recv buf: hello, 4
- 如果先关闭写端,读端返回值
len==0
就break
退出 - 如果先关闭读端,写端会收到
SIGPIPE
信号退出
读管道
- 有数据,read返回实际读到的字节数
- 无数据
- 写端全部关闭,read返回0
- 写端没有全部关闭,read阻塞等待
写管道
- 读端全部关闭,进程异常终止
- 读端没有全部关闭
- 管道满了,write阻塞
- 没有满, write写数据,返回写入的字节数
简易聊天功能
进程A先写后读
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 1. 判断管道文件是否存在
int ret = access("fifo1", F_OK);
if (ret == -1) {
// 2. 创建管道文件
int ret = mkfifo("fifo1", 0664);
}
ret = access("fifo2", F_OK);
if (ret == -1) {
// 2. 创建管道文件
int ret = mkfifo("fifo2", 0664);
}
// 3. 打开管道文件
int fdw = open("fifo1", O_WRONLY); // 用来发送数据
int fdr = open("fifo2", O_RDONLY); // 用来接收数据
// 4. 循环写读数据
char buf[128];
while (1) {
memset(buf, 0, 128);
fgets(buf, 128, stdin);
int ret = write(fdw, buf, strlen(buf));
if (ret == -1) {
perror("write");
exit(0);
}
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if (ret <= 0) {
perror("read");
break;
}
printf("buf: %s\n", buf);
}
close(fdw);
close(fdr);// 关闭文件描述符
return 0;
}
进程B先读后写
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 1. 判断管道文件是否存在
int ret = access("fifo1", F_OK);
if (ret == -1) {
// 2. 创建管道文件
int ret = mkfifo("fifo1", 0664);
}
ret = access("fifo2", F_OK);
if (ret == -1) {
int ret = mkfifo("fifo2", 0664);
}
// 3. 打开管道文件
int fdr = open("fifo1", O_RDONLY); // 用来读
int fdw = open("fifo2", O_WRONLY); // 用来写
// 4. 循环读写数据
char buf[128];
while (1) {
memset(buf, 0, 128);
int ret = read(fdr, buf, 128);
if (ret <= 0) {
perror("read");
break;
}
printf("buf: %s\n", buf);
memset(buf, 0, 128);
fgets(buf, 128, stdin);
ret = write(fdw, buf, strlen(buf));
if (ret == -1) {
perror("write");
exit(0);
}
}
close(fdw);
close(fdr);// 关闭文件描述符
return 0;
}
$ ./a
create fifo1
create fifo2
open fifo1, wait to write
open fifo2, wait to read
hello // 先写数据发送给B,然后阻塞等待读取A的数据
buf: nice to meet you // 读到了,等待终端输入
$ ./b
open fifo1, wait to read
open fifo2, wait to write
buf: hello // 读到了A发送的数据,等待终端输入
nice to meet you // 发送数据给A
有名管道和无名管道的异同点
- 相同点:open打开管道文件以后,在内存中开辟了一块空间,管道的内容在内存中存放,有两个指针——头指针(指向写的位置)和尾指针(指向读的位置)指向它。读写数据都是在给内存的操作,并且都是半双工通讯。
- 区别:有名管道在任意进程之间使用,无名管道在父子进程之间使用。
全双工、半双工、单工通讯的区别
- 单工:方向是固定的,只有一个方向可以写,例如广播
- 半双工:方向不固定,但在某一刻只能有一个方向进行写,例如对讲机
- 全双工:两个方向都可以同时写,例如打电话