【Linux】进程间通信

一、本质

       不同进程看到同一份资源

二、目的

        让多个进程协同完成同一个事情

数据传输在一个在线购物网站中,一个进程负责从数据库中获取用户的购物车数据,另一个进程负责根据购物车数据计算总价和运费,这就需要数据在两个进程间传输
资源共享在一个打印服务器中,多个进程都需要使用打印机这个资源。通过进程间通信来协调打印机的使用,避免多个进程同时访问导致冲突,实现资源的共享和合理分配
事件通知在一个监控系统中,当传感器检测到异常情况(如温度过高)时,一个进程负责收集这个事件信息,并通过进程间通信通知另一个进程采取相应的控制措施(如启动风扇降温)
进程控制gdb 调试 进程,gdb 这个进程需要与被调试进程进行数据通信,信息传递

三、进程间通信方式

1. 进程间通信的设计思路

        进程一定在保证各自的独立性的基础上,进行通信。这就表明了进程间通信不可以直接访问进程内部,而是要通过一个双方都可以访问的空间来进行通信。

        该空间不可以由通信进程来提供,因为如果由通信进程来提供,则也属于进程的资源,不能保证进程独立性。

进程间通信的设计思路为:

  • 有一个可以交换进程数据的空间
  • 该空间不可以由通信进程提供,而是由 OS 提供

进程间通信的本质:

        让不同进程看到同一份资源

2. 四种通信方式

        OS 提供的空间结构不同(如:数组、链表、栈、队列等),就决定了有不同的通信形式

(1)管道(采用队列结构)

 a. 前置知识 

        若分别以 “ r ”,“ w ”方式打开同一个文件,那么struct_file就回存在两份,但是这两份中的文件缓冲区为同一份,此时向该缓冲区进行读写,就会导致读写错乱

        至于为什么会读写错乱,当我们在内部创建线程时,无法保证我们读到的,是我们希望得到的数据,而是有可能为其他线程写入的数据

如何解决?

        创建子进程,让父进程 写 / 读,子进程 读 / 写

        在父进程同时以读写来打开文件描述符后,创建子进程,此时子进程就会将父进程的相关内核数据结构都复制一份,所以子进程的文件描述符表也是指向父进程的文件描述符表指向的 struct file 上,我们此时只需要关掉子进程的写/读端,父进程的读/写端,就可以在保证读写不错乱的情况下,来让父子进程进行通信,因为此时父子进程已经看到了同一份资源(struct file中的缓冲区),并且这不属于通信进程的资源,而是 OS 创建的。

        这种基于文件的,让不同进程看到同一份资源的通信方式,叫做管道!

     (管道只能单向通信,这是因为管道是一个缓冲区,如果是全双工,我们保证不了读写的独立性,即缓冲区只能被一方读取,另一方写入)

【注】:

        struct file 结构体允许被多个进程的文件描述符表中的指针指向,我们关闭一个 fd ,实际上是将struct file 结构体的引用计数减一,同时断开指向。直到 struct file 结构体的引用计数为 0 ,此时 OS才会清除这个 struct file 结构体。

b. 匿名管道
原理
使用 pipe 系统调用,创建一个内存级文件
创建子进程,让父子进程看到相同的信息,关闭父子进程不需要的文件描述符
int pipe(int pipefd[2]);

pipefd  :  输出型参数,pipefd[0] 为“ r ”,pipefd[1] 为 “ w ”

【注】:

  • 匿名管道 被读取后,数据将不再存在于匿名管道中
四种情况

I:写端休眠,读端正常

        此时管道内无数据,并且写端不关闭 fd ,那读端就要阻塞等待,直到管道中有数据

II:管道被写满 && 读端不关闭 fd 

        写端写满后,就要阻塞等待,直到读端去读

III:写端不写 && 关闭 fd

        读端会把管道的数据读完,最后 read 返回值为 0 ,表示读结束

IV:读端不读 && 读端关闭

        写端去向管道内写入数据,此时 OS 会直接发送 13 号信号(SIGPIPE)终止写端进程,因为一个永远不会被读的缓冲区,写入就没有意义。

五种特性

I:只能单向通信(半双工)

        这是因为我们要完成父子进程之间的数据传输,就必须要保证读端读到的数据都是写端写入的,若是全双工,则无法保证读端读到的数据就是写端写入的,还有可能是自己写入的,就会造成数据混乱。

        所以若想双向通信,则可以建立两个匿名管道

