一. 进程间通信介绍
进程间通信(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 类,进行子进程任务分配,和终止回收进程完整操作。
下面是代码样例:
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; };
cpp#include "comm.hpp" int main() { Namefifo fifo(".",NAME); FifoOper readopen(PATH,NAME); readopen.OpenforRead(); readopen.Read(); readopen.Close(); return 0; }
cpp#include "comm.hpp" int main() { FifoOper writeopen(PATH,NAME); writeopen.OpenforWrite(); writeopen.Write(); writeopen.Close(); return 0; }
五. IPC 资源
进程间通信当下主要有两套标准:
System V:本地通信,如共享内存(shared memory,简称shm),消息队列(message queue,简称msq),信号量(semaphore,简称sem)
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)
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; }
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. 消息队列的概念
消息队列是操作系统为我们提供的内核级队列,**多个进程将消息以数据块的形式存储在消息队列中,通过访问消息队列完成进程间通信。**消息队列的本质是一个链表,链表的每个结点就是一个消息。我们用户需要对消息类型和消息体进行结构体定义。
如:
cppstruct 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:发送数据到消息队列
cppint msgsnd(int msqid,const void* msgq,size_t msgsz,int msgflg)
返回值:成功返回0,失败返回-1
参数:
msqid:消息队列标识符
msgp:放入消息队列的数据
msgsz:数据大小
msgflg:选项
{0:消息队列满时进行阻塞等待,直到消息写进队列
IPC_NOWAIT:当消息队列已满时候不等待,立即返回-1}
msgrcv:从消息队列读取数据
cppssize_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:消息队列的控制
cppint 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(); } };
cpp#include "Msgq.hpp" int main() { string msg; Client c; while (true) { fflush(stdout); c.Recv(msg, CLIENT); } return 0; }
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操作
cppint semop(int semid,struct sembuf* sops,unsigned nsops)
返回值:成功返回0,失败返回-1
参数:
semid:信号量标识符
sops:信号量结构体数组
nsops:设置结构体个数
semctl:控制信号量
cppint 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; };
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; }