目录
1.System V共享内存
1.介绍&原理
- 共享内存提供者 --> OS
- –> System V IPC****资源,生命周期随内核
- IPC资源必须删除,否则不会自动清除,除非重启
- 共享内存区是最快的IPC形式
- 一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核
- 换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
- 注意:共享内存没有进行访问控制(同步与互斥)
2.共享内存函数
shmget()
- **功能:**用来创建共享内存
- 原型:int shmget(key_t key, size_t size, int shmflg);
- 参数:
- key**:**这个共享内存段名字
- 是多少不重要,只要能够在创作系统唯一即可
- server && client使用同一个key,只要key值相同,就是看到了同一个共享内存
- size**:**共享内存大小 --> 最好是页(PAGE:4096)的整数倍
- shmflg**:**由九个权限标志构成,用法和创建文件时使用的mode模式标志是一样的
-
IPC_CREAT**:**创建共享内存,如果底层已经存在,获取之,并返回,如果不存在,创建之,并返回
-
IPC_CREAT|IPC_EXCL**:**如果底层不存在,创建之,并返回,如果底层存在,返回出错
- 返回成功一定是一个全新的shm
-
IPC_EXCL单独使用没有意义
-
为0时,尝试获取一个已经存在的共享内存段,如果该共享内存段不存在,则返回错误
-
- key**:**这个共享内存段名字
- **返回值:**共享内存的用户层标识符,类似曾经的fd
- 成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
ftok()
- **功能:**用pathname和proj_id创建一个System V IPC key
- *原型:key_t ftok(const char pathname, int proj_id);
- 参数:
- pathname**:**指向一个存在且可访问的文件(路径)
- proj_id**:**可随便填
- **返回值:**成功返回key;失败返回-1
shmat()
- 功能:将共享内存段连接到进程地址空间
- **原型:void shmat(int shmid, const void shmaddr, int shmflg);
- 参数:
- shmid**:**共享内存标识
- shmaddr**:**指定链接的地址
- shmaddr为NULL,核心自动选择一个地址
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍
- 公式:shmaddr - (shmaddr % SHMLBA)
- shmflg**:它的两个可能取值是SHM_RND和SHM_RDONLY**
- 填0表示使用默认的连接方式
- SHM_RDONLY,表示连接操作用来只读共享内存
- **返回值:**成功返回一个指针,指向共享内存第一个字节;失败返回-1
shmdt()
- **功能:**将共享内存段与当前进程脱离
- *原型:int shmdt(const void shmaddr);
- 参数:
- shmaddr**:**由shmat所返回的指针
- 返回值:成功返回0;失败返回-1
- **注意:**将共享内存段与当前进程脱离不等于删除共享内存段
shmctl()
- **功能:**用于控制共享内存
- *原型:int shmctl(int shmid, int cmd, struct shmid_ds buf);
- 参数:
- shmid**:**由shmget返回的共享内存标识码
- cmd**:**cmd:将要采取的动作
- IPC_STAT**:把shmid_ds结构中的数据设置为共享内存的当前关联值**
- IPC_SET**:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值**
- IPC_RMID:删除共享内存段
- **返回值:**成功返回0;失败返回-1
3.shmid vs key
- 只有创建的时候用key,大部分情况用户访问共享内存,都用的是shmid
4.思考问题
- 共享内存被删除后,则其他线程直接无法通信?
- 错误
- 共享内存的删除操作并非直接删除,而是拒绝后续映射,只有在当前映射链接数为0时,表示没有进程访问了,才会真正被删除
5.共享内存使用
- 共享内存创建的时候,默认被清成全0
- 将共享内存当成一个大字符串 --> char buffer[SHM_SIZE]
- 通信双方使用shm,一方直接向共享内存中写入数据,另一方就可以立马看到对方写入的数据
- 所以共享内存是所有进程间通信(IPC)中,速度最快的,不需要过多的拷贝 --> 不需要将数据给操作系统
- 共享内存缺乏访问控制,会带来并发问题
- 但如果想一定程度的访问控制呢?–> 通过管道控制
- comm.hpp
#define PATH_NAME "/home/snowk"
#define PROJ_ID 0x233
#define SHM_SIZE 4096 // 共享内存大小,最好是页(PAGE:4096)整数倍
#define FIFO_NAME "./fifo"
#define READ O_RDONLY
#define WRITE O_WRONLY
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo(FIFO_NAME, 0666);
assert(n == 0);
(void)n;
Log("create fifo success", NOTICE) << endl;
}
~Init()
{
unlink(FIFO_NAME);
Log("remove fifo success", NOTICE) << endl;
}
};
int OpenFifo(string pathname, int flags)
{
int fd = open(pathname.c_str(), flags);
assert(fd >= 0);
return fd;
}
void CloseFifo(int fd)
{
close(fd);
}
void Wait(int fd)
{
Log("等待中...", NOTICE) << endl;
uint32_t tmp = 0;
ssize_t s = read(fd, &tmp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
}
void Signal(int fd)
{
uint32_t tmp = 1;
ssize_t s = write(fd, &tmp, sizeof(uint32_t)); // 写什么不重要,主要是通过管道来进行访问控制
assert(s == sizeof(uint32_t));
(void)s;
Log("唤醒中", NOTICE) << endl;
}
- Server
// 程序加载的时候,自动构建全局变量,调用该类的构造函数 --> 创建管道文件
// 程序退出的时候,全局变量析构,自动删除管道文件
Init init;
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof buffer, "0x%x", k);
return buffer;
}
int main()
{
// 1.创建公共的key值
key_t k = ftok(PATH_NAME, PROJ_ID);
// 2.创建共享内存 -- 建议创建一个全新的共享内存 -- 通信的发起者
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
// 3.将指定的共享内存,挂接到自己的地址空间
char *shmaddr = (char*)shmat(shmid, nullptr, 0);
// 这里就是通信逻辑,将共享内存看作一个大数组 -> char buffer[SHM_SIZE]
// 这里添加一定的访问控制(依赖管道)
int fd = OpenFifo(FIFO_NAME, READ);
while(true)
{
Wait(fd); // 写慢,读快,管道没有数据的时候,读必须等待
cout << shmaddr << endl;
if(strcmp(shmaddr, "quit") == 0)
{
break;
}
}
// 4.将指定的共享内存,从自己的地址空间去关联
int n = shmdt(shmaddr);
// 5.删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
CloseFifo(fd);
return 0;
}
- Client
int main()
{
// 1.创建公共的key值
key_t k = ftok(PATH_NAME, PROJ_ID);
// 2.获取共享内存
int shmid = shmget(k, SHM_SIZE, 0);
// 3.将指定的共享内存,挂接到自己的地址空间
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
// 通信逻辑
int fd = OpenFifo(FIFO_NAME, WRITE);
while(true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1); // 从键盘读数据,存入shm
if (s > 0)
{
shmaddr[s - 1] = 0; // 最后会键入\n,把\n替换成\0
Signal(fd); // 已经获取到数据,唤醒服务端
if(strcmp(shmaddr, "quit") == 0)
{
break;
}
}
}
CloseFifo(fd);
// 4.将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
// client不需要shmctl删除
return 0;
}
2.System V信号量
0.前情提要
- 把多个进程(执行流)看到的公共的一份资源 --> 临界资源
- 把自己的进程,访问临界资源的代码 --> 临界区
- 多个执行流,互相运行的时候互相干扰,主要是因为不加保护地访问了同样的资源(临界资源)
- 在非临界区,多个执行流互相是不影响的
- 为了更好地进行临界区的保护,可以让多执行流在任何时刻,都只有一个进程进入临界区 –> 互斥
- **原子性:**要么不做,要么做完,没有中间状态
1.信号量
- 每一个进程想进入临界资源,访问临界资源中的一部分,不能让进程直接去使用临界资源,而得先申请信号量
- 只要申请信号量成功,临界资源内部,一定给你预留了想要的资源
- 申请信号量本质是对临界资源的一种预定机制
- 信号量本质:计数器
- 申请信号量:信号量计数器-- --> P操作 --> 必须是原子的
- 释放信号量:信号量计数器++ --> V操作 --> 必须是原子的