Linux进程间通信之管道

一,进程通信简介

进程间通信(Inter-Process Communication, IPC)是指在两个或者多个不同进程
间传递或者交换信息,通过信息的传递建立几个进程间的联系,协调⼀个系统中的
多个进程之间的行为。
每个进程各⾃有不同的⽤户地址空间,任何⼀个进程的全局变量在另⼀个进程中都
看不到,所以进程之间要交换数据必须通过内核,在内核中开辟⼀块缓冲区,进程A把
数据从⽤户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读⾛,内核提供的这种
机制称为进程间通信。
不同进程间的通信本质是让不同的进程看到⼀份公共的资源来实现通信。
进程通信应⽤场景:
  1.数据传输:⼀个进程需要将它的数据发送给另⼀个进程,发送的数据量在⼀个字节到⼏兆字节之间。
  2.共享数据:多个进程想要操作共享数据,⼀个进程对共享数据的修改,别的进程应该⽴刻看到。
  3.通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它(它们)发⽣了某种事件(如进程终⽌时要通知⽗进程)。
  4.进程控制:有些进程希望完全控制另⼀个进程的执⾏(如Debug进程),此时控制进程希望能够拦截另⼀个进程的所有陷⼊和异常,并能够及时知道它的状态改变。
进程间的通信⽅式主要有:
  
1)管道(有名管道和⽆名管道)
2)消息队列
3)共享内存
4)信号量
5)信号
6 ) socket
本章先讲述进程通信中的管道。

二,进程通信之管道

1.1 进程通信之管道简介

Linux中管道是Linux中的⼀种极为重要的 并且⽐较古⽼的通信⽅式,我们把⼀个进程链接到另⼀个进程的⼀个数据流称为⼀个"管道"; 管道即⼀个大小固定的缓冲区,也是⼀种⽂件。它可以将⼀个程序的输出传递为另⼀个程序的输⼊。
管道分为两种:⼀种是匿名管道,⼀种是命名管道。
其中⽆名管道在实际中⽤处最为⼴泛,两者最⼤的区别在于有名管道就像数据结构中所讲的队列。有先进先出原则,可⽤函数mkfifo()创建,而无名管道只能用于有亲缘关系的进程之间。

1.2 无名管道

1)无名管道简介

⽆名管道是⼀种功半双⼯的通信⽅式,数据只能单向流动,⽽且只能在具有亲缘关系的进程之间通信,进程间的亲缘关系通常是指⽗⼦进程关系。平时我们说的管道通常指匿名管道。在 Linux 中,管道的实现并没有使⽤专⻔的数据结构,⽽是借助了⽂件系统的file结构和VFS的索引节点inode。
管道提供流式服务,⼀般来说进程退出,管道释放,所以管道的⽣命周期随进程。


在Linux Shell中,匿名管道可以通过管道符号 | 创建,例如: cat my.txt | g rep aa 。此时cat是⽗进程,grep是⼦进程,cat进程的标准输出通过管道对接 grep进程的标准输⼊。

创建⼀个管道后,会获取⼀对描述符,⽤于读取和写⼊,写进程在管道的尾端写⼊数据,读进程在管道的首端读出数据。数据读出后将从管道中移⾛,其它读进程都不能再读到这些数据。
无名管道的特点
(1)半双⼯,数据在同⼀时刻只能在⼀个⽅向上流动
(2)管道不是普通的⽂件,不属于某个⽂件系统,其只存在于内存中
(3)管道没有名字,只能在具有公共祖先的进程之间使⽤
(4)管道的缓冲区⼤⼩是有限的,在linux中,该缓冲区的⼤⼩固定为4k。
查看管道容量可以通过ulimit -a命令,查看到的pipo size定义的是内核管道缓冲区的⼤⼩,这个值的⼤⼩是由内核设定的。

2)管道的创建

