Skip to content
Published at:

进程通信

进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。

但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC:Inter Processes Communication )。

管道

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发送给另一个进程。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

管道pipe

管道是一种最基本的IPC机制,伤于有血缘关系的进程之间,完成数据的传递;调用pipe系统函数即可创建一个管道.有如下特质:

  • 其本质是一个伪文件(实为内核缓冲区)
  • 由两个文件描述符线上服务,一个表示读端,一个表示写端
  • 规定数据从管道的写端流入管道, 从读端流出

internal:内核使用环形队列机制,借助内核缓冲区(4K)实现

缓冲区大小:ulimit -a

管道的局限性:

  • 数据不能进程自己写,自己读
  • 管道中数据不可反复读取.一旦读走,管道中不再存在
  • 采用半双工通信方式,数据只能单方向上流动
  • 只能在有公共祖先的进程间使用管道

常见的通信方式分类:单工通信,半双工通信,全双工通信

  • int pipe(int pipefd[2]);

pipe_demo.c

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

int main() {
    int fd_pipe[2] = {0};  // fd_pipe[0] -> read ; fd_pipe[1] --> write
    pid_t pid;

    // 创建管道
    int ret = pipe(fd_pipe);
    if (ret < 0) {
        perror("pipe");
        return ret;
    }

    pid = fork();
    if (pid == 0) {
        // child
        char buf[] = "hello; from child";
        // 往管道写端写数据
        write(fd_pipe[1], buf, strlen(buf));

        _exit(0);
    } else if (pid > 0) {
        // parent
        char str[50] = {0};

        // 从管道里读数据
        read(fd_pipe[0], str, sizeof(str));
        printf("str=[%s]\n", str);
    }

    return 0;
}

有名管道fifo

管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO文件。

命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。

命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:

  • FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
  • 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
  • FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。

fifo_w_demo.c

c
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    int len = 0;

    // create fifo file
    // int mkfifo(const char *pathname, mode_t mode);
    char* file_name = "fifo_file";
    if (access(file_name, F_OK) != 0) {
        int ret = mkfifo(file_name, 0664);
        if (ret == -1) {
            perror("failed to create fifo file");
            return ret;
        }
    }

    int fd = open(file_name, O_WRONLY);

    char* buf = "fife: write data";
    while (true) {
        sleep(1);
        len = write(fd, buf, strlen(buf));
        if (len == -1) {
            perror("Failed to write:");
            close(fd);
            break;
        }
        printf("write data; len = %d\n", len);
    }
    return 0;
}

fifo_r_demo.c

c
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    int len = 0;
    char* file_name = "fifo_file";
    int fd = open(file_name, O_RDONLY);

    while (true) {
        sleep(1);
        char buf[1024] = "";
        len = read(fd, buf, sizeof(buf));  // fife: write data
        if (len == -1) {
            perror("Failed to read fifo:");
            close(fd);
            break;
        }

        printf("buf = %s\n", buf);
        memset(buf, 0, 1024);
    }
    return 0;
}

共享存储映射

mmap/munmap函数

  • void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 映射一个文件或设备到内存
  • int munmap(void \*addr, size_t length); 解映射一个文件或设备

参数:

  • addr : 指定映射的起始地址, 通常设为NULL, 由系统指定
  • length:映射到内存的文件长度
  • prot:映射区的保护方式, 最常用的 :
    • 读:PROT_READ
    • 写:PROT_WRITE
    • 读写:PROT_READ | PROT_WRITE
  • flags:映射区的特性, 可以是
    • MAP_SHARED : 写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
    • MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write), 对此区域所做的修改不会写回原文件。
  • fd:由open返回的文件描述符, 代表要映射的文件。
  • offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射

mmap_demo0.c:创建共享存储映射

c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    // 1. file
    int len = 1024;
    int fd = open("xixi.txt", O_RDWR);  //读写文件
    ftruncate(fd, len);

    // 一个文件映射到内存,ptr指向此内存
    void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap error");
        exit(1);
    }
    close(fd);  //关闭文件

    char buf[4096];
    printf("buf = %s\n", (char*)ptr);      // 从内存中读数据,等价于从文件中读取内容
    strcpy((char*)ptr, "this is a test");  //写内容

    int ret = munmap(ptr, len);
    if (ret == -1) {
        perror("munmap error");
        exit(1);
    }
    return 0;
}

mmap_demo1.c子父进程通过

c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    int len = 1024;
    // 1. create file
    int fd = open("xixi.txt", O_RDWR | O_CREAT, 0664);  // 打开一个文件
    ftruncate(fd, len);

    // 2. create mmap memory
    void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("Failed to mmap:");
        exit(1);
    }
    close(fd);

    // 创建子进程
    pid_t pid = fork();
    if (pid == 0) {  // child
        // 4. read
        sleep(1);
        printf("%s\n", (char*)ptr);
    } else if (pid > 0) {  // parent
        // 3. write
        printf("write data:\n");
        strcpy((char*)ptr, "data from parent process");
        wait(NULL);  // 回收子进程资源
    }

    // 释放内存映射区
    int ret = munmap(ptr, len);
    if (ret == -1) {
        perror("munmap error");
        exit(1);
    }
    return 0;
}

信号

信号是 Linux 进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

“中断”在我们生活中经常遇到,譬如,我正在房间里打游戏,突然送快递的来了,把正在玩游戏的我给“中断”了,我去签收快递( 处理中断 ),处理完成后,再继续玩我的游戏。

这里我们学习的“信号”就是属于这么一种“中断”。我们在终端上敲“Ctrl+c”,就产生一个“中断”,相当于产生一个信号,接着就会处理这么一个“中断任务”(默认的处理方式为中断当前进程)。

信号特点:

  • 简单
  • 不能携带大量信息
  • 满足某个特设条件才发送

信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。

一个完整的信号周期包括三个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。如下图所示:

注意:这里信号的产生,注册,注销时信号的内部机制,而不是信号的函数实现。