II:自带同步机制

        只有匿名管道中有数据,才可以读,否则读端阻塞等待;只有匿名管道中没被写满,写端才能写入,否则写端阻塞等待。

        这就是同步机制,不像????

III:只适用于有血缘关系的进程来通信

IV:父子进程退出,管道自动释放

        因为文件的生命周期是随进程的,而管道是内存级文件,只有struct file ,没有磁盘对应的实体。

V:管道是面向字节流的

        以字节为单位进行读取

知识迁移:命令行的管道命令
cat file.c | grep .h

        bash 会创建一个管道,两个子进程,“ | ” 前的关闭读端,“ | ” 后的关闭写端,通过管道传输数据。

代码实现
/**
 * 匿名管道的使用
*/
#include "Log.hpp"

#include <iostream>

#include <string.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    // 创建管道,pipefd[0] -> r、pipefd[1] -> w
    int pipefd[2] = {0};
    int ret = pipe(pipefd);
    if(ret == -1)
    {
        _log.LogMessage(ClassFile, Error, "pipe error : %s\n", strerror(errno));
    }
    // 创建子进程
    int pid = fork();
    if(pid == -1)
    {
        _log.LogMessage(ClassFile, Error, "fork error : %s\n", strerror(errno));
    }
    else if(pid == 0) // 子进程负责写
    {
        close(pipefd[0]);
        char buffer[1024];
        while(1)
        {
            std::cin.getline(buffer, 1024);
            if(strcmp(buffer, "quit") == 0)
            {
                std::cout << "写端退出" << std::endl;
                break;
            }

            int ret_write = write(pipefd[1], buffer, 1024);
            if(ret_write == -1)
            {
                _log.LogMessage(ClassFile, Error, "write error : %s\n", strerror(errno));
                std::cout << "写数据失败" << std::endl;
                break;
            }
        }
        close(pipefd[1]);
        exit(0);
    }
    // 父进程负责读
    close(pipefd[1]);
    char buffer[1024];
    while(1)
    {
        int ret_read = read(pipefd[0], buffer, sizeof(buffer));
        if(ret_read == -1) // 读取失败
        {
            _log.LogMessage(ClassFile, Error, "read error : %s\n", strerror(errno));
            break;
        }
        else if(ret_read == 0) // 对端关闭,读到末尾
        {
            std::cout << "对端退出,读到结尾了" << std::endl;
            break;
        }
        else // 读取成功
        {
            buffer[ret_read] = 0;
            std::cout << "写端说 : " << buffer << std::endl;
        }
    }
    close(pipefd[0]);
    // 父进程回收子进程资源
    int ret_wait = wait(nullptr);
    if(ret_wait == -1)
    {
        _log.LogMessage(ClassFile, Error, "wait error : %s\n", strerror(errno));
    }
    else
    {
        std::cout << "等待成功,父进程退出" << std::endl;
    }
    return 0;
}
c. 命名管道(会用)
原理

让不同的进程通信

【注】

  • 不同的进程使用的命名管道文件的 struct file 为同一个
  • 命名管道文件只用于进程间通信,不会将缓冲区的内容刷新到磁盘上
  • 先打开读端会阻塞等待,直到写端打开并写入数据
创建命名管道文件的方式

命令行方式:

mkfifo file_name

库函数实现:

// 创建命名管道文件
int mkfifo(const char *pathname, mode_t mode);
// 删除文件
int unlink(const char *pathname);
代码实现
// 读端代码read.cc
#include <iostream>

#include <cstring>

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

const int SIZE = 1024;
const char* PIPE = "./file.pipe";
int main()
{
    umask(0);
    //1. 建立命名管道文件
    int ret = mkfifo(PIPE, 0666);
    if(ret < 0)
    {
        std::cout << "mkfifo error : " << strerror(errno) << std::endl;
        exit(-1);
    }
    // 2. 以读的方式打开
    int rfd = open(PIPE, O_RDONLY);
    if(rfd < 0)
    {
        std::cout << "open error : " << strerror(errno) << std::endl;
        unlink(PIPE);
        exit(-1);
    }
    char buffer[SIZE];
    // 3. 一直从命名管道中读取
    while(true)
    {
        int read_ret = read(rfd, buffer, sizeof(buffer) - 1);
        if(read_ret < 0)
        {
            std::cout << "read error : " << strerror(errno) << std::endl;
            break;
        }
        else if(read_ret == 0)
        {
            std::cout << "writer is closed && the size of buffer is null" << std::endl;
            std::cout << "reader exit" << std::endl;
            break;
        }
        else
        {
            buffer[read_ret] = 0;
            std::cout << "writer send : " << buffer << std::endl;
        }
    }
    close(rfd);
    unlink(PIPE);
    return 0;
}
// 写端代码write.cc
#include <iostream>

