进程间通信(管道/消息队列/共享内存/信号量)

news2024/10/2 20:36:31

目录

  • 一、进程间通信介绍
    • 1.1 进程间通信的目的
    • 1.2 进程间通信的发展
    • 1.3 进程间通信的分类
  • 二、管道
    • 2.1 什么是管道?
    • 2.2 匿名管道
    • 2.3 实现匿名管道通信的代码
    • 2.4 用fork来共享管道原理
    • 2.5 站在文件描述符角度-深度理解管道
    • 2.6 站在内核角度-管道本质
    • 2.7 管道读写的规则
    • 2.8 管道特点
    • 2.9 进程池版匿名管道通信的场景代码
  • 三、命名管道
    • 3.1 创建一个命名管道
    • 3.2 匿名管道与命名管道的区别
    • 3.3 命名管道的打开规则
    • 3.4 自主实现打印日志的小插件 -- log.hpp
    • 3.4 用命名管道实现server/client通信
  • 四、system V 共享内存
    • 4.1 共享内存示意图
    • 4.2 共享内存数据结构
    • 4.2 共享内存函数
      • 4.2.1 shmget函数
      • 4.2.2 shmat函数
      • 4.2.3 shmdt函数
      • 4.2.4 shmctl函数
    • 4.4 用共享内存实现server/client通信
  • 五、system V消息队列
  • 六、进程互斥
  • 七、进程间通信(管道/消息队列/共享内存/信号量)整体概括一览图

一、进程间通信介绍

1.1 进程间通信的目的

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

1.2 进程间通信的发展

1、管道。
2、System V进程间通信。
3、POSIX进程间通信。

1.3 进程间通信的分类

管道:匿名管道pipe和命名管道。
System V IPC:
1、System V 消息队列。
2、System V 共享内存。
3、System V 信号量。

POSIX IPC
1、消息队列
2、共享内存
3、信号量
4、互斥量
5、条件变量
6、读写锁

二、管道

2.1 什么是管道?

管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
在这里插入图片描述

2.2 匿名管道

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误码
在这里插入图片描述
在这里插入图片描述
创建管道文件成功返回0,创建失败返回-1,同时错误码被设置。管道文件是内存级文件,只在进程间通信时有效,进程结束后直接被释放,管道文件的内容不会被刷新到磁盘上。
在这里插入图片描述

2.3 实现匿名管道通信的代码

#include <iostream>
using namespace std;
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>

#define NUM 1024

void writer(int wfd)
{
    string msg="I am child";
    pid_t id=getpid();
    char buff[NUM]={0};
    snprintf(buff,sizeof(buff),"%s : %d",msg.c_str(),id);
    int num=0;
    //int n=5;
    char ch='a';

    while(true)
    {
        sleep(1);
        num++;
        //子进程向管道里写内容
        write(wfd,buff,sizeof(buff));
        //write(wfd,&ch,1);
        //cout<<num<<endl;
        //cout<<buff<<" "<<num<<endl;
    }
}

void reader(int rfd)
{
    char buff[NUM]={0};
    int num=0;
    int cnt=5;
    while(cnt--)
    {
        //sleep(50);
        num++;
        buff[0]='\0';
        //父进程向管道里读内容
        ssize_t n=read(rfd,buff,sizeof(buff));
        if(n==0)
        {
            //写端退出
            cout<<"写端退出啦"<<endl;
            break;
        }
        else if(n<0)
        {
            //读取失败
            break;
        }
        //读到的内容当作字符串
        buff[n]='\0';
        cout<<"father get a msg : "<<buff<<" "<<getpid()<<" "<<num<<endl;
    }
}

int main()
{
    int pipefd[2]={0};
    int ret=pipe(pipefd);
    if(ret<0)
    {
        perror("pipe fail");
        exit(1);
    }
    int id=fork();
    if(id==0)
    {
        //子进程
        //子进程负责写,关闭管道的读端
        close(pipefd[0]);
        writer(pipefd[1]); 
        close(pipefd[1]);
        exit(2);
    }

    //父进程负责读,关闭进程的写端
    close(pipefd[1]);
    reader(pipefd[0]);
    close(pipefd[0]);

    //父进程
    sleep(3);
    int status=0;
    //进程等待
    int tmp=waitpid(id,&status,0);
    if(tmp>=0)
    {
        cout<<"father wait success!"<<endl;
        printf("exit code : %d , exit sig : %d\n",(status>>8)&0xFF,status&0x7F);
    }
    
    sleep(5);
    return 0;
}

2.4 用fork来共享管道原理

在这里插入图片描述

2.5 站在文件描述符角度-深度理解管道

在这里插入图片描述

在这里插入图片描述

