【Linux】进程间通信相关知识详细梳理

目录

 

1. 进程间通信目的

2. 进程间通信的方式

2.1 管道

1. 匿名管道

 2. 匿名管道原理

3. 命名管道 

4. 管道读写规则

2.2 System V IPC

1.System V的背景

2. System V的特性

3. System V IPC(进程间通信)

3.1 消息队列(Message Queue)

3.2 共享内存(Shared Memory)

3.3  信号量(Semaphore)

3.4 信号(Signals)


 

1. 进程间通信目的

        进程间通信(Inter-Process Communication, IPC)的目的是为了在操作系统中不同进程之间共享数据、协调操作和同步执行。

        具体来说,进程间通信的主要目的包括以下几方面:

1. 数据交换

  • 目的:实现多个进程之间的数据共享和传递。
  • 场景
    • 客户端与服务器之间的数据交互(如 Web 服务器与浏览器)。
    • 多个子进程需要共享计算结果或中间数据。
  • 示例
    • 一个进程从传感器读取数据,另一个进程处理和存储这些数据。

2. 资源共享

  • 目的:协调多个进程访问同一资源(如文件、共享内存、数据库等),避免冲突和资源竞争。
  • 场景
    • 数据库系统中,多个进程访问同一数据表。
    • 多个进程共享同一个打印机资源。
  • 示例
    • 使用信号量控制对共享文件的并发写操作。

3. 进程同步

  • 目的:确保多个进程按照正确的顺序执行,以避免数据不一致或竞争条件。
  • 场景
    • 一个进程的输出需要作为另一个进程的输入。
    • 父进程需要等待子进程完成某个任务后再继续执行。
  • 示例
    • 使用信号量或条件变量实现生产者-消费者模型。

4. 事件通知

  • 目的:通知进程某个事件的发生,使其采取相应的操作。
  • 场景
    • 文件系统通知应用程序某个文件被修改。
    • 父进程收到子进程的退出信号。
  • 示例
    • 使用信号(Signal)通知进程接收到外部中断。

5. 任务协作

  • 目的:实现多个进程协同工作,完成复杂任务。
  • 场景
    • 在分布式系统中,不同进程分布在多个节点上,需要协作完成任务。
    • 数据处理流水线中,不同进程负责不同处理阶段。
  • 示例
    • 使用管道或消息队列在不同进程之间传递任务。

6. 提高系统效率

  • 目的:通过进程间通信优化资源利用率,提高系统的整体效率。
  • 场景
    • 在多核系统中,将任务分配到多个进程并行处理。
    • 数据密集型应用中,一个进程负责 I/O,另一个进程负责计算。
  • 示例
    • 使用共享内存提高进程间大数据量传输的效率。

7. 隔离与安全

  • 目的:通过进程间通信在进程之间建立明确的边界,同时提供数据共享的机制。
  • 场景
    • 沙箱环境中,限制进程的直接交互,仅允许通过 IPC 通道通信。
    • 系统进程与用户进程之间通过安全通道通信。
  • 示例
    • 使用消息队列在高安全性环境中传递数据。

2. 进程间通信的方式

下面我们来着重介绍linux中进程间通信的两种方式:

2.1 管道

1. 匿名管道

特点:

  • 只能用于父子进程之间通信:匿名管道只能在具有亲缘关系的进程(如父进程与子进程)之间使用,因为管道由文件描述符标识,而文件描述符只能在亲缘进程间继承。
  • 没有名字:管道在内核中存在,但没有名字,仅通过文件描述符访问。
  • 全双工限制:单个匿名管道通常是单向的写端向读端发送数据。如果需要双向通信,需要创建两个管道。
  • 自动关闭:当所有进程都关闭了管道的文件描述符时,管道会被自动回收。

管道其实是一个很形象的名称,用示意图来理解:

        这种通信方式就像在进程之间装了管道一样,可以实现数据的‘‘单向流动’’。

pipe函数

  • 功能:创建一个未命名管道(匿名管道)。
  • 原型
    int pipe(int pipefd[2]);
    
  • 参数
    • pipefd:包含两个文件描述符的数组,pipefd[0] 用于读取,pipefd[1] 用于写入。
  • 返回值
    • 成功:返回 0;
    • 失败:返回 -1,并设置 errno
  • 使用限制
    • 匿名管道只能用于具有亲缘关系的进程(父子、兄弟进程等)。

示例:

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

int main() {
    int pipefd[2];
    char buf[1024];
    pipe(pipefd); // 创建管道
    if (fork() == 0) {  // 子进程
        close(pipefd[0]); // 关闭读端
        write(pipefd[1], "Hello from child", 17); // 写数据
        close(pipefd[1]); // 关闭写端
    } else {  // 父进程
        close(pipefd[1]); // 关闭写端
        read(pipefd[0], buf, sizeof(buf)); // 读取数据
        printf("Parent received: %s\n", buf);
        close(pipefd[0]); // 关闭读端
    }
    return 0;
}

