4.详解Linux进程间通信

目录

  • 进程间通信的介绍
  • 文件级别通信原理
  • 匿名管道通信原理
  • 进行管道通信(代码)
  • 管道的特点
  • 实现进程池
  • 命名管道
  • system V通信–共享内存
  • System V通信–共享内存、消息队列和信号量的关系
  • 信号量

进程间通信的介绍

什么是进程间通信?
两个或者多个进程实现数据层面的交互。因为进程是具有独立性的,所以这必然会导致进程通信的成本比较高

为什么要进程间通信?
有些时候我们是需要多进程协同完成某种业务内容的

怎么实现进程间通信?
a.进程间通信的本质:必须让不同的进程看到同一份“资源”—“资源”是什么?是特定形式的内存空间
b.这个“资源”是谁提供?一般是操作系统。为什么不是我们两个进程中的一个呢?假设一个进程提供,那么这个资源属于谁?而且进程是具有独立性的,这样做必然会破坏进程的独立性
c.我们进程访问这个空间进行通信,本质就是访问操作系统,所以系统提供了系统调用接口。文件系统IPC通信模块定制的标准:POSIX–让通信可以跨主机、systemV–聚焦在本地通信(其中有共享内存、消息队列、信号量)
d.基于文件级别的通信方式–管道

文件级别通信原理

在这里插入图片描述

我们把不用打开磁盘文件,操作系统自己创建的struct_file叫做内存级文件

匿名管道通信原理

如何让两个进程看到同一份管道文件呢?fork创建子进程,这种方式形成的管道叫匿名管道

在这里插入图片描述

站在内核的角度看原理
在这里插入图片描述
因为只能进行单向通信,所以叫管道。
父子进程能管道通信,那么兄弟之间行不行?孙子之间行不行?都行,只要有血缘关系就行

进行管道通信(代码)

介绍pipe接口
在这里插入图片描述
pipefd[2]为输出型参数,pipe[0]对应的是读端文件描述符,pipe[1]对应的是写端文件描述符
成功返回0,错误返回-1

int main()
{
    int pipefd[2];
    int n = pipe(pipefd);
    cout << "pipefd[0]:" << pipefd[0] << "    " << "pipefd[1]:" << pipefd[1] << endl ;
    return 0;
}

打印的结果为
在这里插入图片描述


写一个父子进程匿名管道通信的代码

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    int pipefd[2];
    int n = pipe(pipefd);
    if (n < 0)//一定要养成打印错误信息的好习惯,因为这是以后代码差错的关键
    {
        perror("create pipe failed");
        exit(2);
    }
    pid_t pid = fork();
    if (pid == 0)//子进程
    {
        close(pipefd[1]);//子进程关闭写端,只进行读取
        char readBuffer[1024];
        int m = read(pipefd[0], readBuffer, sizeof(readBuffer) - 1);//为什么要减1?返回的m最多是1023
        if (m <= 0)
        {
            perror("read failed");
            exit(1);
        }
        readBuffer[m] = 0;//如果不减一,则m可能是1024,readBuffer[1024]=0则会越界访问,这就是为什么要减一
        printf("child process:%d getMessage:%s\n", getpid(), readBuffer);
        close(pipefd[0]);//退出进程前,关闭读端文件描述符,当然也可以不关,因为进程退出会自动关闭描述符
        exit(0);
    }
    //父进程
    close(pipefd[0]);
    char writeBuffer[1024];
    snprintf(writeBuffer, sizeof(writeBuffer), "I am father proceess:%d", getpid());
    printf("father process:%d send message\n", getpid());
    write(pipefd[1], writeBuffer, strlen(writeBuffer));
    int status = 0;
    n = waitpid(pid, &status, 0);//获取子进程状态信息,0~6位存储了信号的信息,8~15位存储了退出码信息
    if (n < 0)
    {
        perror("waitpid failed");
        exit(3);
    }
    printf("father process waitpid success, child process exitcode: %d, sig: %d\n", (status>>8) & 0xFF, status & 0x7F);
    return 0;
}

运行结果
在这里插入图片描述

匿名管道的特点

1.具有血缘关系的进程进行进程间通信
2.管道只能单向通信
3.父子进程是会进程协同、同步与互斥的---->能保护管道文件的数据安全
4.管道是面向字节流的
5.管道是基于文件的,而文件的生命周期是随进程的
6.管道是有固定大小的

验证管道是有固定大小

