进程间通信

一. 进程间通信介绍

进程间通信(interprocess communication)英文缩写 IPC ,进程间通信是指,进程间进行数据传输,资源共享 (多个进程共享同一份资源),消息通知 (进程发送消息通知发生某中事件),协同工作(完全控制另一个进程)的机制。由于每个进程都有自己独立的地址空间,所以无法直接访问彼此内存。因此需要第三者来辅助进程间进行通信。

常见的进程间通信方式有管道,匿名管道,命名管道,共享内存,信号量,消息队列。

二. 管道

**管道的本质就是文件。**文件由操作系统管理,能被多个进程同时进行访问。每个通信进程都可以打开文件,进行文件的写入操作,其他进程就可以进行读的操作,这样就完成了两个进程之间的通信。文件是存储在磁盘上的,而在磁盘上的读写效率很低,所以管道文件是在内存中进行的,是一个内存级的文件。

管道通信**只允许单向通信,**也就是有一方读另一方写,不存在双方都能读写的操作。若需要进行双向通信,就要创建两个管道。

那为何要如此呢?

首先管道的核心功能是进行两个进程间的信息传递。只设计单向通道,就可以无需解决双向数据冲突和同步的问题。若强制设计成双向通道,反而增加了复杂度,我们需要区分读写操作,处理读写冲突等。所以说,单向通道反而提升了效率。

管道继续往下细分,分为匿名管道和命名管道。


在讲解匿名管道之前,我们要先清楚一点。进程间通信的核心是"安全传递数据",包括了"用什么传(载体)","传给谁(标识)","怎么传(格式)","有序传(同步)","安全传(权限)"。不同的 IPC 机制就是这些要素的组合,用来适配不同的场景需求。

通信载体即不同的通信通道,共享内存,文件系统,内核缓冲区等。标识用于定位传输的对象。传递时要遵循约定的格式进行。遵守规则排队传单独传(同步与互斥)。并且判断权限。

三. 匿名管道

1. 匿名管道的概念

匿名管道,顾名思义就是没有名字的管道。而我们清楚,管道的传输需要有标识,所以其他文件无法通过文件打开该管道。所以匿名管道只能进行有继承关系的进程间通信。

2. 匿名管道的使用

我们先来认识一个函数

管道是一个文件,而用open函数可以直接创建一个文件。但是操作系统为了该文件是一个内存级的文件,专门创建一个 pipe 接口。

pipe:创建一个匿名管道文件(内存级)

cpp 复制代码
#include <unistd.h>

int pipe(int pipefd[2]);

返回值0,失败-1。

参数:pipefd下标为0是读端,为1是写端。

首先父进程创建一个管道文件,通过 pipe(pipefd)方式创建一个管道,其中 pipefd 是一个大小为2的数组;若返回值为0表示成功;紧接着创建子进程,子进程关闭读端,进行写入操作(write);而父进程关闭写端,进行读入操作(read)。

(1)管道读写规则

读操作规则:

当管道中有数据时,读操作会立即返回读到数据;

当管道中没有数据时,若写端关闭,则读端返回0;若写端未关闭,读端阻塞等待写端写入。

写操作规则:

当管道空闲时,写操作直接进行写入。

当管道已满且读端存在时,写端操作阻塞,直到有空闲空间。

当管道已满且读端关闭时,写端触发SIGPIPE信号。

(2)管道特点

匿名管道只能用于具有祖先关系的进程,进程退出,管道释放,所以管道的生命周期随进程。管道是半双工的,数据只能向一个方向流,若需要双方通信,需要建立两个管道。

3. 通过文件描述符深度理解管道

父进程首先创建了管道,创建了子进程。管道中的pipefd[0] ,pipefd[1],通过文件标识符表进行读写的分配,子进程会继承父进程的文件标识符表,同时每使用一个标识符,对应的计数器就会增加。

当我们的父进程要对多个子进程进行通信时,由于子进程会接收到继承下来的文件标识符表,因此也会继承下来文件标识符的指向次数。所以我们要相应的对子进程进行处理,关闭多余的通道。

下面是一张手绘的图表:

如上图,父进程进行写操作,子进程进行读操作。在第一个子进程上,我们关闭3号文件标识符(读),而子进程需要关闭4(写);第二个子进程继承了父进程,此时4号被占用,所以3是读,5是写,以此类推。 那么第三个子进程就是由6号向3号写入。

由于文件标识符表存在计数器记录指向次数,所以当子进程继承下来文件标识符表时,需要关闭冗余的文件,使计数器始终为1。例如3号标识符传递到第三个子进程时,已经指向了4次(1次父进程,3次子进程)我们需要减少对应的次数。

4. 创建进程池处理任务

进程池处理任务,是通过管理多个子进程,对子进程进行任务分配,采用轮询的方式,给子进程分配任务,达到多进程共同完成任务。