注意:管道要在子进程创建前创建,这样才能继承到管道。 

 2. 匿名管道原理

为什么管道要在子进程创建前创建呢?我们来看看匿名管道的原理:

 由上图可知,管道也是文件,由进程的文件描述符表管理,这也符合linux一切皆文件的特性。

同时,管道并不是普通的文件,而是由内核级的文件缓冲区实现的

尽管管道使用了文件描述符,但它与常规文件有所不同:

  1. 管道是临时的

    • 管道数据存储在内存中,而普通文件的数据存储在磁盘上。
    • 管道缓冲区在进程关闭或系统重启时会被释放。
  2. 管道是流式的

    • 管道类似于数据流,没有文件指针和随机访问功能。
    • 写入的数据必须按顺序读取,不能像普通文件一样定位(seek)。
  3. 管道大小有限

    • 管道缓冲区的大小固定,超出缓冲区的写入操作会阻塞,直到读端读取部分数据释放空间。

管道关闭情形:

情况 1:写端关闭,读端未关闭

当读端调用 read()

  • 如果管道中还有数据,read() 会继续读取这些数据。
  • 如果管道中没有数据,read() 返回 0,表示管道已到达 EOF(End of File)
#include <unistd.h>
#include <stdio.h>

int main() {
    int pipefd[2];
    char buf[1024];
    pipe(pipefd);

    if (fork() == 0) {  // 子进程
        close(pipefd[0]); // 关闭读端
        write(pipefd[1], "Hello", 5); // 写入数据
        close(pipefd[1]); // 关闭写端
    } else {  // 父进程
        close(pipefd[1]); // 关闭写端
        int n = read(pipefd[0], buf, sizeof(buf));
        while (n > 0) {
            write(STDOUT_FILENO, buf, n); // 输出数据
            n = read(pipefd[0], buf, sizeof(buf));
        }
        // 检测到 EOF
        if (n == 0) {
            printf("\nWrite end closed.\n");
        }
        close(pipefd[0]);
    }
    return 0;
}

情况2:读端关闭,写端未关闭

当写端调用 write()

  • 如果缓冲区中还有未读取的数据,写端仍可以继续写入,直到缓冲区满为止。
  • 一旦缓冲区满,write() 调用会触发一个 SIGPIPE 信号,并终止进程(默认行为)。
  • 如果进程捕获了 SIGPIPEwrite() 会返回 -1,并设置 errno 为 EPIPE
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>

void handle_sigpipe(int sig) { //捕捉信号
    printf("SIGPIPE caught: %d\n", sig);
}

int main() {
    int pipefd[2];
    pipe(pipefd);
    signal(SIGPIPE,handle_sigpipe);//设置信号处理方法
    if (fork() == 0) {  // 子进程
        close(pipefd[0]); // 关闭读端
        sleep(1); // 等待父进程关闭读端
        if (write(pipefd[1], "Hello", 5) == -1) {
            perror("Write error");
        }
        close(pipefd[1]); // 关闭写端
    } else {  // 父进程
        close(pipefd[1]); // 关闭写端
        close(pipefd[0]); // 关闭读端
        sleep(2);         // 等待子进程写入
    }
    return 0;
}

3. 命名管道 

        管道应用的⼀个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
        如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
        命名管道是⼀种特殊类型的文件,和管道类似。

命名管道的特点:

  1. 文件系统中的实体: 命名管道是一个存在于文件系统中的文件,可以通过 ls 查看。
  2. 双向通信: 数据按先进先出的规则流动,可以实现双向通信,但通常是单向使用。
  3. 阻塞特性: 如果一端没有准备好读取/写入,另一端的操作会阻塞,直到对应的操作准备好。
  4. 跨进程通信: 支持任意两个进程之间的数据交换,不要求进程具有亲缘关系。

创建命名管道:

使用 mkfifo 命令:

mkfifo mypipe

使用系统调用:

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

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

参数说明:

  • pathname 是命名管道的路径。
  • mode 指定权限,比如 0666 允许所有用户读写。

移除命名管道:

使用unlink命令:

unlink mypipe

使用系统调用: 

#include <unistd.h>

int unlink(const char *pathname);

参数

  • pathname:需要删除的文件的路径(绝对路径或相对路径)。

返回值

  • 返回 0:表示成功删除文件。
  • 返回 -1:表示删除失败,并设置 errno 来说明错误原因。

使用命名管道通信示例:

 进程A写数据:

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

int main() {
    int fd;
    char *fifo = "/tmp/mypipe";

    // 打开命名管道,写入数据
    fd = open(fifo, O_WRONLY);
    write(fd, "Hello from A\n", 13);
    close(fd);

    return 0;
}

