Linux【进程间通信】

目录

一、什么是进程间通信

管道

管道的原理 

二、匿名管道 

1.简单写一个管道

2.总结管道的特点,理解以前的管道

3.扩展 

如何写一个进程池?

创建Makefile文件

创建我们的任务头文件Task.cpp

创建我们的主程序文件

管道读写规则

三、命名管道

mkfifo 

制作管道实验

1.日志头文件Log.hpp

2.公共头文件comm.hpp

3.客户端文件client.cc

4.服务端文件server.cc 

四、system v共享内存

shmget

ftok

shmctl

SHMAT

SHMDT

 1.创建makefile文件

2.日志头文件Log.hpp

3.共享的头文件comm.hpp

4.客户端文件shmClient.cc

5.服务端头文件shmServer.cc

6.初步实现进程间通信

 进程间通信(客户端我们自己输入,服务器端读取数据)

用管道控制共享内存的访问控制 

五、信号量


一、什么是进程间通信

进程的运行具有独立性,有独立的页表,pcb,等等父子进程之间,数据不相干扰

这就让我们进程想要通信的难度比较大。

因为操作系统在设计的时候,它本身就是独立的。

进程间通信的本质: 

先让不同的进程看到同一份资源(内存空间) 

为什么要进行进程间通信?

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

(交换数据、空值、通知等目标)

进程间通信的必要性

单进程的,那么也就无法使用并发能力,更加无法实现多进程协同 

进程间通信不是目的,而是手段,为了实现多进程协同。

进程通信的技术背景

1.进程是具有独立性的。虚拟地址空间+页表 保证进程运行的独立性(进程内核数据接口+进程的代码和数据) 

2.通信的成本会比较高

进程间通信的本质理解

1.进程间通信的前提,首先需要让不同的进程看到同一块“内存”(特定的结构组织) 

2.所以所谓的进程看到同一块“内存”,属于哪一个进程呢?不能隶属于任何一个进程,而应该更强调共享。

进程间通信分类:

进程间通信的方式也有一些标准

1.Linux原生提供--管道
        匿名管道pipe
        命名管道


2.System V IPC(侧重于本地通信(单机通信))--多进程
        System V 消息队列
        System V 共享内存
        System V 信号量


3.POSIX IPC(侧重于网络通信)--多线程
        消息队列
        共享内存
        信号量
        互斥量
        条件变量
        读写锁

        

标准在我们使用者看来,都是接口上具有一定的规律

管道

什么是管道?

(天然气管道、石油管道、自来水管道……)

(这里我们简化一下,有一个入口和一个出口的管道)

1.只能单向通信。

(一般都是天然气公司把天然气输送到你家,不是你家把天然气输送到天然气公司)

2.管道内传输的都是资源

计算机通信领域的设计者,设计了一种单向通信的方式--管道

计算机中的管道:

传输资源->数据! 

管道的原理 

管道通信背后是进程之间通过管道进行通信

下面的|就是将第一个命令的执行结果作为参数传给后面那个命令 

Linux【进程间通信】_第1张图片

 因为中间的数据资源不属于任何一个进程。

Linux【进程间通信】_第2张图片​​​​​​​

上面是我们的再基础IO中所说过的文件系统。

那么当前进程如果创建了子进程,会发生什么呢?

struct file*fd_array[]文件描述符表要不要拷贝给子进程呢?

首先这个表表示这个进程和文件的描述符表之间的关系,可就是当前的进程可以看到哪些被打开的文件。这个必须拷贝给子进程!

拷贝只是第一次拷贝,之后父子进程持有的就是独立的表结构。

那么这个表指向的一堆文件要不要拷贝给子进程呢?

不 ,我们不需要。与进程相关的都会被拷贝,与文件相关的并不会被拷贝!

Linux【进程间通信】_第3张图片

父子进程的struct file_struct是一样的,所以里面的文件指针都是一样的,所以我们打开的文件也是一样的。

(比防说我们父子进程在打印到屏幕上的时候,都是打印到1号文件中,都是打印到同一个显示器上!)

所以我们这里的struct file是能被父进程访问,也能被子进程访问

所以我们的父子进程是不是看到了一份公共文件,这个也就是我们的管道! 

(管道的临时文件不需要刷新到磁盘)

双方进程各自关闭不需要的文件描述符

让父进程进行写入,子进程进行读取,父进程写,就需要关闭读,保留写,子进程关闭写,保留读的功能。每个进程内部都有各自的文件描述符。

也就是让不同的进程看到同一份资源。 

Linux下,一切皆文件,管道就是我们上面的共享文件。 

这里的文件是属于内核的,所有的进程间通信都是内核级别的。

那两进程进行通信,需不需要将这个管道文件保存到磁盘?

不需要!

进程间通信的管道必须在内存中,是一个纯内存的通信方式,如果还要写磁盘的话,我们的通信效率就太低了。

并且进程间通信的数据往往属于临时数据,不需要将数据持久化保存 

二、匿名管道 

 #include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

Linux【进程间通信】_第4张图片

像下面的|就是一个简单的管道 