设计思路:

首先,我们将通信管道抽象成一个 Channel 类,主要保存写端文件标识符,和子进程的 pid 信息;我们对通信管道完成了描述,接下来我们需要对其进行组织,我们用 vector 将其组织,封装成 ChannelManager 类,同时完成对管道的插入操作,打印操作,停止等待操作;然后,我们将需要执行的任务封装成 TaskManager 类,使任务可以轮询进行,让空闲的子进程完成相应任务;最后将 ChannelManager 和 TaskManager 封装成 ProcessPool 类,进行子进程任务分配,和终止回收进程完整操作。

下面是代码样例:

Main.cc

cpp 复制代码
#include "ProcessPool.hpp"

int main()
{
    ProcessPooL pp(5);

    pp.Start();

    int cnt = 10;
    while(cnt--)
    {
        sleep(2);
        pp.Run();
    }
    pp.StopProcess();
    return 0;
}

Task.hpp:

cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <functional>
using namespace std;
using task_t = function<void()>;

void PrintLog()
{
    cout << "这是一个打印日志" << endl;
}

void DownLoad()
{
    cout << "这是一个下载任务" << endl;
}

void Upload()
{
    cout << "这是一个更新任务" << endl;
}

class TaskManager
{
public:
    TaskManager()
    {
    }

    ~TaskManager()
    {
    }

    int Code()
    {
        return rand() % _task.size();
    }

    void Register(task_t t)
    {
        _task.push_back(t);
    }

    void Execute(int code)
    {
        if (code >= 0 && code < _task.size())
        {
            _task[code]();//这里问题
        }
    }

private:
    vector<task_t> _task;
};

ProcessPool.hpp:

cpp 复制代码
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
using namespace std;

class Channel
{
public:
    Channel(int fd, pid_t id)
        : _wfd(fd), _fid(id)
    {
        _name = "Channel-" + to_string(_fid) + '-' + to_string(_wfd);
    }

    ~Channel()
    {
    }

    string Name()
    {
        return _name;
    }

    int Fd()
    {
        return _wfd;
    }

    pid_t id()
    {
        return _fid;
    }

    void Send(int code)
    {
        // int n = write(_fid, &code, sizeof(code));
        int n = write(_wfd, &code, sizeof(code));
        (void)n;
    }

    void Close()
    {
        close(_wfd);
    }

    void Wait()
    {
        pid_t pid = waitpid(_fid, nullptr, 0);
        (void)pid;
    }

private:
    int _wfd;     // 文件标识符
    string _name; // 进程名字
    pid_t _fid;   // 子进程pid
};

class ChannelManager
{
public:
    ChannelManager()
        : _next(0)
    {
    }

    void Insert(int fd, pid_t pid)
    {
        _channels.emplace_back(fd, pid);
    }

    void ChannelPrint()
    {
        for (auto &c : _channels)
        {
            cout << "进程名字:" << c.Name() << endl;
        }
    }

    void Stop()
    {
        for (auto &c : _channels)
        {
            cout << "进程关闭:" << c.Name() << endl;
            c.Close();
        }
    }

    void Wait()
    {
        for (auto &c : _channels)
        {
            cout << "进程回收:" << c.Name() << endl;
            c.Wait();
        }
    }

    Channel &Select()
    {
        auto &c = _channels[_next++];
        _next %= _channels.size();
        return c;
    }

    ~ChannelManager()
    {
    }

private:
    vector<Channel> _channels;
    int _next;
};

class ProcessPooL
{
public:
    ProcessPooL(int num)
        : _num(num)
    {
        _tm.Register(PrintLog);
        _tm.Register(Upload);
        _tm.Register(DownLoad);
    }

    ~ProcessPooL()
    {
    }

    void Work(int rfd)
    {
        while (true)
        {
            int code = 0;
            //cout << "子进程read开始"  <<endl;
            ssize_t n = read(rfd, &code, sizeof(code));
            //cout << "子进程read结束"  <<endl;
            if (n > 0)
            {
                if (n != sizeof(code))
                {
                    continue;
                }
                cout << "子进程[" << getpid() << "]:接收到一个任务码" << endl;
                sleep(1);
                _tm.Execute(code);
            }
            else if (n == 0)
            {
                cout << "子进程[" << getpid() << "]退出" << endl;
                sleep(1);
                break;
            }
            else
            {
                cout << "读取错误" << endl;
                break;
            }
        }
    }

    bool Start()
    {
        for (int i = 0; i < _num; i++)
        {
            int fd[2] = {0};
            int n = pipe(fd);
            if (n < 0)
            {
                cout << "创建管道失败" << endl;
                return false;
            }

            pid_t id = fork();
            if (id < 0)
            {
                cout << "创建进程失败" << endl;
                return false;
            }
            else if (id == 0) // 子进程 读
            {
                close(fd[1]);
                Work(fd[0]);
                close(fd[0]);
                exit(0);
            }
            else // 父进程 写
            {
                close(fd[0]);
                _cm.Insert(fd[1], id);
                // close(fd[1]);
            }
        }
        return true;
    }