2.6 站在内核角度-管道本质

在这里插入图片描述
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,也符合“Linux下一切皆文件思想"。

2.7 管道读写的规则

当没有数据可读时:
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将不再保证写入的原子性。

2.8 管道特点

只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程。
一般而言,内核会对管道操作进行同步与互斥。
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
在这里插入图片描述

2.9 进程池版匿名管道通信的场景代码

ProcessPoll.cc

#include <iostream>
using namespace std;
#include <unistd.h>
#include <vector>
#include <sys/wait.h>
#include <sys/stat.h>
#include <cstdlib>
#include <assert.h>
#include "task.hpp"


#define NUM 5

//任务集
vector<task_t> tasks;

//先描述
class channel 
{
public:
    channel(const int fd,pid_t id,string& processname)
        :_fd(fd)
        ,_id(id)
        ,_processname(processname)
    {}
    
public:
    int _fd;//发送任务的文件描述符
    pid_t _id;//子进程的pid
    string _processname;//子进程的名字 -- 方便打印日志
};

//子进程跑任务
void slaver()
{
    while(true)
    {
        sleep(1);
        int cmdcode=0;
        //读取父进程下达的指令,执行对应的任务函数
        int n=read(0,&cmdcode,sizeof(int));
        if(n>0)
        {   
            cout<<"子进程(pid : "<<getpid()<<",cmdcode : "<<cmdcode<<") get a task"<<" 执行 ";
            tasks[cmdcode]();
            cout<<endl;
        }
        else if(n==0)
        {
            cout<<"父进程退出啦!!!"<<endl;
            break;
        }
        else
        {
            cout<<"读取出错"<<endl;
            break;
        }
    }
}

void InitProcessPool(vector<channel>& channels)
{
    //记录从第二个子进程开始的进程继承父进程的文件描述符的
    //个数,即从第二个子进程开始的后面的所有进程都有指向前面
    //所有子进程的文件描述符,即前面的子进程不止能从管道文件
    //读取父进程写的内容,后面的子进程也能给前面的子进程写内容,
    //这就不满足管道文件只能单向通信的定义,所以要记录后面子进程
    //从父进程中继承的文件描述符数量,在后面创建的子进程后要关
    //闭自己指向前面子进程的文件描述符
    vector<int> fd_arr;
    for(int i=0;i<NUM;i++)
    {
        int pipefd[2]={0};
        int n=pipe(pipefd);
        pid_t id=fork();
        if(id==0)
        {
            //子进程负责读
            close(pipefd[1]);
            //重定向,想让我们的子进程只从0号描述符中读取信息
            dup2(pipefd[0],0);
            close(pipefd[0]);
            //把后面子进程指向前面子进程写端的文件描述符关闭掉,
            //保证管道只有一个读写文件描述符指向
            for(const auto& fd:fd_arr)
            {
                close(fd);
            }
            slaver();
            cout<<"子进程 process : "<<getpid()<<" "<<i<<" quit"<<endl<<endl;

            exit(0);
        }
        //父进程
        close(pipefd[0]);
        fd_arr.push_back(pipefd[1]);
        string name="process-"+to_string(i);
        channels.push_back(channel(pipefd[1],id,name));

    }
}

void DisPlay(vector<channel>& channels)
{
    for(const auto& e:channels)
    {
        cout<<"fd:"<<e._fd<<" id:"<<e._id<<" processname "<<e._processname<<endl;
    }
}

void menu()
{
    cout<<"#############################################"<<endl;
    cout<<"######### 1、更新日志  2、投篮 ##############"<<endl;
    cout<<"######### 3、抢断      4、传球 ##############"<<endl;
    cout<<"##########0、退出               #############"<<endl;
    cout<<"#############################################"<<endl;
}

void ctrlSlaver(vector<channel>& channels)
{
    while(true)
    {
        sleep(1);
        menu();
        cout<<"Please Enter: ";
        int select=0;
        cin>>select;
        if(select<=0||select>=5)
        {
            break;
        }
        int op=select-1;
        //sleep(1);
        ssize_t n=write(channels[op]._fd,&op,sizeof(int));
    }
}

void QuitProcess(vector<channel>& channels)
{
    for(int i=0;i<NUM;i++)
    {
        close(channels[i]._fd);
        waitpid(-1,nullptr,0);
        cout<<"等待成功,子进程"<<channels[i]._processname<<" "<<"id为:"<<channels[i]._id<<endl;
    }
}

int main()
{
    srand((unsigned int)time(nullptr));
    LoadTask(tasks);

    //进程池
    vector<channel> channels;

    //初始化进程池
    InitProcessPool(channels);
    DisPlay(channels);

    //主进程控制进程池,其实就是父进程给子进程安排任务
    ctrlSlaver(channels);
    //sleep(100);
    
    //释放进程池中的进程
    QuitProcess(channels);

    return 0;
}


