linux笔记归纳9:进程间通信

进程间通信

目录

进程间通信

一、进程间通信理解

1.1.进程间通信的概念

1.2.进程间通信的目的

1.3.进程间通信的方式

二、管道

三、匿名管道

3.1.匿名管道的概念

3.2.pipe函数

3.3.匿名管道的理解

3.4.实例代码

3.5.匿名管道的特性

3.6.四种通信情况

四、进程池

4.1.管道的容量

4.2.管道写入的原子性

4.3.基于管道的进程池

4.4.隐藏BUG

五、命名管道

5.1.mkfifo命令

5.2.mkfifo函数

5.3.命名管道 VS 匿名管道

5.4.命名管道通信

 六、System V共享内存

6.1.共享内存的原理

6.2.共享内存的接口

6.2.1.shmget函数

 6.2.2.shmctl函数

 6.2.3.shmat函数

6.2.4.shmdt函数

6.2.5.ftok函数

6.2.6.ipcs命令

6.2.7.ipcrm命令

6.3.共享内存的实现

七、System V消息队列

7.1.消息队列的原理

7.2.消息队列的接口

7.2.1.msgget函数

7.2.2.msgctl函数

7.2.3.msgsnd函数

7.2.4.msgrcv函数

 7.2.5.ftok函数

7.2.6.ipcs命令

7.2.7.ipcrm命令

7.3.责任链模式与消息队列

八、System V信号量

8.1.关于并发编程的一些概念

8.2.信号量的理解

8.3.信号量的接口

8.3.1.semget函数

 8.3.2.semctl函数

 8.3.3.semop函数

8.3.4.ftok函数

8.3.5.ipcs命令

8.3.6.ipcrm命令

8.4.基于建造者模式的信号量

九、mmap文件映射

9.1.概述

9.2.接口介绍

9.3.写入映射

9.4.读取映射

9.5.模拟实现malloc

十、内核IPC资源的组织管理


一、进程间通信理解

1.1.进程间通信的概念

进程间通信(Inter Process Communication):多个进程间进行数据交换和通信的技术

进程间通信的本质:让不同的进程先看到同一份资源(内存,不能由进程申请而是由OS提供)

开发者会设计一套统一的通信接口

1.2.进程间通信的目的

多个进程之间需要数据传输

多个进程之间需要共享资源

多个进程之间需要通知事件

多个进程之间需要进程控制

1.3.进程间通信的方式

  • 基于文件的管道通信
  • System V进程间通信
  • POSIX进程间通信

二、管道

基于已有的技术通过文件直接进行通信

将文件缓冲区的数据刷新给另一个进程

管道:从一个进程连接到另一个进程的一个数据流(Unix早期进程间通信的形式)

三、匿名管道

3.1.匿名管道的概念

匿名管道:内存级文件,不需要文件路径,也没有文件名,常用作父子通信

子进程通过继承父进程的文件描述符表,来确认打开的是同一个管道文件

3.2.pipe函数

功能:创建匿名管道

int pipe(int fd[2]);
参数:
fd:⽂件描述符数组
fd[0]:表示读端
fd[1]:表示写端
返回值:成功返回0,失败返回错误代码

3.3.匿名管道的理解

3.4.实例代码

从键盘输入数据,写入管道,读取管道,输出到屏幕

实验代码

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

//子进程写
void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        //将字符串内容写入数组
        snprintf(buffer,sizeof(buffer),"I am child,pid: %d,cnt: %d",getpid(),cnt++);
        //向文件写入时不用写\0
        write(wfd,buffer,strlen(buffer));
        sleep(1);
    }
}

//父进程读
void FatherRead(int rfd)
{
    char buffer[1024];
    while(true)
    {
        buffer[0] = 0;
        //将文件内容读取到数组
        ssize_t n = read(rfd,buffer,sizeof(buffer) - 1);
        if(n > 0)
        {
            //读取文件内容时要加\0
            buffer[n] = 0;
            std::cout << "child say: " << buffer << std::endl;
        }
    }
}

int main()
{
    //1. 创建管道
    int fds[2] = {0};//fds[0]:读 fds[1]:写
    int n = pipe(fds);
    if(n < 0)
    {
        std::cerr << "pipe error" << std::endl;
        return 1;
    }
    std::cout << "fds[0]: " << fds[0] << std::endl;
    std::cout << "fds[1]: " << fds[1] << std::endl;
    
    //2. 创建子进程
    pid_t id = fork();
    if(id == 0)
    {
        //关闭子进程读
        close(fds[0]);
        //子进程写入
        ChildWrite(fds[1]);
        //关闭子进程写
        close(fds[1]);
        exit(0);
    }
    //3. 形成通信管道(父进程读,子进程写)
    //关闭父进程写
    close(fds[1]);
    //父进程读取
    FatherRead(fds[0]);
    //回收子进程
    waitpid(id,nullptr,0);
    //关闭父进程读
    close(fds[0]);
    return 0;
}

实验现象

3.5.匿名管道的特性

特性一:只能是具有血缘关系的进程间通信

特性二:管道文件进行IO操作具有同步机制

特性三:管道是面向字节流的,怎么读与怎么写没有必然关系

  • 子进程写的快,父进程读的慢:

  • 子进程写的慢,父进程读的快:

特性四:管道是单向通信的,子进程写数据,父进程读数据,属于半双工

  • 半双工:任何一个时刻,一方发送,一方接收
  • 全双工:任何一个时刻,双方可以同时收发

特性五:(管道)文件的生命周期由进程决定

3.6.四种通信情况

  • 写慢读快

读端进程阻塞等待写端进程写入

  • 写快读慢

写满后,写端进程阻塞等待读端

  • 写关读开

read的返回值0表示文件结束

  • 写开读关

写端写入无意义,浪费内存空间,OS发送异常信号杀掉写端进程

实验代码

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

//子进程写
void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while(true)
    {
        //将字符串内容写入数组
        snprintf(buffer,sizeof(buffer),"I am child,pid: %d,cnt: %d",getpid(),cnt++);
        //向文件写入时不用写\0
        write(wfd,buffer,strlen(buffer));
        printf("child: %d\n",cnt);
        sleep(2);
    }
}

//父进程读
void FatherRead(int rfd)
{
    char buffer[1024];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd,buffer,sizeof(buffer) - 1);
        if(n > 0)
        {
            //读取文件内容时要加\0
            buffer[n] = 0;
            std::cout << "child say: " << buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "n : " << n << std::endl;
            std::cout << "child 退出 father也退出" << std::endl;
            break;
        }
        else
        {
            break;
        }
        break;
    }
}

int main()
{
    //1. 创建管道
    int fds[2] = {0};//fds[0]:读 fds[1]:写
    int n = pipe(fds);
    if(n < 0)
    {
        std::cerr << "pipe error" << std::endl;
        return 1;
    }
    std::cout << "fds[0]: " << fds[0] << std::endl;
    std::cout << "fds[1]: " << fds[1] << std::endl;
    
    //2. 创建子进程
    pid_t id = fork();
    if(id == 0)
    {
        //关闭子进程读
        close(fds[0]);
        //子进程写入
        ChildWrite(fds[1]);
        //关闭子进程写
        close(fds[1]);
        exit(0);
    }
    //3. 形成通信管道(父进程读,子进程写)
    //关闭父进程写
    close(fds[1]);
    //父进程读取
    FatherRead(fds[0]);
    //关闭父进程读
    close(fds[0]);
    sleep(5);
    //回收子进程
    int status = 0;
    int ret = waitpid(id,&status,0);
    if(ret > 0)
    {
        printf("exit code : %d,exit signal : %d\n",(status >> 8) & 0xff,status & 0x7f);
        sleep(5);
    }
    return 0;
}