    void Run()
    {
        int task_code = _tm.Code();
        auto &c = _cm.Select();
        cout << "选择了一个进程:" << c.Name() << endl;
        c.Send(task_code);
        cout << "发送一个任务码:" << task_code << endl;
        sleep(1);
    }

    void StopProcess()
    {
        _cm.Stop();
        _cm.Wait();
    }

    void PrintProcess()
    {
        _cm.ChannelPrint();
    }

private:
    ChannelManager _cm;
    TaskManager _tm;
    int _num; // 子进程数量
};

#endif

四. 命名管道

1. 命名管道的概念

命名管道(first in,first out),顾名思义就是有名字的管道。因为存在名字,所以进程就可以通过名字找到该管道,进行任意进程间的通信。 它的本质也很简单,命名管道就是一个管道文件,它从磁盘上加载到内存中,文件系统为它分配特殊的inode,内核为其分配管道缓冲区。当进程需要进行传输信息时,通过open调用,对文件路径进行查找。这样两个进程就看见了同一份资源,就可以实现不同进程之间的通信。

2. 命名管道的使用

首先来认识几个函数:

mkfifo:

使用命令行创建命名管道 / 使用系统调用接口创建命名管道

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char* pathname,mode_t mode);

返回值:成功为0,失败为-1

参数:pathname:路径名 mode:权限

unlink:

删除管道文件

cpp 复制代码
#include <unistd.h>

int unlink(const char* pathname);

返回值:成功为0,失败为-1

3. 用命名管道实现sever&client通信

学习完命名管道的操作,我们尝试用命名管道的方式,设计一个通信信道,来进行进程间互相通信。即 server 处发送信息,client 处接收信息。

设计思路:

根据先描述再组织的思想,来完成这个信道,首先,描述这个命名管道,我们对其封装一个 Namefifo 类,用于创建命名管道和删除命名管道。接着,对于命名管道的使用,我们封装一个FifoOper 类,用于对使用者读写的权限设置,和读写关闭操作。

下面是代码样例:

comm.hpp:

cpp 复制代码
#include <iostream>
#include <stdio.h>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

#define PATH "."
#define NAME "fifo"

class Namefifo
{
public:
    Namefifo(const string &path, const string &name)
        : _path(path), _name(name)
    {
        _fifoname = _path + '/' + _name;
        umask(0);
        int n = mkfifo(_fifoname.c_str(), 0666);
        if (n == 0)
        {
            cout << "success make fifo" << endl;
        }
        else
        {
            ERR_EXIT("mkfifo");
        }
    }

    ~Namefifo()
    {
        int n = unlink(_fifoname.c_str());
        if (n == 0)
        {
            cout << "success unlink" << endl;
        }
        else
        {
            ERR_EXIT("unlink");
        }
    }

private:
    string _fifoname;
    string _path;
    string _name;
};

class FifoOper
{
public:
    FifoOper(const string &path, const string &name)
        : _path(path), _name(name), _fd(-1)
    {
        _fifoname = _path + '/' + _name;
    }

    void OpenforRead()
    {
        _fd = open(_fifoname.c_str(), O_RDONLY);
        if (_fd == -1)
        {
            ERR_EXIT("open");
        }
        cout << "OpenforRead success" << endl;
    }

    void OpenforWrite()
    {
        _fd = open(_fifoname.c_str(), O_WRONLY);
        if (_fd == -1)
        {
            ERR_EXIT("write");
        }
        cout << "OpenforWrite success" << endl;
    }

    void Read()
    {
        while (true)
        {
            char buffer[1024];
            buffer[0] = {0};
            int number = read(_fd, buffer, sizeof(buffer) - 1);
            if (number > 0)
            {
                buffer[number] = 0;
                cout << "Client say#" << buffer << endl;
            }
            else if (number == 0)
            {
                cout << "cilent quit" << endl;
                break;
            }
            else
            {
                cout << "read error" << endl;
                break;
            }
        }
    }

    void Write()
    {
        int cnt = 0;
        pid_t id = getpid();
        string message;
        while (true)
        {
            cout << "Please write#";
            getline(cin, message);
            message += (", message number: " + to_string(cnt++) + ", [" + to_string(id) + "]");
            write(_fd, message.c_str(), message.size());
        }
    }

    void Close()
    {
        if (_fd > 0)
        {
            close(_fd);
        }
    }

    ~FifoOper()
    {
    }

private:
    string _path;
    string _name;
    string _fifoname;
    int _fd;
};

server.cc

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

