• [Linux] 进程间通信基础


    在这里插入图片描述


    📄前言

    你是否了解进程间是如何通信的呢?你是否知道管道的工作原理呢?管道是Linux中最基本的也是最常用的进程间通信手段,----(Todo)

    进程间通信基础

    概念

    进程间通信(Inter-Process Communication)简称IPC,是不同进程之间传递数据的手段、接口。它是多进程间协同工作的核心机制。

    Linux 主要的IPC接口有:

    1. 管道(pipe)
    2. 信号(Signal)
    3. 共享内存(Shared Memory)
    4. 信号量(Semaphore)
    5. 消息队列(Message Queue)
    6. 套接字(Socket)

    本文将着重介绍管道与共享内存两种方式。

    管道

    概念

    管道大概是最多Linux用户使用过的一种的IPC接口了吧,只要有学过控制台代码,你就一定会认识它。它是一种先进先出的结构,数据的流向是单向的,最简单的使用管道的方法就是在 shell 中使用 “ | ”.

    echo "管道的使用方法" | grep "test.c"
    # 其功能就像它的名字,将进程数据通过一个管子传到另一个进程
    # [进程A] ---> |管道| ---> [进程B]
    
    • 1
    • 2
    • 3

    管道的类型有两种

    • 匿名管道: 主要用于父子进程间的通信,在文件系统中没有一个实际的名称,进程结束便消失。

    • 命名管道: 可用于不同进程之间的通信,与匿名管道不通命名管道在文件系统中有一个实际名称,可长久存在,并且可以通过两个管道来实现双向通信。

    管道的工作原理

    管道在实质上可以看成缓冲区,一端用于写,一端用于读。在进程将数据写入管道时,数据被储存在系统内核的缓冲区中,而不是直接存到管道文件中,管道文件只是作为一个标识符用于不同进程打开管道。正所谓实践出真知,其他的原理,让我们通过 Coding 来知晓吧。

    • 匿名管道的使用:
    // 匿名管道与命名管道的使用
    
    // 头文件 unistd.h
    // int pipe(int pipefd[2]);
    // pipefd[0]被设为管道的读端,[1]为写端
    
    const char *msg = "i like linux!";
    
    void test_1()
    {
        char buf[1024] = {0};
        int fd[2];
        pipe(fd);
        pid_t id = fork();	//生成子进程
        if(id < 0)  exit(1);
        else if(id == 0)	// 子进程入口
        {
            close(fd[0]);
            write(fd[1], msg, strlen(msg));
            close(fd[1]);
            exit(0);
        }
        // 父进程
        close(fd[1]);   //关闭读端
        read(fd[0], buf, sizeof(buf));
        printf("%s\n", buf);	
        close(fd[0]);
        wait(NULL);	//等待子进程结束
    }
    
    //  	  父进程                          子进程
    //  +----------------+             +----------------+
    //  |                |   系统缓冲区  |                |
    //  |   写入 fd[1]    |------------>|   读取 fd[0]   |
    //  |                |     管道     |                |
    //  +----------------+             +----------------+
    //		写入端						    读入端
    
    // 你可能会有些疑问,为什么要用这么多个close。
    // 关闭不需要用的文件描述符是个良好的编程习惯
    // 因为文件描述符是有限的资源,而且不关闭会在某些情况下,导致进程阻塞。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 命名管道的使用:
    // 头文件 sys/types.h sys/stat.h
    // int mkfifo(const char* pathname, mode_t mode) 创建管道文件
    // pathname:管道的名称    mode:设置文件的权限。
    
    // server.cpp
    int main()
    {
        int ret = mkfifo("fifo", 0644);
    	const char* msg = "server:i love linux"
        int fd = open("fifo", O_WRONLY);	// O_WRONLY:只写模式
    
        for (int i = 0; i < 10; ++i)
        {
            write(fd, msg, strlen(msg));	//读端没有打开,进程则会阻塞
            sleep(1);
        }
    
        return 0;
    }
    
    // client.cpp
    int main()
    {
        int fd = open("fifo", O_RDONLY);  // O_RDONLY:只读模式
        char buf[1024];
    
        while (1)
        {	// 如果管道写端没有打开,进程则会阻塞
            ssize_t n = read(fd, buf, sizeof(buf));
            buf[n] = '\n';
            if (n == 0)	// 读端断开连接,返回0
            {
                printf("process exit\n");
                return 0;
            }
            else if (n > 0)
            {
                write(1, buf, n + 1);	// 向标准输出打印
            }
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    管道的读写规则:

    • 当管道读写端任意一端未打开,则进程会阻塞等待。
    • 管道写满了,尝试写入的进程会阻塞
    • 管道无数据可读,如果写端关闭则返回0,写端没有关闭则阻塞进程。

    模拟实现shell中的管道

    int main()
    {
        int pipefd[2];
        
        if(pipe(pipefd) == -1 && errno != EEXIST)
        {
            perror("pipe");
            exit(1);
        }
    
        pid_t pid = fork();
        if(pid == 0)
        {	//子进程
            dup2(pipefd[1], STDOUT_FILENO);	
            //dup = duplicate(复制),将pipefd[1]复制到标准输出(1号)
            // 像标准输出打印的数据都讲传入管道写端
            close(pipefd[0]);	
            close(pipefd[1]);	// 已经将其复制到标准输出,可以关闭
            char* argv[] = {"ls", "-l", nullptr};
            execvp(argv[0], argv);	//执行命令
            exit(0);
        }
    
        pid_t pid2 = fork();
        if(pid2 == 0)
        {	//子进程
            dup2(pipefd[0], STDIN_FILENO);	//将fd[0]复制到标准输入
            close(pipefd[0]);
            close(pipefd[1]);
    
            char* argv[] = {"grep", (char*)"test", nullptr};
            execvp(argv[0], argv);
            exit(0);
        }
    
        close(pipefd[0]);
        close(pipefd[1]);
        wait(nullptr);
        wait(nullptr);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    共享内存

    概念

    共享内存是所有IPC机制中最快的一种机制,它能使得多个进程访问同一块内存区域,而不用在进程间复制拷贝。 例如管道,它实际就是系统中的一块缓存区,两个进程间交流就必须将缓冲区的数据拷贝的自己内存中。共享内存则是在物理内存中开辟一段空间,然后通过页表将其映射到程序的共享区,进程直接对内存进行读写。

    +---------+         +-------------------+        +---------+
    |         |  ---->  |                   |  <---- |         |
    | Process |         |   Shared Memory   |        | Process |
    |    A    |  <----  |       Segment     |  ----> |    B    |
    |         |         |                   |        |         |
    +---------+         +-------------------+        +---------+
         ^                                               ^
         |                                               |
        Read                                           Write
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接口的介绍

    在使用共享内存前,必须先简单介绍一下System V 与 POSIX,它们是UNIX系统的两种不同系统标准,而在Linux上他们两者的接口都有兼容,接下来我们要使用的共享内存属于System V的。

    要使用共享内存就得先知道如何检查系统中的共享内存,以及如果程序遇到异常时,如何删除共享内存。

    #显示共享内存
    ipcs -m 
    # 使用 ipcs 命令来检查system V的通信信息,默认情况下显示所有的资源。
    
    # 删除共享内存
    ipcrm [shm|msg|sem] ID ... 
    # 选项
    # -m 根据共享内存的shmid来删除
    # -M 根据共享内存的shmeky来删除
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    共享内存的接口:

    // 接口介绍
    // 涉及头文件:  
    
    key_t ftok(const char* pathname, int proj_id);
    //使用ftok来生成唯一的key值,参数为路径名与项目ID
    
    int shmget(key_t key, size_t size, int shmflg);	
    // 返回值为shmid
    // 根据key来生成共享,size为共享内存的字节数只能为1024的倍数。
    // shmflg:共享内存的权限,一般使用像 0644,IPC_CREAT、IPC_EXCL等。
    
    // 挂载共享内存
    void* shmat(int shmid, const void* shmaddr, int shmflg);
    // shmid 为shmget的返回值
    // shmaddr:将共享内存连接到当前进程地址空间的特定地址
    // shmflg:一般为0或者SHM_RDONLY(只读)
    
    //取消挂载共享内存
    int shmdt(const void* shmaddr);
    // shmaddr :共享内存的地址
    
    //控制共享内存
    int shmctl(int shmid, int cmd, struct shmid_ds* buf);
    // shmid:共享内存的id号
    // cmd:控制命令,一般为IPC_STAT(获取共享内存的状态)、IPC_SET(设置共享内存的参数)、IPC_RMID(删除共享内存)
    // buf:指向shmid_ds 结构的指针,用于设置共享内存或存储共享内存的状态。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    共享内存的使用

    共享内存主要用于两个不同进程数据的交流,这里将使用两个不同进程,一个用于写数据,一个用于读数据。因为共享内存的读写是无法预测的(对方不知道你何时写了),所以需要用到命名管道来辅助。

    server.cpp:

    // server.cpp
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    const char* pathname = "/home/catianri/code";
    
    const int project_id = 0x11223344;
    
    int main() {
        key_t key = ftok(pathname, project_id); // 创建唯一的key
        int cnt = 0, code = 0;	// code用于通知对方进程,已经开始写入。
        int shmid = shmget(key, 1024, 0666|IPC_CREAT); // 创建共享内存
    
        mkfifo(".fifo", 0666);	//创建命名管道
        int fd = open(".fifo", O_WRONLY);	
    
        int* arr = static_cast<int*>(shmat(shmid, nullptr, 0)); // 将共享内存附加到进程的地址空间
        while(cnt < 10)
        {
            arr[cnt++] = cnt;	// 向共享内存写入数据 
            write(fd, &code, sizeof(int));	// 通知另一个进程
            sleep(1);
        }
    
        shmdt(arr); // 断开共享内存连接
    
        shmctl(shmid, IPC_RMID, NULL); // 销毁共享内存
        close(fd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    client.cpp:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    const char* pathname = "/home/catianri/code";
    
    const int project_id = 0x11223344;
    
    int main() {
        key_t key = ftok(pathname, project_id); // 使用相同的文件和项目ID来创建key
        int shmid = shmget(key, 1024, 0666|IPC_CREAT); // 连接到共享内存
    
        int *arr = static_cast<int*>(shmat(shmid, nullptr, 0)); // 将共享内存附加到进程的地址空间
        mkfifo(".fifo", 0666);
        int fd = open(".fifo", O_RDONLY);
        int* code, cnt = 0;
    
        while(1)
        {
            ssize_t n = read(fd, &code, sizeof(int));
            if(n == 0)	//server已经断开写端
            {
                close(fd);
    			std::cout << "process exit" << std::endl;
            }
            else 
            {	//拿取共享内存中的数据。
                std::cout << arr[cnt++] << " ";
                fflush(stdout);	//刷新缓冲区。
            }
        }
    
        shmdt(arr); // 断开共享内存连接
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    共享内存的特点:

    • 高效性: 共享内存是最快的IPC方式,它允许进程直接访问共享数据,避免了拷贝操作。
    • 灵活性: 共享内存提供了高度的灵活性,开发者可以根据需要自定义共享数据的结构和管理方式。
    • 手动同步: 由于可以多进程同时访问,所以需要外部同步机制。
    • 复杂性: 与其他IPC机制(如管道和消息队列)相比,共享内存的使用和管理更为复杂。

    📓总结

    特性管道System V共享内存
    性能和效率数据需要在进程间复制,存在额外的CPU开销和延迟。允许多个进程直接访问同一内存区,减少了数据复制,提高了效率。
    使用场景适用于顺序数据流通信,常用于父子进程或紧密相关进程间。适用于性能要求高的场景,如大数据处理、实时系统,因为它几乎无延迟地实现数据共享。
    同步机制自带同步机制,写入端和读取端会在必要时阻塞,直到对方准备好。需要额外的同步机制(如信号量、互斥锁)来防止数据竞争和保证一致性。
    容错性和可靠性相对简单可靠,但需要正确处理EOF和管道破裂等情况。需要仔细管理资源和同步,避免竞态条件、死锁或数据损坏。
    易用性API简单,容易实现和维护,但功能有限。提供了一套功能丰富的API,但相较于管道,使用和维护更加复杂。

    📜博客主页:主页
    📫我的专栏:C++
    📱我的github:github

  • 相关阅读:
    8.绘制旗帜(TRIANGLE_FAN)
    Java 设计模式——状态模式
    【vue3】keep-alive缓存组件
    gopacket reassembly源码分析
    C++实现Date类
    hdu3549--Flow Problem(初识最大流)
    程序分析与优化 - 6 循环优化
    CentOS部署Redis(详细)
    深入理解ES6模块化:语法、特性与最佳实践
    超高速PCIe实时运动控制卡与应用方案将亮相深圳NEPCON,正运动技术邀您前来体验!
  • 原文地址:https://blog.csdn.net/CaTianRi/article/details/136676154