C语言 多进程编程(二)管道

简介: 本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。

进程间通信

关于多进程的通信

  • linux 下的进程通信⼿段基本上是从 Unix 平台上的进程通信⼿段继承⽽来的。
  • 每个进程都有⾃⼰独⽴的地址空间, 当两个不同进程需要进⾏交互时, 就需要使⽤进程间通讯
  • 进程间通讯分为单个计算机的进程间通讯与局域⽹的计算机的进程间通讯
  • 进程间通讯⽅式有 管道, 信号, 消息队列, 共享内存,⽹络

管道

  • 管道分为 ⽆名管道(匿名管道) 与 有名管道
    • ⽆名管道⽤于⽗⼦进程之间通讯
    • 有名管道⽤于任意进程之间通讯

管道的本质是在内存建⽴⼀段缓冲区,由操作系统内核来负责创建与管理, 具体通讯模型如下:
img_31.png

无名管道(匿名管道)

- 无名管道属于单向通讯
- ⽆名管道只能⽤于 ⽗⼦进程通讯
- ⽆名管道发送端叫做 写端, 接收端叫做 读端
- ⽆名管道读端与写端抽象成两个⽂件进⾏操作,在⽆名管道创建成功之后,则会返回读端与写端的⽂件描述符

img_32.png

创建无名管道

  • 创建⽆名管道需要系统调用 pipe()
  • pipe() 函数原型如下:
#include <unistd.h>

int pipe(int pipefd[2]);
  • pipefd[2] 是一个数组,数组的两个元素分别代表读端和写端的⽂件描述符
  • pipefd[0] 代表读端,pipefd[1] 代表写端
  • 函数成功返回 0,否则返回 -1 并设置 errno 变量

示例:创建子进程,父进程通过管道向子进程发送消息

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
   
   
    //创建子进程,父进程通过管道向子进程发送消息
    pid_t cpid;//子进程ID
    int ret;//返回值
    int pipefd[2];//管道文件描述符

    ret = pipe(pipefd);//创建管道,内核会将文件描述符号放入pipefd数组中
    if (ret < 0) {
   
   //创建失败
        perror("pipe");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1

    }
    cpid = fork();//创建子进程
    if (cpid < 0) {
   
   //创建失败
        perror("fork");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1

    }else if (cpid == 0) {
   
   //子进程

        ssize_t rbytes;//此变量在父进程不存在,在子进程中使用 //ssize_t是read函数的返回类型,表示读取的字节数
        close(pipefd[1]);//关闭写端,子进程只读

        //操作管道
        char buf[1024];
        printf("子进程开始等待父进程发送消息...\n");
        rbytes = read(pipefd[0], buf, sizeof(buf));//从管道中读取数据,当管道没有数据,没有相应的进程往管道写,这里会阻塞
        if (rbytes < 0) {
   
   //读取失败
            perror("read");//打印错误信息
            close(pipefd[0]);//关闭读端
            exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
        }
        //读到了数据
        printf("子进程收到消息: %s\n", buf);//打印接收到的消息
        //操作结束


        close(pipefd[0]);//最后关闭子进程的读端,避免阻塞
    }else if (cpid > 0) {
   
   //父进程
        ssize_t wbytes;//此变量在父进程中使用,在子进程不存在
        close(pipefd[0]);//关闭读端,父进程只写

        //操作管道
        char buf[1024];
        strcpy(buf, "Hello, child!");//发送的消息
        wbytes = write(pipefd[1], buf, strlen(buf));
        if (wbytes < 0) {
   
   //发送失败
            perror("write");//打印错误信息
            wait(NULL);//此时写错误要,等待子进程结束
            close(pipefd[1]);//关闭写端
            exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
        }
        //发送成功
        printf("父进程发送消息: %s\n", buf);//打印发送的消息
        //操作结束

        close(pipefd[1]);//最后关闭父进程的写端,
        wait(NULL);//正常等待子进程结束

    }
    return 0;
}

无名管道(匿名管道) 的特点