函数名称:pipe
函数原型:int pipe(int fd[2])
所属头文件:#include <unistd.h>
功能:创建⼀个匿名管道
参数说明:
  fd:管道的2个⽂件描述符,fd表示int类型数组的⾸地址。当管道建⽴时,它会自动创建这两个⽂件描述符:
  fd[0]是管道读端的描述符
  fd[1]是管道写端的描述符
返回值:成功返回0,否则返回-1,错误码放在errno 。
例1、创建匿名管道,查看管道两端的描述符

#include <stdio.h>
#include <unistd.h> // 包含了 pipe, fork, write, read, close 函数
#include <stdlib.h>  // 包含了 perror, exit 函数
#include <string.h>  // 包含了 strlen 函数
#include <sys/wait.h> // 包含了 wait 函数

int main() {
    int fd[2]; // 定义一个数组,fd[0]用于读,fd[1]用于写
    pid_t pid;
    char buffer[100]; // 用于存放读取数据的缓冲区

    // 1. 创建匿名管道
    int ret = pipe(fd);
    if (ret < 0) {
        // 如果创建失败,打印错误信息并退出
        perror("pipe create failed");
        return -1;
    }

    // 打印成功创建的读写两端的文件描述符
    printf("管道创建成功,读端 fd[0]:%d, 写端 fd[1]:%d\n", fd[0], fd[1]);

    // 2. 创建子进程
    pid = fork();
    if (pid < 0) {
        // 如果创建子进程失败
        perror("fork failed");
        return -1;
    } 
    else if (pid == 0) {
        // --- 这里是子进程的代码 ---
        // 子进程负责从管道读取数据

        printf("这里是子进程 (PID: %d),准备从管道读取数据...\n", getpid());

        // **关键步骤**: 子进程不写入,所以关闭写端。
        close(fd[1]); 
        
        // 从管道的读端(fd[0])读取数据
        ssize_t bytes_read = read(fd[0], buffer, sizeof(buffer) - 1);
        if (bytes_read > 0) {
            // 为读取的字符串添加结束符
            buffer[bytes_read] = '\0';
            printf("子进程收到消息: \"%s\"\n", buffer);
        } else {
            perror("child read failed");
        }

        // **关键步骤**: 读取完毕,关闭读端。
        close(fd[0]);
        exit(0); // 子进程任务完成,退出

    } else {
        // --- 这里是父进程的代码 ---
        // 父进程负责向管道写入数据

        printf("这里是父进程 (PID: %d),准备向管道写入数据...\n", getpid());

        // **关键步骤**: 父进程不读取,所以关闭读端。
        close(fd[0]);

        const char* message = "Hello child process, I am your father!";
        
        // 向管道的写端(fd[1])写入数据
        write(fd[1], message, strlen(message));

        // **关键步骤**: 写入完毕,关闭写端。这会向读端发送一个EOF信号。
        close(fd[1]);

        // 等待子进程结束,回收其资源,防止僵尸进程
        wait(NULL); 
        printf("父进程:子进程已结束,程序退出。\n");
    }

    return 0;
}

运行结果:

管道对于管道两端的进程⽽⾔,就是⼀个⽂件,但它不是普通的⽂件,它不属于某种⽂件系统,⽽是⾃⽴⻔户,单独构成⼀种⽂件系统,并且只存在于内存中。
管道的读写⽤最基本的read()/write()系统调⽤来实现。
例2、单进程读写⽆名管道
注意:当管道读取端没有关闭且管道已满时,write()会被阻塞;⽽当管道写⼊端没有关闭且管道为空时,read()会被阻塞。当然,如果管道的读写两端都被关闭,管道就会消失。
例3、父子进程通信
⽗进程中创建管道,⼦进程负责写数据,⽗进程负责读取数据
数据只能向⼀个⽅向流动;需要双⽅通信时,需要建⽴起两个管道。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2]; // fd[0] 是管道的读取端, fd[1] 是管道的写入端
    pid_t pid;
    char read_buffer[128]; // 用于父进程读取数据的缓冲区

    // 1. 在 fork() 之前创建管道
    if (pipe(fd) == -1) {
        perror("pipe create failed");
        return 1;
    }

    // 2. 创建子进程
    pid = fork();

    if (pid < 0) {
        perror("fork failed");
        return 1;
    }

    if (pid == 0) {
        // --- 子进程代码块 (写入数据) ---

        printf("子进程 (PID: %d): 准备写入数据到管道...\n", getpid());

        // **关键步骤**: 子进程只负责写,因此关闭它不需要的读取端
        close(fd[0]);

        const char* message = "Hello Father, this is your child process!";
        
        // 向管道的写入端 (fd[1]) 写入消息
        write(fd[1], message, strlen(message));
        
        // **关键步骤**: 写入完成后,关闭写入端。
        // 这会向管道的读取端发送一个文件结束(EOF)信号,
        // 告诉正在读取的进程数据已经发送完毕。
        close(fd[1]); 

        printf("子进程: 数据已发送,现在退出。\n");
        exit(0); // 任务完成,子进程退出

    } else {
        // --- 父进程代码块 (读取数据) ---
        
        printf("父进程 (PID: %d): 等待从管道读取子进程的数据...\n", getpid());
        
        // **关键步骤**: 父进程只负责读,因此关闭它不需要的写入端
        close(fd[1]);

        // 从管道的读取端 (fd[0]) 读取数据
        // read() 会阻塞,直到管道中有数据或者所有写入端都已关闭
        ssize_t num_bytes = read(fd[0], read_buffer, sizeof(read_buffer) - 1);
        
        if (num_bytes > 0) {
            // 将读取到的数据变成一个合法的C字符串 (添加空终止符)
            read_buffer[num_bytes] = '\0';
            printf("父进程: 成功收到消息: \"%s\"\n", read_buffer);
        } else {
            // 如果read返回0或-1,说明读取失败或管道已关闭
            perror("parent read failed");
        }

        // **关键步骤**: 读取完毕后,关闭读取端
        close(fd[0]);

        // 等待子进程完全终止,防止其成为僵尸进程
        wait(NULL);

        printf("父进程: 已回收子进程,程序结束。\n");
    }

    return 0;
}

输出结果:
注意:必须在系统调⽤fork之前,调⽤pipe,否则⼦进程将不会继承管道的⽂件描
述符。

1.3 有名管道

1)有名管道简介

在⽂件系统中存在⼀个⽂件标识(⽂件名),但是管道⽂件不占据磁盘空间,需要传递的数据存在内存区域。
特点:
有名管道可以使互不相关的两个进程互相通信。
有名管道可以通过路径名来指出,并且在⽂件系统中可⻅,但内容存放在内存中
访问命名管道⽂件与访问⽂件系统中的其他⽂件相似,都是需要先⽤open打开⽂件,然
后对⽂件进⾏读写数据。两个进程结束后,信息会自动消失,但管道⽂件路径依 然存在。
进程通过⽂件IO来操作有名管道,可以⽤read()和write()来读和写
有名管道遵循先进先出规则
不⽀持如lseek() 操作

2)有名管道创建

1、可以通过命令⾏创建: mkfifo filename
2、通过系统函数调⽤创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
参数说明:pathname:欲建⽴的FIFO ⽂件路径和⽂件名;
 mode: FIFO ⽂件权限