int main()
{
    Namefifo fifo(".",NAME);
    FifoOper readopen(PATH,NAME);
    readopen.OpenforRead();
    readopen.Read();
    readopen.Close();
    return 0;
}

client.cc

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

int main()
{
    FifoOper writeopen(PATH,NAME);
    writeopen.OpenforWrite();
    writeopen.Write();
    writeopen.Close();
    return 0;
}

五. IPC 资源

进程间通信当下主要有两套标准:

  1. System V:本地通信,如共享内存(shared memory,简称shm),消息队列(message queue,简称msq),信号量(semaphore,简称sem)

  2. POSIX:让进程可以跨主机通信(网络通信)

而System V 的这三种通信都是操作系统为进程间通信提供公共空间,也叫 IPC 资源。

1. IPC 资源的标识符

多个进程打开同一份资源时,必须有一个标识符来找到该资源。通常情况下,内核创建出一个共享资源时,会为其分配一个编号,并且将编号公开。由此,引出了通信资源标识符 key 。

也就是说,创建出的每一份 IPC 资源都会有一份独一无二的 key 值,而进程拿着这份 key 值就可以访问到该资源。key 值不是用户自己定义的,是调用了系统接口 ftok ,根据用户传入的参数自动生成一个 key 值。

下面我们来认识一个函数

ftok:

将一个文件路径和一个项目标识符转换成一个唯一的键值。pathname 为已存在的路径名,proj_id 是一个8位的项目标识符。

成功返回key值,失败返回-1

key 是操作系统区分通信资源的唯一标识符,但是用户不能直接使用 key,操作系统为用户提供了专门的标识符。

shmid(共享资源),semid(信号量),msqid(消息队列)

它们的格式都为 xxxxxid 的形式。

虽然key值是独一无二的,但是系统提供的这些标识符却不是。在不同的区域中,标识符有可能是一样的。例如shmid为1,semid也可以为1.

2. IPC 资源组织方式

首先,最上层是由操作系统为用户专门提供的接口,这里我们用共享内存举例。struct shmid_ds 里面存储了共享内存的属性信息,此时的内核key值不会直接暴露给用户。再往下一层就是内核层,struct shmid_kernel 这是内核对于该资源一些特殊属性的管理(此时仍未暴露key)。在shmid_ds 结构体中,首位成员存在着struct ipc_perm sherm_perm 结构体,此结构体存储的是IPC资源一些基础共性结构(key值存储处)。通过指针数组ipc_perm*perm[] 来存储不同资源的到xxxx_perm 的指针,通过首位的不同类型映射(c语言的多态)来找到。

3. IPC 资源的操作命令及特点

ipcs:查看IPC资源

cpp 复制代码
ipcs  -q  //查看消息队列
ipcs  -m  //查看共享内存
ipcs  -s  //查看信号量数组
ipcs      //查看所有

ipcrm:删除 IPC 资源

cpp 复制代码
ipcrm  -m  123  //删掉shmid为123的共享内存
ipcrm  -q  456  //删掉msqid为456的消息队列
ipcrm  -s  789  //删掉semid为789的信号量

IPC 资源接口相似性较高,如 shmget,msgget,semget,shmctl,msgctl,semctl。原因是操作系统底层对于他们的管理模式非常的类似,结构也很类似。

创建 IPC 资源常用的选项 xxxxxget

IPC_CREAT:如果IPC资源不存在就创建,存在就获取 xxxid

IPC_EXCL:如果IPC资源不存在就创建,存在就报错(要与 IPC_CREAT一同使用

权限:设置权限限制,如(IPC_CREAT | IPC_EXCL | 0666)

控制 IPC 资源常用选项 xxxxxctl:

IPC_STAT:从内核数据结构获取 IPC 资源属性

IPC_SET:将设置好的属性设置进 IPC 资源

IPC_RMID:删除 IPC 资源

关于每一种资源的接口我将放在下文讲解,这里主要讲述一个共性。

六. System V 共享内存

1. 共享内存的概念与原理

共享内存就是在内存上开辟一块公共的空间,提供个进程使用。

进程向操作系统申请一块共享内存空间,操作系统为其在内存上开辟一块物理地址,并且分配 key 值和 shmid ;接着通过页表的方式,将物理地址映射成虚拟地址,与该进程的虚拟地址空间建立联系(存储在共享区中),这样单个进程就完成了与共享内存的挂接操作;最后,任何进程都可以拿着 shmid 来与该共享内存完成挂接操作,这样两个进程就关联到了一块空间上,就可以完成通信。

挂接(attach):关联进程与共享内存

取关联(detach):取消共享内存与进程的链接

2. 共享内存的接口

shmget:创建共享内存

cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key,size_t size,int shmflg)

返回值:成功返回shmid,失败返回-1

参数:

key:共享内存标识符

size:共享内存大小,单位字节