makefile:

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

task.hpp

#include <iostream>
using namespace std;
#include <unistd.h>
#include <vector>
#include <sys/wait.h>
#include <sys/stat.h>
#include <cstdlib>



typedef void(*task_t)();

void task1()
{
    cout<<"NBA2K 更新日志"<<endl;
}

void task2()
{
    cout<<"NBA2K 投篮"<<endl;
}

void task3()
{
    cout<<"NBA2K 抢断"<<endl;
}

void task4()
{
    cout<<"NBA2K 传球"<<endl;
}

void LoadTask(vector<task_t>& tasks)
{
    tasks.push_back(task1);
    tasks.push_back(task2);
    tasks.push_back(task3);
    tasks.push_back(task4);
}

三、命名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。

3.1 创建一个命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令: mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
在这里插入图片描述

3.2 匿名管道与命名管道的区别

匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open。
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。

3.3 命名管道的打开规则

如果当前打开操作是为了读而打开FIFO时,
O_NONBLOCK disable:阻塞直到有相应进程为了写而打开该FIFO
O_NONBLOCK enable:立刻返回成功。

如果当前打开操作是为了写而打开FIFO时,
O_NONBLOCK disable:阻塞直到有相应进程为了读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

3.4 自主实现打印日志的小插件 – log.hpp

#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <time.h>
#include <stdarg.h>

// 日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
//向一个文件中打印
#define OneFile 2
//向多个文件中打印,按照后缀把不同的日志信息打印到不同的文件中,分类打印
#define Classfile 3
#define SIZE 1024

//默认的日志文件名
#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        //打印的方式,默认向显示器打印
        printMethod = Screen;
        //日志的目录
        path = "./log/";
    }

    void Enable(int mothod)
    {
        printMethod = mothod;
    }

    string LevelToString(int level)
    {
        switch (level)
        {
        case Info:
        {
            return "Info";
        }
        case Debug:
        {
            return "Debug";
        }
        case Warning:
        {
            return "Warning";
        }
        case Error:
        {
            return "Error";
        }
        case Fatal:
        {
            return "Fatal";
        }
        default:
        {
            return "None";
        }
        }
    }

    void printlog(int level,const string& logtxt)
    {
        switch(printMethod)
        {
        case Screen:
        {
            cout<<logtxt<<endl;
            break;
        }
        case OneFile:
        {
            PrintOneFile(LogFile,logtxt);
            break;
        }
        case Classfile:
        {
            PrintClassfile(level,logtxt);
            break;
        }
        default:
        {
            break;
        }
        }
    }

    void PrintOneFile(const string& logname,const string& logtxt)
    {
        string _logname=path+logname;
        int fd=open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
        if(fd<0)
        {
            perror("open fail");
            return;
        }

        write(fd,logtxt.c_str(),logtxt.size());

        close(fd);

    }

    void PrintClassfile(int level,const string& logtxt)
    {
        string filename=LogFile;
        filename+='.';
        filename+=LevelToString(level);
        PrintOneFile(filename,logtxt);
    }

    void operator()(int level,const char* format,...)
    {
        time_t t=time(nullptr);
        struct tm* ctime=localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer,SIZE,"[%s][%d-%d-%d %d:%d:%d]",LevelToString(level).c_str(),
        ctime->tm_year+1900,ctime->tm_mon+1,ctime->tm_mday,
        ctime->tm_hour,ctime->tm_min,ctime->tm_sec);

        //本质是一个char*的指针
        va_list s;
        //初始化s指针,使s指向可变参数列表format的第一个元素
        va_start(s,format);
        char rightbuffer[SIZE]={0};
        //从可变参数列表的format的第一个元素开始向后取出size个参数
        vsnprintf(rightbuffer,SIZE,format,s);
        //相当于把s指针置空
        va_end(s);

        //拼接日志信息
        char logtxt[SIZE*2];
        snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightbuffer);

        //打印
        printlog(level,logtxt);
    }

    ~Log()
    {
    }

private:
    // 打印方法
    int printMethod;
    string path;
};

3.4 用命名管道实现server/client通信

makefile:

.PHONY:all
all:client server

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

comm.hpp

#pragma once

#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <cstring>

#define FIFO_FILE "./myfifo"
#define MODE 0664
#define NUM 1024

enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR,
    FIFO_WRITE_ERR
};