实验现象

四、进程池

4.1.管道的容量

64KB(65536 / 1024)

4.2.管道写入的原子性

单次写入数据长度≤PIPE_BUF(4KB),OS保证原子写入

单次写入数据长度>PIPE_BUF(4KB),为非原子写入

原子写入:多进程同时向同一个管道写数据时,单次写入的数据不会出现穿插和拆分

示例:进程A写"aaa",进程B写“bbb”,读端只会读到“aaabbb”或"bbbaaa",不会"aababba"

4.3.基于管道的进程池

进程池:父进程通过管道暂停和唤醒子进程,发送任务码让子进程执行相应任务

  • ProcessPool.hpp
//.hpp文件:头文件与源文件没有分离
//.cxx文件:与.cc和.cpp一样,都是C++源文件的后缀
//法一:#pragma once
//法二:条件编译
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__

//头文件
#include <iostream>
#include <vector>
#include <unistd.h>
#include <cstdlib>
#include <sys/wait.h>
#include "Task.hpp"

//管道类(先描述)
class Channel
{
public:
    //构造
    Channel(int fd, pid_t id)
        :_wfd(fd)
        ,_subid(id)
    {
        _name = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid);//整型转字符串
    }
    //发送任务码
    void Send(int code)
    {
        int n = write(_wfd, &code, sizeof(code));
        (void)n;//避免编译报警(对于未使用变量的检查)
    }
    //关闭写文件描述符
    void Close()
    {
        close(_wfd);
    }
    //回收子进程
    void Wait()
    {
        pid_t rid = waitpid(_subid,nullptr,0);
        (void)rid;
    }
    //析构
    ~Channel(){}
    //Get方法
    int Fd()
    {
        return _wfd;
    }
    pid_t SubId()
    {
        return _subid;
    }
    std::string Name()
    {
        return _name;
    }
private:
    int _wfd;//写文件描述符
    int _subid;//子进程pid
    std::string _name;//管道名字
};

//管道管理类(再组织)
class ChannelManager
{
public:
    //构造
    ChannelManager()
        :_next(0)
    {}
    //构建管道
    void Insert(int wfd, pid_t subid)
    {
        //Channel c(wfd,subid);
        //_channels.push_back(std::move(c));
        _channels.emplace_back(wfd,subid);
    }
    //选择管道
    Channel &Select()
    {
        auto &c = _channels[_next];
        _next++;
        _next %= _channels.size();
        return c;
    }
    //打印管道
    void PrintChannel()
    {
        for(auto &channel : _channels)
        {
            std::cout << channel.Name() << std::endl;
        }
    }
    //结束子进程
    void StopSubProcess()
    {
        for(auto &channel : _channels)
        {
            channel.Close();
            std::cout << "关闭: " << channel.Name() << std::endl;
        }
    }
    //回收子进程
    void WaitSubProcess()
    {
        for(auto &channel : _channels)
        {
            channel.Wait();
            std::cout << "回收: " << channel.Name() << std::endl;
        }
    }
    //析构
    ~ChannelManager(){}
private:
    std::vector<Channel> _channels;//管道管理数组
    int _next;
};

//构建管道数目
const int gdefaultnum = 5;

//进程池类
class ProcessPool
{
public:
    //构造
    ProcessPool(int num)
        :_process_num(num)
    {
        //注册任务
        _tm.Register(Printlog);
        _tm.Register(Download);
        _tm.Register(Upload);
    }
    //子进程工作
    void Work(int rfd)
    {
        while(true)
        {
            int code = 0;
            ssize_t n = read(rfd, &code, sizeof(code));
            if(n > 0)
            {
                //如果读取任务码错误
                if(n != sizeof(code))
                {
                    //继续读取
                    continue;
                }
                std::cout << "子进程[" << getpid() << "]收到一个任务码: " << code << std::endl;
                //子进程执行任务
                _tm.Execute(code);
            }
            else if(n == 0)
            {
                std::cout << "子进程退出" << std::endl;
                break;
            }
            else
            {
                std::cout << "读取错误" << std::endl;
                break;
            }
        }
    }
    //启动进程池
    bool Start()
    {
        for(int i = 0; i < _process_num; i++)
        {
            //创建管道文件(父写子读)
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if(n < 0)
            {
                return false;
            }

            //创建子进程
            pid_t subid = fork();
            if(subid < 0)
            {
                return false;
            }
            else if(subid == 0)
            {
                //关闭子进程写
                close(pipefd[1]);
                //子进程工作
                Work(pipefd[0]);
                //关闭子进程读
                close(pipefd[0]);
                //子进程退出
                exit(0);
            }
            else
            {
                //关闭父进程读
                close(pipefd[0]);
                //构建管道
                _cm.Insert(pipefd[1],subid);
            }
        }
        return true;
    }
    //调试
    void Debug()
    {
        _cm.PrintChannel();
    }
    //运行进程池
    void Run()
    {
        //1. 选择一个任务
        int task_code = _tm.Code();

        //2. 选择一个管道/子进程(负载均衡地选择子进程完成任务)
        //方法一:轮询(✔)
        //方法二:随机
        //方法三;channel添加负载指标
        auto &c = _cm.Select();
        std::cout << "选择了一个子进程: " << c.Name() << std::endl;

        //3. 发送任务
        c.Send(task_code);
        std::cout << "发送了一个任务码: " << task_code << std::endl;
    }
    //结束进程池
    void Stop()
    {
        //关闭父进程所有的写文件描述符
        _cm.StopSubProcess();
        //回收所有的子进程
        _cm.WaitSubProcess();
    }
    //析构
    ~ProcessPool(){}
private:
    ChannelManager _cm;//管道管理变量
    TaskManager _tm;//任务管理变量
    int _process_num;//进程个数
};

#endif
  • Task.hpp
#pragma once

#include <iostream>
#include <vector>
#include <time.h>

typedef void (*task_t)();

/////////////////////debug//////////////////////////
void Printlog()
{
    std::cout << "我是一个打印日志的任务" << std::endl;
}

void Download()
{
    std::cout << "我是一个下载的任务" << std::endl;
}

void Upload()
{
    std::cout << "我是一个上传的任务" << std::endl;
}
/////////////////////debug//////////////////////////

class TaskManager
{
public:
    //构造
    TaskManager()
    {
        srand(time(nullptr));
    }
    //注册任务
    void Register(task_t t)
    {
        _tasks.push_back(t);
    }
    //生成任务码
    int Code()
    {
        return rand() % _tasks.size();
    }
    //执行任务
    void Execute(int code)
    {
        if(code >= 0 && code < _tasks.size())
        {
            _tasks[code]();
        }
    }
    //析构
    ~TaskManager()
    {}
private:
    std::vector<task_t> _tasks;
};
  • Main.cc
#include "ProcessPool.hpp"

