Linux应用进程间通信一( 匿名管道)

Linux应用进程间通信一( 匿名管道)

一、进程间通信的概念

进程间通信(Inter-Process Communication, IPC)是指不同进程之间为了交换数据、信息或协调工作而进行的通信。因为进程是操作系统中独立运行的实体,每个进程都有自己的地址空间、数据和代码,所以默认情况下,进程之间无法直接共享数据。IPC 提供了一些机制和方法,允许进程之间以安全、高效的方式进行数据交换。

1.1、进程间通信的目的

进程间通信的主要目的是:

  1. 共享数据:不同进程可以共享资源(例如数据、文件、设备等)。
  2. 协调工作:多个进程可以协作完成某项任务,比如协同处理计算任务、互相同步等。
  3. 消息传递:进程可以发送或接收消息,以便相互传递信息,进行指令或数据交互。

1.2、IPC的分类

进程间通信的方式通常可以分为两大类:

  1. 直接通信(Message Passing):进程之间通过消息来进行数据交换。可以是同步或异步的。
  2. 间接通信(Shared Memory):多个进程通过共享一块内存区域来交换数据。

1.3、常见的IPC机制

  1. 管道(Pipe)

    • 无名管道(Unnamed Pipe):用于父子进程之间的通信,或者同一父进程创建的多个子进程之间的通信。它是单向的,数据只能从写端流向读端。
    • 命名管道(Named Pipe):也称为FIFO,是一种可以在没有父子关系的进程之间进行通信的方式。它也支持单向或双向通信。
    • 管道通过内核提供的缓冲区来交换数据。
  2. 消息队列(Message Queue)

    • 允许进程以消息的形式进行通信,消息队列会将多个消息排队存储,进程可以按顺序获取消息。
    • 它是通过内存共享或内核中维护的队列来实现的。
  3. 共享内存(Shared Memory)

    • 进程将某块内存区域映射到自己的地址空间,多个进程可以通过该共享内存来访问相同的数据。这是一种非常高效的通信方式。
    • 共享内存本身并不提供同步机制,因此进程需要额外的同步机制来避免并发访问的冲突。
  4. 信号(Signal)

    • 信号是操作系统用于通知进程发生某些事件的机制。进程可以通过接收信号来进行特定的操作,例如中断、终止或暂停。
    • 信号一般用于进程之间的简单通知或事件处理,而不是传输数据。
  5. 套接字(Socket)

    • 套接字是一种网络通信机制,可以在不同主机或不同机器上的进程之间进行通信。套接字不仅适用于同一台机器上的进程间通信,也适用于跨网络的通信。
    • 可以是面向连接的(TCP)或无连接的(UDP)。
  6. 信号量(Semaphore)

    • 信号量是一种用于进程间同步的机制。它用于控制多个进程对共享资源的访问,防止多个进程同时修改共享资源而产生冲突。
    • 它不直接用于数据交换,而是用于进程同步和互斥。
  7. 共享内存与信号量结合

    • 共享内存提供高速的数据交换通道,而信号量可以用来实现对共享内存的访问控制,以防止竞争条件。

1.4、IPC的分类总结

IPC类型典型特征用途
管道(Pipe)单向、匿名或命名简单的进程间数据传递
消息队列(MQ)消息传递、异步、FIFO异步的消息传递
共享内存(SHM)高效、进程间共享内存区高速数据交换
信号(Signal)通知、事件驱动简单的事件通知
套接字(Socket)跨进程、跨机器通信(支持TCP/UDP)网络通信
信号量(Semaphore)控制进程对共享资源的访问(同步、互斥)进程同步、互斥控制

1.5、IPC的同步问题

当多个进程共享同一资源时,需要考虑进程同步问题,避免出现数据竞争、死锁等问题。常见的同步机制包括:

  • 互斥锁(Mutex):确保同一时刻只有一个进程能访问共享资源。
  • 条件变量(Condition Variable):允许进程根据某些条件来等待或通知其他进程。
  • 信号量(Semaphore):控制对共享资源的访问,通过计数器的方式来限制并发访问。

二、匿名管道

