【进程间通信原理1】进程间通信与管道(匿名管道与命名管道)详解

1. 进程间通信介绍

首先简单介绍 程间通信的目的、发展与分类:

1.1 进程间通信的目的

进程间通信的主要目的是 使独立运行的进程能够相互协作、共享数据和资源,以实现更复杂的任务和功能。通过进程间通信,不同的进程可以在系统中进行协调合作,实现以下几个主要目标:

  1. 数据共享:进程间通信允许多个进程访问和共享相同的数据或资源,从而避免了数据复制的开销,提高了运行效率。

  2. 信息传递:进程可以通过通信机制向其他进程发送消息、通知或信号,以实现进程间的协调和同步。

  3. 资源共享:进程间通信使得多个进程可以共享系统资源,如文件、设备、内存等,从而更加高效地利用系统资源。

  4. 进程协作:不同的进程可以通过通信实现协同工作,各自承担不同的任务,最终完成一个更大规模的工作。

  5. 并发控制:通过进程间通信,可以实现对共享资源的并发访问控制,确保各个进程安全地访问共享资源,避免竞争条件和死锁问题的发生。


1.2 进程间通信的发展

进程间通信发展主要分为下面三个阶段:

  1. 管道(Pipes): 管道是最早的进程间通信方式之一,主要用于在同一台计算机上的父子进程或兄弟进程之间进行通信。管道是单向的,通常用于将一个进程的输出连接到另一个进程的输入。

  2. System V进程间通信(System V IPC): System V IPC是一组通信机制,包括消息队列(Message Queues)、信号量(Semaphores)和共享内存(Shared
    Memory)。这些机制允许不同进程之间在同一计算机上进行数据交换和共享。这些通信机制通常比管道更灵活,允许更复杂的数据结构和通信模式。

  3. POSIX进程间通信: POSIX进程间通信是对System V IPC的一种替代,提供了更简单和更具移植性的进程间通信方法。它包括消息队列、信号量、共享内存等,但使用起来更符合POSIX标准,因此在不同UNIX系统之间更具可移植性。


1.3 进程间通信的分类

根据上面介绍的进程间通信的发展,可以给进程间通信作如下分类:

管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

1.4 命令: ipcs && ipcrm

ipcsipcrm 都是在 UNIX/Linux 系统下用于管理和操作进程间通信(IPC)资源的命令。下面对它们进行详细解释:

  1. ipcs 命令:

    • ipcs 命令用于显示当前系统中的 IPC 资源信息。
    • 可以使用不同的选项来查看不同类型的 IPC 资源,包括消息队列、共享内存和信号量。
    • 常见的选项包括:
      • -q:显示消息队列的信息。
      • -m:显示共享内存的信息。
      • -s:显示信号量的信息。
    • 示例:ipcs -q 可以查看当前系统中的消息队列信息。
  2. ipcrm 命令:

    • ipcrm 命令用于删除 IPC 资源,包括消息队列、共享内存和信号量。
    • 可以指定不同的选项来删除特定类型的 IPC 资源。
    • 常见的选项包括:
      • -q:删除指定的消息队列。
      • -m:删除指定的共享内存。
      • -s:删除指定的信号量。
    • 示例:ipcrm -q <消息队列ID> 可以删除指定的消息队列。

需要注意的是,使用 ipcrm 命令删除 IPC 资源需要谨慎操作,因为删除后无法恢复,并且可能会影响正在使用该资源的进程。在使用 ipcrm命令时,务必确保正确指定要删除的资源类型和标识符。


2. 管道

2.1 管道 概念

管道是Unix中最古老的进程间通信的形式。

我们把从 一个进程连接到另一个进程的一个数据流称为一个“管道”

在Unix和类Unix系统中,管道通常是通过 | 符号来表示的,它可以将一个进程的标准输出连接到另一个进程的标准输入,形成一个数据流的传输通道

管道的基本特点包括:

  • 单向传输: 管道是单向的,一个进程的输出只能传递给另一个进程的输入,而不能反向传输。
  • 线性连接: 管道连接的进程通常是线性的,即一个进程的输出可以直接连接到另一个进程的输入,形成一个线性的数据流传输。
  • 实时数据流: 管道传输的数据是实时的,即一个进程写入管道的数据可以立即被另一个进程读取。

在这里插入图片描述


2.2 匿名管道 / pipe

pipe() 是一个系统调用,用于创建匿名管道(Anonymous Pipe),它是进程间通信(IPC)的一种简单机制:

pipe() 函数的原型

#include <unistd.h>

int pipe(int pipefd[2]);