int main()
{
    //1. 创建进程池类型对象
    ProcessPool pp(gdefaultnum);
    //2. 启动进程池
    pp.Start();
    //3. 自动派发任务
    int cnt = 10;
    while(cnt--)
    {
        //4. 运行进程池
        pp.Run();
        sleep(1);
    }
    //5. 回收进程池
    pp.Stop();
    sleep(1000);
    return 0;
}
  • Makefile
process_pool:Main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f process_pool

实验现象

4.4.隐藏BUG

 

方案一:倒着关闭写端

最后一个进程只有父进程的一个写端,关闭后可以关闭该进程指向其他进程管道的写端

class ChannelManager
{
public:
    //......
    void CloseAndWait()
    {
        for(int i = _channels.size() - 1; i >= 0; i--)
        {
            _channels[i].Close();
            std::cout << "关闭: " << _channels[i].Name() << std::endl;
            channels[i].Wait();
            std::cout << "回收: " << _channels[i].Name() << std::endl;
        }
    }
    //......
}

方案二:让父进程单独指向每个管道的写端

通过管道管理数组,将子进程继承下来的堂兄进程写段全都关闭

class ChannelManager
{
public:
    //......
    void CloseAll()
    {
        for(auto &channel : _channels)
        {
            channel.Close();
        }
    }

    void CloseAndWait()
    {
        for(auto &channel : channels)
        {
            _channels[i].Close();
            std::cout << "关闭: " << _channels[i].Name() << std::endl;
            channels[i].Wait();
            std::cout << "回收: " << _channels[i].Name() << std::endl;
        }
    }
    //......
}


class ProcessPool
{
public:
    //......
    bool Start()
    {
        for(int i = 0; i < _process_num ; i++)
        {
            //创建管道文件(父写子读)
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if(n < 0)
            {
                return false;
            }

            //创建子进程
            pid_t subid = fork();
            if(subid < 0)
            {
                return false;
            }
            else if(subid == 0)
            {
                _cm.CloseAll();
                //关闭子进程写
                close(pipefd[1]);
                //子进程工作
                Work(pipefd[0]);
                //关闭子进程读
                close(pipefd[0]);
                //子进程退出
                exit(0);
            }
            else 
            {
                //关闭父进程读
                close(pipefd[0]);
                //构建管道
                _cm.Insert(pipefd[1],subid);
            }
        }
        return true;
    }
    //......
}

五、命名管道

匿名管道:只能是具有血缘关系的进程进行进程间通信

命名管道:可以在不相关的进程直接交互数据

命名管道文件只会被打开,不会被刷新到磁盘

5.1.mkfifo命令

mkfifo命令:创建命名管道

unlink命令:删除文件

 

5.2.mkfifo函数

 mkfifo函数:创建命名管道

unlink函数:删除文件

5.3.命名管道 VS 匿名管道

匿名管道:由pipe函数创建并打开

命名管道:由mkfifo函数创建,由open函数打开

唯一的区别在于创建和打开方式不同

5.4.命名管道通信

  • comm.hpp
​
#pragma once

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

#define PATH "."
#define FILENAME "fifo"

// 命名管道类
class NamedIpcFifo
{
public:
    NamedIpcFifo(const std::string &path, const std::string &name)
        : _path(path), _name(name)
    {
        _fifoname = _path + "/" + _name;
        // 创建管道文件
        umask(0); // 设置权限掩码
        int n = mkfifo(_fifoname.c_str(), 0666);
        if (n < 0)
        {
            std::cerr << "mkdir fifo error" << std::endl;
        }
        else
        {
            std::cout << "mkfifo success" << std::endl;
        }
    }

    ~NamedIpcFifo()
    {
        // 删除管道文件
        int n = unlink(_fifoname.c_str());
        if (n == 0)
        {
            std::cout << "remove fifo success" << std::endl;
        }
        else
        {
            std::cout << "remove fifo failed" << std::endl;
        }
    }

private:
    std::string _path;     // 管道所在目录路径
    std::string _name;     // 管道文件名称
    std::string _fifoname; // 管道完整路径
};

// 文件操作类
class FileOper
{
public:
    FileOper(const std::string &path, const std::string &name)
        : _path(path), _name(name), _fd(-1)
    {
        _fifoname = _path + "/" + _name;
    }
    // 打开文件(读)
    void OpenForRead()
    {
        // 写入方没有执行open的时候,读取方就要在open内部阻塞
        // 直到有人把管道文件打开了,open才会返回
        _fd = open(FILENAME, O_RDONLY);
        if (_fd < 0)
        {
            std::cerr << "open fifo error" << std::endl;
            return;
        }
        std::cout << "open fifo success" << std::endl;
    }
    // 打开文件(写)
    void OpenForWrite()
    {
        _fd = open(FILENAME, O_WRONLY);
        if (_fd < 0)
        {
            std::cerr << "open fifo error" << std::endl;
            return;
        }
        std::cout << "open fifo success" << std::endl;
    }
    // 写文件
    void Write()
    {
        std::string message;
        int cnt = 1;
        pid_t id = getpid();
        while (true)
        {
            std::cout << "Please Enter# ";
            std::getline(std::cin, message);
            message += (", message number: " + std::to_string(cnt++) + "[" + std::to_string(id) + "]");
            write(_fd, message.c_str(), message.size());
        }
    }
    // 读文件
    void Read()
    {
        while (true)
        {
            char buffer[1024];
            int number = read(_fd, buffer, sizeof(buffer) - 1);
            if (number > 0)
            {
                buffer[number] = 0;
                std::cout << "Client Say# " << buffer << std::endl;
            }
            else if (number == 0)
            {
                std::cout << "client quit! me too!" << std::endl;
                break;
            }
            else
            {
                std::cout << "read error" << std::endl;
                break;
            }
        }
    }
    // 关闭文件
    void Close()
    {
        if(_fd > 0)
        {
            close(_fd);
        }
    }
    ~FileOper()
    {}
private:
    std::string _path;     // 管道所在目录路径
    std::string _name;     // 管道文件名称
    std::string _fifoname; // 管道完整路径
    int _fd;               // 管道文件描述符
};

​
  • server.cc
#include "comm.hpp"

int main()
{
    // 在当前目录下创建管道文件
    NamedIpcFifo fifo(PATH, FILENAME);

    // 文件操作
    FileOper readerfile(PATH, FILENAME);
    readerfile.OpenForRead();
    readerfile.Read();
    readerfile.Close();

    return 0;
}
  • client.cc
#include "comm.hpp"

int main()
{
    // 文件操作
    FileOper writerfile(PATH, FILENAME);
    writerfile.OpenForWrite();
    writerfile.Write();
    writerfile.Close();

    return 0;
}
  • Makefile
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f client server

 六、System V共享内存

System V:一种模块标准,是三种进程间通信机制的总称

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

6.1.共享内存的原理

多组进程使用不同的共享内存进行通信

多个共享内存同时存在就需要进行管理(先描述,再组织)

共享内存由描述自己的结构体对象与物理内存

进程与共享内存的关系就是内核数据结构关系

6.2.共享内存的接口

6.2.1.shmget函数

作用:创建共享内存

参数1:共享内存的键值(不是由内核形成,而是在用户层构建并传入OS)

参数2:共享内存的大小(4KB的整数倍,向上取整)

参数3:共享内存的标志位

  • IPC_CREAT:获取共享内存
  • IPC_CREAT | IPC_EXCL | 权限码:创建共享内存

返回值:

  • 成功:共享内存标识符
  • 失败:-1

 6.2.2.shmctl函数