#include <cstring>

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

const int SIZE = 1024;
const char *PIPE = "./file.pipe";

int main()
{
    // 1. 以写的方式打开
    int wfd = open(PIPE, O_WRONLY);
    if (wfd < 0)
    {
        std::cout << "open error : " << strerror(errno) << std::endl;
        exit(-1);
    }

    // 2. 向命名管道中写入
    char buffer[SIZE];
    while(true)
    {
        std::cout << "enter# ";
        std::cin.getline(buffer, sizeof(buffer));
        if(strcmp(buffer, "quit") == 0)
        {
            std::cout << "writer exit" << std::endl;
            break;
        }
        int write_ret = write(wfd, buffer, strlen(buffer));
        if(write_ret < 0)
        {
            std::cout << "write error : " << strerror(errno) << std::endl;
            break;
        }
    }
    close(wfd);
    return 0;
}
d. 共享内存(会用)
原理

        OS 在内存中开辟一段空间,将这段空间通过页表映射到进程地址空间的共享区中,让不同的进程看到同一份资源,这段资源/空间就叫共享内存 shm。

        每个 shm 在内核中都有唯一性标识符

问题

OS 一定会管理内存中存在的很多共享内存(先描述,再组织),但我们如何保证不同的进程指向的是希望指向的共享内存呢?

        通过共享内存的唯一性标识符

共享内存的生命周期?

        shm 的生命周期随 OS,即我们创建后不主动释放,则一直存在,直到 OS 退出,因为 OS 相当于 shm 的管理者。

        这里与文件操作不同,文件的生命周期随进程,因为我们在打开一个文件时,首先会将文件加载到内存中,struct file 的引用计数自增,返回给上层一个 fd ,即使我们不主动关闭 fd,当进程退出时,进程的内核数据结构都会释放,fd也就会被释放,此时 OS 检测到struct file 的引用计数为0,就会释放该文件。

共享内存创建流程
创建共享内存shmget
建立映射关系shmat
传输信息使用映射后的起始地址
删除映射关系shmdt
删除共享内存shmctl
优化:使用pipe来提供同步机制

创建共享内存:shmget

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

使用 ftok 函数 来形成唯一 key(极大情况唯一),ftok 函数通过算法来形成的 key ,ftok 的参数随便写。

size开辟共享内存的空间大小,单位是字节,但是在内核(kernel)中,shm 的大小是以 4kb 为基本单位的,所以建议申请 4nkb
shmflg

IPC_CREAT :shm 不存在则创建,反之获取该shm

IPC_CREAT | IPC_EXCL:shm 不存在则创建,反之出错

返回值

成功返回 shm 的标识符,类似于 fd 

失败返回-1

  • OS 无法做到 key 的唯一性,必须由用户传入
  • 不同的进程约定使用相同的 ftok 函数即可看到同一个共享内存shm

建立映射关系:shmat

 void *shmat(int shmid, const void *shmaddr, int shmflg);
shmidshm 的 id
shmaddr用户指明将 shm 映射到哪个虚拟地址
填 nullptr 由 OS 自由选择
shmflg设置为 0
返回值

成功返回 shm 映射到的地址空间中的起始地址
(虚拟地址可以不同,但是物理地址一定相同)

失败返回(void*)-1

传输信息:

        直接通过建立映射后的地址来读写

删除映射关系:shmdt

 int shmdt(const void *shmaddr);
shmaddr映射到的地址空间的起始地址(shmat的返回值)
返回值成功返回0,失败返回-1

删除共享内存:shmctl

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

shm 的 id

cmd

对 shm 的操作
IPC_STAT:获取 shm 的所有属性
IPC_SET:设置 shm 的属性

IPC_RMID:删除 shm

返回值成功返回0,失败返回-1

优化:使用匹配提供同步机制

/**/

【注】

  • 默认情况,shm 的读端不会管写端,共享内存不提供同步机制,会导致数据不一致
  • shm 是所有进程间通信最快的

        因为在建立映射的时候,我们拿到的是进程地址空间中共享区的起始地址,直接可以使用起始地址通过页表来找到 shm,不用使用系统调用!而管道则要通过使用系统调用来进行数据的读写,共享内存直接使用地址来进行读写。

  • key 是用来让 OS 来区分 shm 的唯一性
  • shmid 是用来在代码级 和 指令级 控制 shm 的