2.1、管道的创建  

  管道是由调用pipe函数来创建:

1、pipe() 函数的工作原理

pipe() 函数在调用时会创建一个单向的管道,并返回一个文件描述符数组。该数组包含两个文件描述符:

  • pipefd[0]:用于读取数据(读端)。
  • pipefd[1]:用于写入数据(写端)。

pipe() 创建的管道在内核中有一个缓冲区,数据从写端写入后,会存储在该缓冲区中,直到从读端读取为止。

2、pipe() 函数原型

int pipe(int pipefd[2]);
  • 参数pipefd 是一个整数数组,其中 pipefd[0] 是读端文件描述符,pipefd[1] 是写端文件描述符。
  • 返回值
    • 如果成功,返回 0
    • 如果失败,返回 -1,并设置 errno。 

2.2、管道如何实现进程间的通信

(1)父进程创建管道,得到两个件描述符指向管道的两端

(2)父进程fork出子进程,子进程也有两个文件描述符指向同管道。

(3)父进程关闭fd[0],子进程关闭fd[1],即子进程关闭管道读端,父进程关闭管道写端(因为管道只支持单向通信)。子进程可以往管道中写,父进程可以从管道中读,管道是由环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。

2.3、如何利用代码实现管道通信

1. 简单的管道通信

这个示例展示了如何使用 pipe() 创建管道,并通过 fork() 创建子进程,父进程向管道写入数据,子进程从管道读取数据。

代码实现:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    int pipefd[2];    // 管道的文件描述符
    pid_t pid;
    char write_msg[] = "Hello from parent process!";
    char read_msg[100];

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        return 1;
    }

    // 创建子进程
    pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }

    if (pid > 0) {  // 父进程
        close(pipefd[0]);  // 关闭管道的读端
        write(pipefd[1], write_msg, strlen(write_msg) + 1);  // 向管道写数据
        close(pipefd[1]);  // 关闭管道的写端
    } else {  // 子进程
        close(pipefd[1]);  // 关闭管道的写端
        read(pipefd[0], read_msg, sizeof(read_msg));  // 从管道读取数据
        printf("Child process received: %s\n", read_msg);
        close(pipefd[0]);  // 关闭管道的读端
    }

    return 0;
}

解释:

  1. 创建管道

    • pipe(pipefd) 创建了一个管道,pipefd[0] 为读端,pipefd[1] 为写端。
    • pipe() 返回 0 表示成功,返回 -1 表示失败。
  2. 父进程

    • 父进程使用 write(pipefd[1], write_msg, strlen(write_msg) + 1) 向管道的写端写入数据。
    • 写入完成后,父进程关闭写端 pipefd[1]
  3. 子进程

    • 子进程使用 read(pipefd[0], read_msg, sizeof(read_msg)) 从管道的读端读取数据。
    • 读取完成后,子进程关闭读端 pipefd[0]

2. 进程间双向通信(双管道)

如果我们需要双向通信,可以使用两个管道,一个用于从父进程到子进程的数据传输,另一个用于从子进程到父进程的数据传输。

代码实现:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    int pipefd1[2], pipefd2[2];  // 两个管道
    pid_t pid;
    char parent_msg[] = "Hello from parent!";
    char child_msg[] = "Hello from child!";
    char parent_read[100], child_read[100];

    // 创建两个管道
    if (pipe(pipefd1) == -1 || pipe(pipefd2) == -1) {
        perror("pipe");
        return 1;
    }

    // 创建子进程
    pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }

    if (pid > 0) {  // 父进程
        close(pipefd1[0]);  // 关闭第一个管道的读端
        close(pipefd2[1]);  // 关闭第二个管道的写端

        // 向第一个管道写数据
        write(pipefd1[1], parent_msg, strlen(parent_msg) + 1);
        close(pipefd1[1]);  // 写入完成后关闭写端

        // 从第二个管道读取数据
        read(pipefd2[0], parent_read, sizeof(parent_read));
        printf("Parent received: %s\n", parent_read);
        close(pipefd2[0]);  // 读取完成后关闭读端

    } else {  // 子进程
        close(pipefd1[1]);  // 关闭第一个管道的写端
        close(pipefd2[0]);  // 关闭第二个管道的读端

        // 从第一个管道读取数据
        read(pipefd1[0], child_read, sizeof(child_read));
        printf("Child received: %s\n", child_read);
        close(pipefd1[0]);  // 读取完成后关闭读端

        // 向第二个管道写数据
        write(pipefd2[1], child_msg, strlen(child_msg) + 1);
        close(pipefd2[1]);  // 写入完成后关闭写端
    }

    return 0;
}