shmflg:功能选项 {

IPC_CREAT:如果IPC资源不存在就创建,存在就获取 xxxid

IPC_EXCL:如果IPC资源不存在就创建,存在就报错(要与 IPC_CREAT一同使用)

权限:设置权限限制,如(IPC_CREAT | IPC_EXCL | 0666) }

shmat:挂接进程到共享内存上

cpp 复制代码
#include <sys/types.h>
#include <sys/shm.h>

void* shmat(int shmid,const void* shmaddr,int shmflg);

返回值:成功返回共享内存地址(虚拟),失败返回-1

参数:

shmid:指定共享内存标识符

shmaddr:可以将共享内存映射到指定虚拟地址上(可以不用该参数)

shmflg:设置读写权限,可以不设置

shmdt:去关联进程

cpp 复制代码
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void* shmaddr)

返回值:成功返回0,失败返回-1

参数:

shmaddr:共享内存起始地址

shmctl:共享内存控制

cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid,int cmd,struct shmid_ds *buf)

返回值:成功返回0,失败返回-1

参数:

shmid:共享内存标识符

cmd:共享内存控制选项

{

IPC_STAT:从内核数据结构获取 IPC 资源属性

IPC_SET:将设置好的属性设置进 IPC 资源

IPC_RMID:删除 IPC 资源

}

buf:获取内存属性(不需要可以设置成NULL)

3. 共享内存实现通信

我们要设计一个使用命名管道作为信号传递,共享内存进行实际数据传输。**客户端写入数据,通过命名管道发送信号给进程,进程接收信号,进程读数据。**这样用管道进行通信,共享内存进行实际传输,可以避免服务器无效等待或读取数据不完整。

设计思路:

基于我们上文创建的管道文件,我们只需要添加唤醒和等待功能。基于共享内存,我们仍然采用先描述再组织的方法进行。

shm.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include "Comm.hpp"
using namespace std;

#define Creator "creator"
#define User "user"
const int gsize = 4096;
const int gmode = 0666;
const int gaultid = -1;
const string pathname = ".";
const int projid = 0x66;

class Shm
{
private:
    void Attach() // 挂接
    {
        _start_shm = shmat(_shmid, nullptr, 0);
        if ((long long)_start_shm < 0)
        {
            ERR_EXIT("shmat");
        }
        cout << "Attach success!" << endl;
    }

    void Detach() // 取消挂接
    {
        int n = shmdt(_start_shm);
        if (n == 0)
        {
            cout << "Detach success!" << endl;
        }
    }

    void CreateHelp(int flag) // 创建内存空间
    {
        printf("key:0x%x\n", _key);
        _shmid = shmget(_key, _size, flag);
        if (_shmid < 0)
        {
            ERR_EXIT("shmget");
        }
        cout << "shmget success!" << endl;
    }

    // 用户权限设置
    void Create() // 创建者
    {
        CreateHelp(IPC_CREAT | IPC_EXCL | gmode);
    }

    void Get() // 用户使用者
    {
        CreateHelp(IPC_CREAT);
    }

    void Destroy() // 销毁
    {
        Detach();
        if (_user_type == Creator)
        {
            int n = shmctl(_shmid, IPC_RMID, nullptr);
            if (n == 0)
            {
                cout << "共享内存销毁成功" << endl;
            }
            else
            {
               ERR_EXIT("shmctl");
            }
        }
    }

public:
    Shm(const string &user, int projid, const string &pathname)
        : _user_type(user), _size(gsize), _start_shm(nullptr), _shmid(gaultid), _num(0)
    {
        _key = ftok(pathname.c_str(), projid); 
        if (_key < 0)
        {
            ERR_EXIT("ftok");
        }
        if (_user_type == Creator)
        {
            Create();
        }
        else if (_user_type == User)
        {
            Get();
        }
        else
        {
        }
        Attach();
    }

    ~Shm()
    {
        cout << _user_type << endl;
        if (_user_type == Creator)
        {
            Destroy();
        }
    }

    void Print() // 打印key值和起始地址
    {
        struct shmid_ds ds;
        int n = shmctl(_shmid, IPC_STAT, &ds);
        printf("key:0x%x\n", ds.shm_perm.__key);
        printf("shm_segsz:%ld\n", ds.shm_segsz);
    }

    int size()
    {
        return _size;
    }

    void *_memstart()
    {
        printf("_memstart:%p\n", _start_shm);
        return _start_shm;
    }

private:
    string _user_type;
    key_t _key;
    int _size;
    void *_start_shm;
    int _shmid;
    int _num;
};

Fifo.hpp:

cpp 复制代码
#pragma once

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

#define PATH "."
#define FILENAME "temp"