进程B读数据:

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

int main() {
    int fd;
    char *fifo = "/tmp/mypipe";
    char buffer[128];

    // 打开命名管道,读取数据
    fd = open(fifo, O_RDONLY);
    read(fd, buffer, sizeof(buffer));
    printf("Received: %s", buffer);
    close(fd);

    return 0;
}

4. 管道读写规则

  1. 当没有数据可读时

    • O_NONBLOCK 禁用read 调用会阻塞,即进程暂停执行,一直等到有数据到来为止。
    • O_NONBLOCK 启用read 调用返回 -1errno 的值为 EAGAIN
  2. 当管道满的时候

    • O_NONBLOCK 禁用write 调用会阻塞,直到有进程读取数据。
    • O_NONBLOCK 启用write 调用返回 -1errno 的值为 EAGAIN
  3. 特殊情况

    • 如果所有管道写端对应的文件描述符被关闭,则 read 返回 0
    • 如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生信号 SIGPIPE,进而可能导致 write 进程退出。
  4. 写入数据的原子性

    • 当要写入的数据量不大于 PIPE_BUF 时,Linux 将保证写入操作的原子性。
    • 当要写入的数据量大于 PIPE_BUF 时,Linux 将不再保证写入操作的原子性。

补充说明

  • O_NONBLOCK 是文件描述符的一个标志,当启用时,read 和 write 操作不会阻塞进程。
  • PIPE_BUF 是管道的最大原子写入缓冲区大小(通常为 4096 字节,具体取决于系统实现)。

 

2.2 System V IPC

1.System V的背景

  1. 来源与发展

    • System V 是基于更早的 UNIX 版本(如 UNIX V7 和 UNIX System III)开发的,首次发布于 1983 年
    • 作为 UNIX 的一个商用分支,System V 在企业和服务器领域得到了广泛应用。
    • 它与 BSD UNIX(由加州大学伯克利分校开发)是 UNIX 生态系统的两个主要分支。
  2. 主要版本

    • System V Release 1 (1983):最初版本。
    • System V Release 2 (1984):增加了虚拟内存支持。
    • System V Release 3 (1987):引入了更广泛的网络功能和 IPC(进程间通信)。
    • System V Release 4 (1989):结合了 BSD、System V 和 SunOS 的特性,成为 UNIX 系统的重要里程碑。

2. System V的特性

        System V 在 UNIX 系统中引入了一系列新功能和设计理念,这些特性后来成为许多 UNIX 和类 UNIX 系统的标准:

1. System V IPC(进程间通信)

System V 提供了一套强大的 IPC 机制,包括:

  • 消息队列:用于进程之间的消息传递。
  • 共享内存:允许进程共享内存区域,提供高效的数据交换。
  • 信号量:用于进程同步和资源访问控制。

这些 IPC 特性广泛应用于需要进程协作的系统中。

2. 文件系统和目录结构

System V 引入了一些标准化的文件系统和目录结构:

  • 目录标准
    • /etc:系统配置文件。
    • /var:可变数据存储(如日志文件)。
    • /usr:用户级工具和库。
  • 支持 文件锁定,以避免多个进程同时修改文件内容。

3. 启动和服务管理

System V 引入了 init 系统,这是 UNIX 系统中的服务管理框架:

  • 使用 /etc/inittab 文件定义系统的运行级别(runlevel)。
  • 系统启动时按照运行级别加载不同的服务和脚本。

现代影响:System V 的 init 系统成为后来的服务管理系统(如 Linux 的 SysVinit 和 systemd)的基础。

4. 进程管理

System V 提供了强大的进程管理工具和功能:

  • ps:查看当前系统中的进程。
  • kill:向进程发送信号以终止或控制进程。
  • wait 和 fork:支持父子进程管理和创建。

5. 网络和通信

System V Release 4 引入了支持 TCP/IP 协议的网络栈,扩展了网络功能,使其能够适应现代网络环境。

3. System V IPC(进程间通信)

        System V 提供了多种进程间通信(Inter-Process Communication,IPC)方式,这些方式可以用于在不同的进程间交换数据、同步操作等。以下是 System V 支持的主要 IPC 方式及其特点