共享内存的指令
ipcs

查看当前的消息队列、共享内存、信号量
-m 查看共享内存

-q 查看消息队列

-s 查看信号量

perms:shm的读写权限

nattch:几个进程与该shm关联

ipcrm

-m 删除共享内存

-q  删除消息队列

-s  删除信号量

代码实现
/*用于封装共享内存读端实现接口*/
#pragma once

#include <iostream>
#include <string>

#include <cstring>

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

#include "Log.hpp"

namespace my_read_shm
{
    enum ExitError
    {
        FtokError = 1,
        ShmgetError,
        ShmatError,
        ShmdtError,
        ShmctlError,
        MkfifoError,
        OpenError,
        ReadError
    };

    class Reader_Shm
    {
    public:
        Reader_Shm(const std::string &pathname, const int id, int shm_size)
            : _shmid(0)
            , _size(shm_size)
            , _key(0)
            , _read_buffer(nullptr)
            , _pipe_file("./file.pipe")
            , _rfd(-1)
        {
            // 1. 构建shm唯一性标识key
            _key = ftok(pathname.c_str(), id);
            if (_key == -1)
            {
                _log.LogMessage(ClassFile, Error, "read ftok error : %s\n", strerror(errno));
                exit(FtokError);
            }
            _log.LogMessage(ClassFile, Info, "read ftok success\n");
            // 2. 创建共享内存
            CreShm();
            // 3. 建立映射关系
            CreMap();
        }

        ~Reader_Shm()
        {
            // 1. 删除映射关系
            DelMap();
            // 2. 删除共享内存
            DelShm();
            // 3. 关闭并删除pipe
            DelPipe();
        }

        void Read()
        {
            TouchPipe();
            char buffer[_size];
            while (true)
            {
                read(_rfd, buffer, 1);
                strcpy(buffer, (const char *)_read_buffer);
                if (strcmp(buffer, "quit") == 0)
                {
                    std::cout << "writer quit, so I quit" << std::endl;
                    break;
                }
                std::cout << "writer send# " << buffer << std::endl;
            }
        }

    private:
        // 创建共享内存
        void CreShm()
        {
            _shmid = shmget(_key, _size, IPC_CREAT | IPC_EXCL);
            if (_shmid < 0)
            {
                _log.LogMessage(ClassFile, Error, "read CreShm error : %s\n", strerror(errno));
                exit(ShmgetError);
            }
            _log.LogMessage(ClassFile, Info, "read CreShm success\n");
        }
        // 创建映射关系
        void CreMap()
        {
            _read_buffer = shmat(_shmid, nullptr, 0);
            if (_read_buffer == (void *)-1)
            {
                _log.LogMessage(ClassFile, Error, "read CreMap error : %s\n", strerror(errno));
                exit(ShmatError);
                // 删除共享内存
                DelShm();
            }
            _log.LogMessage(ClassFile, Info, "read CreMap success\n");
        }
        // 删除共享内存
        void DelShm()
        {
            int ret_shmctl = shmctl(_shmid, IPC_RMID, nullptr);
            if (ret_shmctl < 0)
            {
                _log.LogMessage(ClassFile, Error, "read DelShm error : %s\n", strerror(errno));
                exit(ShmctlError);
            }
            _log.LogMessage(ClassFile, Info, "read DelShm success\n");
        }
        // 删除映射关系
        void DelMap()
        {
            int ret_shmdt = shmdt(_read_buffer);
            if (ret_shmdt < 0)
            {
                _log.LogMessage(ClassFile, Error, "read DelMap error : %s\n", strerror(errno));
                DelShm();
                exit(ShmdtError);
            }
            _log.LogMessage(ClassFile, Info, "read DelMap success\n");
        }
        // 创建命名管道
        void TouchPipe()
        {
            // 建立命名管道用于同步机制
            int ret_mkfifo = mkfifo(_pipe_file.c_str(), 0666);
            if (ret_mkfifo < 0)
            {
                _log.LogMessage(ClassFile, Error, "read mkfifo error : %s\n", strerror(errno));
                exit(MkfifoError);
            }
            _rfd = open(_pipe_file.c_str(), O_RDONLY);
            if (_rfd < 0)
            {
                _log.LogMessage(ClassFile, Error, "read open error : %s\n", strerror(errno));
                exit(OpenError);
            }
        }
        // 关闭文件描述符,删除命名管道
        void DelPipe()
        {
            close(_rfd);
            unlink(_pipe_file.c_str());
        }