class NamedFifo
{
public:
    NamedFifo(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;
        }
    }
    ~NamedFifo()
    {
        // 删除管道文件
        int n = unlink(_fifoname.c_str());
        if (n == 0)
        {
            // ERR_EXIT("unlink"); // bug在这里,先析构fifo,导致shm的析构没有被调用
        }
        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()
    {
        _fd = open(_fifoname.c_str(), O_RDONLY);
        if (_fd < 0)
        {
            ERR_EXIT("open");
        }
        std::cout << "open fifo success" << std::endl;
    }
    void OpenForWrite()
    {
        // write
        _fd = open(_fifoname.c_str(), O_WRONLY);
        if (_fd < 0)
        {
            ERR_EXIT("open");
        }
        std::cout << "open fifo success" << std::endl;
    }
    void Close()
    {
        if (_fd > 0)
            close(_fd);
    }
    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;
    }
    ~FileOper()
    {
    }

private:
    std::string _path;
    std::string _name;
    std::string _fifoname;
    int _fd;
};

Comm.hpp:

cpp 复制代码
#pragma once

#include <cstdio>
#include <cstdlib>

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

server.cc

cpp 复制代码
#include "shm.hpp"
#include "Fifo.hpp"
int main()
{
    Shm shm(Creator,projid,pathname);
    // sleep(5);
    shm.Print();


    NamedFifo fifo(PATH, FILENAME);

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

    char *mem = (char *)shm._memstart();
    while (true)
    {
        if (readerfile.Wait())
        {
            printf("%s\n", mem);
        }
        else
            break;
    }

    readerfile.Close();
    std::cout << "server end normal!" << std::endl; // server段的析构函数没有被成功调用!
    return 0;
}

client.cc

cpp 复制代码
#include "shm.hpp"
#include "Fifo.hpp"

int main()
{
    FileOper writerfile(PATH, FILENAME);
    writerfile.OpenForWrite();

    Shm shm(User,projid,pathname);
    char *mem = (char*)shm._memstart();
   
    
    int index = 0;
    for (char c = 'A'; c <= 'B'; c++, index += 2)
    {
        sleep(1);
        mem[index] = c;
        mem[index + 1] = c;
        sleep(1);
        mem[index + 2] = 0;

        writerfile.Wakeup();
    }

    writerfile.Close();
    return 0;
}

七. System V 消息队列

1. 消息队列的概念

消息队列是操作系统为我们提供的内核级队列,**多个进程将消息以数据块的形式存储在消息队列中,通过访问消息队列完成进程间通信。**消息队列的本质是一个链表,链表的每个结点就是一个消息。我们用户需要对消息类型和消息体进行结构体定义。

如:

cpp 复制代码
struct msgbuf
{
   long mtype;
   char mtext[];
}

消息类型必须是一个一个的字段,为long类型。

消息队列的通信方式也很简单

A进程将消息类型和消息数据写入消息对象中,消息队列对进程A的消息对象进行复制放到队列末尾,进程B将消息对象从队头复制到自己的消息对象中,然后将头结点删除,这样就完成了通信。

2. 消息队列接口

msgget:创建消息队列

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key,int msgflg);

返回值:成功返回msgid,失败返回-1

参数:

key:内核消息队列标识符

msgflg:选项

{

IPC_CREAT:如果IPC资源不存在就创建,存在就获取 xxxid

IPC_EXCL:如果IPC资源不存在就创建,存在就报错(要与 IPC_CREAT一同使用)

权限:设置权限限制,如(IPC_CREAT | IPC_EXCL | 0666)}

msgsnd:发送数据到消息队列

cpp 复制代码
int msgsnd(int msqid,const void* msgq,size_t msgsz,int msgflg)

返回值:成功返回0,失败返回-1

参数:

msqid:消息队列标识符

msgp:放入消息队列的数据

msgsz:数据大小

msgflg:选项

{0:消息队列满时进行阻塞等待,直到消息写进队列

IPC_NOWAIT:当消息队列已满时候不等待,立即返回-1}

msgrcv:从消息队列读取数据

cpp 复制代码
ssize_t msgrcv(int msqid,void* msgp,size_t msgsz,long mtype,int msgflg);

返回值:成功返回实际数据大小,失败返回-1

参数:

msqid:消息队列标识符

msgsz:期望读取数据大小

msgp:输出型参数,读取到数据块

mtype:消息类型

{0:接收第一个消息

大于0:接收消息为mtype的消息

小于0:接收消息小于mtype绝对值的消息}

msgflg:选项

{0:没有消息时阻塞式等待

IPC_NOWAIT:没有消息时不等待,返回-1

IPC_EXCEPT:与mtype配合使用,返回第一个类型不为type的消息}

msgctl:消息队列的控制

cpp 复制代码
int msgctl(int msqid,int cmd,struct msqid_ds* buf);

返回值:成功返回0,失败返回-1

参数:

msqid:消息队列标识符

cmd:功能选项

{

IPC_STAT:从内核数据结构获取 IPC 资源属性

IPC_SET:将设置好的属性设置进 IPC 资源

IPC_RMID:删除 IPC 资源}