作用:控制共享内存

参数1:共享内存标识符

参数2:共享内存设置命令

  • IPC_RMID:删除共享内存
  • IPC_STAT:拷贝共享内存属性

参数3:共享内存属性表

键值会被设置到共享内存的描述结构体

 6.2.3.shmat函数

作用:将共享内存挂接到进程的虚拟地址空间中

参数1:共享内存标识符

参数2:采用固定地址挂接,不需要传NULL

参数3:共享内存标志位,传0使用默认设置

返回值:

  • 成功:虚拟空间的起始地址
  • 失败:-1

6.2.4.shmdt函数

作用:将共享内存与虚拟内存去关联,减少共享内存结构体的引用计数

6.2.5.ftok函数

作用:生成键值

参数1:任意路径名字

参数2:任意整型变量

6.2.6.ipcs命令

ipcs -m:查看共享内存

6.2.7.ipcrm命令

ipcrm -m shmid:删除共享内存

6.3.共享内存的实现

读写共享内存没有使用系统调用

共享区属于用户空间而非内核的

所以共享内存是进程通信最快的

  • 映射后读写直接被对方看到
  • 不需要系统调用或写入内容

但是通信双方没有保护机制,共享内存的写端只写了一部分就会被读端读取,导致数据不一致

有保护机制:

  • AA
  • AA BB
  • AA BB CC

无保护机制:

  • A
  • AA
  • AA B
  • AA BB

可以使用命名管道的同步机制对共享内存进行局部保护

进程与进程的共享内存映射:SystemV

进程与磁盘文件的共享内存映射:mmap

  • shm.hpp
#pragma once

#include <iostream>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#include "comm.hpp"

const int gdefaultid = -1;        // 错误共享内存标识符
const int gsize = 4096;           // 共享内存空间
const std::string pathname = "."; // 路径名称
const int projid = 0x66;          // 生成key的任意id
const int gmode = 0666;           // 共享内存权限
#define CREATER "creater"
#define USER "user"

// 共享内存类
class Shm
{
private:
    void CreateHelper(int flg)
    {
        printf("key: 0x%x\n", _key);
        _shmid = shmget(_key, gsize, flg);
        if (_shmid < 0)
        {
            ERR_EXIT("shmget");
        }
        printf("shmid: %d\n", _shmid);
    }

    // 创建一个共享内存
    void Create()
    {
        CreateHelper(IPC_CREAT | IPC_EXCL | gmode);
    }

    // 关联一个共享内存
    void Attach()
    {
        _start_mem = shmat(_shmid, nullptr, 0);
        if ((long long)_start_mem < 0)
        {
            ERR_EXIT("shmat");
        }
        printf("attach success\n");
    }

    // 去关联一个共享内存
    void Detach()
    {
        int n = shmdt(_start_mem);
        if (n == 0)
        {
            printf("Detach success\n");
        }
    }

    // 获取一个共享内存
    void Get()
    {
        CreateHelper(IPC_CREAT);
    }

    // 删除一个共享内存
    void Destroy()
    {
        Detach();
        if (_usertype == CREATER)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            if (n > 0)
            {
                printf("shmctl delete shm: %d success!\n", _shmid);
            }
            else
            {
                ERR_EXIT("shmctl\n");
            }
        }
    }

public:
    Shm(const std::string &pathname, int projid, const std::string &usertype)
        : _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype)
    {
        _key = ftok(pathname.c_str(), projid);
        if (_key < 0)
        {
            ERR_EXIT("ftok");
        }
        if (_usertype == CREATER)
        {
            Create();
        }
        else if (_usertype == USER)
        {
            Get();
        }
        else
        {
        }
        Attach();
    }

    // 返回起始虚拟地址
    void *VirtualAddr()
    {
        printf("VirtualAddr: %p\n", _start_mem);
        return _start_mem;
    }

    // 获取共享内存大小
    int Size()
    {
        return _size;
    }

    // 获取共享内存的属性
    void Attr()
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        printf("shm_segsz: %ld\n", ds.shm_segsz);
        printf("key: 0x%x\n", ds.shm_perm.__key);
    }

    ~Shm()
    {
        std::cout << _usertype << std::endl;
        Destroy();
    }

private:
    int _shmid;            // 共享空间标识符
    key_t _key;            // key值
    int _size;             // 共享空间大小
    void *_start_mem;      // 起始的虚拟地址
    std::string _usertype; // 用户类型
};
  • Fifo.hpp
#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "comm.hpp"

#define PATH "."
#define FILENAME "fifo"

// 命名管道类
class NamedIpcFifo
{
public:
    NamedIpcFifo(const std::string &path, const std::string &name)
        : _path(path), _name(name)
    {
        _fifoname = _path + "/" + _name;
        // 创建管道文件
        umask(0); // 设置权限掩码
        int n = mkfifo(_fifoname.c_str(), 0666);
        if (n < 0)
        {
            ERR_EXIT("mkfifo");
        }
        else
        {
            std::cout << "mkfifo success" << std::endl;
        }
    }

    ~NamedIpcFifo()
    {
        // 删除管道文件
        int n = unlink(_fifoname.c_str());
        if (n == 0)
        {
            //bug:先析构fifo后,进程直接退出,导致shm析构没有被调用
            //ERR_EXIT("unlink");
        }
        else
        {
            std::cout << "remove fifo failed" << std::endl;
        }
    }

private:
    std::string _path;     // 管道所在目录路径
    std::string _name;     // 管道文件名称
    std::string _fifoname; // 管道完整路径
};

// 文件操作类
class FileOper
{
public:
    FileOper(const std::string &path, const std::string &name)
        : _path(path), _name(name), _fd(-1)
    {
        _fifoname = _path + "/" + _name;
    }
    // 打开文件(读)
    void OpenForRead()
    {
        // 写入方没有执行open的时候,读取方就要在open内部阻塞
        // 直到有人把管道文件打开了,open才会返回
        _fd = open(FILENAME, O_RDONLY);
        if (_fd < 0)
        {
            ERR_EXIT("openforread");
        }
        std::cout << "open fifo success" << std::endl;
    }
    // 打开文件(写)
    void OpenForWrite()
    {
        _fd = open(FILENAME, O_WRONLY);
        if (_fd < 0)
        {
            ERR_EXIT("openforwrite");
        }
        std::cout << "open fifo success" << std::endl;
    }
    // 唤醒读端
    void Wakeup()
    {
        char c = 'c';
        int n = write(_fd, &c, 1);
        printf("尝试唤醒: %d\n", n);
    }
    // 等待写端
    bool Wait()
    {
        char c;
        int number = read(_fd, &c, 1);
        if (number > 0)
        {
            printf("读端醒来: %d\n", number);
            return true;
        }
        return false;
    }
    // 关闭文件
    void Close()
    {
        if (_fd > 0)
        {
            close(_fd);
        }
    }
    ~FileOper()
    {
    }

private:
    std::string _path;     // 管道所在目录路径
    std::string _name;     // 管道文件名称
    std::string _fifoname; // 管道完整路径
    int _fd;               // 管道文件描述符
};
  • server.cc
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    // 创建共享内存对象
    Shm shm(pathname, projid, CREATER);

    // 获取共享内存属性
    sleep(5);
    shm.Attr();

    // 创建管道文件对象
    NamedIpcFifo fifo(PATH, FILENAME);

    // 文件操作
    FileOper readerfile(PATH, FILENAME);
    readerfile.OpenForRead();

    // 指向共享内存的指针
    char *mem = (char *)shm.VirtualAddr();
    while (true)
    {
        // 管道未写入时读端默认阻塞,等待写端写入
        if(readerfile.Wait())
        {
            printf("%s\n", mem);
        }
        else
        {
            break;
        }
    }
    readerfile.Close();
    return 0;
}
  • client.cc