int main()
{
    int pipefd[2];
    int n = pipe(pipefd);
    int count = 0;
    while (true)
    {
        char c = 'a';
        write(pipefd[1], &c, 1);
        count++;
        cout << count << endl;
    }
    return 0;
}

执行结果
在这里插入图片描述
所以管道的固定大小是65536个字节,64KB
命令行ulimit -a看到的pipe size是512*8=4096字节,这又是什么呢?
在这里插入图片描述
用man -7 pipe查看手册
这个是PIPE_BUF的大小,小于PIPE_BUF则是原子的读写操作
在这里插入图片描述

管道文件数据安全的体现,管道的4种情况
1.读写端正常,管道如果为空,读端就要阻塞
2.读写端正常,管道如果被写端,写端就要阻塞
3.读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾, 不会被阻塞
4.写端是正常写入,读端关闭了,操作系统就要杀掉正在写入的进程。如何杀掉?通过信号杀掉–因为读端关闭,写端将会毫无意义,操作系统是不会做低效浪费等类似的工作的

证明第4点
父进程关闭读端,子进程写内容。父进程获取子进程退出信息

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    int pipefd[2];
    int n = pipe(pipefd);
    if (n < 0)
    {
        perror("create pipe failed");
        exit(2);
    }
    pid_t pid = fork();
    if (pid == 0)//子进程
    {
        close(pipefd[0]);

        char writeBuffer[1024];
        printf("child process:%d send message\n", getpid());
        snprintf(writeBuffer, sizeof(writeBuffer), "I am child proceess:%d", getpid());
        write(pipefd[1], writeBuffer, strlen(writeBuffer));

        sleep(3);//3秒后子进程关闭读端文件描述符
        close(pipefd[1]);
        exit(0);
    }
    //父进程
    close(pipefd[1]);
    close(pipefd[0]);
    int status = 0;
    n = waitpid(pid, &status, 0);
    if (n < 0)
    {
        perror("waitpid failed");
        exit(3);
    }
    printf("waitpid child success:exitcode: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
    close(pipefd[0]);
    return 0;
}

运行结果在这里插入图片描述
父进程获取的子进程退出信号是13号,为管道的退出信号
在这里插入图片描述

命令行管道实现原理
在这里插入图片描述
ls进程的输出重定向到管道的写端,grep的输入重定向到管道的读端

实现进程池

池是什么意思?如你去打水,每天都要走两公里,每天都要走两公里,效率太低。你则修了个池子,一次就打很多水存到池子里,则不用天天打水,直接到池子里取水即可
操作系统也是一样的,系统调用函数效率很低,所以为了不用频繁调用某些系统调用接口如fork函数,所以有了进程池的概念。

进程池原理
在这里插入图片描述
进程池代码

//processPool.hpp文件
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <vector>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

namespace processPoolspace
{
    int gnums = 10;//进程池进程的数量

    //如果管理进程池?先描述,再组织
    struct subproc
    {
        subproc(int writefd, int pid, string processName = "")
            : _processName(processName), _writefd(writefd), _pid(pid)
        {}
        string _processName;//进程的名字
        int _writefd;//进程对应管道的写端
        int _pid;//进程的pid
    };

    class processPool
    {
    public:
        processPool()
        {}
        void processPoolInit()
        {
            vector<int> oldfd;//子进程会继承父进程的写端,将父进程的写端记录下来
            for (int i = 0; i < gnums; ++i)
            {
                int pipefd[2];
                int m = pipe(pipefd);
                if (m < 0)
                {
                    perror("pipe create failed");
                    exit(1);
                }
                int n = fork();
                if (n < 0)
                {
                    perror("fork failed");
                    exit(1);
                }
                if (n == 0)
                {
                    close(pipefd[1]);//子进程关闭写端
                    while (1)
                    {
                        //创建子进程先关闭从父进程继承下来的管道的写端
                        for (int i = 0; i < oldfd.size(); ++i)
                        {
                            close(oldfd[i]);
                        }
                        char readBuffer[1024];
                        int m = read(pipefd[0], readBuffer, sizeof(readBuffer) - 1);//读取父进程发过来的任务
                        if (m > 0)
                        {
                            readBuffer[m] = 0;
                            cout << "进程:"<< i + 1 <<"处理任务:"<< readBuffer << endl;
                        }
                        else if (m == 0)
                        {
                            //m == 0说明了写端关闭,则子进程也要关闭读端并结束程序了
                            cout << "进程:" << i + 1 << "写端关闭, me too" << endl;
                            close(pipefd[0]);
                            exit(0);
                        }
                        else
                        {
                            perror("read failed");
                            close(pipefd[0]);
                            exit(2);
                        }
                    }
                }
                close(pipefd[0]);//父进程关闭读端
                string processName = "processName:" + to_string(i + 1);
                nums.push_back(subproc(pipefd[1], n, processName));//将对象存入数组中,以后对进程池的管理就变成了对数组的管理
                oldfd.push_back(pipefd[1]);//记录目前父进程的所有连接管道的写端
            }
        }
        void quitProcess()
        {
            for (int i = 0; i < nums.size(); ++i)
            {
                close(nums[i]._writefd);
                int n = waitpid(nums[i]._pid, nullptr, 0);
                if (n < 0)
                {
                    perror("waitpid failed");
                    exit(1);
                }
                else 
                {
                    cout << "waitpid:" << nums[i]._processName << "success" << endl;
                }
            }
        }
    public:
        vector<subproc> nums;//对进程池的管理转换成了对数组的管理
    };
}
//process.cc文件
#include "processPool.hpp"
#include <string.h>