3.1 消息队列(Message Queue

特点:

  • 消息队列允许多个进程通过消息形式交换数据。
  • 消息以 消息类型 和 消息内容 的形式存储在内核中,接收者可以根据消息类型选择性地读取消息。
  • 适合异步通信,因为发送和接收操作之间不需要直接关联。
3.2 共享内存(Shared Memory

特点:

  • 共享内存允许多个进程共享一块内存区域,实现高速通信。
  • 是 System V IPC 中速度最快的方式。
  • 通常需要借助信号量来进行同步,防止多个进程同时访问共享内存导致数据不一致。

下面主要来说一下共享内存实现通信:

关键函数

1. shmget

shmget 用于创建或获取一个共享内存段。

函数原型

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

参数说明

  • key:共享内存段的标识符,类似文件的路径名。多个进程通过相同的 key 访问同一块内存。

常用ftok指定文件路径以及项目号生成,生成的key值唯一

  • size:共享内存段的大小(以字节为单位)。若段已存在,size 应小于或等于已存在段的大小。
  • shmflg
    • IPC_CREAT:如果共享内存段不存在,则创建。
    • IPC_EXCL:与 IPC_CREAT 结合使用,如果共享内存段已存在,则返回错误。
    • 权限标志:与文件权限类似,例如 0666(可读写)。

返回值

  • 成功:返回共享内存段的标识符(shmid)。
  • 失败:返回 -1,并设置 errno

示例:

key_t key = ftok("path", 65);
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
if (shmid == -1) {
    perror("shmget failed");
}

2. shmat

shmat 用于将共享内存段附加到当前进程的地址空间。

函数原型

#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数说明

  • shmid:共享内存段的标识符,由 shmget 返回。
  • shmaddr
    • NULL:由系统决定附加的地址。
    • 非 NULL:尝试附加到指定地址(一般不推荐)。
  • shmflg
    • 0:默认读写。
    • SHM_RDONLY:只读模式。

返回值

  • 成功:返回共享内存段的首地址。
  • 失败:返回 (void *) -1,并设置 errno

示例

void *shared_memory = shmat(shmid, NULL, 0);
if (shared_memory == (void *)-1) {
    perror("shmat failed");
}

3. shmdt

shmdt 用于将共享内存段从当前进程的地址空间分离。

函数原型

#include <sys/shm.h>

int shmdt(const void *shmaddr);

参数说明

  • shmaddr:共享内存段的首地址,由 shmat 返回。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

示例

if (shmdt(shared_memory) == -1) {
    perror("shmdt failed");
}

4. shmctl

shmctl 用于控制共享内存段的行为,比如删除、获取信息或修改权限。

函数原型

#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数说明

  • shmid:共享内存段的标识符。
  • cmd
    • IPC_STAT:获取共享内存段信息,填充到 buf
    • IPC_SET:设置共享内存段的权限,取自 buf
    • IPC_RMID:删除共享内存段。
  • buf
    • 用于存储或提供共享内存段的信息,结构为 struct shmid_ds

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

示例

struct shmid_ds buf;
if (shmctl(shmid, IPC_STAT, &buf) == -1) {
    perror("shmctl failed");
}

// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
    perror("shmctl failed");
}

完整代码示例: 

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

#define SHM_SIZE 1024  // 共享内存大小

int main() {
    // 创建一个键值
    key_t key = ftok("shmfile", 65);
    if (key == -1) {
        perror("ftok failed");
        exit(EXIT_FAILURE);
    }

    // 创建共享内存段
    int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);
    if (shmid == -1) {
        perror("shmget failed");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程:读取共享内存中的数据
        char *shared_memory = (char *)shmat(shmid, NULL, 0);
        if (shared_memory == (char *)-1) {
            perror("shmat failed");
            exit(EXIT_FAILURE);
        }

        printf("Child process reading from shared memory: %s\n", shared_memory);

        // 分离共享内存
        if (shmdt(shared_memory) == -1) {
            perror("shmdt failed");
            exit(EXIT_FAILURE);
        }

        exit(EXIT_SUCCESS);
    } else {
        // 父进程:写入共享内存
        char *shared_memory = (char *)shmat(shmid, NULL, 0);
        if (shared_memory == (char *)-1) {
            perror("shmat failed");
            exit(EXIT_FAILURE);
        }

        // 写入数据到共享内存
        const char *message = "Hello from parent process!";
        strncpy(shared_memory, message, SHM_SIZE);

        printf("Parent process wrote to shared memory: %s\n", message);

        // 分离共享内存
        if (shmdt(shared_memory) == -1) {
            perror("shmdt failed");
            exit(EXIT_FAILURE);
        }

        // 等待子进程完成
        wait(NULL);

        // 删除共享内存段
        if (shmctl(shmid, IPC_RMID, NULL) == -1) {
            perror("shmctl failed");
            exit(EXIT_FAILURE);
        }

        printf("Parent process deleted shared memory.\n");
    }

    return 0;
}
3.3  信号量(Semaphore

特点

  • 信号量是一种计数器机制,用于管理对共享资源的访问。
  • 典型用途是同步进程或线程,避免多个进程同时访问共享资源(如共享内存)。
  • System V 的信号量支持一组信号量操作,而不是单个信号量。
3.4 信号(Signals

特点

  • 信号是一种简单的异步通知机制,用于通知进程发生了某些事件。
  • 信号用于进程间通信的应用范围有限,但常用于中断处理或通知某个特定事件(如 SIGCHLDSIGINT)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值