返回值:成功,返回0,否则返回-1,错误码放在errno
mode参数可能的值主要有:O_RDONLY,O_WRONLY,RDWR(不推荐⽤),O_NONBLOCK
其中选项O_NONBLOCK表示⾮阻塞,加上这个选项后,表示open调⽤是⾮阻塞的,
如果没有这个选项,则表示open调⽤是阻塞的。
//删除有名管道
int unlink(const char *path);//path为创建的有名管道
注意:如果写进程打开,读进程没有打开,则会阻塞写进程。
对于阻塞问题的说明:
对于以只读⽅式(O_RDONLY)打开的FIFO⽂件,如果open调⽤是阻塞的(即第⼆个参数为O_RDONLY),除⾮有⼀个进程以写⽅式打开同⼀个FIFO,否则它不会返回;
如果open调⽤是⾮阻塞的的(即第⼆个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写⽅式打开同⼀个FIFO⽂件,open调⽤将成功并⽴即返回。
对于以只写⽅式(O_WRONLY)打开的FIFO⽂件,如果open调⽤是阻塞的(即第⼆个参数为O_WRONLY),open调⽤将被阻塞,直到有⼀个进程以只读⽅式打开同⼀个FIFO⽂件为⽌;如果open调⽤是⾮阻塞的(即第⼆个参数为O_WRONLY |O_NONBLOCK),open总会⽴即返回,但如果没有其他进程以只读⽅式打开同⼀个FIFO⽂件,open调⽤将返回-1,并且FIFO也不会被打开。

3)有名管道读写操作

1、两个进程已经完成打开有名管道操作,阻塞读操作按以下⽅式执⾏:
a)如果管道中⽆数据,读进程默认阻塞;  
b)如果管道中有数据,但数据⼩于欲读取数据量,读出所有数据后返回;
c)如果管道中有数据,但数据⼤于欲读取数据量,读出期望数据后返回;
2、两个进程已经完成打开有名管道操作,阻塞写操作按以下⽅式执⾏:  
a)如果管道中⽆空间,写操作阻塞;
b)如果管道中有空间,但空间⼩于欲写⼊数据量,写满空间后阻塞;
c)如果管道中有空间,但空间⼤于欲写⼊数据量,写⼈数据后返回;

需要注意的是系统对任意时刻的FIFO的⻓度是有限制的,它由#define PIPE_BUF
定义,在头⽂件limits.h中。
例1:有名管道通的基本使⽤(单进程)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

// 定义管道在文件系统中的路径
#define FIFO_PATH "/tmp/myfifo"

int main() {
    char write_buf[] = "Hello FIFO!";
    char read_buf[128];
    int fd;

    // 1. 创建有名管道
    // mkfifo() 的第二个参数是文件权限,类似 chmod
    // 如果文件已存在,会返回-1,所以通常需要检查错误
    if (mkfifo(FIFO_PATH, 0666) == -1) {
        // 使用 perror 可以打印出具体的错误原因,例如 "File exists"
        perror("mkfifo error"); 
        // 如果只是因为文件已存在,我们依然可以继续使用它
    }
    printf("有名管道 '%s' 创建/已存在。\n", FIFO_PATH);

    // 2. 以只写模式打开管道
    printf("正在以只写方式打开管道...\n");
    fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open for write error");
        return 1;
    }
    
    // 3. 向管道写入数据
    write(fd, write_buf, strlen(write_buf));
    printf("已向管道写入: \"%s\"\n", write_buf);
    close(fd); // 关闭文件描述符

    // 4. 以只读模式打开管道
    printf("正在以只读方式打开管道...\n");
    fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open for read error");
        return 1;
    }

    // 5. 从管道读取数据
    ssize_t bytes_read = read(fd, read_buf, sizeof(read_buf) - 1);
    read_buf[bytes_read] = '\0'; // 确保字符串正确结尾
    printf("从管道读出: \"%s\"\n", read_buf);
    close(fd);

    // 6. 删除管道文件
    if (unlink(FIFO_PATH) == 0) {
        printf("有名管道 '%s' 已被成功删除。\n", FIFO_PATH);
    }

    return 0;
}

运行结果:

例2:有名管道的⽗⼦进程间通信
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>

#define FIFO_PATH "/tmp/myfifo_pc"