#include "Shm.hpp"
#include "Fifo.hpp"

int main()
{
    // 文件操作
    FileOper writerfile(PATH, FILENAME);
    writerfile.OpenForWrite();

    // 获取共享内存对象
    Shm shm(pathname, projid, USER);

    // 指向共享内存的指针
    char *mem = (char *)shm.VirtualAddr();

    int index = 0;
    for (char c = 'A'; c <= 'Z'; c++, index += 2)
    {
        sleep(2);

        // 向共享内存写
        mem[index] = c;
        mem[index + 1] = c;
        mem[index + 2] = 0;
        
        // 唤醒读端读取
        writerfile.Wakeup();
    }

    writerfile.Close();
    return 0;
}
  • comm.hpp
#pragma once

#include <cstdio>
#include <cstdlib>

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
  • Makefile
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f client server

七、System V消息队列

7.1.消息队列的原理

消息队列将该资源维护成一个队列

消息队列的生命周期随内核

消息队列提供了从一个进程向另外一个进程发送一块数据段方法

操作系统要对多个消息队列进行管理,先描述,再组织

两个进程通过约定key值保证看到同一个消息队列

消息队列的结构与内核表示

struct msqid_ds
{
    struct ipc_perm msg_perm;
    struct msg msg_first;
    / first message on queue, unused * /
                                  struct msg msg_last;
    / last message in queue, unused * /
                                 __kernel_time_t msg_stime; /* last msgsnd time */
    __kernel_time_t msg_rtime;                              /* last msgrcv time */
    __kernel_time_t msg_ctime;                              /* last change time */
    unsigned long msg_lcbytes;                              /* Reuse junk fields for 32 bit */
    unsigned long msg_lqbytes;                              /* ditto */
    unsigned short msg_cbytes;                              /* current number of bytes on queue */
    unsigned short msg_qnum;                                /* number of messages in queue */
    unsigned short msg_qbytes;                              /* max number of bytes on queue */
    __kernel_ipc_pid_t msg_lspid;                           /* pid of last msgsnd */
    __kernel_ipc_pid_t msg_lrpid;                           /* last receive pid */
};

7.2.消息队列的接口

7.2.1.msgget函数

作用:创建一个消息队列

参数1:消息队列的键值

参数2:消息队列的标志位

  • IPC_CREAT:获取消息队列
  • IPC_CREAT | IPC_EXCL | 权限码:创建消息队列

返回值:

  • 成功:消息队列描述符
  • 失败:-1

7.2.2.msgctl函数

作用:控制消息队列

参数1:消息队列标识符

参数2:消息队列设置命令

  • IPC_RMID:删除消息队列
  • IPC_STAT:获取消息队列属性

参数3:消息队列属性表

7.2.3.msgsnd函数

作用:发送数据

参数1:消息队列标识符

参数2:消息节点结构体指针

参数3:消息内容的大小

参数4:消息类型

参数5:默认设置为0

返回值:

  • 成功:拷贝的字节数
  • 失败:-1

消息节点结构体

  • mtype:消息类型(区分进程)
  • mtext:消息内容

7.2.4.msgrcv函数

作用:接收数据

参数1:消息队列标识符

参数2:消息节点结构体指针

参数3:消息内容的大小

参数4:默认设置为0

 7.2.5.ftok函数

作用:生成键值

参数1:任意路径名字

参数2:任意整型变量

7.2.6.ipcs命令

ipcs -q:查看消息队列

7.2.7.ipcrm命令

ipcrm -q msqid:删除消息队列

7.3.责任链模式与消息队列

新需求:

客户端发送输入内容,时间,进程PID

服务器将收到的内容,持久化保存文件

文件过大时进行切片,保存指定目录下

行为类设计模式:责任链模式、观察者模式

将请求沿着处理者链进行传递,每个处理者对请求进行检查

如果处理者能够处理该请求,就进行处理

如果处理者不能处理该请求,就给下一个

  • MsgQueue.hpp
#ifndef MSGQUEUE_HPP
#define MSGQUEUE_HPP

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define PATHNAME "/home/ubuntu/test/MsgQueue/MsgQueue.hpp"
#define PROJID 0x123

const int default_fd = -1;
const int default_size = 1024;

#define GET_MSGQUEUE (IPC_CREAT)
#define CREATE_MSGQUEUE (IPC_CREAT | IPC_EXCL | 0666)

class MsgQueue
{
    struct msgbuf
    {
        long mtype;
        char mtext[default_size];
    };

public:
    MsgQueue()
        : _msgfd(default_fd)
    {
    }

    // 创建消息队列
    void Create(int flag)
    {
        // 获取键值
        key_t key = ftok(PATHNAME, PROJID);
        if (key == -1)
        {
            std::cerr << "ftok error" << std::endl;
            exit(1);
        }
        std::cout << "Get key: " << std::hex << key << std::endl;

        // 创建消息队列
        _msgfd = msgget(key, flag);
        if (_msgfd == -1)
        {
            std::cerr << "msgget error" << std::endl;
            exit(2);
        }
        std::cout << "msgqueue created: " << _msgfd << std::endl;
    }

    // 发送消息
    void Send(int type, std::string &text)
    {
        // 填充消息节点
        struct msgbuf msg;
        memset(&msg, 0, sizeof(msg));
        msg.mtype = type;
        memcpy(msg.mtext, text.c_str(), text.size());

        // 发送消息节点
        int n = msgsnd(_msgfd, &msg, sizeof(msg.mtext), 0);
        if (n == -1)
        {
            std::cerr << "msgsnd error" << std::endl;
            return;
        }
        std::cout << "send message: " << text << std::endl;
    }

    // 接收消息
    void Recv(int type, std::string &text)
    {
        struct msgbuf msg;
        int n = msgrcv(_msgfd, &msg, sizeof(msg.mtext), type, 0);
        if (n == -1)
        {
            std::cerr << "msgrcv error" << std::endl;
            return;
        }
        msg.mtext[n] = '\0';
        text = msg.mtext;
        type = msg.mtype;
    }

    // 获取消息队列中的属性
    void GetAtter()
    {
        struct msqid_ds outbuffer;
        int n = msgctl(_msgfd, IPC_STAT, &outbuffer);
        if(n == -1)
        {
            std::cerr << "msgctl error" << std::endl;
            return;
        }
        std::cout << "msgqueue attr: " << std::hex << outbuffer.msg_perm.__key << std::endl;
    }

    // 删除消息队列
    void Destroy()
    {
        int n = msgctl(_msgfd, IPC_RMID, 0);
        if (n == -1)
        {
            std::cerr << "msgctl error" << std::endl;
        }
        std::cout << "msgqueue destroyed" << std::endl;
    }

    ~MsgQueue()
    {
    }

private:
    int _msgfd;
};

// 定义消息类型
#define MSG_TYPE_CLIENT 1
#define MSG_TYPE_SERVER 2