using namespace std;
using namespace processPoolspace;

//任务菜单
void Menu()
{
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}

//派发任务
void Task(const processPool& pp)
{
    vector<string> task = {"", "刷新日志", "刷新出来野怪", "检测软件是否更新", "更新用的血量和蓝量"};
    int input = 0;
    int m = 0;
    while (1)
    {
        Menu();
        cin >> input;
        if (input <= 0 || input >= 5)
        {
            break;
        }
        subproc process = pp.nums[rand() % pp.nums.size()];//随机挑选进程
        cout << "进程" << process._processName << "处理任务" << input << endl;
        m = write(process._writefd, task[input].c_str(), task[input].size());
        if (m < 0)
        {
            perror("write failed");
            continue;
        }
    }
}
int main()
{
    //随机数,随机挑选进程来执行任务
    srand(time(NULL));
    processPool pp;
    //初始化进程
    pp.processPoolInit();
    Task(pp);
    //执行完任务后退出进程
    pp.quitProcess();
    return 0;
}

命名管道

匿名管道通信的前提是有血缘关系的两个进程,而命名管道可以在毫不相干的两个进程间通信
命令mkfifo [文件名] 创建命名管道
在这里插入图片描述
ehco输出重定向到管道
在这里插入图片描述

会阻塞
cat输入重定向到管道
在这里插入图片描述
可以看到达到了echo进程和cat进程通信的目的


问题:两个进程看到的同一份资源是什么?文件。你们两个进程怎么知道打开的是同一文件?
因为是同路径下同一个文件名,而路径+文件名具有唯一性

mkfifo函数,第一个参数是文件名,第二个参数是创建文件的权限
在这里插入图片描述
unlink函数,删除某一文件
在这里插入图片描述

不同进程的通信代码

//comm.hpp代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

void Init()
{
    //在当前路径下创建一个管道文件
    int n = mkfifo("./fifo", 0666);
    if (n < 0)
    {
        perror("mkfifo failed");
        exit(1);
    }
}
void Writer()
{
    int fd = open("fifo", O_WRONLY | O_TRUNC);
    if (fd < 0)
    {
        perror("open failed");
        exit(2);
    }
    while (1)
    {
        string s;
        cout << "Enter#";
        getline(cin, s);
        int m = write(fd, s.c_str(), s.size());
        if (m < 0)
        {
            perror("write failed");
            exit(1);
        }
    }
}
void Read()
{
    int fd = open("fifo", O_RDONLY);
    if (fd < 0)
    {
        perror("open failed");
        exit(2);
    }
    while (1)
    {
        char buffer[1024];
        int n = read(fd, buffer, sizeof(buffer) - 1);
        if (n < 0)
        {
            perror("read failed");
            exit(1);
        }
        if (n == 0)
        {
            cout << "读端退出, 我也退出" << endl;
            exit(1);
        }
        buffer[n] = 0;
        cout << buffer << endl;
    }
}

void Destroy()
{
    //结束时删除管道文件
    unlink("fifo");
}

//server.cc代码
#include “comm.hpp”

int main()
{
Init();
Read();
Destroy();
return 0;
}

//client.cc代码
#include "comm.hpp"

int main()
{
    Writer();
    return 0;
}

执行进程
若只打开管道的读端,写端没有打开。则会阻塞
在这里插入图片描述
若只打开管道的写端,读端没有打开。则会阻塞
在这里插入图片描述
有一端没打开,open就会阻塞
都打开了则可以开始通信了
在这里插入图片描述