class Init
{
public:
    Init()
    {
        // 创建命名管道文件,这是一个临时的管道文件,是内存级别文件
        // 只作为进程间通信用,不会刷新到磁盘上
        int n = mkfifo(FIFO_FILE, MODE);
        if (n < 0)
        {
            perror("mkfifo fail");
            exit(FIFO_CREATE_ERR);
        }
    }

    ~Init()
    {
        // 关闭命名管道文件
        int n = unlink(FIFO_FILE);
        if (n < 0)
        {
            perror("unlink fail");
            exit(FIFO_DELETE_ERR);
        }
    }
};

log.hpp

#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <time.h>
#include <stdarg.h>

// 日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
//向一个文件中打印
#define OneFile 2
//向多个文件中打印,按照后缀把不同的日志信息打印到不同的文件中,分类打印
#define Classfile 3
#define SIZE 1024

//默认的日志文件名
#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        //打印的方式,默认向显示器打印
        printMethod = Screen;
        //日志的目录
        path = "./log/";
    }

    void Enable(int mothod)
    {
        printMethod = mothod;
    }

    string LevelToString(int level)
    {
        switch (level)
        {
        case Info:
        {
            return "Info";
        }
        case Debug:
        {
            return "Debug";
        }
        case Warning:
        {
            return "Warning";
        }
        case Error:
        {
            return "Error";
        }
        case Fatal:
        {
            return "Fatal";
        }
        default:
        {
            return "None";
        }
        }
    }

    void printlog(int level,const string& logtxt)
    {
        switch(printMethod)
        {
        case Screen:
        {
            cout<<logtxt<<endl;
            break;
        }
        case OneFile:
        {
            PrintOneFile(LogFile,logtxt);
            break;
        }
        case Classfile:
        {
            PrintClassfile(level,logtxt);
            break;
        }
        default:
        {
            break;
        }
        }
    }

    void PrintOneFile(const string& logname,const string& logtxt)
    {
        string _logname=path+logname;
        int fd=open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
        if(fd<0)
        {
            perror("open fail");
            return;
        }

        write(fd,logtxt.c_str(),logtxt.size());

        close(fd);

    }

    void PrintClassfile(int level,const string& logtxt)
    {
        string filename=LogFile;
        filename+='.';
        filename+=LevelToString(level);
        PrintOneFile(filename,logtxt);
    }

    void operator()(int level,const char* format,...)
    {
        time_t t=time(nullptr);
        struct tm* ctime=localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer,SIZE,"[%s][%d-%d-%d %d:%d:%d]",LevelToString(level).c_str(),
        ctime->tm_year+1900,ctime->tm_mon+1,ctime->tm_mday,
        ctime->tm_hour,ctime->tm_min,ctime->tm_sec);

        //本质是一个char*的指针
        va_list s;
        //初始化s指针,使s指向可变参数列表format的第一个元素
        va_start(s,format);
        char rightbuffer[SIZE]={0};
        //从可变参数列表的format的第一个元素开始向后取出size个参数
        vsnprintf(rightbuffer,SIZE,format,s);
        //相当于把s指针置空
        va_end(s);

        //拼接日志信息
        char logtxt[SIZE*2];
        snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightbuffer);

        //打印
        printlog(level,logtxt);
    }

    ~Log()
    {
    }

private:
    // 打印方法
    int printMethod;
    string path;
};

server.cc

#include "comm.hpp"
#include "log.hpp"

int main()
{
    // 创建命名管道,创建该类就会自动创建管道文件了
    Init init;

    // 创建日志结构体对象
    Log log;
    //指定日志打印的方式
    log.Enable(Screen);

    //创建管道文件后打开文件就可以通信了,命名管道通信其实就是我们平时在创建的
    //文件中写内容,只不过这个文件是共享的罢了,没什么深奥的地方
    int fd = open(FIFO_FILE, O_RDONLY);//等待写入放打开之后,服务端才会打开文件,向后执行
    if (fd < 0)
    {
        log(Fatal, "open fail , exit code:%d exit string:%s", errno, strerror(errno));
        exit(FIFO_OPEN_ERR);
    }

    log(Info, "named pipe created done , exit code:%d exit string:%s", errno, strerror(errno));
    // log(Debug, "exit code:%d exit string:%s", errno, strerror(errno));
    // log(Warning, "exit code:%d exit string:%s", errno, strerror(errno));
    // log(Error, "exit code:%d exit string:%s", errno, strerror(errno));
    // log(Fatal, "exit code:%d exit string:%s", errno, strerror(errno));

    // 读命名管道的内容
    while (true)
    {
        char buff[NUM] = {0};
        ssize_t n = read(fd, buff, sizeof(buff));
        if (n > 0)
        {
            buff[n] = '\0';
            cout << "clien say# " << buff << endl;
        }
        else if (n == 0)
        {
            log(Debug, "client quit,me too!,exit code:%d exit string:%s", errno, strerror(errno));
            break;
        }
        else
        {
            log(Fatal, "read fail,exit code:%d exit string:%s", errno, strerror(errno));
            break;
        }
    }

    // 关闭命名管道
    close(fd);

    // 最后会自动调用析构删除命名管道

    return 0;
}