解释:

  1. 两个管道

    • pipefd1 用于从父进程向子进程发送数据。
    • pipefd2 用于从子进程向父进程发送数据。
  2. 父进程

    • 父进程首先向 pipefd1 写数据(发送给子进程)。
    • 然后从 pipefd2 读取数据(接收子进程的回复)。
  3. 子进程

    • 子进程首先从 pipefd1 读取数据(接收父进程的消息)。
    • 然后向 pipefd2 写数据(发送给父进程)。

3. 管道与进程间的同步

管道的读写操作是阻塞的,意味着:

  • 如果写端的管道缓冲区满,写操作将阻塞,直到有数据被读取。
  • 如果管道中没有数据,读操作将阻塞,直到写端写入数据。

这使得管道通信在多进程模型中非常方便,保证了进程间的同步。

2.4、管道读取数据的四种的情况

(1)读端不读(fd[0]未关闭),写端一直写 。

 (2)写端不写(fd[1]未关闭),但是读端一直读。

(3)读端一直读,且fd[0]保持打开,而写端写了一部分数据不写了,并且关闭fd[1]。

如果一个管道读端一直在读数据,而管道写端的引⽤计数⼤于0决定管道是否会堵塞,引用计数大于0,只读不写会导致管道堵塞。

(4)读端读了一部分数据,不读了且关闭fd[0],写端一直在写且f[1]还保持打开状态。

总结:
  如果一个管道的写端一直在写,而读端的引用计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞;
  如果一个管道的读端一直在读,而写端的引用计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞;
  而当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到件末尾样。

2.5、管道特点

管道的主要特点:

1. 半双工通信

  • 单向传输:管道通常是半双工的,意味着数据只能在一个方向上传输。也就是说,一个管道的写端只能写数据,读端只能读取数据。要实现双向通信,通常需要两个管道(一个用于父进程到子进程,另一个用于子进程到父进程)。

  • 例如,在管道的默认模式下,父进程写入数据到管道,子进程从管道读取数据。

2. 进程间通信

  • 管道是用于进程间通信的,尤其是父子进程之间。通过管道,父进程可以将信息传递给子进程,子进程也可以将结果传回父进程。

  • 管道不支持跨用户或跨网络的进程通信,它只能在同一台机器上的进程之间使用。

3. 内核缓冲区

  • 管道的底层是由内核维护的缓冲区,当数据从一个进程写入管道时,它会首先存储在内核的缓冲区中,另一个进程可以从该缓冲区读取数据。

  • 这个缓冲区通常是有限大小的(例如,默认 64KB 或 4KB),因此如果写入数据超过缓冲区的容量,写操作将被阻塞,直到读端读取数据,腾出空间为止。

4. 阻塞特性

  • 写端阻塞:如果管道的缓冲区已满,写端进程会被阻塞,直到有空间可供写入。

  • 读端阻塞:如果管道中没有数据,读端进程会被阻塞,直到写端写入数据。也就是说,管道的读操作和写操作具有同步性。

  • 管道的这种阻塞特性帮助确保数据流动的顺序一致性,同时也实现了进程间的同步。

5. 匿名管道与命名管道

  • 匿名管道(Anonymous Pipe):它没有名字,通常由 pipe() 系统调用创建,用于父子进程之间的通信。匿名管道只能在相关的进程之间使用,通常是父子进程或同一父进程的兄弟进程。

  • 命名管道(Named Pipe / FIFO):它是一个特殊的文件,存在于文件系统中,可以通过文件路径访问。命名管道允许无关进程之间的通信,不需要父子关系。创建命名管道时使用 mkfifo() 系统调用,进程可以通过管道文件读写数据。