当管道为空,读管道会阻塞读进程

当管道的写端被关闭了,从管道中读取剩余的数据后 会返回0,表示管道已经被关闭

在写入管道时,确保不会超过管道的容量64k, PIPE_BUF;//管道缓冲区大小

  • 当写入的数据达到PIPE_BUF时,write函数会阻塞,直到管道中有足够的空间以原子的方式完成方式

  • 当写入的数据大于PIPE_BUF时,write函数会尽可能多的传输数据,充满这个管道

管道的大小是有限的,不能让父/子进程同时对管道进行读/写操作,否则会导致数据混乱

当一个进程试图向一个管道写入数据,但是没有任何进程拥有该管道的打开着的读取描述符,内核向写入进程发送一个SIGPIPE信号

有名管道(命名管道)

有名管道是在 ⽂件系统中可⻅的⽂件, 但是不占⽤磁盘空间, 仍然在内存中, 可以通过 mkfifo 命令创建有名管道

有名管道与⽆名管道⼀样,在应⽤层是基于⽂件接口进⾏操作

有名管道⽤于 任意进程之间的通讯, 当管道为空时, 读进程会阻塞.
img_33.png

在文件系统中:
文件名以 "p"开头,说明是管道文件,他占用的是内存空间, 并不占用磁盘空间

创建有名管道需要调⽤ mkfifo() 函数

函数头文件:

#include <sys/types.h>
#include <sys/stat.h>

函数原型:

int mkfifo(const char *pathname, mode_t mode);

参数:

  • pathname: 管道文件路径名
  • mode: 管道文件的权限, 如 0666

函数返回值:
成功返回0, 失败返回-1, 并设置errno变量

示例:创建两个没有关联关系的进程,通过有名管道通信

  • 读的进程代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
/*
 * 无名管道(匿名管道) 和 有名管道(命名管道)
 * */

/*
 * 无名管道(匿名管道)
 */
//创建子进程,父进程通过管道向子进程发送消息
int main1() {
   
   

    pid_t cpid;//子进程ID
    int ret;//返回值
    int pipefd[2];//管道文件描述符

    ret = pipe(pipefd);//创建管道,内核会将文件描述符号放入pipefd数组中
    if (ret < 0) {
   
   //创建失败
        perror("pipe");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1

    }
    cpid = fork();//创建子进程
    if (cpid < 0) {
   
   //创建失败
        perror("fork");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1

    }else if (cpid == 0) {
   
   //子进程

        ssize_t rbytes;//此变量在父进程不存在,在子进程中使用 //ssize_t是read函数的返回类型,表示读取的字节数
        close(pipefd[1]);//关闭写端,子进程只读

        //操作管道
        char buf[1024];
        printf("子进程开始等待父进程发送消息...\n");
        rbytes = read(pipefd[0], buf, sizeof(buf));//从管道中读取数据,当管道没有数据,没有相应的进程往管道写,这里会阻塞
        if (rbytes < 0) {
   
   //读取失败
            perror("read");//打印错误信息
            close(pipefd[0]);//关闭读端
            exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
        }
        //读到了数据
        printf("子进程收到消息: %s\n", buf);//打印接收到的消息
        //操作结束


        close(pipefd[0]);//最后关闭子进程的读端,避免阻塞
    }else if (cpid > 0) {
   
   //父进程
        ssize_t wbytes;//此变量在父进程中使用,在子进程不存在
        close(pipefd[0]);//关闭读端,父进程只写

        //操作管道
        char buf[1024];
        strcpy(buf, "Hello, child!");//发送的消息
        wbytes = write(pipefd[1], buf, strlen(buf));
        if (wbytes < 0) {
   
   //发送失败
            perror("write");//打印错误信息
            wait(NULL);//此时写错误要,等待子进程结束
            close(pipefd[1]);//关闭写端
            exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
        }
        //发送成功
        printf("父进程发送消息: %s\n", buf);//打印发送的消息
        //操作结束

        close(pipefd[1]);//最后关闭父进程的写端,
        wait(NULL);//正常等待子进程结束

    }
    return 0;
}


