应用——管道与文件描述符

一、管道(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 会失败,errnoEEXIST,通常忽略该错误。

三、文件描述符与 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。

相关推荐
Aliex_git几秒前
性能优化 - Vue 日常实践优化
前端·javascript·vue.js·笔记·学习·性能优化
栈与堆几秒前
LeetCode-88-合并两个有序数组
java·开发语言·数据结构·python·算法·leetcode·rust
A-花开堪折几秒前
Qemu-NUC980(十一):SPI Controller
linux·arm开发·驱动开发·嵌入式硬件
RisunJan3 分钟前
Linux命令-ipcrm命令(删除Linux系统中的进程间通信(IPC)资源)
linux·运维·服务器
Joren的学习记录5 分钟前
【Linux运维大神系列】Kubernetes详解2(kubeadm部署k8s1.27单节点集群)
linux·运维·kubernetes
创作者mateo7 分钟前
PyTorch 入门学习笔记(实战篇)二
pytorch·笔记·学习
源代码•宸8 分钟前
Leetcode—712. 两个字符串的最小ASCII删除和【中等】
开发语言·后端·算法·leetcode·职场和发展·golang·dp
小当家.1059 分钟前
JVM八股详解(上部):核心原理与内存管理
java·jvm·学习·面试
lbb 小魔仙9 分钟前
【Linux】K8s 集群搭建避坑指南:基于 Linux 内核参数调优的生产级部署方案
linux·运维·kubernetes
逑之11 分钟前
C语言笔记8:操作符
c语言·开发语言·笔记