class Server : public MsgQueue
{
public:
    Server()
    {
        MsgQueue::Create(CREATE_MSGQUEUE);
        std::cout << "server create msgqueue done" << std::endl;
        MsgQueue::GetAtter();
    }
    ~Server()
    {
        MsgQueue::Destroy();
    }

private:
};

class Client : public MsgQueue
{
public:
    Client()
    {
        MsgQueue::Create(GET_MSGQUEUE);
        std::cout << "client get msgqueue done" << std::endl;
    }
    ~Client()
    {
    }

private:
};

#endif
  • ChainOfResp.hpp
#ifndef CHAIN_OF_RESP_HPP
#define CHAIN_OF_RESP_HPP

#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
#include <filesystem> // C++17
#include <fstream>
#include <sys/wait.h>

class HandlerText
{
public:
    virtual ~HandlerText() = default;
    virtual void Excute(const std::string &text) = 0;
    void SetNext(std::shared_ptr<HandlerText> next)
    {
        _next = next;
    }
    void Enable()
    {
        _enable = true;
    }
    void DisEnable()
    {
        _enable = false;
    }

protected:                              // 需要被子类继承
    std::shared_ptr<HandlerText> _next; // 下一个责任链节点
    bool _enable = true;                // 是否使用该节点
};

// 格式化文本
class HandlerTextFormat : public HandlerText
{
public:
    void Excute(const std::string &text) override
    {
        std::string format_result = text + "\n";
        if (_enable)
        {
            std::stringstream ss;
            ss << time(nullptr) << "-" << getpid() << "-" << text << "\n";
            format_result = ss.str();
            std::cout << "Step 1. 格式化消息: " << text << " 结果: " << format_result << std::endl;
        }
        if (_next)
        {
            _next->Excute(format_result); // 将处理结果表现在text内部
        }
        else
        {
            std::cout << "到达责任链处理结尾, 完成责任链处理" << std::endl;
        }
    }
};

// 文件信息: 文件路径 文件名称
std::string defaultfilepath = "./tmp/";
std::string defaultfilename = "test.log";

// 文本保存文件
class HandlerTextSaveFile : public HandlerText
{
public:
    HandlerTextSaveFile(const std::string &filepath = defaultfilepath, const std::string &filename = defaultfilename)
        : _filepath(filepath), _filename(filename)
    {
        // 形成默认的目录名
        if (std::filesystem::exists(_filepath))
        {
            return;
        }
        try
        {
            std::filesystem::create_directories(_filepath);
        }
        catch (std::filesystem::filesystem_error const &e)
        {
            std::cerr << e.what() << '\n';
        }
    }
    void Excute(const std::string &text) override
    {
        // 保存到文件中
        if (_enable)
        {
            std::string file = _filepath + _filename;
            std::ofstream ofs(file, std::ios::app);
            if (!ofs.is_open())
            {
                std::cerr << "open file error: " << file << std::endl;
                return;
            }
            ofs << text;
            ofs.close();
            std::cout << "Step 2. 保存消息: " << text << " 到文件: " << file << std::endl;
        }
        if (_next)
        {
            _next->Excute(text); // 将处理结果表现在text内部
        }
        else
        {
            std::cout << "到达责任链处理结尾, 完成责任链处理" << std::endl;
        }
    }

private:
    std::string _filepath;
    std::string _filename;
};

const int defaultmaxline = 5;

// 检查文件内容大小, 对过大内容打包备份
class HandlerTextBackup : public HandlerText
{
private:
    bool IsOutOfRange(const std::string &file)
    {
        std::ifstream ifs(file);
        if (!ifs.is_open())
        {
            std::cerr << "open file error" << file << std::endl;
            return false;
        }

        int lines = 0;
        std::string line;
        while (std::getline(ifs, line))
        {
            lines++;
        }

        ifs.close();

        return lines > _maxline;
    }
    void Backup(const std::string &file)
    {
        // 文件重命名
        std::string suffix = std::to_string(time(nullptr));
        // ./tmp/test.txt.1589234234
        std::string backup_file = file + "." + suffix;
        // test.txt.1589234234
        std::string src_file = _filename + "." + suffix;
        // test.txt.1589234234.tgz
        std::string tar_file = src_file + ".tgz";

        pid_t pid = fork();
        if (pid == 0)
        {
            // 子进程进行数据备份
            std::filesystem::rename(file, backup_file);
            std::cout << "Step 4. 备份文件: " << file << " 到文件: " << backup_file << std::endl;
            // 对备份文件进行打包
            std::filesystem::current_path(_filepath);
            execlp("tar", "tar", "-czf", tar_file.c_str(), src_file.c_str(), nullptr);
            exit(1);
        }

        int status;
        pid_t rid = waitpid(pid, &status, 0);
        if (rid > 0)
        {
            if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
            {
                // 删除备份文件
                std::filesystem::remove(backup_file);
                std::cout << "Step 5. 删除备份文件: " << backup_file << std::endl;
            }
        }
    }

public:
    HandlerTextBackup(const std::string &filepath = defaultfilepath, const std::string &filename = defaultfilename, const int &maxline = defaultmaxline)
        : _filepath(filepath), _filename(filename), _maxline(maxline)
    {
    }
    void Excute(const std::string &text) override
    {
        if (_enable)
        {
            std::string file = _filepath + _filename;
            std::cout << "Step 3. 检查文件: " << file << " 大小是否超范围 " << std::endl;
            if (IsOutOfRange(file))
            {
                std::cout << "目标文件超范围, 进行切片备份" << file << std::endl;
                Backup(file);
            }
        }
        if (_next)
        {
            _next->Excute(text); // 将处理结果表现在text内部
        }
        else
        {
            std::cout << "到达责任链处理结尾, 完成责任链处理" << std::endl;
        }
    }

private:
    std::string _filepath;
    std::string _filename;
    int _maxline;
};

// 责任链入口
class HandlerEntry
{
public:
    HandlerEntry()
    {
        // 构造责任链节点
        _format = std::make_shared<HandlerTextFormat>();
        _save = std::make_shared<HandlerTextSaveFile>();
        _backup = std::make_shared<HandlerTextBackup>();

        // 设置责任链处理顺序
        _format->SetNext(_save);
        _save->SetNext(_backup);
    }
    void EnableHandler(bool is_format, bool is_save, bool is_backup)
    {
        is_format ? _format->Enable() : _format->DisEnable();
        is_save ? _save->Enable() : _save->DisEnable();
        is_backup ? _backup->Enable() : _backup->DisEnable();
    }
    void Run(const std::string &text)
    {
        _format->Excute(text);
    }
    ~HandlerEntry()
    {
    }

private:
    std::shared_ptr<HandlerText> _format;
    std::shared_ptr<HandlerText> _save;
    std::shared_ptr<HandlerText> _backup;
};

#endif
  • Client.cc
#include "MsgQueue.hpp"

int main()
{
    Client client;
    
    while(true)
    {
        std::string input;
        std::cout << "Please input message: ";
        std::getline(std::cin, input);
        client.Send(MSG_TYPE_CLIENT, input);
        if(input == "exit")
        {
            break;
        }
    }
    return 0;
}
  • Server.cc
#include "MsgQueue.hpp"
#include "ChainOfResp.hpp"