ps axj |head -1 && ps axj|grep sleep

 管道使用完毕之后,记得要关闭文件描述符。

1.简单写一个管道

如何做到让不同的进程,看到同一份资源的呢?

fork让子进程继承的--能够让具有血缘关系的进程进行进程间通信--常用于父子进程。

pipefd[0]:读端

pipefd[1]:写端

int pipe(int pipefd[2]);输出型参数,希望通过它来验证我们的管道成功搭建

#include
#include
#include
using namespace std;
int main()
{
  //1.创建管道
  int pipefd[2]={0};
  int n=pipe(pipefd);
  //在debug模式下assert是有效的,但是release版本下是会无效的
  assert(n!=-1);
  //所以我们这里需要写下面的代码,证明n被使用过
  (void)n;

  cout<<"pipefd[0]"<

Linux【进程间通信】_第5张图片

 ok,我们这里的文件描述符已经成功打开了,接下来我们在进一步搭建我们的管道

#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
    //1.创建管道
    int pipefd[2]={0};//pipefd[0]:读端,pipefd[1]:写端
    int n=pipe(pipefd);
    //在debug模式下assert是有效的,但是release版本下是会无效的
    assert(n!=-1);
    //所以我们这里需要写下面的代码,证明n被使用过
    (void)n;

    //如果是DEBUG模式下就不打印了,相当于就是注释掉了
  #ifdef DEBUG
    cout<<"pipefd[0]"<0)
        {
          //添加\0
          buffer[s]=0;
          cout<<"child get a message["<

 Linux【进程间通信】_第6张图片

为什么我们不写一个全局的缓冲区(buffer)来进行通信呢?

因为有写时拷贝的存在,无法更改通信。

2.总结管道的特点,理解以前的管道

1.管道是一种进程间通信的方式,管道是用来进行具有血缘关系的进程进行进程间通信,常用于父子进程。

2.我们上面的代码中,我们的父进程是1秒钟发送一条消息,但是我们的子进程并没有设置读取信息的时间间隔,但是我们的子进程依旧是跟随父进程的节奏在打印。

那么我们父进程在sleep的期间,子进程在干什么呢?

子进程在等待父进程的写入。

管道是一个文件,显示器也是一个文件,父子同时往显示器写入的时候,有没有说一个会等另一个的情况呢?

没有!

之前我们往显示器上打印的时候,都是交错着疯狂往显示器上打印的。

这种情况,我们将其称为缺乏访问控制

那我们上面的子进程等待父进程的写入,就是具有访问控制。 

也就是说,管道具有通过让进程间协同,提供了访问控制!

管道里满的时候,写的一方要等待读的一方将数据读取

管道空的时候,读取的一方要等待写的一方写入。

下面我们验证一下,下面我们是将父进程循环写入,但是子进程需要等待20秒才写入 

#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
    //1.创建管道
    int pipefd[2]={0};//pipefd[0]:读端,pipefd[1]:写端
    int n=pipe(pipefd);
    //在debug模式下assert是有效的,但是release版本下是会无效的
    assert(n!=-1);
    //所以我们这里需要写下面的代码,证明n被使用过
    (void)n;

    //如果是DEBUG模式下就不打印了,相当于就是注释掉了
  #ifdef DEBUG
    cout<<"pipefd[0]"<0)
        {
          sleep(20);
          //添加\0
          buffer[s]=0;
          cout<<"child get a message["<

 如果缓冲区满了,就不能写入了,就只能等待子进程读取。所以这里子进程读取一次的数据可能是父进程写了好几次的结果

Linux【进程间通信】_第7张图片

3.管道提供的是面向流式的通信服务--面向字节流(需要对应的协议)

你写了十次,但是我可能一次就全部都读取完了。

4.管道是基于文件的,文件的生命周期是随进程的,管道的生命周期是随进程的。

写入的一方,fd没有关闭,如果有数据,就读,没有数据,就等

写入的一方,fd关闭,读取的一方,read会返回0,表示读到了文件的结尾(将缓冲区中的内容读取完毕之后,就可以退出了!)

#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
    //1.创建管道
    int pipefd[2]={0};//pipefd[0]:读端,pipefd[1]:写端
    int n=pipe(pipefd);
    //在debug模式下assert是有效的,但是release版本下是会无效的
    assert(n!=-1);
    //所以我们这里需要写下面的代码,证明n被使用过
    (void)n;

    //如果是DEBUG模式下就不打印了,相当于就是注释掉了
  #ifdef DEBUG
    cout<<"pipefd[0]"<0)
        {
          // sleep(20);
          //写入的一方,fd没有关闭,如果有数据,就读,没有数据,就等
          //写入的一方,fd关闭,读取的一方,read会返回0,表示读到了文件的结尾
          //添加\0
          buffer[s]=0;
          cout<<"child get a message["<0);
    (void)ret;
    //子进程中的pipefd[0]关闭可写可不写,因为进程退出了,进程中的文件描述符也会被关掉
    return 0;
}

 Linux【进程间通信】_第8张图片

