目录
- `🍑system V共享内存 `
- `🍒共享内存的原理`
- `共享内存数据结构`
- `查看和删除共享内存资源的命令`
- `🌻共享内存函数`
- `shmget函数`
- `ftok函数`
- `shmat函数`
- `shmdt函数`
- `shmctl函数`
- `🍃共享内存的优缺点`
- `🦔system V消息队列(了解)`
- `消息队列的相关操作接口`(相关接口的参数与共享内存类似)
- `🦅system V信号量 sem`
- `📚信号量理论`
- `信号量的数据结构:`
- `信号量的相关操作接口`
- `指令查找以及删除信号量:`
🍑system V共享内存
共享内存
是最快
的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到
内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
🍒共享内存的原理
OS
在内存上开辟一段空间,然后与进程构建映射(通过页表),映射到堆栈之间的共享区
中,将该空间的起始地址(虚拟地址)返回给用户,该进程就可以通过虚拟地址访问该内存了。
其中,开辟空间和构建映射关系,需要修改进程地址空间和页表,都是内核数据结构,都是OS做的,所以OS会提供相应的系统调用,如果通过同样的系统调用,就也可以将该空间映射到另一个进程的共享区中,也可以使用虚拟地址访问该内存,所以就让不同的进程看到了同一个内存资源。
共享内存数据结构
一个时刻,在系统中可能存在很多个进程在进行通信,就可能存在很多个共享内存,OS就会将这些共享内存进行管理,先描述再组织。下面是描述共享内存的数据结构
。
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
共享内存存在很多个,那么怎么保证两个进程看到的是同一个共享内存呢?
key_t
:共享内存的唯一值。- 我们只需要两个进程,在使用 ftok 形成key时,使用同样的参数即可。
共享内存,如果进程结束,没有主动释放,则会一直存在,除非重启系统。共享内存的生命周期随内核
。
查看和删除共享内存资源的命令
ipcs -m
:查看系统中指定用户创建的共享内存。
ipcrm -m shmid
:删除指定shmid的共享内存。
🌻共享内存函数
shmget函数
功能:用来创建共享内存。
int shmget(key_t key, size_t size, int shmflg);
参数:
- key:这个共享内存段名字 :共享内存在内核中唯一性的标识。
- size:共享内存大小。在内核中,共享内存的大小是以
4KB为基本单位
的。如果size设置为4097,则实际开辟的是8KB。 - shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。
- 返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。
常用shmflg选项
:
- IPC_CREAT:如果创建的共享内存不存在,就创建,如果存在,直接获取它。
- IPC_CREAT:单独使用没有意义。
- IPC_CREAT | IPC_CREAT:如果创建的共享内存不存在,就创建,如果存在,就会出错返回。
- 可以
指定该共享内存的权限
,直接 或上权限即可(如 |0666)。
ftok函数
功能:生成一个System V IPC key
key_t ftok(const char *pathname, int proj_id);
用法:传入路径+文件名(随便一个字符串也行)和 一个整型,生成一个System V IPC key
.
返回值:成功返回生成的key,失败返回-1,错误码被设置。
shmat函数
功能:将共享内存段连接到进程地址空间(建立映射)
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid:共享内存标识。
- shmaddr:指定连接的地址(虚拟地址)。
- shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY。
- 返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1。
说明:
shmaddr 为 NULL
,核心自动选择一个地址。
shmaddr
不为 NULL
且smflg 无 SHM_RND标记
,则以shmaddr为连接地址。
shmaddr
不为 NULL
且shmflg 设置了 SHM_RND标记
,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -
(shmaddr % SHMLBA)。
shmflg = SHM_RDONLY
,表示连接操作用来只读共享内存。
shmdt函数
功能:将共享内存段与当前进程脱离(去关联)。
int shmdt(const void *shmaddr);
参数:
- shmaddr:由shmat所返回的指针。
- 返回值:成功返回0;失败返回-1。
注意:
将共享内存段与当前进程脱离不等于删除共享内存段。
shmctl函数
功能:用于控制共享内存。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
- shmid:由
shmget
返回的共享内存标识码。 - cmd:将要采取的动作(有三个可取值)。
- buf:指向一个保存着共享内存的模式状态和访问权限的数据结构(如果不关心,直接设置为nullptr)。
- 返回值:成功返回0;失败返回-1。
🍃共享内存的优缺点
-
共享内存优点:
是进程间通信中速度最快的。(管道通信中,进程需要通过系统调用往管道里面读写数据,两个进程通信一次,至少两次系统调用;而共享内存,进程可以通过共享内存的起始虚拟地址,直接访问到内存,一个进程向共享内存中写了数据,另一个进程就可以直接看到,只需要一次系统调用即可)。 -
共享内存缺点:
没有提供进程间任何的协同机制,如果需要,可以自己实现。有些场景不适用,比如我们一个进程发送一个数据,如果没有协同机制,那么可能该进程数据才写一半,另一个进程就读取了,会造成数据不一致。- 可以通过管道来实现同步。示例代码中有体现。
示例代码演示:
Comm.hpp
#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <string>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
using namespace std;
const char *pathname = "/home/whb";
const int proj_id = 0x66;
// 在内核中,共享内存的大小是以4KB为基本单位的. 你只能用你申请的大小。建议申请大小是n*4KB
const int defaultsize = 4096; // 单位是字节
std::string ToHex(key_t k){
char buffer[1024];
snprintf(buffer, sizeof(buffer), "0x%x", k);
return buffer;
}
key_t GetShmKeyOrDie(){
key_t k = ftok(pathname, proj_id);
if (k < 0) {
std::cerr << "ftok error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;
exit(1);
}
return k;
}
int CreateShmOrDie(key_t key, int size, int flag){
int shmid = shmget(key, size, flag);
if (shmid < 0){
std::cerr << "shmget error, errno : " << errno << ", error string: " << strerror(errno) << std::endl;
exit(2);
}
return shmid;
}
int CreateShm(key_t key, int size){
// IPC_CREAT: 不存在就创建,存在就获取
// IPC_EXCL: 没有意义
// IPC_CREAT | IPC_EXCL: 不存在就创建,存在就出错返回
return CreateShmOrDie(key, size, IPC_CREAT | IPC_EXCL | 0666);
}
int GetShm(key_t key, int size){
return CreateShmOrDie(key, size, IPC_CREAT);
}
void DeleteShm(int shmid){
int n = shmctl(shmid, IPC_RMID, nullptr);
if (n < 0){
std::cerr << "shmctl error" << std::endl;
}
else{
std::cout << "shmctl delete shm success, shmid: " << shmid << std::endl;
}
}
void ShmDebug(int shmid){
struct shmid_ds shmds;
int n = shmctl(shmid, IPC_STAT, &shmds);
if (n < 0){
std::cerr << "shmctl error" << std::endl;
return;
}
std::cout << "shmds.shm_segsz: " << shmds.shm_segsz << std::endl;
std::cout << "shmds.shm_nattch:" << shmds.shm_nattch << std::endl;
std::cout << "shmds.shm_ctime:" << shmds.shm_ctime << std::endl;
std::cout << "shmds.shm_perm.__key:" << ToHex(shmds.shm_perm.__key) << std::endl;
}
void *ShmAttach(int shmid){
void *addr = shmat(shmid, nullptr, 0);
if ((long long int)addr == -1){
std::cerr << "shmat error" << std::endl;
return nullptr;
}
return addr;
}
void ShmDetach(void *addr){
int n = shmdt(addr);
if (n < 0){
std::cerr << "shmdt error" << std::endl;
}
}
Fofi.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <cassert>
using namespace std;
#define Mode 0666
#define Path "./fifo"
class Fifo
{
public:
Fifo(const string &path = Path) : _path(path)
{
umask(0);
int n = mkfifo(_path.c_str(), Mode);
if (n == 0)
{
cout << "mkfifo success" << endl;
}
else
{
cerr << "mkfifo failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
}
}
~Fifo()
{
int n = unlink(_path.c_str());
if (n == 0)
{
cout << "remove fifo file " << _path << " success" << endl;
}
else
{
cerr << "remove failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
}
}
private:
string _path; // 文件路径+文件名
};
class Sync{
public:
Sync() : rfd(-1), wfd(-1)
{}
void OpenReadOrDie()
{
rfd = open(Path, O_RDONLY);
if (rfd < 0)
exit(1);
}
void OpenWriteOrDie()
{
wfd = open(Path, O_WRONLY);
if (wfd < 0)
exit(1);
}
bool Wait()
{
bool ret = true;
uint32_t c = 0;
ssize_t n = read(rfd, &c, sizeof(uint32_t));
if (n == sizeof(uint32_t))
{
std::cout << "server wakeup, begin read shm..." << std::endl;
}
else if (n == 0)
{
ret = false;
}
else
{
return false;
}
return ret;
}
void Wakeup()
{
uint32_t c = 0;
ssize_t n = write(wfd, &c, sizeof(c));
assert(n == sizeof(uint32_t));
std::cout << "wakeup server..." << std::endl;
}
~Sync() {}
private:
int rfd;
int wfd;
};
#endif
ShmClient.cc
#include "Comm.hpp"
#include "Fifo.hpp"
#include <unistd.h>
int main()
{
key_t key = GetShmKeyOrDie();
std::cout << "key: " << ToHex(key) << std::endl;
// sleep(2);
int shmid = GetShm(key, defaultsize);
std::cout << "shmid: " << shmid << std::endl;
// sleep(2);
char *addr = (char *)ShmAttach(shmid);
std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
// sleep(5);
memset(addr, 0, defaultsize);
Sync syn;
syn.OpenWriteOrDie();
// 可以进行通信了
for (char c = 'A'; c <= 'Z'; c++) // pipe, fifo, ->read/write->系统调用, shm -> 没有使用系统调用!!
{
addr[c - 'A'] = c;
sleep(1);
syn.Wakeup();
}
ShmDetach(addr);
std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
sleep(5);
return 0;
}
Shm.Server.cc
#include "Comm.hpp"
#include "Fifo.hpp"
#include <unistd.h>
int main(){
// 1. 获取key
key_t key = GetShmKeyOrDie();
std::cout << "key: " << ToHex(key) << std::endl;
// sleep(2);
// 2. 创建共享内存
int shmid = CreateShm(key, defaultsize);
std::cout << "shmid: " << shmid << std::endl;
// sleep(2);
// ShmDebug(shmid);
// 4. 将共享内存和进程进行挂接(关联)
char *addr = (char *)ShmAttach(shmid);
std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
// 0. 先引入管道
Fifo fifo;
Sync syn;
syn.OpenReadOrDie();
// 可以进行通信了
for(;;)
{
if(!syn.Wait()) break;
cout << "shm content: " << addr << std::endl;
}
ShmDetach(addr);
std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;
// 3. 删除共享内存
DeleteShm(shmid);
return 0;
}
🦔system V消息队列(了解)
消息队列
提供了一个从一个进程向另外一个进程发送一个有类型的数据块
的方法。- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
- 特性方面
- IPC资源必须删除,否则不会自动清除,除非重启,所以
system V IPC资源的生命周期随内核。
- IPC资源必须删除,否则不会自动清除,除非重启,所以
进程A与进程B,其中,进程A可以向内核中发送和读取数据块,进程B也可以发送和读取,发送的数据块放到一个队列中,当进程A在发送的时候,另一个进程也可以发送,数据块中有标记位标记是谁发送的。
消息队列的数据结构:
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of creation or last
modification by msgctl() */
unsigned long msg_cbytes; /* # of bytes in queue */
msgqnum_t msg_qnum; /* # number of messages in queue */
msglen_t msg_qbytes; /* Maximum # of bytes in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
struct ipc_perm {
key_t __key; /* Key supplied to msgget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
消息队列的相关操作接口
(相关接口的参数与共享内存类似)
-
创建消息队列
int msgget(key_t key,int msgflg);
-
删除消息队列
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
-
向队列里发送数据
int msgsnd(int msqid,const void* msgp,size_t msgsz,int msgflq);
-
读取数据
ssize_t msgrcv(int msqid,void* msgp,size_t msgsz,long msgtyp,int msgflg);
-
查找消息队列的指令
ipcs -q
-
删除消息队列的指令
ipcrm -q msqid
🦅system V信号量 sem
信号量
主要用于同步
和互斥
的,下面先来看看什么是同步和互斥。
- 进程互斥 ·
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为
进程的互斥
。 - 系统中某些资源一次只允许一个进程使用,称这样的资源为
临界资源
或互斥资源。 - 访问临界资源的代码段叫
临界区
。
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为
- 特性方面
- IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期
随内核
。
- IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期
📚信号量理论
信号量(信号灯)
- 对于一份内存资源(临界资源),如果只允许整体被使用的话,每一次只允许一个进程访问,这种工作做方式效率低。
- 其中,
信号量
就提供了一种机制,将一份内存资源拆分成很多小资源,允许多个进程同时访问,前提:每一个进程访问的是不同的被切分的小资源,就能做到真正的并发访问。
在这种情况下,只需要1.
限制进来的进程数。2.
合理分配资源 即可。
信号量
的本质
是一个计数器,是描述临界资源数量的计数器
。(如下图,假设计数器 int count = 9;)
在访问资源的时候,会经历一下几个步骤:
- 申请信号量,if count>0,则申请成功,并且会将count–,也叫做
p操作
; - 访问临界资源。
- 释放信号量,count++,叫做
v 操作
;
在多进程场景,int不能实现信号量得效果,原因如下:
- count
++/--不是原子的
,所以在++/–过程中,可能有其他进程也会申请信号量,会造成访问冲突问题。(比如现在count=1,然后一个进程来申请,然后再count–过程中,这时count还是1,又有另一个进程来申请信号量,又会count–,count–完成后,就变为成-1了)。 无法在进程间共享
,变量在进程中做修改时,会发生写时拷贝
。所以须让不同的进程先看到同一份资源—计数器资源!所以信号量属于进程间通信。
所有进程,访问临界资源,都必须先申请信号量–所有的进程都得先看到一个信号量–所以信号量本身就是共享资源!所以信号量的申请(++)和释放(–)都必须是原子的。pv操作必须是原子的。
如果信号量的初始值是1,就是互斥,也就是二元信号量,也是后面文章会说到的 锁。
信号量的数据结构:
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Creation time/time of last
modification via semctl() */
unsigned long sem_nsems; /* No. of semaphores in set */
};
struct ipc_perm {
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
信号量的相关操作接口
-
申请信号量
int semget(key_t key, int nsems, int semflg);
-
控制信号量
int semid, int semnum, int cmd, ...);
-
pv操作
int semop(int semid, struct sembuf *sops, size_t nsops);
指令查找以及删除信号量:
- 查找:
ipcs - s
- 删除:
ipcrm -s semid
共享内存 信号量 消息队列 是OS系统特意设计的 system V进程间通信的。
共享内存 信号量 消息队列可以看成同一种资源,IPC资源,OS注定要对IPC资源进程管理–先描述再组织。
我们可以发现,在内核中,无论是共享内存 ,信号量 还是消息队列,描述他们的结构体里的第一个成员都是 kern ipc_perm类型的。在内核中,有一个数据结构ipc_id_ary,里面第一个成员是size,其余都是类似于指针数组,类型为 kern ipc_perm*,当系统创建了一个IPC 资源时,会将该资源的数据结构的第一个成员地址存放在该kern ipc_perm*数组中,这样,系统中的每一个IPC
资源都组织起来了,方便管理。类似于C++中的多态。