client.cc

#include "comm.hpp"
#include "log.hpp"

int main()
{
    Log log;
    //客户端只对管道写文件,无需创建管道文件,当客户端运行程序要和
    //服务端进行通信时,服务端早已创建好命名管道文件了,所以子进程
    //只需要直接打开管道文件即可,这个管道文件的名字是客户端和服务
    //器提前沟通好的,所以直接打开协商好的管道文件进行通信就好了
    int fd=open(FIFO_FILE,O_WRONLY|O_APPEND);
    if(fd<0)
    {
        log(Fatal,"open fail , exit code:%d exit string:%s",errno,strerror(errno));
        exit(FIFO_OPEN_ERR);
    }

    log(Info,"open file done , exit code:%d exit string:%s", errno, strerror(errno));

    //往管道里写文件
    string buff;
    while(true)
    {
        cout<<"Please Enter# ";
        getline(cin,buff);
        ssize_t n=write(fd,buff.c_str(),sizeof(buff));
        if(n<0)
        {
            log(Fatal,"write fail , exit code:%d exit string:%s",errno,strerror(errno));
            exit(FIFO_WRITE_ERR);
        }
    }

    close(fd);
    
    //客户端无需删除管道文件,服务器退出时会自动删除管道文件,管道文件不会刷新到磁盘

    return 0;
}


进程间通信结果:
在这里插入图片描述

四、system V 共享内存

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

4.1 共享内存示意图

在这里插入图片描述

4.2 共享内存数据结构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.2 共享内存函数

4.2.1 shmget函数

功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1

4.2.2 shmat函数

功能:将共享内存段连接到进程地址空间
原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1

说明:

shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

4.2.3 shmdt函数

功能:将共享内存段与当前进程脱离
原型
int shmdt(const void *shmaddr);
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段

4.2.4 shmctl函数

功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1

在这里插入图片描述

4.4 用共享内存实现server/client通信

makefile:

.PHONY:all
all:client server

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


comm.hpp

#pragma once

#include <iostream>
using namespace std;

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "log.hpp"

Log log;
//共享内存的大小一般建议是4096的整数倍,如果申请4097
//那么操作系统给你的是4096*2
const int size=4096;
//这个文件名是说明并不重要,只是作为一个参数去生成一个系统中的唯一的一个key值而已
const string pathname="/home/cmj/test.c";
//这个项目id也是随便定义的,值可以是任意值
const int proj_id=0x6666;

//引入了管道文件
#define FIFO_FILE "./myfifo"
#define MODE 0664
#define NUM 1024

//获取一个唯一的key值,操作系统用于标识一块唯一的共享内存资源
key_t GetKey()
{
    key_t k=ftok(pathname.c_str(),proj_id);
    if(k<0)
    {
        log(Fatal,"ftok fail,exit string:%s ,exit code: %d\n",strerror(errno),errno);
        exit(2);
    }
    log(Info,"ftok success,key is:0x%x\n",k);
    return k;

}

//获取共享内存标识符shmid
int GetShareMemHelper(int flag)
{
    //先获取一个操作系统用于标识共享内存资源的唯一性的key值
    key_t k=GetKey();
    //shmid是共享资源标识符,在用户进程内标识资源的唯一性
    int shmid=shmget(k,size,flag);
    if(shmid<0)
    {
        log(Fatal,"shmget fail,exit string:%s ,exit code: %d\n",strerror(errno),errno);
        exit(1);
    }
    log(Info,"exit string:%s ,exit code: %d\n",strerror(errno),errno);
    return shmid;
    
}

int CreateShm()
{
    //IPC_CREAT单独使用的时候,表示创建共享内存时,
    //该共享内存不存在,就创建,存在,就获取并返回

    //IPC_CREAT和IPC_EXCL同时使用的时候表示创建共享
    //资源时,不存在就创建,存在就出错返回
    //为什么要这样子设计呢?可以保证申请成功了一个共享内存,
    //那么这个共享内存一定是新的
    //IPC_EXCL不单独使用
    //0666是创建时的权限
    return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666);
}

int GetShm()
{
    //获取共享内存
    return GetShareMemHelper(IPC_CREAT);
}




enum
{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR,
    FIFO_WRITE_ERR
};