只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。

 Linux【进程间通信】_第9张图片

5.管道是单向通信的,就会半双工通信的一种特殊情况

作为通信的一方,要么我在发送,要么我在接收,我不能同时接收和发送,这种就称为半双工通信。

有时候呢,我们既可以收,又可以发,这就称为全双工通信。

比防说我们在听老师上课,老师在将,我们在听,这就是半双工通信。

但是如果两个人在吵架,你在吵的时候我也在吵,同时我也在听你说什么,你也在听我说什么,这就是全双工通信。

a.写快,读慢,写满的时候就不能再写了

b.写慢,读快,管道没有数据的时候,读的这一方就必须等待

c.写关,读0,表示读到了文件结尾

d.读关,写继续写,OS终止写进程 

3.扩展 

Linux【进程间通信】_第10张图片

如何写一个进程池?

我们给父进程和每一个子进程建立一个管道,并以固定大小的方式command_code(4kb),给我们的子进程发送指令。

创建Makefile文件

ProcessPool:ProcessPool.cc
	g++ -o $@ $^ -std=c++11 -DEBUG
.PHONY:clean
clean:
	rm -f ProcessPool

创建我们的任务头文件Task.cpp

#pragma once
#include
#include
#include
#include
#include
#include
//返回void,参数为()
typedef std::function func;
std::vector callbacks;
std::unordered_map desc;
void readMySQL()
{
    std::cout<<"sub process["<

创建我们的主程序文件

自动派发任务的版本

#include
#include
#include
#include
#include
#include
#include"Task.hpp"
#include
#include
//默认创建的进程个数
#define PROCESS_NUM 5

using namespace std;


//等待命令
int waitCommand(int waitfd,bool&quit)
{
    uint32_t command=0;
    ssize_t s=read(waitfd,&command,sizeof(command));
    //如果读取到对应的0,那么就是文件描述符关掉了,就直接退出
    if(s==0)
    {
        quit=true;
        return -1;
    }
    //看看有没有读取成功
    assert(s==sizeof(uint32_t));
    return command;
}

//拖过文件描述符像进程发送命令
void SendAndWakeup(pid_t who,int fd,uint32_t command)
{
    write(fd,&command,sizeof(command));
    cout<<"main process: call process"<> slots;
    //先创建多个进程
    for(int i=0;i=0&&command(id,pipedf[1]));
    }
    //父进程派发任务
    //将任务均衡地拍付给每一个任务称为单机版的负载均衡
    srand((unsigned long)time(nullptr) ^ getpid()^2332313L);//让我们的数据源更随机
    while(true)
    {
        int command=rand()%handlerSize();
        //采用随机数的方式,选择子进程来完成任务,这是一种随机数的方式来实现负载均衡。
        int choice=rand()%slots.size();
            //布置任务
            //把任务给指定的进程
            SendAndWakeup(slots[choice].first,slots[choice].second,command);
            sleep(1);
    }
    //关闭fd,结束所有的进程
    //关闭所有的写的文件描述符
    //所有的子进程在读取完之后都会退出
    for(const auto &slot:slots)
    {
        close(slot.second);
    }

    //回收所有的子进程。
    for(const auto &slot:slots)
    {
        //等待全部的子进程
        waitpid(slot.first,nullptr,0);
    }
}

Linux【进程间通信】_第11张图片

手动派发任务的版本 

#include
#include
#include
#include
#include
#include
#include"Task.hpp"
#include
#include
//默认创建的进程个数
#define PROCESS_NUM 5

using namespace std;


//等待命令
int waitCommand(int waitfd,bool&quit)
{
    uint32_t command=0;
    ssize_t s=read(waitfd,&command,sizeof(command));
    //如果读取到对应的0,那么就是文件描述符关掉了,就直接退出
    if(s==0)
    {
        quit=true;
        return -1;
    }
    //看看有没有读取成功
    assert(s==sizeof(uint32_t));
    return command;
}