/*
 * 有名管道(命名管道)
 */
//创建两个没有关联关系的进程,通过有名管道通信
//下方是读的进程代码,写的进程代码在另外一个文件Process2中
#define PATHNAME "/home/gopher/ClionWork/fifo_test"
int main(){
   
   
    int  ret;//创建有名管道返回值
    int fd;//打开文件返回值
    ssize_t rbytes;//读取文件返回值
    char buf[1024]={
   
   0};//读取文件缓冲区

    ret= mkfifo(PATHNAME, 0666);//创建有名管道

//  如果管道存在了,下面代码判断条件会报错
//    if (ret < 0) {//创建失败
//        perror("mkfifo");//打印错误信息
//        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
//    }

    //使用新的判断条件保证管道存在,不会报错
    //关于  errno  文章末尾有更多介绍 他是一个全局变量,用来存放系统调用的错误信息,它用于指示最近一次系统调用或库函数调用中发生的错误类型。
    //errno 等于 ( EEXIST = 17 表示文件存在 EEXIST 是 POSIX 标准中的一个宏) 表示管道已经存在
    if (ret < 0) {
   
    // 创建失败
        if (errno == EEXIST) {
   
   
            // 管道已经存在
            printf("管道已存在,继续执行程序。\n");
        } else {
   
   
            // 打印错误信息
            perror("mkfifo");
            // 退出程序
            exit(EXIT_FAILURE);
        }
    } else {
   
   
        printf("管道创建成功。\n");
    }


    fd = open(PATHNAME, O_RDWR);//打开有名管道
    if (fd < 0) {
   
   //打开失败
        perror("open");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
    }
    //开始读
    printf("进程等待读消息...\n");
    rbytes = read(fd, buf, sizeof(buf));
    if (rbytes < 0) {
   
   //读取失败
        perror("read");//打印错误信息
        close(fd);//关闭有名管道
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
    }
    printf("进程收到消息: %s\n", buf);//打印接收到的消息
    //读结束

    close(fd);//关闭有名管道
    return 0;
}
  • 写的进程代码

/*
 * 有名管道(命名管道)
 */
//创建两个没有关联关系的进程,通过有名管道通信
//这是写的进程
#define PATHNAME "/home/gopher/ClionWork/fifo_test"
int main(){
   
   
//    int  ret;//创建有名管道返回值
    int fd;//打开文件返回值
    ssize_t wbytes;//读取文件返回值
    char buf[]={
   
   "hello world"};//读取文件缓冲区

//读的进程已经创建了
//    ret= mkfifo(PATHNAME, 0666);//创建有名管道
//    if (ret < 0) {//创建失败
//        perror("mkfifo");//打印错误信息
//        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
//    }

    fd = open(PATHNAME, O_WRONLY);//打开有名管道
    if (fd < 0) {
   
   //打开失败
        perror("open");//打印错误信息
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
    }
    //写的进程
    printf("进程写入消息...\n");
    wbytes = write(fd, buf, strlen(buf));//写入消息到有名管道
    if (wbytes < 0) {
   
   //读取失败
        perror("read");//打印错误信息
        close(fd);//关闭有名管道
        exit(EXIT_FAILURE);//退出程序 EXIT_FAILURE 1
    }

    close(fd);//关闭有名管道
    return 0;
}

注意:

如果有名管道的⼀端以只读⽅式打开,它会阻塞到另⼀端以写的⽅式 (只写,读写)

如果有名管道的⼀端以只写⽅式打开,它会阻塞到另⼀端以读的⽅式 (只读,读写)

如果有名管道的⼀端以读写⽅式打开,它不会阻塞,因为读写方式既支持读取又支持写入,满足了管道通信的要求。

有名管道在不同情况下的阻塞特性。当有名管道的一端以只读或只写模式打开时,会等待另一端的对应模式(写或读)来完成通信,从而进行阻塞。而当管道的一端以读写模式打开时,由于该模式同时支持读取和写入,所以不会出现阻塞现象。