class Init
{
public:
    Init()
    {
        // 创建命名管道文件
        int n = mkfifo(FIFO_FILE, MODE);
        if (n < 0)
        {
            perror("mkfifo fail");
            exit(FIFO_CREATE_ERR);
        }
    }

    ~Init()
    {
        // 关闭命名管道文件
        int n = unlink(FIFO_FILE);
        if (n < 0)
        {
            perror("unlink fail");
            exit(FIFO_DELETE_ERR);
        }
    }
};

log.hpp

#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <time.h>
#include <stdarg.h>

// 日志等级
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define OneFile 2
//向多个文件打印
#define Classfile 3
#define SIZE 1024

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }

    void Enable(int mothod)
    {
        printMethod = mothod;
    }

    string LevelToString(int level)
    {
        switch (level)
        {
        case Info:
        {
            return "Info";
        }
        case Debug:
        {
            return "Debug";
        }
        case Warning:
        {
            return "Warning";
        }
        case Error:
        {
            return "Error";
        }
        case Fatal:
        {
            return "Fatal";
        }
        default:
        {
            return "None";
        }
        }
    }

    void printlog(int level,const string& logtxt)
    {
        switch(printMethod)
        {
        case Screen:
        {
            cout<<logtxt<<endl;
            break;
        }
        case OneFile:
        {
            PrintOneFile(LogFile,logtxt);
            break;
        }
        case Classfile:
        {
            PrintClassfile(level,logtxt);
            break;
        }
        default:
        {
            break;
        }
        }
    }

    void PrintOneFile(const string& logname,const string& logtxt)
    {
        string _logname=path+logname;
        int fd=open(_logname.c_str(),O_WRONLY|O_CREAT|O_APPEND,0666);
        if(fd<0)
        {
            perror("open fail");
            return;
        }

        write(fd,logtxt.c_str(),logtxt.size());

        close(fd);

    }

    void PrintClassfile(int level,const string& logtxt)
    {
        string filename=LogFile;
        filename+='.';
        filename+=LevelToString(level);
        PrintOneFile(filename,logtxt);
    }

    void operator()(int level,const char* format,...)
    {
        time_t t=time(nullptr);
        struct tm* ctime=localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer,SIZE,"[%s][%d-%d-%d %d:%d:%d]",LevelToString(level).c_str(),
        ctime->tm_year+1900,ctime->tm_mon+1,ctime->tm_mday,
        ctime->tm_hour,ctime->tm_min,ctime->tm_sec);

        va_list s;
        va_start(s,format);
        char rightbuffer[SIZE]={0};
        vsnprintf(rightbuffer,SIZE,format,s);
        va_end(s);


        char logtxt[SIZE*2];
        snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuffer,rightbuffer);

        printlog(level,logtxt);
    }

    ~Log()
    {
    }

private:
    // 打印方法
    int printMethod;
    string path;
};

server.cc

#include "comm.hpp"

extern Log log;

int main()
{
    Init init;

    //获取共享内存标识符
    int shmid=CreateShm() ;
    log(Info,"shmid: %d\n",shmid);
    //利用唯一的共享内存标识符申请一块共享内存资源
    //nullptr表示共享内存的起始地址在哪由系统随机分配
    char* shmaddr=(char*)shmat(shmid,nullptr,0);
    // ipc code 在这里!!
    // 一旦有人把数据写入到共享内存,其实我们立马能看到了!!
    // 不需要经过系统调用,直接就能看到数据了!
    
    //关于共享内存的内核数据结构对象,可以把所有属性通过shmctl接口拿出来
    struct shmid_ds shmds;
    int fd=open(FIFO_FILE,O_RDONLY);
    if(fd<0)
    {
        log(Fatal,"open FIFO_FILE fail");
        exit(FIFO_OPEN_ERR);
    }
    
    while(true)
    {
        char ch;
        ssize_t s = read(fd,&ch,1);
        if(s<=0)
        {
            break;
        }
        
        //这里就是直接访问共享内存了,无需再调用read或者write这样的系统调用接口了
        cout << "client say@ " << shmaddr << endl; //直接访问共享内存
        sleep(1);

        //设置IPC_STAT标志位可以获取共享资源的属性信息
        shmctl(shmid, IPC_STAT, &shmds);
        cout << "shm size: " << shmds.shm_segsz << endl;
        cout << "shm nattch: " << shmds.shm_nattch << endl;
        printf("shm key: 0x%x\n",  shmds.shm_perm.__key);
        cout << "shm mode: " << shmds.shm_perm.mode << endl<<endl;
    }

    //去关联
    shmdt(shmaddr);
    log(Info,"shmdt success!");

    //设置IPC_RMID标志位先检查共享内存的引用计数是否是0,如果是0,就释放该共享内存,否则不释放
    shmctl(shmid,IPC_RMID,nullptr);
    log(Info,"shmctl destroy success!");

    return 0;
}