    private:
        int _shmid;         // 代码中操作使用的shmid
        int _size;          // shm空间大小
        key_t _key;         // shm在内核中的唯一性标识
        void *_read_buffer; // shm的虚拟地址的起始地址

        std::string _pipe_file; // 命名管道名称
        int _rfd;
    };
}
/*用于封装共享内存写端实现接口*/
#pragma once

#include <iostream>
#include <string>

#include <cstring>

#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/ipc.h>

#include "Log.hpp"

namespace my_write_shm
{
    enum ExitError
    {
        FtokError = 1,
        ShmgetError,
        ShmatError,
        ShmdtError,
        ShmctlError,
        OpenError,
        WriteError
    };

    class Writer_Shm
    {
    public:
        Writer_Shm(const std::string &pathname, const int id, int size)
            : _shmid(0)
            , _key(0)
            , _size(size)
            , _write_buffer(nullptr)
            , _pipe_file("./file.pipe")
            , _wfd(-1)
        {
            // 1. 构建shm唯一性标识key
            _key = ftok(pathname.c_str(), id);
            if (_key == -1)
            {
                _log.LogMessage(ClassFile, Error, "write ftok error : %s\n", strerror(errno));
                exit(FtokError);
            }
            _log.LogMessage(ClassFile, Info, "write ftok success\n");
            // 2. 获取 shm
            GetShm();
            // 3. 建立映射关系
            CreMap();
        }

        ~Writer_Shm()
        {
            // 删除映射
            DelMap();
            DelPipe();
        }

        void Write()
        {
            OpenPipe();
            char buffer[1024];
            while (true)
            {

                std::cout << "Please enter# ";
                std::cin.getline(buffer, sizeof(buffer));
                strcpy((char *)_write_buffer, buffer);
                write(_wfd, buffer, 1);
                if (strcmp(buffer, "quit") == 0)
                {
                    std::cout << "writer exit" << std::endl;
                    break;
                }
            }
        }

    private:
        // 获取shm
        void GetShm()
        {
            _shmid = shmget(_key, _size, IPC_CREAT);
            if (_shmid < 0)
            {
                _log.LogMessage(ClassFile, Error, "write GetShm error : %s\n", strerror(errno));
                exit(ShmgetError);
            }
            _log.LogMessage(ClassFile, Info, "write GetShm success\n");
        }
        // 建立映射
        void CreMap()
        {
            _write_buffer = shmat(_shmid, nullptr, 0);
            if (_write_buffer == (void *)-1)
            {
                _log.LogMessage(ClassFile, Error, "write CreMap error : %s\n", strerror(errno));
                exit(ShmatError);
            }
            _log.LogMessage(ClassFile, Info, "write CreMap success\n");
        }
        // 删除映射
        void DelMap()
        {
            int ret_shmdt = shmdt(_write_buffer);
            if (ret_shmdt < 0)
            {
                _log.LogMessage(ClassFile, Error, "write DelMap error : %s\n", strerror(errno));
                exit(ShmdtError);
            }
            _log.LogMessage(ClassFile, Info, "write DelMap success\n");
        }
        // 打开命名管道文件
        void OpenPipe()
        {
            _wfd = open(_pipe_file.c_str(), O_WRONLY);
            if (_wfd < 0)
            {
                _log.LogMessage(ClassFile, Error, "write open error : %s\n", strerror(errno));
                exit(OpenError);
            }
        }
        // 关闭文件描述符
        void DelPipe()
        {
            close(_wfd);
        }

    private:
        int _shmid; // 代码中操作使用的shmid
        key_t _key; // shm在内核中的唯一性标识
        int _size;
        void *_write_buffer; // shm的虚拟地址的起始地址

        std::string _pipe_file;
        int _wfd;
    };
}
c. 消息队列(知道即可)

已经被淘汰了

d. 信号量

同步与互斥中重点介绍

四、重谈进程间通信

        共享内存、消息队列和信号量是 OS 特意设计的,但是这三者本质却没有与“Linux下一切皆文件”的理念靠拢,所以根据时代的发展,逐渐边缘化。

        而管道的这种方式,是使用了文件的,所以提供了同步的功能。

  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

终将向阳而生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值