int main()
{
    std::string text;
    Server server;
    HandlerEntry he;
    he.EnableHandler(false, true, true);

    while (true)
    {
        // 消息队列为空时阻塞等待
        server.Recv(MSG_TYPE_CLIENT, text);
        std::cout << "Received: " << text << std::endl;
        if(text == "exit")
        {
            break;
        }
        he.Run(text);
    }
    return 0;
}
  • Makefile
all: Client Server

Client:Client.cc
	g++ -o Client Client.cc -std=c++17

Server:Server.cc
	g++ -o Server Server.cc -std=c++17

.PHONY:clean
clean:
	rm -f Client Server

八、System V信号量

8.1.关于并发编程的一些概念

共享内存使两个进程看到同一份资源,为通信提供前提

但是没有保护机制,数据不一致,信号量是一个具体的方案

共享资源:

多个执行流(进程)能看到的同一份公共资源

临界资源:

被保护起来的共享资源

临界区:

在进程中涉及到互斥资源的程序段(访问临界资源对应的代码)

保护临界区代码就是在保护临界资源

互斥:

任何时刻只允许一个执行流访问临界资源

类似于ATM机,每个人进入ATM机取钱之前要先开锁

进去后门会上锁,每次只能有一个人进入提款机取钱

同步:

多个执行流访问临界资源时有一定的顺序性

类似用共享内存加命名管道,通过管道文件的读端阻塞

等规定数据写入共享内存后,向管道文件写入唤醒读端

原子性:

要么做,要么不做

锁本身也要被共享,所以在申请锁的时候,必须是原子的,要么申请好了,要么不申请

8.2.信号量的理解

信号量(信号灯):一个计数器,用来表明临界资源的数量多少

作用:保护临界区

本质:对资源的预定机制

P操作:信号量本身就是共享资源,申请时要减减

P(s)
{
    s.value = s.value--;
    if (s.value < 0)
    {
        // 该进程状态置为等待状状态
        // 将该进程的PCB插⼊⼊相应的等待队列s.queue末尾
    }
}

V操作:进程用完资源退出后,信号量的值要加加

V(s)
{
    s.value = s.value++;
    if (s.value > 0)
    {
        // 唤醒相应等待队列s.queue中等待的⼀个进程
        // 改变其状态为就绪态
        // 并将其插⼊OS就绪队列
    }
}

通过PV操作完成进程对资源的预定机制

多元信号量:资源不是整体使用,通过信号量进行计数

二元信号量:资源整体使用,只有1或者0两态的信号量

8.3.信号量的接口

8.3.1.semget函数

作用:创建信号量集

参数1:信号量集的键值

参数2:信号量集中信号量的个数

参数3:信号量集的标志位

  • IPC_CREAT:获取信号量集
  • IPC_CREAT | IPC_EXCL | 权限码:创建信号量集

返回值:

  • 成功:信号量集标识符
  • 失败:-1

 8.3.2.semctl函数

作用:控制信号量集

参数1:信号量集标识符

参数2:指定一个信号量(下标从0开始)

参数3:信号量集设置命令

  • SETVAL:设置指定信号量计数值
  • IPC_RMID:删除整个的信号量集

参数4:信号量集属性表(semun联合体)

返回值:

  • 成功:0
  • 失败:-1

 8.3.3.semop函数

作用:操作信号量集

参数1:信号量集标识符

参数2:信号量集数组

  • sem_num:信号量集中信号量的下标
  • sem_op:-1进行P操作,+1进行V操作
  • sem_flag:默认为0

参数3:信号量集数组元素个数

操作系统需要对大量的信号量集进行管理,需要先描述,再组织

8.3.4.ftok函数

作用:生成键值

参数1:任意路径名字

参数2:任意整型变量

8.3.5.ipcs命令

ipcs -s:查看信号量集

8.3.6.ipcrm命令

ipcrm -s semid:删除信号量集

8.4.基于建造者模式的信号量

构建类设计模式:建造者模式,工厂模式,单例模式

抽象建造者(Builder):为创建一个产品对象的各个部件指定抽象的接口(基类)

具体建造者(ConcreteBuilder):实现建造者接口,构建并装配产品部件(派生类)

指挥者(Director):使用建造者接口的对象,指导建造者如何构建产品(调用接口)

产品(Product):被建造者构建的复杂对象,通常包含多个组成部分

  • Sem.hpp
#ifndef SEM_HPP
#define SEM_HPP

#include <iostream>
#include <string>
#include <memory>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <vector>

#define GET_SEM (IPC_CREAT)
#define BUILD_SEM (IPC_CREAT | IPC_EXCL)

const std::string SEM_PATH = "/tmp";
const int SEM_PROJ_ID = 0x77;
const int defaultnum = 1;

// 整数转十六进制
std::string intTohex(int num)
{
    char hex[64];
    snprintf(hex, sizeof(hex), "0x%x", num);
    return std::string(hex);
}

// 产品类
class Semaphore
{
private:
    void PV(int who, int data)
    {
        struct sembuf sem_buf;
        sem_buf.sem_num = who;      // 信号量编号
        sem_buf.sem_op = data;      // 操作方式
        sem_buf.sem_flg = SEM_UNDO; // 不关心
        int n = semop(_semid, &sem_buf, 1);
        if (n < 0)
        {
            std::cerr << "semop PV failed" << std::endl;
        }
    }

public:
    Semaphore(int semid)
        : _semid(semid)
    {
    }
    int Id() const
    {
        return _semid;
    }
    void P(int who)
    {
        PV(who, -1);
    }
    void V(int who)
    {
        PV(who, 1);
    }
    ~Semaphore()
    {
        if (_semid >= 0)
        {
            int n = semctl(_semid, 0, IPC_RMID);
            if (n < 0)
            {
                std::cerr << "semctl IPC_RMID failed" << std::endl;
            }
            std::cout << "Semaphore " << _semid << " removed" << std::endl;
        }
    }

private:
    int _semid;
};

// 抽象的建造者
class SemaphoreBuilder
{
public:
    virtual ~SemaphoreBuilder() = default;
    virtual void BuildKey() = 0;                           // 创建信号量集的键值
    virtual void SetPermission(int perm) = 0;              // 设置权限
    virtual void SetSemNum(int num) = 0;                   // 设置信号量集的个数
    virtual void SetInitVal(std::vector<int> initVal) = 0; // 设置信号量初值
    virtual void Build(int flag) = 0;                      // 创建信号量集
    virtual void InitSem() = 0;                            // 初始化信号量集
    virtual std::shared_ptr<Semaphore> GetSem() = 0;       // 获取信号量集
};

// 具体的建造者
class ConcreteSemaphoreBuilder : public SemaphoreBuilder
{
private:
    bool Init(int semid, int num, int val)
    {
        union semun
        {
            int val;
            struct semid_ds *buf;
            unsigned short *array;
            struct seminfo *__buf;
        } un;
        un.val = val;

        int n = semctl(semid, num, SETVAL, un);
        if (n < 0)
        {
            std::cerr << "semctl SETVAL failed" << std::endl;
            return false;
        }
        return true;
    }

public:
    ConcreteSemaphoreBuilder()
    {
    }

    void BuildKey() override
    {
        _key = ftok(SEM_PATH.c_str(), SEM_PROJ_ID);
        if (_key < 0)
        {
            std::cerr << "ftok failed" << std::endl;
            exit(1);
        }
        std::cout << "Get Key: " << intTohex(_key) << std::endl;
    }

    void SetPermission(int perm) override
    {
        _perm = perm;
    }