client.cc

#include "comm.hpp"
#include <string.h>

int main()
{
    //只需要获取共享内存即可,无需创建
    int shmid=GetShm();
    //获取共享内存
    char* shmaddr=(char*)shmat(shmid,nullptr,0);
    
    // 一旦有了共享内存,挂接到自己的地址空间中,你直接把他当成你的内存空间来用即可!
    // 不需要调用系统调用
    
    //这里是引入了管道的代码
    int fd=open(FIFO_FILE,O_WRONLY);
    if(fd<0)
    {
        log(Fatal,"open FIFO_FILE fail");
        exit(FIFO_OPEN_ERR);
    }
    
    while(true)
    {
        cout<<"Please Enter# ";
        //这里才是通过共享内存进行进程间通信的逻辑
        fgets(shmaddr,4096,stdin);
        write(fd,"c",1);
    }

    //去关联即可,无需释放共享内存,由创建方释放即可
    shmdt(shmaddr);


    return 0;
}

效果:
在这里插入图片描述
在这里插入图片描述
注意:共享内存本身没有进行同步与互斥!只不过我们引入了管道才有了同步互斥功能!!!

五、system V消息队列

消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法。
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
特性方面
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

六、进程互斥

由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥。
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。在进程中涉及到互斥资源的程序段叫临界区。

特性方面
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。

七、进程间通信(管道/消息队列/共享内存/信号量)整体概括一览图

在这里插入图片描述
以上就是今天想要跟大家分享的所有内容啦!你学会了吗?如果感觉到有所收获,那么点点小心心点点关注呗,后期还会持续更新Linux系统编程的相关内容哦,我们下期见!!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1241224.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023 年 亚太赛 APMCM (C题)国际大学生数学建模挑战赛 |数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 问题一 为了分析中国新能源电动汽车发展的主要因素&#xf…

电线电缆行业生产管理怎么数字化?

行业介绍 随着市场环境的变化和现代生产管理理念的不断更新&#xff0c;电缆的生产模式也在发生转变&#xff0c;批量小&#xff0c;规格多&#xff0c;交期短的新型制造需求逐年上升&#xff0c;所以企业车间管理的重要性越发凸显&#xff0c;作为企业良性运营的关键&#xf…

MySQL--慢查询(一)

1. 查看慢查询日志是否开启 show variables like slow_query%; show variables like slow_query_log; 参数说明&#xff1a; 1、slow_query_log&#xff1a;这个参数设置为ON&#xff0c;可以捕获执行时间超过一定数值的SQL语句。 2、long_query_time&#xff1a;当SQL语句执行…

汇编-PUSHFD和POPFD标志寄存器值压栈和出栈

PUSHFD指令将32位EFLAGS寄存器内容压入堆栈&#xff0c; 而POPFD指令则将栈顶单元内容弹出到EFLAGS寄存器 格式&#xff1a;

【开源】基于Vue和SpringBoot的学校热点新闻推送系统

项目编号&#xff1a; S 047 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S047&#xff0c;文末获取源码。} 项目编号&#xff1a;S047&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 新闻类型模块2.2 新闻档案模块2.3 新…

opencv- CLAHE 有限对比适应性直方图均衡化

CLAHE&#xff08;Contrast Limited Adaptive Histogram Equalization&#xff09;是一种对比度有限的自适应直方图均衡化技术&#xff0c;它能够提高图像的对比度而又避免过度增强噪声。 在OpenCV中&#xff0c;cv2.createCLAHE() 函数用于创建CLAHE对象&#xff0c;然后可以…

MYSQL索引使用注意事项

索引使用注意事项&#xff1a; 1.索引列运算 不要在索引列上进行运算操作&#xff0c;否则索引将失效&#xff1b; 2.字符串不加引号 字符串类型使用时&#xff0c;不加引号&#xff0c;否则索引将失效&#xff1b; 3.模糊查询 如果仅仅是尾部模糊匹配&#xff0c;索引将不会失…

P4 C++ 条件与分支(if)

前言 今天我们来看看条件语句&#xff0c;换句话说&#xff0c;也就是 if 语句、if else 和 else if 等等这写语句。 我知道大家基本上已经非常了解 if 语句和所有 C 中的分支语句&#xff0c;但我还是鼓励你们继续看完这一讲&#xff0c;这里可能包含一些新东西。我们还会深入…

什么是搜索相关性?AI如何驱动搜索相关性?

