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

一. 进程间通信介绍

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

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

二. 管道

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

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

那为何要如此呢?

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

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


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

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

三. 匿名管道

1. 匿名管道的概念

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

2. 匿名管道的使用

我们先来认识一个函数

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

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

#include
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:

#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:

#pragma once
#include
#include
#include
using namespace std;
using task_t = function;
void PrintLog()
{
cout = 0 && code  _task;
};

ProcessPool.hpp:

#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__
#include
#include
#include
#include
#include
#include
#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  _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  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:

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

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

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

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

 unlink:

删除管道文件

#include
int unlink(const char* pathname);

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

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

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

设计思路:

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

下面是代码样例:

comm.hpp:

#include
#include
#include
#include
#include
#include
#include
#include
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  0)
{
buffer[number] = 0;
cout  0)
{
close(_fd);
}
}
~FifoOper()
{
}
private:
string _path;
string _name;
string _fifoname;
int _fd;
};

 server.cc:

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

 client.cc:

#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资源

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

ipcrm:删除 IPC 资源

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:创建共享内存

#include
#include
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:挂接进程到共享内存上

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

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

参数:

shmid:指定共享内存标识符

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

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

 shmdt:去关联进程

#include
#include
int shmdt(const void* shmaddr)

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

参数:

shmaddr:共享内存起始地址

  shmctl:共享内存控制

#include
#include
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:

#pragma once
#include
#include
#include
#include
#include
#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:

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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)
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:

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

server.cc:

#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:

#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. 消息队列的概念

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

如:

struct msgbuf
{
long mtype;
char mtext[];
}

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

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

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

2. 消息队列接口

msgget:创建消息队列

#include
#include
#include
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:发送数据到消息队列 

int msgsnd(int msqid,const void* msgq,size_t msgsz,int msgflg)

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

 参数:

msqid:消息队列标识符

msgp:放入消息队列的数据

msgsz:数据大小

msgflg:选项

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

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

msgrcv:从消息队列读取数据 

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:消息队列的控制 

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:

#include
#include
#include
#include
#include
#include
#include
#include
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:

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

Server.cc:

#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:创建一个信号量

#include
#include
#include
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操作 

int semop(int semid,struct sembuf* sops,unsigned nsops)

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

 参数:

semid:信号量标识符

sops:信号量结构体数组

nsops:设置结构体个数

semctl:控制信号量 

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:

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
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  Build(int flag)
{
if (_val (n, flag);
}
~SemBuilder()
{
}
private:
int _val;
};

Writer.cc:

#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;
}
posted @ 2025-08-05 08:36  yfceshi  阅读(21)  评论(0)    收藏  举报