system V通信–共享内存

通信的本质:让不同的进程看到同一份资源

共享内存的原理:
在这里插入图片描述
上面的操作都是进程做的不是,原因一样,进程管的是自己,不管别人。是由操作系统做的,所以操作系统一定会提供对应的系统调用接口
在这里插入图片描述
shmget函数的作用是申请一个共享内存
第一个key稍后再说,第二个size是申请共享内存的大小,第三个shmflg是选项,二进制标志位
在这里插入图片描述
最常用的两个选项
IPC_CREAT:如果不存在就创建它,如果存在就获取它
IPC_CREAT | IPC_EXCL:IPC_EXCL无法单独使用,如果不存在就创建它,如果存在就出错返回

再谈谈key:
1.key是一个数字,这个数字是几不重要。关键在于它必须在内核中具有唯一性,能够让不同的进程进行唯一性标识
2.第一个进程可以通过key创建共享内存,第二个之后的进程,只要拿着同一个key就可以和第一个进程看到同一个共享内存了
3.对于已经创建好的共享内存,key在哪?key在共享内存的描述对象中
4.如何有这个key呢?
在这里插入图片描述
ftok函数不同的参数保证算出的key是不同的
第一个参数是文件名(可以为任意路径),第二个参数是id(一字节内的范围随便填)
问题:为什么系统不直接生成一个key呢?
如果系统生成了这个key,那这个进程如何把这个key交给另一个进程呢?所以这和另一个进程相当于是个约定,只要进程知道pathname和id就可以得到相同的key


共享内存的通信代码

在这里插入图片描述
第一个参数shmid是共享内存标识符,
第二个参数最常用的两个选项IPC_STAT获取共享内存的属性,IPC_RMID删除共享内存
第三个参数为输出型参数,配合第二个参数选项的IPC_STAT选项获取属性,如果不想获取属性可以填NULL
在这里插入图片描述
shmat函数对共享内存进行关联
第一个参数是共享内存标识符
第二个参数是你想把共享内存映射到共享区的哪个位置,一般不用填,填NULL即可
第三个参数你拿到共享内存后的权限,默认填0即可
shmdt函数对共享内存进行去关联
参数是该共享内存的起始地址
上面两个函数类似于C语音的malloc和free

//comm.hpp文件
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;

const string path = "/home";//可以随便填
const int SIZE = 4096;//共享内存的大小

const int proj_id = 0x666;//随便填

//获取key值
int GetKey()
{
    
    key_t key = ftok(path.c_str(), proj_id);
    if (key < 0)
    {
        perror("ftok failed");
        exit(1);
    }
    return key;
}

//创建共享内存
int CreateShm()
{
    
    key_t key = GetKey();
    int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid < 0)
    {
        perror("create shared memory failed");
        exit(1);
    }
    return shmid;
}

//获取共享内存
int GetShm()
{
    key_t key = GetKey();
    int shmid = shmget(key, SIZE, IPC_CREAT);
    if (shmid < 0)
    {
        perror("get shared memory failed");
        exit(2);
    }
    return shmid;
}
//Server.cc的代码
#include "comm.hpp"