int main() {
    // 1. 在 fork 之前创建管道
    if (mkfifo(FIFO_PATH, 0666) == -1) {
        perror("mkfifo error");
    }

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork error");
        return 1;
    }

    if (pid == 0) {
        // --- 子进程: 写入者 ---
        printf("子进程 (PID: %d): 准备写入管道。\n", getpid());
        
        // 以只写方式打开管道
        int fd = open(FIFO_PATH, O_WRONLY);
        if (fd == -1) {
            perror("child open error");
            exit(1);
        }

        const char* msg = "Message from child process.";
        write(fd, msg, strlen(msg));
        close(fd);

        printf("子进程: 消息已发送,退出。\n");
        exit(0);

    } else {
        // --- 父进程: 读取者 ---
        printf("父进程 (PID: %d): 等待从管道读取数据。\n", getpid());

        // 以只读方式打开管道
        // 注意:open调用会阻塞,直到有另一个进程以写入方式打开了此管道
        int fd = open(FIFO_PATH, O_RDONLY);
        if (fd == -1) {
            perror("parent open error");
            return 1;
        }

        char read_buf[128];
        ssize_t bytes_read = read(fd, read_buf, sizeof(read_buf) - 1);
        read_buf[bytes_read] = '\0';
        
        printf("父进程: 收到消息 -> \"%s\"\n", read_buf);
        close(fd);
        
        wait(NULL); // 等待子进程结束,回收资源

        // 2. 由父进程负责清理管道文件
        unlink(FIFO_PATH);
        printf("父进程: 清理管道并退出。\n");
    }
    
    return 0;
}

运行结果:
例3:有名管道的不相关进程间通信
不相关进程之间通过有名管道进⾏通信,⾸先需要创建两个程序,⼀ 个负责数据的⽣成,另⼀个负责数据的读取。写端程序如下:
通过mkfifo来创建FIFO⽂件,并且以只写 的⽅式打开,只有当两边的管道都打开的时候才能写进去,否则阻塞在 write() 函数上,如果管道另⼀端打开后被关闭,那么这个时候如果继续 write() FIFO管道,会发出信号 SIGPIPE
如果管道⽂件存在,则直接只读⽅式打开,如果另⼀端没有被打开,则阻塞在 rea
d() 函数上,如果另⼀端打开后关闭,则 read() ⼀直读到EOF也就是0个字节。
运⾏程序是,先运⾏写数据端,再运⾏读数据端。
程序一:写入者 (fifo_writer.c):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FIFO_PATH "/tmp/myfifo_comm"

int main() {
    // 1. 创建管道 (如果不存在)
    mkfifo(FIFO_PATH, 0666);
    
    printf("写入端: 等待读取端连接...\n");
    
    // 2. 以只写方式打开,这会阻塞直到有进程以读方式打开
    int fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open writer error");
        return 1;
    }
    
    printf("写入端: 连接成功,请输入消息:\n> ");
    
    char msg_buf[128];
    // 从标准输入读取一行
    fgets(msg_buf, sizeof(msg_buf), stdin);
    
    // 3. 写入管道
    write(fd, msg_buf, strlen(msg_buf));
    close(fd);
    
    printf("写入端: 消息已发送,程序结束。\n");
    
    return 0;
}
程序二:读取者 (fifo_reader.c):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define FIFO_PATH "/tmp/myfifo_comm"

int main() {
    printf("读取端: 正在打开管道...\n");

    // 1. 以只读方式打开,会阻塞直到有进程以写方式打开
    int fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open reader error");
        return 1;
    }
    
    printf("读取端: 连接成功,等待消息...\n");
    
    char read_buf[128];
    // 2. 读取管道数据
    ssize_t bytes_read = read(fd, read_buf, sizeof(read_buf) - 1);
    
    if (bytes_read > 0) {
        read_buf[bytes_read] = '\0';
        printf("读取端: 收到消息 -> %s", read_buf);
    }
    close(fd);
    
    // 3. 读取者负责清理管道文件
    unlink(FIFO_PATH);
    printf("读取端: 清理管道并退出。\n");
    
    return 0;
}

运行结果:
将读写程序分成两个终端运行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值