    void SetSemNum(int num) override
    {
        _num = num;
    }

    void SetInitVal(std::vector<int> initVal) override
    {
        _initVal = initVal;
    }

    void Build(int flag) override
    {
        int semid = semget(_key, _num, flag | _perm);
        if (semid < 0)
        {
            std::cerr << "semget failed" << std::endl;
            exit(2);
        }
        std::cout << "Get semaphore id: " << semid << std::endl;
        _sem = std::make_shared<Semaphore>(semid);
    }

    void InitSem() override
    {
        if (_num > 0 && _initVal.size() == _num)
        {
            for (int i = 0; i < _num; i++)
            {
                if (!Init(_sem->Id(), i, _initVal[i]))
                {
                    std::cerr << "Init failed" << std::endl;
                    exit(3);
                }
            }
        }
    }

    std::shared_ptr<Semaphore> GetSem()
    {
        return _sem;
    }

private:
    key_t _key;                      // 信号量集的键值
    int _perm;                       // 权限
    int _num;                        // 信号量集的个数
    std::vector<int> _initVal;       // 信号量初值
    std::shared_ptr<Semaphore> _sem; // 产品对象
};

// 指挥者
class Director
{
public:
    void Construct(std::shared_ptr<SemaphoreBuilder> builder, int flag, int perm = 0666, int num = defaultnum, std::vector<int> initVal = {1})
    {
        builder->BuildKey();
        builder->SetPermission(perm);
        builder->SetSemNum(num);
        builder->SetInitVal(initVal);
        builder->Build(flag);
        if (flag == BUILD_SEM)
        {
            builder->InitSem();
        }
    }

private:
};

#endif
  • Write.cc
#include "Sem_V2.hpp"
#include <unistd.h>
#include <ctime>
#include <cstdio>

int main()
{
    // 基于抽象接口类的具体建造者
    std::shared_ptr<SemaphoreBuilder> builder = std::make_shared<ConcreteSemaphoreBuilder>();
    // 指挥者对象
    std::shared_ptr<Director> director = std::make_shared<Director>();
    // 在指挥者的指导下, 完成建造过程
    director->Construct(builder, BUILD_SEM, 0600, 3, {1, 2, 3});
    // 获取信号量集
    auto fsem = builder->GetSem();

    srand(time(0) ^ getpid());

    // 要求父子进程打印的F和C成对出现(保证打印是原子的)
    pid_t pid = fork();
    if (pid == 0)
    {
        director->Construct(builder, GET_SEM);
        auto csem = builder->GetSem();
        while (true)
        {
            csem->P(0);
            printf("C");
            usleep(rand() % 95270);
            fflush(stdout);
            printf("C");
            usleep(rand() % 43990);
            fflush(stdout);
            csem->V(0);
        }
    }
    while (true)
    {
        fsem->P(0);
        printf("F");
        usleep(rand() % 95270);
        fflush(stdout);
        printf("F");
        usleep(rand() % 43990);
        fflush(stdout);
        fsem->V(0);
    }

    return 0;
}

九、mmap文件映射

9.1.概述

Linux提供的系统调用

将一个进程打开的文件映射到虚拟地址空间

直接使用虚拟地址,对文件内容的增删改查

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

int munmap(void *addr, size_t length);

传统文件读写:打开文件,获取文件描述符,通过read和write系统调用进行数据的拷贝

9.2.接口介绍

mmap函数

参数1:NULL-由系统指定一个挂接的起始地址

参数2:映射的虚拟地址空间大小(单位为4KB,向上取整)

参数3:映射区域的内存保护属性(权限)

  • PROT_READ:映射区域可读
  • PROT_WRITE:映射区域可写
  • PROT_EXEC:映射区域可执行

参数4:映射类型

  • MAP_PRIVATE:私有映射(写入后不会反应磁盘文件)
  • MAP_SHARED:共享映射(写入后直接同步磁盘文件)

参数5:要映射的文件的文件描述符

参数6:文件中的起始偏移量

返回值:

  • 成功:虚拟空间的起始地址
  • 失败:(void *) -1

9.3.写入映射

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

#define PAGE_SIZE 4096

// write_map filename
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " filename" << std::endl;
        return 1;
    }

    // 打开目标文件
    int fd = ::open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(fd < 0)
    {
        std::cout << "Failed to open file: " << argv[1] << std::endl;
        return 2;
    }

    // 调整文件大小, 进行合法映射(默认用0填充)
    if(ftruncate(fd, PAGE_SIZE) == -1)
    {
        std::cout << "Failed to truncate file: " << argv[1] << std::endl;
        return 3;
    }

    // 文件映射
    char *shmaddr = (char *)::mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(shmaddr == MAP_FAILED)
    {
        std::cout << "Failed to mmap file: " << argv[1] << std::endl;
        return 4;
    }

    // 文件操作
    for(char c = 'a'; c <= 'z'; c++)
    {
        shmaddr[c - 'a'] = c;
        sleep(1);
    }

    // 关闭映射
    if(::munmap(shmaddr, PAGE_SIZE) == -1)
    {
        std::cout << "Failed to munmap file: " << argv[1] << std::endl;
        return 5;
    }

    // 关闭目标文件
    ::close(fd);

    return 0;
}

9.4.读取映射

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <cstring>

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " filemame" << std::endl;
        return 1;
    }

    // 打开目标文件
    std::string filename = argv[1];
    int fd = ::open(filename.c_str(), O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "open error" << std::endl;
        return 2;
    }

    // 获取⽂件⼤⼩
    struct stat st;
    ::fstat(fd, &st);

    // 文件映射
    char *mmap_addr = (char *)::mmap(nullptr, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
    if (mmap_addr == MAP_FAILED)
    {
        perror("mmap");
        return 3;
    }

    // 文件操作
    std::cout << "File content: " << mmap_addr << std::endl;

    // 关闭映射
    ::munmap(mmap_addr, st.st_size);

    // 关闭目标⽂件
    ::close(fd);

    return 0;
}

9.5.模拟实现malloc

#include <iostream>
#include <cstdio>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

// mmap分配内存
void *my_malloc(size_t size)
{
    if (size > 0)
    {
        void *ptr = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (ptr == MAP_FAILED)
        {
            return nullptr;
        }
        return ptr;
    }
    return nullptr;
}

// mmap释放内存
void my_free(void *start, size_t size)
{
    if(start != nullptr && size > 0)
    {
        int ret = ::munmap(start, size);
        if(ret != 0)
        {
            perror("munmap");
        }
    }
}

int main()
{
    // 申请内存
    char *ptr = (char *)my_malloc(1024);
    if(ptr == nullptr)
    {
        std::cerr << "malloc failed" << std::endl;
        return 1;
    }

    // 打印起始地址
    printf("Allocated memory at address: %p\n", ptr);

    // 将内存值设置为A
    memset(ptr, 'A', 1024);

    // 打印内存中的值
    for (int i = 0; i < 1024; i++)
    {
        printf("%c ", ptr[i]);
        fflush(stdout);
        sleep(1);
    }

    // 释放内存
    my_free(ptr, 1024);

    return 0;
}

十、内核IPC资源的组织管理

共享内存,消息队列,信号量用key来区分唯一性

在操作系统中共享内存,消息队列,信号量被当作了同一种资源,叫做System V IPC

IPC资源在内核中的组织形式

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值