//拖过文件描述符像进程发送命令
void SendAndWakeup(pid_t who,int fd,uint32_t command)
{
    write(fd,&command,sizeof(command));
    cout<<"main process: call process"<> slots;
    //先创建多个进程
    for(int i=0;i=0&&command(id,pipedf[1]));
    }
    //父进程派发任务
    //将任务均衡地拍付给每一个任务称为单机版的负载均衡
    srand((unsigned long)time(nullptr) ^ getpid()^2332313L);//让我们的数据源更随机
    while(true)
    {
        int select;
        int command;
        cout<<"##########################################"<"<>select;
        if(select==1)
        {
            showHandler();
        }
        else if(select=2)
        {
            cout<<"Enter Your Command>";
            //选择任务
            cin>>command;
            //发送命令,并且唤醒子进程
            //选择进程

        //选择一个任务,如果这个任务是从网络中来的?
        // int command=rand()%handlerSize();
        //采用随机数的方式,选择进程来完成任务,这是一种随机数的方式来实现负载均衡。
            int choice=rand()%slots.size();
        //     //布置任务
        //     //把任务给指定的进程
            SendAndWakeup(slots[choice].first,slots[choice].second,command);
            // sleep(1);
        }else
        {
            cout<<"该指令不再可以选择的范围内"<

管道读写规则

当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性

Linux【进程间通信】_第12张图片

三、命名管道

要两个不同的进程看到同一份资源,才能建立起管道。

磁盘上可以创建一个管道文件

管道文件可以被打开,但是不会将内存数据进行刷新到磁盘。

该文件一定在系统路径中,路径具有唯一性。

双方进程就可以通过管道文件的路径,看到同一份资源!

与匿名文件的差别是匿名文件仅仅是在内存中创建了一个文件,由父子进程进行访问

 而命名管道的管道文件也是内存中的文件,但是在磁盘上有一个映射,有文件目录。

mkfifo 

Linux【进程间通信】_第13张图片

mkfifo name_pipe

p开头的,我们将其称为管道文件

Linux【进程间通信】_第14张图片

echo "hello world" >name_pipe

我写了,但是对方还没有打开,此时这个文件就处于阻塞状态

我们将数据从管道中独取出来

cat

 

现在我们尝试循环输入hello world 

 while :; do echo "hello world"; sleep 1; done >name_pipe

在另外一个终端进行接收 

cat 

Linux【进程间通信】_第15张图片

删除管道文件

unlink name_pipe

Linux【进程间通信】_第16张图片

制作管道实验

man 3 mkfifo

Linux【进程间通信】_第17张图片

1.日志头文件Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include
#include


#define Debug 0
#define Notice 1
#define Waring 2
#define Error 3


//定义日志的几种状态
const std::string msg[]={
    "Debug",
    "Notice",
    "Warning",
    "Error"
};
std::ostream &Log(std::string message,int level)
{
    //时间,日志信息
    std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<

2.公共头文件comm.hpp

#ifndef _COMM_H_
#define _COMM_H_

#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
using namespace std;
//设置管道文件的权限
#define MODE 0666
//缓冲区的大小
#define SIZE 128
//管道文件的路径
string ipcPath="./fifo.ipc";


#endif

3.客户端文件client.cc

#include"comm.hpp"

int main()
{
    //1.获取管道文件,以写的方式打开
    int fd=open(ipcPath.c_str(),O_WRONLY);
    if(fd<0)
    {
        perror("open");
        exit(1);
    }
    //2.ipc过程
    //创建要发送的字符串
    string buffer;
    while(true)
    {
        cout<<"Please Enter Messaage Line  :> ";
        //将字符串放入buffer中
        std::getline(std::cin,buffer);
        //将buffer写入管道文件中
        write(fd,buffer.c_str(),buffer.size());
    }
    //3.关闭文件
    close(fd);
    return 0;
}

4.服务端文件server.cc 

#include"comm.hpp"
#include
static void getMessage(int fd)
{
    char buffer[SIZE];
    while(true)
    {
        memset(buffer,'\0',sizeof(buffer));
        ssize_t s=read(fd,buffer,sizeof(buffer)-1);
        if(s>0){
            cout<<"["<"<0){
            cout<<"["<"<

使用makefile创建我们的工程 

.PHONY:all
all:client mutiServer

client:client.cc
	g++ -o $@ $^ -std=c++11
mutiServer:server.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f client mutiServer

我们观察到这里是我们创建的三个管道争抢着接收我们的客户端发送的信息的,谁抢到了谁就将信息发送出来。

四、system v共享内存

1.原理:

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

Linux【进程间通信】_第18张图片

共享内存的建立

这一块共享内存既不属于左边的进程也不属于右边的进程,它属于操作系统。共享内存的提供者是操作系统。

共享内存是操作系统单独设立的内核模块,专门负责进程间通信。

如果每一对进程通信都需要用共享内存,那么操作系统要不要对其进行管理呢?

(管道的管理和文件系统的管理差不多,所以操作系统不用单独地去管理)

先描述,再组织!

重新理解共享内存

 共享内存=共享内存块+对应的共享内存的内核数据结构

shmget

Linux【进程间通信】_第19张图片

创建并获取共享内存,size为大小,shmflg为IPC_CREAT或者IPC_EXCL,其中IPC_CREAT就是0

IPC_CREAT:创建共享内存的时候,如果系统底层已经存在,那么直接获取之,并且返回,如果不存在,就创建之,并返回。

IPC_EXCL:单独使用IPC_EXCL是没有意义的

IPC_CREAT和IPC_EXCL合起来使用,如果底层不存在,创建之,并返回,如果底层存在,出错返回。

返回成功,一定是一个全新的shm。

返回值共享内存的用户层标识符,类似曾经的fd

这里的key是什么呢

要通信的对方进程,怎么保证对方能看到,并且看到的就是我创建饿共享内存呢?

通过key,key的数据是多少不重要,只要能够在系统中唯一即可。server&&client使用同一个key,只要key值相同,就是看到了同一个共享内存。

使用同样的算法规则,形成唯一的key值就可以了。

只有创建的时候用key,大部分情况用户访问共享内存,都用的是shmid

ftok

Linux【进程间通信】_第20张图片

需要传入一个路径(pathname)和项目ID, 

ftok算法就是将路径和项目id合并起来,形成一个唯一值。但是由于这里创建出来的键值底层可能也会有,所以我们这里的ftok不一定会创建成功

让我们客户端和服务器端的key值相同

客户端 

#include"comm.hpp"
int main()
{
    key_t k =ftok(PATH_NAME,PROJ_ID);
    Log("create key done",Debug)<<"Client say :"<

服务器端 

#include"comm.hpp"
int main()
{
    //1.创建公共的key值
    key_t k =ftok(PATH_NAME,PROJ_ID);
    Log("create key done",Debug)<<"Server say:"<

comm.hpp

#pragma once

#include
#include
#include
#include
#include
#include "Log.hpp"
using namespace std;

#define PATH_NAME "/home/zhuyuan"
#define  PROJ_ID 0x66

makefile

.PHONY:all
all:client server

client:shmClient.cc
	g++ -o $@ $^ -std=c++11
server:shmServer.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f client server

Linux【进程间通信】_第21张图片

 ipcs -m查看系统当中的共享内存

Linux【进程间通信】_第22张图片

我运行完了,但是共享内存还在。

ipcrm -m关闭共享内存

 Linux【进程间通信】_第23张图片

system V IPC资源的生命周期随内核!除非重启,否则一直存在

1.手动删除

2.代码删除

我们可以使用下面的shmctl来关闭共享内存

shmctl

Linux【进程间通信】_第24张图片

监控脚本

while :; do ipcs -m ;sleep 1; done

attach关联 detach不关联,

n表示个数,attach表示关联,表示有多少个进程和我们的共享内存是关联的

​​​​​​​ 

SHMAT

接下来我们就要创建共享内存

shmat的参数中,shmaddr共享内存的虚拟地址,我们写0,让操作系统帮我们填写,shmflg,选择挂载方式,我们写0,让操作系统进行填写 

Linux【进程间通信】_第25张图片

SHMDT

 然后将我们创建的共享内存和我们的程序关联起来,shmaddr就是我们共享内存的虚拟地址

Linux【进程间通信】_第26张图片

 1.创建makefile文件

.PHONY:all
all:client server

client:shmClient.cc
	g++ -o $@ $^ -std=c++11
server:shmServer.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f client server

2.日志头文件Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include
#include


#define Debug 0
#define Notice 1
#define Waring 2
#define Error 3

//不同的状态
const std::string msg[]={
    "Debug",
    "Notice",
    "Warning",
    "Error"
};
//将时间,状态和信息介入日志中,并且打印出来
std::ostream &Log(std::string message,int level)
{
    //时间,日志信息
    std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<

3.共享的头文件comm.hpp

#pragma once

#include
#include
#include
#include
#include
#include "Log.hpp"
#include
#include
using namespace std;

//定义项目的路径,为了我们的key值的生成
#define PATH_NAME "/home/zhuyuan"
//定义项目的id,这个值可以自己取
#define  PROJ_ID 0x66
#define SHM_SIZE 4096//共享内存的大小,最好是页(PAGE:4096)的整数倍

4.客户端文件shmClient.cc

#include"comm.hpp"
int main()
{
    //创建我们上述的key值,用于我们的两个进程都找到这一块共享内存
    key_t k =ftok(PATH_NAME,PROJ_ID);
    //如果key值创建失败了,也就是我们的底层已经有这一个key值了,它会返回-1
    if(k<0)
    {
        Log("create key failed",Error)<<"Client say :"<

5.服务端头文件shmServer.cc

#include"comm.hpp"

//将k转换成十六进制进行输出
string TransToHex(key_t k)
{
    char buffer[32];
    snprintf(buffer,sizeof buffer,"0x%x",k);
    return buffer;
}
int main()
{
    //1.创建公共的key值
    //这里的创建key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端相同的key值
    key_t k =ftok(PATH_NAME,PROJ_ID);
    //如果创建失败了
    assert(k!=-1);
    Log("create key done",Debug)<<"Server say:"<

这里我们先启动server,也就是服务器的程序,然后启动客户端的程序,然后在监控脚本中查看我们的共享内存的连接状况

​​​​​​​Linux【进程间通信】_第27张图片 

监控脚本的结果截图 

这里我们的服务器端程序先被启动,创建了共享内存,在将共享内存挂载到自己的地中空间之后休眠了10秒,然后再去除与共享内存的关联,然后再等待10秒过后,我们的客户端将共享内存的空间给释放 

这里我们的客户端程序后被启动,在开辟了共享内存之后休眠了10秒,在链接了共享内存之后由休眠了10秒,然后再等待10秒过后

所以我们观察到我们的共享内存的链技术先是从0变成了1,也就是我们的服务器端连接了,然后再变成了2,也就是我们的客户端也连接了,然后又变成了1,也就是我们的服务器端退出了,然后变成0,也就是我们的客户端也退出了

Linux【进程间通信】_第28张图片

堆栈之间的共享区域是属于内核的还是用户的?

这一部分区域是属于用户空间的

也就是不用经过系统调用,可以直接访问

双方进程如果想要通信,直接进行内存级的读和写即可。

为什么pipe,fifo都要通过read,write来进行通信,为什么?

像read和write都是系统调用接口。

这样的接口调用都是属于系统调用。

因为其调用的管道其实都是属于文件,而文件是内核当中的特定数据结构,是由操作系统进行维护的。

而我们的共享内存是在堆栈之间的,是属于用户的空间

​​​​​​​我们上面的所有的工作 属于什么工作呢?

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

6.初步实现进程间通信

shmClient

#include"comm.hpp"
int main()
{
    Log("child pid is:",Debug)<

 shmServer

#include"comm.hpp"

//将k转换成十六进制进行输出
string TransToHex(key_t k)
{
    char buffer[32];
    snprintf(buffer,sizeof buffer,"0x%x",k);
    return buffer;
}
int main()
{
    //1.创建公共的key值
    //这里的创建key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端相同的key值
    key_t k =ftok(PATH_NAME,PROJ_ID);
    //如果创建失败了
    assert(k!=-1);
    Log("create key done",Debug)<<"Server say:"<

Linux【进程间通信】_第29张图片

结论1:只要是通信双方使用共享内存,一方直接向共享内存中写入数据,另一方马上就可以看见这些数据

所以共享内存是所有的进程间通信(IPC)速度最快的!

为什么呢?

因为共享内存不需要过多的拷贝。

不需要将操作数据交给操作系统

 管道

1.从键盘到我们的自己定义的缓冲区是我们的第一次拷贝

2.从我们自己定义的缓冲区到管道文件是第二次拷贝

3.从我们的管道文件拷贝到我们的用户层缓冲区是我们的第三次拷贝

4.从我们的自己定义的缓冲区到打印到我们的屏幕上是我们的第四次拷贝

Linux【进程间通信】_第30张图片

 共享内存

Linux【进程间通信】_第31张图片

 进程间通信(客户端我们自己输入,服务器端读取数据)

shmClient

#include"comm.hpp"
int main()
{
    Log("child pid is:",Debug)<0)
        {   //我们从标准输入读取到的字符串假设是abcd回车,也就是abcd\n
            //但是由于我们是想要和沃尔玛的呢字符串quit进行比较,所以我们需要将\n修改成是\0
            shmaddr[s-1]=0;
            if(strcmp(shmaddr,"quit")==0)
            {
                break;
            }
        }
    }
    
    //去关联
    //将共享内存段与当前进程脱离
    int n=shmdt(shmaddr);
    //如果脱离失败了,就打印日志
    assert(n!= -1);
    Log("detach shm success",Debug)<<"Client say :"<

 shmServer

#include"comm.hpp"

//将k转换成十六进制进行输出
string TransToHex(key_t k)
{
    char buffer[32];
    snprintf(buffer,sizeof buffer,"0x%x",k);
    return buffer;
}
int main()
{
    //1.创建公共的key值
    //这里的创建key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端相同的key值
    key_t k =ftok(PATH_NAME,PROJ_ID);
    //如果创建失败了
    assert(k!=-1);
    Log("create key done",Debug)<<"Server say:"<

Linux【进程间通信】_第32张图片

如果管道里面没有数据了,我们就没有办法读取了,如果我们的管道已经被写满了,我们的写入端就没有办法写入了。这就是管道的阻塞,也就是管道的访问控制,可以协调管道两端的访问控制。

结论二:共享内存缺乏访问控制。

共享内存天生就是为了给我们提供一种快速地访问内存的操作机制,所以我们的共享内存没有任何关于访问控制。

无论共享内存中有没有数据,我们的server都会不停地读取,甚至读取和写入方根本就不知道对方的存在!不会因为没有内容就没有办法读取 

这会导致并发问题

假设我们的客户端想要完整地发送hello world

如果没有控制的话,我们就可能在客户端只输入了hello的时候,我们的服务器端就将数据读取走了。

我们就将其称为数据不一致问题

上面的相关问题称为临界区和临界区资源问题

用管道控制共享内存的访问控制 

如果我想实现共享内存的访问控制呢?

能实现,可以通过管道来实现。

可以将管道的同步共享能力迁移到我们的共享内存中

也就是我们的数据写完了,让server读取的时候,server才能读取,没有让server读的时候,server就不能读取

log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include
#include


#define Debug 0
#define Notice 1
#define Waring 2
#define Error 3

//不同的状态
const std::string msg[]={
    "Debug",
    "Notice",
    "Warning",
    "Error"
};
//将时间,状态和信息介入日志中,并且打印出来
std::ostream &Log(std::string message,int level)
{
    //时间,日志信息
    std::cout<<"|"<<(unsigned)time(nullptr)<<"|"<

comm.hpp

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Log.hpp"
using namespace std;

#define PATH_NAME "/home/zhuyuan"
#define  PROJ_ID 0x66
#define SHM_SIZE 4096//共享内存的大小,最好是页(PAGE:4096)的整数倍
#include 

//创建一个管道文件名的宏定义
#define FIFO_NAME "./fifo"

//创建一个类,用于创建管道
class Init
{
public:
    Init()
    {
        //将权限掩码设置为0
        umask(0);
        //创建我们的管道,分别传入管道文件的地址,还有我们的读写的权限
        int n = mkfifo(FIFO_NAME, 0666);

        //判断我们的管道是否创建成功
        assert(n == 0);
        //让其不要有告警
        (void)n;
        Log("create fifo success",Notice) << "\n";
    }
    ~Init()
    {
        //将我们的管道删除
        unlink(FIFO_NAME);
        Log("remove fifo success",Notice) << "\n";
    }
};

//定义我们的读取和写入模式
#define READ O_RDONLY
#define WRITE O_WRONLY

//封装接口,打开我们的文件
int OpenFIFO(std::string pathname, int flags)
{
    //要打开的文件的路径还有打开文件的模式
    int fd = open(pathname.c_str(), flags);
    //判断是否打开成功
    assert(fd >= 0);
    return fd;
}

//让进程进行等待
void Wait(int fd)
{
    Log("等待中....", Notice) << "\n";
    //将我们的temp写入我们的管道中。

    uint32_t temp = 0;
    //将数据从fd管道中读取到我们的tmp中,读取4个字节的大小
    ssize_t s = read(fd, &temp, sizeof(uint32_t));
    //返回的s是否是我们读取到的字节的个数
    assert(s == sizeof(uint32_t));
    //置于从管道中读取到了是什么不重要,只是为了让它在这里进行等待
    (void)s;
}

//唤醒另外一个进程
void Signal(int fd)
{
    uint32_t temp = 1;
    //将我们的1写入我们的管道中
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    Log("唤醒中....", Notice) << "\n";
}
//关闭我们的管道文件
void CloseFifo(int fd)
{
    close(fd);
}

shmClint.cc

#include"comm.hpp"
int main()
{
    Log("child pid is:",Debug)<0)
        {   //我们从标准输入读取到的字符串假设是abcd回车,也就是abcd\n
            //但是由于我们是想要和沃尔玛的呢字符串quit进行比较,所以我们需要将\n修改成是\0
            shmaddr[s-1]=0;
            //客户端写入成功之后,唤醒服务端
            Signal(fd);
            if(strcmp(shmaddr,"quit")==0)
            {
                break;
            }
        }
    }
    CloseFifo(fd);
    //去关联
    //将共享内存段与当前进程脱离
    int n=shmdt(shmaddr);
    //如果脱离失败了,就打印日志
    assert(n!= -1);
    Log("detach shm success",Debug)<<"Client say :"<

shmServer

#include"comm.hpp"
//创建一个全局对象,对应的程序在加载的时候会自动构建全局变量,也就是调用该类的构造函数
//也就是会帮我们自动创建管道文件
//在我们的程序退出的时候,会自动进行析构,然后删除我们的管道文件

Init init;


//将k转换成十六进制进行输出
string TransToHex(key_t k)
{
    char buffer[32];
    snprintf(buffer,sizeof buffer,"0x%x",k);
    return buffer;
}
int main()
{
    //1.创建公共的key值
    //这里的创建key值的参数和我们的客户端是一样的,所以我们能够得到一个和客户端相同的key值
    key_t k =ftok(PATH_NAME,PROJ_ID);
    //如果创建失败了
    assert(k!=-1);
    Log("create key done",Debug)<<"Server say:"<

Linux【进程间通信】_第33张图片

上面的代码就是利用了我们管道的特性,如果我们的管道中没有内容,我们的读取的进程就会被阻塞,如果我们的管道已经填满了,我们的管道的写入进程就会被阻塞。

这里我们的server端进行wait等待我们的client端往管道中写入数据,一旦我们的client端往管道中写入数据的话,我们的server端就会读取到,然后进行打印我们从客户端读取到的信息 

五、信号量

基于对于共享内存的理解

1.为了让进程间通信->首先让不同的进程之间看到同一份资源

->我们之前讲的所有的通信方式,本质都是优先解决一个问题

->让我们的不同进程看到同一份资源

->让不同的进程看到了同一份资源,比如内存共享,也带来了一些时序问题,造成数据不一致的问题!

也就是我写我的,你写你的,我们两个之间没有任何的相关的问题。

1.我们把多个进程(执行流)看到的公共的一份资源称为临界资源 

2.我们把自己的进程,访问临界资源的代码称为临界区

3.所以多个执行流互相运行的时候互相干扰,主要是我们不加保护地访问了同样的一份资源(临界资源)。在非临界区,多个执行流互相是不影响的!

        为了更好地进行临界区的保护,可以让多执行流在任何时候,都只有一个进程进入临界区,也就是我们所说的互斥

(你需要等到我访问完了,你才能进行访问)

什么是信号量

假设我们去看电影,电影里面只有一个VIP放映的位置。

我们都想要去座那个位置。但是这个位置在任何时刻只能由一个人座。当我坐的时候,你不能打扰我。我占有这个座位,就称为我正在执行自己的临界区代码。

这就是互斥

互斥的本质是串行,但是串行的话,我们的多线程的效率就会降低。

但是,如果我们的电影院中实际上有几百个位置,一般所有人在看电影之前都需要买票,来解决谁坐在哪里的问题。

电影院就是我们的临界资源。

我们每个人都想看电影,也就是我们每一个人都想执行自己的临界区代码

看电影一定要有座位(放映厅里面的一个资源)

那么这个座位真正属于你,是不是你自己坐在这个位置上,这个座位才属于你呢?

并不是!

我们为了避免多个人竞争同一个位置,我们先买票。

在整个放映期间,这个位置都是给你留着的。

也就是说,只要你买了票,你就拥有这个座位的使用权。

这里的买票的本质就是座位的预定机制

也就是说我们有多个线程,想要访问这个临界区资源的不同部分,只要保证这些进程在临界区中访问的是不同的部分,就能够保证这些进程并发地执行。

这就好比是我们上面的例子中,有200个人看电影,每一个人都有一个不同的位置的话,这两百个人就可以同时看一场电影。

为了做到这一点,我们必须让每一个进程想进入临界资源,访问临界资源中的一部分,我们不能让进程直接去使用临界资源,正如不能让用户直接去电影院占座位。也就是说你得先申请信号量(也就是说你得先买票!)

买票的时候,每一张票都有不同的编号,不同的座位编号。

信号量的本质就是一个计数器,也及时类似于int count =n;(不准确)

比方说我们的票有200张,我们这里的计数器也就是200。

每一个进程想要进入临界区都需要申请信号量。 

申请信号量的本质:

让信号量计数器--

也就是卖出了一张票,我们的作为就少了一个

只要申请信号量成功,我们临界资源内部一定给你预留了你想要的资源。

所以说申请信号量的本质其实是对临界资源的一种预定机制 

申请信号量->访问临界资源->释放信号量(你把电影看晚了,这个票就作废了,这个椅子又可以给别人使用了。)

释放信号量也就是让我们的信号量++ 

 信号量是一个计数器

int n=10用一个整数,能不能表示信号量呢?

父进程n--

子进程n--

不能用一个整型去代表信号量。因为即便是父子进程,在发生n--的时候,也会发生写时拷贝,并不能同时对一个n进行操作,父子进程的n是一人一个的,没办法同时进行影响。

假设能让多个进程(整数n在共享内存中)看到同一份全局变量,大家都进行申请信号量n--

也是不可以的!

对于我们的client端

先n--

 写入数据到共享内存中

n++

对于我们的server端

也先n--

读取共享内存

然后n++

他们同时对一个变量进行n--的时候是会出问题的!

在我们的计算机中,我们想要进行n--,其是一种数据运算,我们的计算机汇总只有cpu具有计算能力,而我们的数据放在n的位置上,而n在内存中。也就是计算要在cpu内,而变量在内存中。

那么cpu在执行我们的指令的时候

1、首先需要load将内存中的数据加载到cpu内的寄存器中(读指令) 

2.n--(分析&&执行指令)

3.将CPU修改完毕的n写回内存(写回结果)

执行流在执行的时候,在任何时候都可能被切换!

上面的执行--操作有三步,随时可能会被切换掉。

寄存器只有一套,被所有的执行流共享,但是寄存器里面的数据属于每一个执行流,是与该执行流的上下文!

进程在被切换的时候,是需要进行上下文保护&&上下文恢复的。 

假设这里我们的n是2

所以我们的client进入到cpu中,此时我们得到n等于2,也就是我们运行到上述的第一步,我们的client进程就被切换掉了,我们将client对应的数据和需要执行第几部(n=2,第二步)都记录下来。那我们寄存器中的n还是2,我们进程client中的n是2。

然后server 进程到CPU上运行了,将2--变成了1。

然后我们的client回来了,我们的client回来的时候需要将我们的原先的进程上下文给恢复,也就是直接将我们此时为1的信号量重置为了2,然后执行--操作,变成了1。

也就代表着我们此时的信号量已经不能准确地表示我们的进入临界区的进程个数了!

n--:因为时序问题,而导师n有中间状态,可能导致数据不一致!

我们称这里的n为不安全的。

如果一个n--操作只有一行汇编,

那么该操作是原子的!

(我们上面的n--并不是原子的) 

什么是原子性?

原子性:要么不做,要么做完,没有中间状态,我们就将其称为原子性! 

上面我们的整数计数器也成了临界资源!

所以上面的整数计数器首先需要保证自身是安全的,然后才能保证我们的线程是安全的。 

信号量计数器

申请信号量-》计数器--      -》P操作-》必须是原子的

释放信号量-》计数器++     -》V操作-》必须是原子的 

信号量是对临界资源的预定机制!

信号量本身也是一份临界资源,也需要保持其原子性 

 想要让我们所有的进程都看到这一份同样的信号量就需要进程间通信。(看到一份同样的资源)

你可能感兴趣的:(Linux,C++,进程,通信,进程池)