int main()
{
    //创建共享内存
    int shmid = CreateShm();
    //和共享内存关联
    char* start = (char*)shmat(shmid, NULL, 0);
    while (1)
    {
        //读取共享内存的内容
        printf("读取的内容是:%s\n", start);
        sleep(1);
    }
    //和共享内存去关联
    shmdt(start);
    //释放共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}
//client.cc的代码
#include <iostream>
#include <stdio.h>
#include <string.h>
#include "comm.hpp"
int main()
{
    //获取同一份资源
    int shmid = GetShm();
    //和共享内存关联
    char* start = (char*)shmat(shmid, NULL, 0);
    const char* ch = "hello, I am process a";
    string s = "hello, I am process a";
    for (int i = 0; i < 10; ++i)
    {
        s += to_string(i);
        //直接进行通信,不用缓冲区,直接拷贝到共享内存
        memcpy(start, s.c_str(), s.size());
        sleep(1);
    }
    //通信完成,和共享内存去关联
    shmdt(start);
    return 0;
}

运行结果在这里插入图片描述
你可能在运行时出现了这种情况,原因是你之前创建的共享内存没有释放
在这里插入图片描述
那要怎么释放呢?
ipcs -m命令可以查看正在使用的共享内存
在这里插入图片描述
右边的数字是正在与此共享内存关联的进程有几个

ipcrm -m [shmid]可以删除该共享内存
在这里插入图片描述

查看共享内存的属性代码
在这里插入图片描述

#include "comm.hpp"

int main()
{
    //创建共享内存
    int shmid = CreateShm();
    //和共享内存关联
    char* start = (char*)shmat(shmid, NULL, 0);
    while (1)
    {
        //读取共享内存的内容
        printf("读取的内容是:%s\n", start);
        struct shmid_ds statIpc;
        shmctl(shmid, IPC_STAT, &statIpc);
        cout << "shm size: " << statIpc.shm_segsz << endl;
        cout << "shm nattch: " << statIpc.shm_nattch << endl;
        printf("0x%x\n", statIpc.shm_perm.__key); 
        cout << "shm mode: " << statIpc.shm_perm.mode << endl;
        shmdt(start);
        sleep(1);
    }
    //和共享内存去关联
    shmdt(start);
    //释放共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

运行结果
在这里插入图片描述
当有其他进程也来关联的时候可看到关联数有1变为了2


共享内存的特性
1.共享内存没有同步互斥之类的保护机制
2.共享内存是所有的进程间通信中速度最快的

System V通信–共享内存、消息队列和信号量的关系

进程间通信本质:让不同的进程看到同一份资源
而这同一份资源的类别决定了你是什么样的通信方式,如:同一份资源是文件缓冲区—>管道。同一份资源是内存块—>共享内存。同一份资源是队列—>消息队列

消息队列原理
1.让不同的进程看到同一个队列
2.允许不同的进程向内核中发送带类型的数据块
在这里插入图片描述

消息队列的接口
和共享内存通信系统提供的接口很像近
在这里插入图片描述
进行发送数据块和接收数据块的接口
因为被历史淘汰了,所以不做过多介绍,知道有即可
在这里插入图片描述
同样信号量也有一批类似的接口

shmid、msgid、semid究竟是什么呢?
在这里插入图片描述

信号量

讲一些概念
数据不一致问题:A进程正在写入,写入了一部分,就被B进程拿走了,导致双方发和收的数据不完整,如共享内存会遇到。
共享资源:A和B进程看到的同一份资源。如果不加保护,会导致数据不一致问题
加锁、互斥:任何时刻,只允许一个执行流访问共享资源
临界资源:共享的,任何时刻只允许一个执行流访问的资源
临界区:正在访问临界资源的代码(客观意义上的正在访问临界资源,不是主观意义的访问临界资源的代码,一般是很少一部分的代码)

理解信号量
当我们看电影的时候,是先买票再去看电影,买票的时候我们看到了还剩100张票,买到票后就还剩99张票了,其中有一张票属于了我自己,即已经对电影院的其中一个座位(资源)进行了预订。
信号量就是计数器,记录资源(票)还剩多少。每买一张票,计数器就要减1,电影院里的资源就少一个。计数器为0就代表没有资源能申请了

信号量的好处是什么?
通过信号量申请到临界资源以后,其他进程也都申请到临界资源以后,就能并发的访问公共资源,因为访问的不是同一个公共资源,不会导致数据不一致的问题。效率高

扩展一下:如果电影院里面只有一个座位呢,我们只需要一个值为1的计数器,只有一个人能抢到票,只有一个人能进电影院,看电影期间只有一个执行流在访问临界资源–这就是互斥的概念
我们把值只能为0,1两态的计数器叫做二元信号量—本质就是一个锁

思考:要访问临界资源,先要申请信号量计数器资源,信号量计数器不也是共享资源吗??
有一百张票,每一人申请票就少一个。用代码表示int cnt = 100; cnt–;
而cnt–;c语言上是一条语句,汇编语言是三条语句。
1.将cnt变量的内容从内存中拿取CPU寄存器里面
2.CPU内进行减减操作
3.将CPU内的计算结果写回cnt变量的内存的位置
在这三步中的任何一步,都有可能进程在允许的时候,随时被切换走。是发生数据不一致问题
那么信号量如何保证自己的安全呢?在其内部将这三步设置为原子操作,即要么全部完成,要么不完成

申请信号量,本质是对计数器–,称为P操作
释放资源,释放信号量,本质是对计数器进行++操作,称为V操作
申请和释放PV操作–是原子的!

信号量凭什么是进程间通信的一种?
1.通信不仅仅是通信数据,互相协同也是
2.要协同,本质也是通信,信号量首先要被所有的通信进程看到!

关于信号量的接口及其使用部分,在多线程部分再进行操作说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值