参数

  • pipefd:一个整型数组,包含两个文件描述符:
    • pipefd[0]:用于从管道读取数据的文件描述符(读端)。
    • pipefd[1]:用于向管道写入数据的文件描述符(写端)。

返回值

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 来指示错误的类型。

在这里插入图片描述

下面是一个简单的例子,演示了如何使用 pipe() 在父子进程之间进行通信:

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

int main() {
    int pipefd[2];
    pid_t cpid;
    char buf;

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

    // 创建子进程
    cpid = fork();
    if (cpid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (cpid == 0) {    // 子进程
        close(pipefd[1]);          // 关闭写端

        // 从管道中读取数据
        while (read(pipefd[0], &buf, 1) > 0) {
            write(STDOUT_FILENO, &buf, 1);
        }

        write(STDOUT_FILENO, "\n", 1);
        close(pipefd[0]);  // 关闭读端
        _exit(EXIT_SUCCESS);

    } else {            // 父进程
        close(pipefd[0]);          // 关闭读端

        // 向管道中写入数据
        const char *msg = "Hello from parent";
        write(pipefd[1], msg, sizeof(msg));

        close(pipefd[1]);  // 关闭写端
        wait(NULL);        // 等待子进程结束
        exit(EXIT_SUCCESS);
    }
}

在这个例子中,父进程创建了一个管道,然后创建了一个子进程。父进程向管道写入消息,子进程从管道读取消息并将其输出到标准输出


2.3 理解管道

① 文件描述符角度 理解管道

通过下面三步去理解:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
通过这种方式,父进程和子进程利用共享的文件描述符实现了进程间的通信,即管道在文件描述符层面上的工作原理。


② 内核角度 探寻管道本质

在这里插入图片描述

所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”

2.4 管道读写规则

对于不同情况,管道有以下的读写规则:

  1. 读端没有数据可读时的行为

    • 如果管道没有设置为非阻塞模式(O_NONBLOCK disable:“读取操作(read调用)会阻塞,即进程会暂停执行,直到有数据可读为止。
    • 如果管道设置了非阻塞模式(O_NONBLOCK enable:读取操作将立即返回-1,并且errno会被设置为EAGAIN或EWOULDBLOCK,表示当前没有数据可用。
  2. 管道满时的行为

    • 当写入端向管道写入数据而管道已经满了时: 如果管道没有设置为非阻塞模式,写入操作(write调用)会阻塞,直到有进程从管道中读取数据,释放空间。
    • 如果管道设置了非阻塞模式: 写入操作将立即返回-1,并且errno会被设置为EAGAIN,表示管道已满,无法写入更多数据。
  3. 管道关闭的影响

    • 当所有写入端对应的文件描述符(write端)被关闭时,读取操作(read调用)会返回0,表示已经读取到了所有数据,并且没有更多数据可以读取了。
    • 当所有读取端对应的文件描述符(read端)被关闭时,写入操作(write调用)会产生信号SIGPIPE。默认情况下,这会导致写入进程终止,除非进程通过信号处理机制忽略或处理该信号。
  4. 原子性的保证

    • 在Linux中,当要写入的数据量不超过PIPE_BUF(通常是4096字节): 时,写入操作是原子性的,即要么写入的数据全部成功,要么一个字节也不会写入。这保证了小于等于PIPE_BUF大小的写操作是原子的。
    • 当要写入的数据量大于PIPE_BUF时: Linux不再保证写入的原子性。这时候写入操作可能会被信号打断,或者只写入部分数据。

2.5 管道的特点

  1. 亲缘关系进程之间通信

    • 管道通常由一个进程创建,并且通常用于具有亲缘关系(父子关系或兄弟关系)的进程之间进行通信。(管道在创建时与创建它的进程相关联)
  2. 流式通信

    • 管道提供流式服务,数据可以按顺序从一个进程流向另一个进程,类似于数据流的形式。
  3. 管道生命周期与进程相关

    • 管道的生命周期通常与创建它的进程相关联。一旦所有引用该管道的进程都关闭了对应的文件描述符,管道就会被操作系统自动释放。
  4. 管道生命周期与进程相关

    • 管道操作在内核中会进行同步和互斥管理,确保在多进程访问时数据的正确传输和读写操作的安全性。
  5. 半双工特性

    • 管道是半双工的,这意味着数据只能在一个方向上流动。如果需要双向通信,通常需要创建两个管道,或者采用其他的进程间通信机制,比如命名管道或共享内存。
  6. 数据传输的原子性:

  • 当要写入的数据量不超过PIPE_BUF(通常为4096字节)时,Linux系统会保证写操作的原子性。这意味着要么写入的所有数据都成功传输到管道,要么一个字节也不会写入。

在这里插入图片描述
(利用管道实现双向通信)


2.6 命名管道

命名管道(Named Pipe)是一种特殊类型的文件,它允许无关的进程间进行双向通信。与匿名管道不同的是,命名管道可以在文件系统中被命名,并通过文件名来进行访问,因此它也被称为 FIFO(First In, First Out)


① 特点和用途:

  1. 文件系统中的特殊文件
    • 命名管道在文件系统中表现为一个特殊类型的文件,类似于普通文件,但具有特定的FIFO属性。
  2. 无关进程的通信
    • 不像匿名管道那样仅限于具有亲缘关系的进程,命名管道可以由任意进程访问,只要它们知道该管道的文件名。
  3. 双向通信
    • 命名管道支持双向通信,进程可以同时在管道的两端进行读写操作。
  4. 生命周期与文件系统相关联
    • 命名管道的生命周期与文件系统相关联。一旦创建并且没有进程引用它,文件系统会在最后一个进程关闭它时将其删除。
  5. 用途
    • 命名管道通常用于解决多个进程间的数据传输问题,特别是在需要非阻塞通信、长期存在的通信链路或者需要在无关进程间传递大量数据时。

② 创建和使用命名管道:

  1. Unix/Linux系统中,可以直接在命令行使用命令 mkfifo 来创建命名管道,例如:
mkfifo mypipe

这将在当前目录创建一个名为 mypipe 的命名管道文件。然后可以像操作普通文件一样,通过文件名在不同的进程中进行数据读写操作。

命名管道也可以在程序中创建,利用mkfifo:

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

③ 注意事项:

  • 命名管道的数据是以字节流的形式进行传输的,因此需要进程在读取时处理好数据的分隔和解析。
  • 使用命名管道时需要注意同步和互斥问题,以避免多进程同时操作导致的数据错乱或丢失。

④ 命名管道的打开规则

  1. 以读模式打开 FIFO

    • 如果以只读方式(O_RDONLY)打开 FIFO,并且 O_NONBLOCK 没有设置(即阻塞模式),那么进程将阻塞,直到有其他进程以写方式打开该 FIFO 为止。这种情况下,打开操作会一直等待,直到 FIFO 可以被成功打开为止。
    • 如果以只读方式打开 FIFO 并且设置了 O_NONBLOCK(非阻塞模式),那么 open 函数会立即返回成功,即使当前没有进程以写方式打开该 FIFO。
  2. 以写模式打开 FIFO

    • 如果以只写方式(O_WRONLY)打开 FIFO,并且 O_NONBLOCK 没有设置(阻塞模式),那么进程将阻塞,直到有其他进程以读方式打开该 FIFO。打开操作会一直等待,直到 FIFO 可以被成功打开为止。
    • 如果以只写方式打开 FIFO 并且设置了 O_NONBLOCK(非阻塞模式),那么 open 函数会立即返回失败,并且错误码为 ENXIO,表示没有其他进程以读方式打开该 FIFO,无法进行写操作。

⑤ 实例1:用命名管道实现文件拷贝

我们用示例代码展示:如何使用命名管道在两个进程之间进行文件拷贝:

  1. 读取进程(读文件并写入命名管道)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#define FIFO_FILE "myfifo"
#define MAX_BUFFER_SIZE 1024

int main() {
    int fd_fifo;
    FILE *fp_source;
    char buffer[MAX_BUFFER_SIZE];

    // 打开命名管道(写模式)
    fd_fifo = open(FIFO_FILE, O_WRONLY);

    // 打开源文件(需要拷贝的文件)
    fp_source = fopen("source.txt", "r");

    // 从源文件读取内容,并写入命名管道
    while (fgets(buffer, MAX_BUFFER_SIZE, fp_source) != NULL) {
        write(fd_fifo, buffer, sizeof(buffer));
    }

    // 关闭文件和管道
    fclose(fp_source);
    close(fd_fifo);

    return 0;
}
  1. 写入进程(从命名管道读取数据并写入目标文件)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

#define FIFO_FILE "myfifo"
#define MAX_BUFFER_SIZE 1024

int main() {
    int fd_fifo;
    FILE *fp_dest;
    char buffer[MAX_BUFFER_SIZE];
    ssize_t bytes_read;

    // 打开命名管道(读模式)
    fd_fifo = open(FIFO_FILE, O_RDONLY);

    // 创建目标文件(拷贝后的文件)
    fp_dest = fopen("destination.txt", "w");

    // 从命名管道读取内容,并写入目标文件
    while ((bytes_read = read(fd_fifo, buffer, sizeof(buffer))) > 0) {
        fwrite(buffer, 1, bytes_read, fp_dest);
    }

    // 关闭文件和管道
    fclose(fp_dest);
    close(fd_fifo);

    return 0;
}

此时只需执行读进程与写进程就可以直接实现文件拷贝


⑥ 实例2:用命名管道实现server&client通信

在这个例子中,我们将展示如何使用命名管道(FIFO)来实现一个简单的服务器(server)和客户端(client)之间的通信。服务器将从客户端接收消息,并且可以向客户端发送响应。

  1. 服务器端
    • 服务器端负责接收来自客户端的消息,并可以发送响应。
// server.c

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

#define FIFO_FILE "myfifo"

int main() {
    int fd_fifo;
    char read_buffer[BUFSIZ];
    char write_buffer[BUFSIZ];
    int bytes_read;

    // 创建命名管道(如果不存在)
    mkfifo(FIFO_FILE, 0666);

    printf("Server started, waiting for clients...\n");

    // 打开命名管道(读模式)
    fd_fifo = open(FIFO_FILE, O_RDONLY);

    while (1) {
        // 从命名管道中读取数据
        bytes_read = read(fd_fifo, read_buffer, sizeof(read_buffer));
        if (bytes_read > 0) {
            read_buffer[bytes_read] = '\0';
            printf("Received: %s\n", read_buffer);

            // 模拟处理消息(这里简单地回复消息)
            sprintf(write_buffer, "Server received: %s", read_buffer);

            // 打开命名管道(写模式)
            int fd_write = open(FIFO_FILE, O_WRONLY);
            write(fd_write, write_buffer, strlen(write_buffer) + 1);
            close(fd_write);
        }
    }

    // 关闭命名管道
    close(fd_fifo);
    unlink(FIFO_FILE);

    return 0;
}
  1. 客户端
    • 客户端向服务器发送消息,并等待服务器的响应。
// client.c

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

#define FIFO_FILE "myfifo"

int main() {
    int fd_fifo;
    char write_buffer[BUFSIZ];
    char read_buffer[BUFSIZ];
    int bytes_read;

    // 打开命名管道(写模式)
    fd_fifo = open(FIFO_FILE, O_WRONLY);

    while (1) {
        // 从标准输入读取消息
        printf("Enter message to send: ");
        fgets(write_buffer, sizeof(write_buffer), stdin);
        write_buffer[strlen(write_buffer) - 1] = '\0'; // 去除末尾的换行符

        // 将消息写入命名管道
        write(fd_fifo, write_buffer, strlen(write_buffer) + 1);

        // 打开命名管道(读模式)
        int fd_read = open(FIFO_FILE, O_RDONLY);
        // 读取服务器的响应
        bytes_read = read(fd_read, read_buffer, sizeof(read_buffer));
        if (bytes_read > 0) {
            printf("Server response: %s\n", read_buffer);
        }
        close(fd_read);
    }

    // 关闭命名管道
    close(fd_fifo);

    return 0;
}

2.7 匿名管道与命名管道的区别

有以下主要区别:

  1. 命名和访问方式

    • 匿名管道:没有在文件系统中显示的文件名,仅存在于内存中。它只能用于具有亲缘关系的进程间通信,通常通过 pipe 系统调用创建,返回两个文件描述符用于读和写。
    • 命名管道:在文件系统中有名字(路径),因此可以被多个无关的进程访问。通过 mkfifo 命令或者 mkfifo 系统调用来创建,它创建的文件实际上是一个 FIFO 文件,可以像普通文件一样操作。
  2. 进程关系要求

    • 匿名管道:仅限于具有亲缘关系(如父子进程或兄弟进程)的进程之间进行通信。
    • 命名管道:允许任意无关的进程通过文件系统中的路径来访问和进行通信。
  3. 生命周期和持久性

    • 匿名管道:在进程关闭相关的文件描述符或终止时自动销毁,不存在持久性。
    • 命名管道:持久存在于文件系统中,直到显式删除或系统关闭时才会被删除。
  4. 命名和访问方式

    • 匿名管道:适用于需要临时、快速的单向数据传输,例如父子进程间的简单数据交换。
    • 命名管道:适用于长期存在的、需要多个进程之间双向通信的场景,如服务器和客户端之间的通信。
  5. 访问权限

    • 匿名管道:进程间创建管道时共享访问权限,通常无需额外的权限管理。
    • 命名管道:在文件系统中以文件的形式存在,因此可以通过文件系统的权限机制来控制访问。

匿名管道适用于简单的进程间通信需求,而命名管道则提供了更灵活和持久的通信方式,适用于更复杂和长期的通信场景。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值