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.3.命名管道 VS 匿名管道)

5.4.命名管道通信

[六、System V共享内存](#六、System V共享内存)

6.1.共享内存的原理

6.2.共享内存的接口

6.2.1.shmget函数

6.2.2.ftok函数

6.2.3.ipcs命令

6.2.4.ipcrm命令

6.2.5.shmctl函数

6.2.6.shmat函数

6.2.7.shmdt函数

6.3.共享内存的实现

[七、System V消息队列](#七、System V消息队列)

7.1.消息队列的原理

7.2.消息队列的接口

7.2.1.msgget函数

7.2.2.msgctl函数

7.2.3.ftok函数

7.2.4.msgsnd函数

7.2.5.msgrcv函数

7.2.6.ipcs命令

7.2.6.ipcrm命令

[八、System V信号量](#八、System V信号量)

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

8.2.信号量的理解

8.3.信号量的接口

8.3.1.semget函数

8.3.2.ftok函数

8.3.3.ipcs命令

8.3.4.ipcrm命令

8.3.5.semctl函数

8.3.6.semop函数

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


一、进程间通信理解

1.1.进程间通信的概念

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

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

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

1.2.进程间通信的目的

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

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

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

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

1.3.进程间通信的方式

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

二、管道

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

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

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

三、匿名管道

3.1.匿名管道的概念

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

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

3.2.pipe函数

功能:创建匿名管道

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

3.3.匿名管道的理解

3.4.实例代码

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

实验代码

cpp 复制代码
#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发送异常信号杀掉写端进程

实验代码

cpp 复制代码
#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
cpp 复制代码
//.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
cpp 复制代码
#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;
};
cpp 复制代码
#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
cpp 复制代码
process_pool:Main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f process_pool

实验现象

4.4.隐藏BUG

**方案一:**倒着关闭写端

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

cpp 复制代码
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;
        }
    }
    //......
}

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

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

cpp 复制代码
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
cpp 复制代码
​
#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;               // 管道文件描述符
};

​
cpp 复制代码
#include "comm.hpp"

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

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

    return 0;
}
cpp 复制代码
#include "comm.hpp"

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

    return 0;
}
  • Makefile
cpp 复制代码
.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:key值,不是由内核形成,而是在用户层构建并传入OS
  • 参数2:共享内存大小,必须是4KB(4096)的整数倍,否则会向上取整
  • 参数3:设置共享内存标志位,设置共享内存的权限

IPC_CREAT(获取):如果目标共享内存不存在,就创建,否则打开这个存在的共享内存并返回

(IPC_CREAT |) IPC_EXCL(创建):单独使用无意义,如果目标共享内存存在,就出错返回

key值类似命名管道约定同一个管道文件,保证共享内存存在,标识共享内存的唯一性

返回值:

  • 创建成功:合法的共享内存标识符(类似文件描述符)
  • 创建失败:-1
6.2.2.ftok函数

作用:将路径名与id整数的字符混合,通过算法构建一个key值,减少key值冲突

6.2.3.ipcs命令

**ipcs -m:**查看共享内存

6.2.4.ipcrm命令

**ipcrm -m shmid:**删除共享内存

用户层使用shmid管理共享内存,key只用来给内核区分唯一性

6.2.5.shmctl函数

作用:控制共享内存,删除共享内存

参数1:共享内存标识符

参数2:控制命令

IPC_RMID:删除共享内存

IPC_STAT:拷贝共享内存属性

参数3:共享内存属性表

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

6.2.6.shmat函数

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

返回值:

挂接成功:虚拟空间的起始虚拟地址

挂接失败:-1

参数1:共享内存标识符

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

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

6.2.7.shmdt函数

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

6.3.共享内存的实现

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

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

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

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

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

有保护机制:

AA

AA BB

AA BB CC

无保护机制:

A

AA

AA B

AA BB

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

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

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

  • shm.hpp
cpp 复制代码
#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
cpp 复制代码
#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;               // 管道文件描述符
};
cpp 复制代码
#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;
}
cpp 复制代码
#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
cpp 复制代码
#pragma once

#include <cstdio>
#include <cstdlib>

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)
  • Makefile
cpp 复制代码
.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.消息队列的原理

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

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

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

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

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

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

7.2.消息队列的接口

7.2.1.msgget函数

作用:创建一个消息队列

7.2.2.msgctl函数

作用:控制消息队列

7.2.3.ftok函数

作用:生成key值

7.2.4.msgsnd函数

作用:发送数据

节点数据的结构体

mtype:数据类型(区分进程)

mtext:数据内容

7.2.5.msgrcv函数

作用:接收数据

7.2.6.ipcs命令

ipcs -q:查看消息队列

7.2.6.ipcrm命令

ipcrm -q msqid:删除消息队列

八、System V信号量

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

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

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

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

**临界资源:**被保护起来的共享资源

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

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

**互斥:**任何时刻只允许一个执行流访问资源

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

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

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

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

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

**原子性:**要么做,要么不做

学校学习:要么别学,要么考全校第一

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

要么申请好了,要么不申请

8.2.信号量的理解

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

**作用:**保护临界区

**本质:**对资源的预定机制

信号量本身就是共享资源,申请时要减减,具有原子性(P操作)

进程用完资源退出后,信号量的值要加加,具有原子性(V操作)

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

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

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

信号量与通信的关系

先访问信号量P,每个进程都得先看到同一个信号量

不是传递数据才是IPC通信,通知、互斥、同步也是

8.3.信号量的接口

8.3.1.semget函数

作用:创建信号量或信号量集

参数1:key值

参数2:信号量的个数

参数3:信号量集标志位

8.3.2.ftok函数

作用:生成key值

8.3.3.ipcs命令

ipcs -s:查看信号量或信号量集

8.3.4.ipcrm命令

ipcrm -s semid:删除信号量或信号量集

8.3.5.semctl函数

作用:控制信号量或信号量集

参数1:信号量集标识符

参数2:通过SET_VAL命令设置的信号量初值

参数3:信号量设置命令

8.3.6.semop函数

作用:对信号量集操作

参数1:信号量集标识符

参数2:信号量集数组

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

sem_num:信号量集中信号量的下标

sem_op:-1进行P操作,+1进行V操作

sem_flag:默认为0

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

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

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

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

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

相关推荐
Zhang~Ling1 小时前
深入学习Linux: 命令行参数与环境变量详解
linux·服务器
疯狂打码的少年1 小时前
【软件工程】结构化设计:结构化设计方法(SC图)
笔记·软件工程
十月的皮皮1 小时前
C语言学习笔记20260716-编译与链接
c语言·笔记·学习
会飞的小新1 小时前
《深入理解 DDR Memory Margin》系列之第四章 从 Bit Error 到 ECC——服务器为什么能够发现内存错误?
运维·服务器
Shadow(⊙o⊙)1 小时前
日志与线程池
linux·c++·算法
樂禮1 小时前
Windows-Arch Linux 键鼠共享 & 剪贴板同步配置
linux·运维·windows
CHANG_THE_WORLD1 小时前
Linux C 多线程 TCP 并发服务器:从 `accept()` 到工作线程的完整解析
linux·服务器·c语言
影视飓风TIM2 小时前
Linux基础入门笔记:命令、内核架构与压缩传输全梳理
linux·笔记·架构
敲上瘾2 小时前
Linux系统调用mmap文件映射与malloc简单模拟实现指南
linux·服务器·c语言·缓存