buf:输出型参数,用来获取队列属性

3. 消息队列通信

我们要封装一个消息队列,然后使得进程AB直接可以通过消息队列进行通信

Msgq.hpp:

cpp 复制代码
#include <iostream>
#include <time.h>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/msg.h>
using namespace std;

#define GET_MSG (IPC_CREAT | IPC_EXCL | 0666)
#define USE_MSG IPC_CREAT
const string pathname = ".";
const int proj_id = 0x66;
long CLIENT = 1;
long SERVER = 2;

class Messagequeue
{
    struct msg_t
    {
        long _mtype;
        char _mtext[1024];
    };

public:
    Messagequeue()
    {
    }
    void Create(int flag)
    {
        key_t k = ftok(pathname.c_str(), proj_id);
        if (k < 0)
        {
            cout << "ftok fail" << endl;
            exit(1);
        }
        cout << "ftok success" << endl;

        _msgid = msgget(k, flag);
        if (_msgid < 0)
        {
            cout << "msgget fail" << endl;
            exit(2);
        }
        cout << "msgget success" << endl;
    }
    void Recv(string &in, long &type)
    {
        msg_t msg;
        int n = msgrcv(_msgid, &msg, sizeof(msg._mtext), type, 0);
        if (n < 0)
        {
            cout << "fail msgrcv" << endl;
            exit(5);
        }
        cout << "msgrcv success" << endl;
        msg._mtext[n] = '\0';
        in = msg._mtext;
        cout << "recev Mes:" << in << endl;
    }
    void Send(const string &out, long &type)
    {
        //sleep(5);
        msg_t msg;
        memset(msg._mtext, 0, sizeof(msg._mtext));
        msg._mtype = type;
        memcpy(msg._mtext, out.c_str(), out.size());
        int n = msgsnd(_msgid, &msg, out.size(), 0);
        if (n < 0)
        {
            cout << "msgsnd fail" << endl;
            exit(4);
        }
        cout << "msgsnd success" << endl;
    }
    void Destroy()
    {
        int n = msgctl(_msgid, IPC_RMID, nullptr);
        if (n < 0)
        {
            cout << "msgctl fail" << endl;
            exit(3);
        }
        cout << "msgctl remove" << endl;
    }
    ~Messagequeue()
    {
    }

private:
    int _msgid;
};

class Client : public Messagequeue
{
public:
    Client()
    {
        Messagequeue::Create(USE_MSG);
    }
};

class Server : public Messagequeue
{
public:
    Server()
    {
        Messagequeue::Create(GET_MSG);
    }
    ~Server()
    {
        Messagequeue::Destroy();
    }
};

Client.cc

cpp 复制代码
#include "Msgq.hpp"

int main()
{
    string msg;
    Client c;
    while (true)
    {
        fflush(stdout);
        c.Recv(msg, CLIENT);
    }
    return 0;
}

Server.cc

cpp 复制代码
#include "Msgq.hpp"

int main()
{
    string msg = "hello";
    Server c;
    while (true)
    {
        sleep(5);
        c.Send(msg, CLIENT);
    }
    return 0;
}

八. System V 信号量

1. 信号量的概念

信号量是一个计数器,记录着某中资源的数量,它的本质就是一个计数器。 当进程需要使用这个资源的时候,信号量就减一;用完该资源的时候,信号量就加一,我们将这一放一收称为 P V 操作。信号量的作用是对公共资源进行保护,但是信号量本身就是公共资源,所以为了对信号量进行保护,避免多个进程同时对信号量进行申请导致出错,信号量的操作必须是原子操作(执行过程不能被打断)。

2. 信号量的接口

semget:创建一个信号量

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key,int nsems,int semflg);

返回值:成功返回信号量标识符 semid,失败返回-1

参数:

key:内核信号量标识符 key 值

nsems:需要申请的信号量数量

semflg:信号量选项

{

IPC_CREAT:如果IPC资源不存在就创建,存在就获取 xxxid

IPC_EXCL:如果IPC资源不存在就创建,存在就报错(要与 IPC_CREAT一同使用)

权限:设置权限限制,如(IPC_CREAT | IPC_EXCL | 0666)}

semop:定义信号量PV操作

cpp 复制代码
int semop(int semid,struct sembuf* sops,unsigned nsops)

返回值:成功返回0,失败返回-1

参数:

semid:信号量标识符

sops:信号量结构体数组

nsops:设置结构体个数

semctl:控制信号量

cpp 复制代码
int semctl(int semid,int semnum,int cmd,...)

返回值:成功返回0,失败返回-1

参数:

semid:信号量标识符

semnum:信号量下标

cmd:控制选项

{

IPC_STAT:从内核数据结构获取 IPC 资源属性

IPC_SET:将设置好的属性设置进 IPC 资源

IPC_RMID:删除 IPC 资源}