确保在合适的场景下选择合适的文件打开模式,避免不必要的阻塞,从而提高程序运行的效率和稳定性。

缺点

  • 打开时需要读写一起进行,否则会阻塞,管道大小是64k
  • 半双工的工作模式,如果和多个进程通信,需要创建多个管道

    半双工(Half-Duplex)通信是一种数据传输模式,允许设备在同一个信道上进行双向通信,但不能同时进行。

    这意味着同一时间内,数据只能在一个方向上传输,而另一个方向则需要等待。

    举个例子,对讲机就是典型的半双工设备。当一个人在发言时,另一方只能接收,无法同时发言。要进行双向交流,双方需要轮流按下发话键来切换发送和接收状态。

    半双工通信的优点包括:

    节省带宽:因为信道资源是共享的,减少了频带的使用。

    简单实现:相对于全双工,半双工的实现原理和机制更为简单,有助于降低设备成本。

    缺点则主要在于效率较低,因为在同一个时间内只能单方向传输数据,可能导致通信延迟。

    优点

    • 可以实现任意进程间的通信,操作起来和文件操作一样

关于判断管道是否存在,可以使用errno

errno

使用他要引入errno.h

errno 是一个全局变量,在几乎所有的 C 语言和 POSIX 标准的编程环境中都存在。

它用于指示最近一次系统调用或库函数调用中发生的错误类型。

即,通过检查 errno 的值,程序员可以确定错误发生的原因并进行相应的处理。

每次系统调用或库函数调用失败时,会设置 errno 的值。
不同的错误类型由不同的错误码标示,
例如,EEXIST 表示试图创建一个已经存在的文件或目录,ENOMEM 表示内存不足等等。

在 POSIX 的环境中,以下是一些常用的错误码及其说明:
所有错误码都定义在 errno-base.h 头文件中。

EEXIST - 文件已存在。

ENOENT - 文件或目录不存在。

ENOMEM - 内存不足。

EACCES - 权限被拒绝。

EINVAL - 无效的参数。

EBUSY - 设备或资源正忙。

EAGAIN - 资源暂时不可用。

EPIPE - 管道已关闭。

EINTR - 系统调用被中断。

EIO - 输入/输出错误。

EISDIR - 试图打开一个目录。

EFBIG - 文件太大。

EFBIG - 文件太大。

EOVERFLOW - 值过大。

errno 提供了一种标准化的方式来报告和处理错误,使得程序可靠和健壮。

