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

一、进程间通信介绍

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系统编程的相关内容哦,我们下期见!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值