一、管道(Pipe)
1. 基本概念
-
管道是一种半双工的通信方式,数据只能单向流动。
-
只能在具有亲缘关系的进程之间使用。
-
管道本质上是一个内核缓冲区,通过文件描述符进行读写操作。
-
包括读端
fd[0]和写端fd[1]。
2. 创建管道
int fd[2];
pipe(fd);
3. 读阻塞(Read Blocking)
- 如果管道为空,读操作会阻塞,直到有数据写入。
示例代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
int fd[2]={0};
int ret = pipe(fd);
if(-1 == ret)
{
perror("pipe error\n");
return 1;
}
pid_t pid = fork();
if(pid>0)
{
close(fd[0]);
int i = 3;
while(i--)
{
printf("father 准备数据\n");
sleep(1);
}
char buf[1024]="hello ,son";
write(fd[1],buf,strlen(buf));
close(fd[1]);
}
else if(0== pid)
{
close(fd[1]);
char buf[1024]={0};
// 读阻塞 示例
read(fd[0],buf,sizeof(buf));
printf("father say:%s\n",buf);
close(fd[0]);
}
else
{
perror("fork");
return 1;
}
return 0;
}
-
子进程调用
read时,父进程尚未写入数据,子进程阻塞等待。 -
父进程睡眠3秒后写入数据,子进程才继续执行。
4. 写阻塞(Write Blocking)
- 如果管道缓冲区满,写操作会阻塞,直到有空间写入。
示例代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
int fd[2]={0};
int ret = pipe(fd);
if(-1 == ret)
{
perror("pipe error\n");
return 1;
}
pid_t pid = fork();
if(pid>0)
{
close(fd[0]);
char buf[1024]={0};
memset(buf,'a',sizeof(buf));
int i = 0 ;
// 写阻塞 示例
for(i=0;i<65;i++)
{
write(fd[1],buf,sizeof(buf));
printf("%d\n",i);
}
close(fd[1]);
}
else if(0== pid)
{
close(fd[1]);
while(1)
{
sleep(1);
}
close(fd[0]);
}
else
{
perror("fork");
return 1;
}
return 0;
}
-
父进程循环写入数据,直到管道满,后续写入会阻塞。
-
子进程不读数据,导致写端持续阻塞。
5. 管道破裂(Broken Pipe)
- 当读端关闭,写端继续写入时,会触发 SIGPIPE 信号,默认行为是终止进程。
示例代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd[2] = {0};
int ret = pipe(fd);
if (-1 == ret)
{
perror("pipe error\n");
return 1;
}
pid_t pid = fork();
if (pid > 0)
{
close(fd[0]);
sleep(3);
char buf[1024] = "hello ,son";
//管道破裂 gdb 查看
//Program received signal SIGPIPE, Broken pipe
write(fd[1], buf, strlen(buf));
printf("--------------\n");
close(fd[1]);
}
else if (0 == pid)
{
close(fd[1]);
close(fd[0]);
exit(1);
}
else
{
perror("fork");
return 1;
}
return 0;
}
- 子进程关闭了读端,父进程写入数据时触发管道破裂。
6. 读取管道结束(EOF)
- 当写端关闭,读端读取完所有数据后,
read返回0,表示管道结束。
示例代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd[2] = {0};
int ret = pipe(fd);
if (-1 == ret)
{
perror("pipe error\n");
return 1;
}
pid_t pid = fork();
if (pid > 0)
{
close(fd[0]);
char buf[1024] = "hello ,son";
write(fd[1], buf, strlen(buf));
close(fd[1]);
exit(0);
}
else if (0 == pid)
{
close(fd[1]);
sleep(3);
while (1)
{
char buf[1024] = {0};
int ret = read(fd[0], buf, sizeof(buf));
// ret ==0 enf of pipe
if(ret<=0)
{
break;
}
printf("father say:%s\n", buf);
}
close(fd[0]);
}
else
{
perror("fork");
return 1;
}
return 0;
}
-
父进程写入数据后关闭写端。
-
子进程读取数据,当
read返回0时,退出循环。
7. 管道复制文件
- 管道可用于父子进程间传递大量数据,如文件复制。
示例代码:
cpp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
int fd[2] = {0};
int ret = pipe(fd);
if (-1 == ret)
{
perror("pipe error\n");
return 1;
}
pid_t pid = fork();
if (pid > 0)
{
close(fd[0]);
int src_fd = open("/home/linux/1.png", O_RDONLY);
if (-1 == src_fd)
{
perror("open src");
return 1;
}
while (1)
{
char buf[1024] = {0};
int rd_ret = read(src_fd, buf, sizeof(buf));
if(rd_ret<=0)
{
break;
}
write(fd[1], buf, rd_ret);
}
close(fd[1]);
close(src_fd);
}
else if (0 == pid)
{
close(fd[1]);
int dst_fd = open("2.png",O_WRONLY|O_CREAT|O_TRUNC,0666);
if (-1 == dst_fd)
{
perror("open dst");
return 1;
}
while(1)
{
char buf[1024]={0};
int rd_ret = read(fd[0],buf,sizeof(buf));
if(rd_ret<=0)
{
break;
}
write(dst_fd,buf,rd_ret);
}
close(fd[0]);
close(dst_fd);
}
else
{
perror("fork");
return 1;
}
return 0;
}
-
父进程读取文件内容,通过管道传递给子进程。
-
子进程从管道读取数据并写入目标文件。
二、命名管道(FIFO)
1. 基本概念
-
FIFO 是一种命名管道,存在于文件系统中。
-
可用于无亲缘关系的进程间通信。
-
使用
mkfifo创建 FIFO 文件。
2. 创建 FIFO
mkfifo("myfifo", 0666);
3. 读写阻塞特性
-
读端先打开:会阻塞,直到写端打开。
-
写端先打开:会阻塞,直到读端打开。
示例代码:
-
写端程序
cpp#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> int main(int argc, char **argv) { int ret = mkfifo("myfifo", 0666); if (-1 == ret) { // 如果是文件已存在的错误,程序就继续运行 if (EEXIST == errno) { } else //如果是其他的错误,进程结束 { perror("mkfifo"); return 1; } } // open 会阻塞, //写段先运行 ,写段会等读段出现, // 读段先运行 ,读段会等写段出现, int fd = open("myfifo", O_WRONLY); if (-1 == fd) { perror("open"); return 1; } char buf[] = "hello,friend...\n"; write(fd, buf, strlen(buf) + 1); close(fd); return 0; } -
读端程序
cpp#include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { int ret = mkfifo("myfifo", 0666); if (-1 == ret) { // 如果是文件已存在的错误,程序就继续运行 if (EEXIST == errno) { } else //如果是其他的错误,进程结束 { perror("mkfifo"); return 1; } } // open 会阻塞, //写段先运行 ,写段会等读段出现, // 读段先运行 ,读段会等写段出现, int fd = open("myfifo", O_RDONLY); if (-1 == fd) { perror("open"); return 1; } char buf[1024] = {0}; read(fd, buf, sizeof(buf)); printf("fifo_w:%s\n", buf); close(fd); // remove("myfifo"); return 0; }
4. 错误处理
- 如果 FIFO 已存在,
mkfifo会失败,errno为EEXIST,通常忽略该错误。
三、文件描述符与 FILE* 转换
1. fileno:从 FILE* 获取文件描述符
FILE *fp = fopen("/etc/passwd", "r");
int fd = fileno(fp);
示例代码:
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
FILE* fp = fopen("/etc/passwd","r");
if(NULL== fp)
{
perror("fopen");
return 1;
}
//fgets/fputs
// FILE* -> int
int fd = fileno(fp);
char buf[100]={0};
read(fd,buf,sizeof(buf)-1);
printf("%s",buf);
fclose(fp);
return 0;
}
-
使用
fopen打开文件得到FILE*。 -
使用
fileno获取对应的文件描述符,用于read等系统调用。
2. fdopen:从文件描述符获取 FILE*
int fd = open("/etc/passwd", O_RDONLY);
FILE *fp = fdopen(fd, "r");
示例代码:
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int fd = open("/etc/passwd",O_RDONLY);
if(-1== fd)
{
perror("open");
return 1;
}
//read /write
char buf[100]={0};
FILE* fp = fdopen(fd,"r") ;
if(NULL == fp)
{
perror("fdopen");
return 1;
}
fgets(buf,sizeof(buf),fp);
printf("buf :%s",buf);
fclose(fp);
return 0;
}
-
使用
open打开文件得到文件描述符。 -
使用
fdopen转换为FILE*,用于fgets等标准 I/O 函数。
四、总结
| 通信方式 | 特点 | 适用场景 |
|---|---|---|
| 无名管道 | 半双工、亲缘进程、内存缓冲区 | 父子进程间通信 |
| 命名管道 | 有名字、文件系统可见 | 无亲缘关系进程间通信 |
| 文件描述符 | 系统调用、低级 I/O | 直接操作文件或设备 |
| FILE* | 标准 I/O、带缓冲区 | 高级文本或流式操作 |
注意事项:
-
管道通信时,及时关闭不需要的文件描述符。
-
注意处理 SIGPIPE 信号,避免进程意外终止。
-
FIFO 使用时注意读写端的阻塞与同步。
-
文件描述符与 FILE* 可以相互转换,便于混合使用系统调用和标准 I/O。