6. 数据流动特性

  • 管道数据的流动是由生产者-消费者模型控制的,生产者(写端)写入数据,消费者(读端)读取数据。生产者和消费者之间的数据流是同步的,写入操作会等待读取操作完成,反之亦然。

7. 数据传递是字节流

  • 管道是以字节流(byte stream)的方式传递数据,写入的数据没有结构化的限制,数据会按照字节流的形式传递给读取端。因此,读取端可能需要自行处理数据的分割和解析。

8. 匿名管道不支持进程分离

  • 使用匿名管道时,通信双方需要在同一父子进程关系下。也就是说,管道无法跨越不同的进程组或会话,无法进行完全独立的进程间通信。如果进程之间没有父子关系,不能直接使用匿名管道进行通信。

9. 内存使用

  • 管道的数据存储在内核的缓冲区中,不需要用户进程管理内存分配。内核会自动管理管道的缓冲区大小,处理读写操作。

10. 不支持随机访问

  • 管道的读写是顺序的,一旦数据被写入到管道中,数据只能按照顺序读取。不能随机访问管道中的任何数据,数据只能按照先进先出的顺序传输。

11. 管道生命周期

  • 管道的生命周期由文件描述符的生命周期决定。一旦相关的文件描述符被关闭,管道就会被销毁。

12. 安全性和访问控制

  • 管道的访问权限通常由文件系统控制。匿名管道的读写权限会受到文件描述符的权限控制,而命名管道的权限则是由文件系统的文件权限控制的。

  • 对于命名管道,可以设置不同的访问权限,允许不同的进程以不同的方式(读、写、执行等)访问。

2.6、管道容量大小

测试管道容量大小只需要将写端一直写,读端不读且不关闭fd[0],即可。 
测试代码:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define PIPE_BUF 4096  // 设定写入数据块的大小,通常为 4KB(也可以尝试更大)

int main() {
    int pipe_fd[2];  // 存储管道的文件描述符,pipe_fd[0] 是读端,pipe_fd[1] 是写端
    char buffer[PIPE_BUF];  // 缓存数据缓冲区
    ssize_t bytes_written;

    // 创建管道
    if (pipe(pipe_fd) == -1) {
        perror("pipe failed");
        exit(1);
    }

    // 填充缓冲区
    memset(buffer, 'A', PIPE_BUF);

    printf("开始测试管道容量...\n");

    // 一直向管道写数据,直到管道的缓冲区满
    while (1) {
        bytes_written = write(pipe_fd[1], buffer, PIPE_BUF);

        // 如果写入的数据小于缓冲区大小,说明管道已满
        if (bytes_written < PIPE_BUF) {
            printf("管道缓冲区已满!写入的数据字节数: %ld\n", bytes_written);
            break;
        }
    }

    // 关闭写端(通常关闭管道时读端需要读完所有数据)
    close(pipe_fd[1]);

    return 0;
}

代码解析:

  1. 创建管道:使用 pipe(pipe_fd) 创建一个匿名管道。pipe_fd[0] 是管道的读取端,pipe_fd[1] 是写入端。

  2. 填充缓冲区:创建一个 buffer,并将其填充为字母 'A',然后反复将这个缓冲区写入管道中。

  3. 写入循环:使用 write() 函数不断地向管道写入数据,直到管道的缓冲区满为止。如果写入的字节数小于 PIPE_BUF,说明管道已满。

  4. 关闭管道:在测试结束后,关闭写端 pipe_fd[1],这样可以避免不必要的资源占用。

说明:

  • 该程序会测试管道的容量,并在管道容量达到限制时输出提示。
  • 注意,如果系统的管道容量较小,write() 操作可能会在管道容量满时阻塞,直到有数据被读取来释放空间。如果想避免阻塞,可以使用非阻塞模式或通过 select/poll 等机制监控管道的状态。
  • 你可以根据实际情况调整 PIPE_BUF 的大小来测试不同大小的数据写入。

  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值