3. 信号量使用实践

我们设计一个类对信号量进行封装,使得更好的进行资源管理,采用建造者模式进行代码编写。我们要实现的结果就是通过PV操作使字母成双成对的打印出来。

代码样例:

Sem.hpp:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <time.h>
#include <memory>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
using namespace std;

#define GET_SEM (IPC_CREAT | IPC_EXCL | 0666)
#define USE_SEM IPC_CREAT
const string pathname = ".";
const int proj_id = 0x88;
const int _nums = 1;

class Sem
{
public:
    Sem(int semid, int flag)
        : _semid(semid), _flag(flag)
    {
    }
    void P()
    {
        PV(-1);
    }
    void V()
    {
        PV(1);
    }
    ~Sem()
    {
        if (_flag == GET_SEM)
        {
            int n = semctl(_semid, 0, IPC_RMID);
            if (n == -1)
            {
                cerr << "fail remove!" << endl;
            }
            else
            {
                cout << "success remove" << endl;
            }
        }
    }

private:
    void PV(int data)
    {
        struct sembuf sb;
        sb.sem_flg = SEM_UNDO;
        sb.sem_num = 0;
        sb.sem_op = data;
        int n = semop(_semid,&sb,1);
        if(n == -1)
        {
            cerr<<"fail PV"<<endl;
            exit(4);
        }
    }

private:
    int _semid;
    int _flag;
};

class SemBuilder
{
public:
    SemBuilder()
        : _val(-1)
    {
    }
    SemBuilder &SET_VAL(int val)
    {
        _val = val;
        return *this;
    }
    shared_ptr<Sem> Build(int flag)
    {
        if (_val < 0)
        {
            cout << "you must SETVAL first" << endl;
            return nullptr;
        }

        key_t k = ftok(pathname.c_str(), proj_id);
        if (k == -1)
        {
            cerr << "ftok error" << endl;
            exit(1);
        }
        cout << "ftok success!" << endl;

        int n = semget(k, 1, flag);
        if (n == -1)
        {
            cerr << "semget error" << endl;
            exit(2);
        }
        cout << "semget success!" << endl;

        if (flag == GET_SEM)
        {
            union Semun
            {
                int val;
                struct semid_ds *buf;
                unsigned short *array;
                struct seminfo *__buf;
            } un;
            un.val = _val;
            int x = semctl(n, 0, SETVAL, un);
            if (x < 0)
            {
                cerr << "semctl SETVAL error" << endl;
                exit(3);
            }
            cout << "SETVAL success" << endl;
        }
        return make_shared<Sem>(n, flag);
    }
    ~SemBuilder()
    {
    }

private:
    int _val;
};

Writer.cc

cpp 复制代码
#include "Sem.hpp"

int main()
{
    SemBuilder SB;
    auto fsem = SB.SET_VAL(1).Build(GET_SEM);

    pid_t n = fork();
    srand(time(0) ^ getpid());
    if (n == 0) // 子进程
    {
        auto zsem = SB.Build(USE_SEM);
        int cnt = 10;
        while (cnt--)
        {
            zsem->P();
            printf("B");
            usleep(rand() % 9566);
            fflush(stdout);
            printf("B");
            usleep(rand() % 5200);
            fflush(stdout);
            zsem->V();
        }
    }

    // 父进程
    int cnt = 10;
    while (cnt--)
    {
        fsem->P();
        printf("S");
        usleep(rand() % 4576);
        fflush(stdout);
        printf("S");
        usleep(rand() % 5555);
        fflush(stdout);
        fsem->V();
    }
    cout << endl;

    return 0;
}
相关推荐
木子欢儿14 分钟前
在 Debian 12 上安装 Xfce 桌面
java·linux·运维·服务器·debian
Vdeilae16 分钟前
debian 时间同步 设置ntp服务端 客户端
java·服务器·debian
coder_lorraine31 分钟前
【Linux系列】Linux Snap 安装与使用指南:高效管理应用的神器
linux·运维
LLLLYYYRRRRRTT35 分钟前
9. Linux 交换空间管理
linux·数据库·redis
zhuyan1081 小时前
【ROS2】常用命令
linux·运维·服务器
涛思数据(TDengine)1 小时前
可信数据库大会现场,TDengine 时序数据库展示核电场景下的高性能与 AI 创新
大数据·运维·数据库·人工智能·时序数据库·tdengine·涛思数据
DARLING Zero two♡1 小时前
【Linux操作系统】简学深悟启示录:进程初步
linux·运维·服务器
努力一点9481 小时前
ubuntu22.04系统实践 linux基础入门命令(三) 用户管理命令
linux·运维·服务器·人工智能·ubuntu·gpu算力
Runner.DUT2 小时前
SRIO入门之官方例程仿真验证
服务器·网络·数据库