相关文章
|
10天前
|
安全 开发者 Python
揭秘Python IPC:进程间的秘密对话,让你的系统编程更上一层楼
【9月更文挑战第8天】在系统编程中,进程间通信(IPC)是实现多进程协作的关键技术。IPC机制如管道、队列、共享内存和套接字,使进程能在独立内存空间中共享信息,提升系统并发性和灵活性。Python提供了丰富的IPC工具,如`multiprocessing.Pipe()`和`multiprocessing.Queue()`,简化了进程间通信的实现。本文将从理论到实践,详细介绍各种IPC机制的特点和应用场景,帮助开发者构建高效、可靠的多进程应用。掌握Python IPC,让系统编程更加得心应手。
13 4
|
14天前
|
网络协议 C语言
C语言 网络编程(十三)并发的TCP服务端-以进程完成功能
这段代码实现了一个基于TCP协议的多进程并发服务端和客户端程序。服务端通过创建子进程来处理多个客户端连接,解决了粘包问题,并支持不定长数据传输。客户端则循环发送数据并接收服务端回传的信息,同样处理了粘包问题。程序通过自定义的数据长度前缀确保了数据的完整性和准确性。
|
14天前
|
C语言
C语言 网络编程(八)并发的UDP服务端 以进程完成功能
这段代码展示了如何使用多进程处理 UDP 客户端和服务端通信。客户端通过发送登录请求与服务端建立连接,并与服务端新建的子进程进行数据交换。服务端则负责接收请求,验证登录信息,并创建子进程处理客户端的具体请求。子进程会创建一个新的套接字与客户端通信,实现数据收发功能。此方案有效利用了多进程的优势,提高了系统的并发处理能力。
|
14天前
|
消息中间件 Unix Linux
C语言 多进程编程(五)消息队列
本文介绍了Linux系统中多进程通信之消息队列的使用方法。首先通过`ftok()`函数生成消息队列的唯一ID,然后使用`msgget()`创建消息队列,并通过`msgctl()`进行操作,如删除队列。接着,通过`msgsnd()`函数发送消息到消息队列,使用`msgrcv()`函数从队列中接收消息。文章提供了详细的函数原型、参数说明及示例代码,帮助读者理解和应用消息队列进行进程间通信。
|
14天前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
14天前
|
Linux C语言
C语言 多进程编程(七)信号量
本文档详细介绍了进程间通信中的信号量机制。首先解释了资源竞争、临界资源和临界区的概念,并重点阐述了信号量如何解决这些问题。信号量作为一种协调共享资源访问的机制,包括互斥和同步两方面。文档还详细描述了无名信号量的初始化、等待、释放及销毁等操作,并提供了相应的 C 语言示例代码。此外,还介绍了如何创建信号量集合、初始化信号量以及信号量的操作方法。最后,通过实际示例展示了信号量在进程互斥和同步中的应用,包括如何使用信号量避免资源竞争,并实现了父子进程间的同步输出。附带的 `sem.h` 和 `sem.c` 文件提供了信号量操作的具体实现。
|
6天前
|
存储 Serverless C语言
【C语言基础考研向】11 gets函数与puts函数及str系列字符串操作函数
本文介绍了C语言中的`gets`和`puts`函数,`gets`用于从标准输入读取字符串直至换行符,并自动添加字符串结束标志`\0`。`puts`则用于向标准输出打印字符串并自动换行。此外,文章还详细讲解了`str`系列字符串操作函数,包括统计字符串长度的`strlen`、复制字符串的`strcpy`、比较字符串的`strcmp`以及拼接字符串的`strcat`。通过示例代码展示了这些函数的具体应用及注意事项。
|
9天前
|
存储 C语言
C语言程序设计核心详解 第十章:位运算和c语言文件操作详解_文件操作函数
本文详细介绍了C语言中的位运算和文件操作。位运算包括按位与、或、异或、取反、左移和右移等六种运算符及其复合赋值运算符,每种运算符的功能和应用场景都有具体说明。文件操作部分则涵盖了文件的概念、分类、文件类型指针、文件的打开与关闭、读写操作及当前读写位置的调整等内容,提供了丰富的示例帮助理解。通过对本文的学习,读者可以全面掌握C语言中的位运算和文件处理技术。
|
9天前
|
存储 C语言
C语言程序设计核心详解 第七章 函数和预编译命令
本章介绍C语言中的函数定义与使用,以及预编译命令。主要内容包括函数的定义格式、调用方式和示例分析。C程序结构分为`main()`单框架或多子函数框架。函数不能嵌套定义但可互相调用。变量具有类型、作用范围和存储类别三种属性,其中作用范围分为局部和全局。预编译命令包括文件包含和宏定义,宏定义分为无参和带参两种形式。此外,还介绍了变量的存储类别及其特点。通过实例详细解析了函数调用过程及宏定义的应用。
|
14天前
|
Linux C语言
C语言 多进程编程(三)信号处理方式和自定义处理函数
本文详细介绍了Linux系统中进程间通信的关键机制——信号。首先解释了信号作为一种异步通知机制的特点及其主要来源,接着列举了常见的信号类型及其定义。文章进一步探讨了信号的处理流程和Linux中处理信号的方式,包括忽略信号、捕捉信号以及执行默认操作。此外,通过具体示例演示了如何创建子进程并通过信号进行控制。最后,讲解了如何通过`signal`函数自定义信号处理函数,并提供了完整的示例代码,展示了父子进程之间通过信号进行通信的过程。