训练数据驱动机器学习&#xff0c;机器学习促进丰富的人机交互体验。在快速迭代的互联网时代&#xff0c;我们不断被各种广告铺盖&#xff0c;甚至经常细思极恐&#xff0c;“天呐&#xff0c;小红书怎么知道我面膜没了。”这都是算法和机器学习的鬼斧神工洞察着用户的搜索意图…

今年嵌入式行情这么差吗?学了三年至今无面?

先不说嵌入式行情&#xff0c;目前来看&#xff0c;我感觉是整体的行情都不太好。 之前郭嘉公布的失业率&#xff0c;后来停止公布了&#xff0c;至于为什么&#xff0c;这里就不说了吧。 此处省略N个字&#xff0c;下面说说我身边的情况&#xff0c;可见一斑。 先说公司裁员 我…

Linux基础命令3

移动&#xff0c;剪切文件 普通文件的移动剪切 现在在这儿 上图中&#xff0c;mv y.x ./tmp的意思&#xff0c;就是将当前路径下的y.x文件进行剪切&#xff0c;然后放到路径为当前路径下的tmp目录文件夹里面 操作完成后可以cd tmp&#xff0c;ls看到y.x文件已经在里面了 现在…

DolphinDB 浙商银行 | 第二期现场培训圆满结束

自 DolphinDB 高级工程师计划开展以来&#xff0c;客户们纷纷响应&#xff0c;除了定期收看我们每周三开设的线上公开课外&#xff0c;也有部分客户报名参加了 “总部工程师培训计划” 。 上周&#xff0c;我们迎来了总部培训的第二期学员&#xff1a;来自浙商银行的4位策略研…

Cookie与Session

文章目录 Cookie的介绍Cookie的由来什么是CookieCookie原理Cookie覆盖浏览器查看Cookie 在Django中操作Cookie设置Cookie查询浏览器携带的Cookie删除Cookie Cookie校验登录session Cookie的介绍 Cookie的由来 首先我们都应该明白HTTP协议是无连接的。 无状态的意思是每次请求…

联想拯救者Lenovo Legion R9000K 2021H(82N6)原装出厂Windows10/Win11系统ISO镜像

链接&#xff1a;https://pan.baidu.com/s/13NkeCXNdV0Ib5eeRnZUeAQ?pwdnlr7 提取码&#xff1a;nlr7 拯救者笔记本电脑原厂WIN系统自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文…

《数据仓库入门实践》

前言&#xff1a; 1、问什么要写这篇博客&#xff1f; 随着自己在数仓岗位工作的年限增加&#xff0c;对数仓的理解和认知也在发生着变化 所有用这篇博客来记录工作中用到的知识点与经验 2、这篇博客主要记录了哪些内容&#xff1f; 在日常工作中&#xff0c;发现刚接触不久数仓…

故障识别:CNN-BiLSTM-SelfAttention时空特征融合多头自注意力机制的故障识别程序,数据由Excel导入,直接运行!

适用平台&#xff1a;Matlab2023版及以上 本程序参考中文EI期刊《基于CNN-BiLSTM 的滚动轴承变工况故障诊断方法法》&#xff0c;程序注释清晰&#xff0c;干货满满&#xff0c;下面对文章和程序做简要介绍。 在CNN-BiLSTM-SelfAttention故障识别模型中&#xff0c;结合了卷积…

Vue中的$nextTick

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Vue篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来vue篇专栏内容:vue中的$nextTick 目录 &#x1f40b;Vue中的$nextTick有什么作用&#xff1f; &#x1f40b;一、…

易点易动设备管理系统:提升企业设备维修效率的工具

在现代企业运营中&#xff0c;设备的正常运行和及时维修至关重要。然而&#xff0c;传统的设备维修管理方法往往效率低下、易出错&#xff0c;给企业带来了不小的困扰。为了解决这一问题&#xff0c;易点易动设备管理系统应运而生。作为一款先进的智能化系统&#xff0c;易点易…

绝地求生:PUBG全新强化比赛验证系统即将上线,外挂的末日要来了?

就在之前官博发布了一则公告 将在未来的更新中上线强化版的比赛验证系统 具体的变更内容为&#xff1a;从原本的SMS验证&#xff08;短信验证&#xff09;变成了ARS验证&#xff08;语音验证码验证&#xff09;。看起来好像跟原本的验证方式没有太大区别。不过很多黑号他们是没…

3分钟使用 WebSocket 搭建属于自己的聊天室(WebSocket 原理、应用解析)

文章目录 WebSocket 的由来WebSocket 是什么WebSocket 优缺点优点缺点 WebSocket 适用场景主流浏览器对 WebSocket 的兼容性WebSocket 通信过程以及原理建立连接具体过程示例Sec-WebSocket-KeySec-WebSocket-Extensions 数据通信数据帧帧头&#xff08;Frame Header&#xff09…