进程间通信之共享内存与信号量
System V进程间通信
system V 进程通信是一组在 Unix 和类 Unix 系统中用于进程间通信的机制,主要三种方式:共享内存、消息队列与信号量
今天我们一起来对共享内存进行详细的学习,并了解信号量的基本概念
1. 共享内存 shm
我们常说:进程间通信的本质实际上就是让不同进程看到同一份资源。在之前,我们学习了基于文件的进程间通信(管道),今天,我们来学习基于内存的进程间通信:
1.1 共享内存的基本实现逻辑
我们可以在物理内存申请一块空间,然后让两个不同的进程通过页表的映射将这个相同的物理内存映射到自己的虚拟地址空间,这样,这两个不同的进程就看到相同的内存资源了。如图:
同时我们也应该清楚,在操作系统中,肯定有很多对进程都在使用共享内存进程通信,因此共享内存绝对不止一个,所以操作系统肯定要对这些共享内存做管理,如何管理:
先描述,再组织:将描述共享内存的各种属性(如编号、大小)封装成内核数据结构,这样,操作系统对共享内存的管理就变成了对共享内存内核数据结构的增删查改
此外,由于共享内存在内存中不只存在一个,那么,如何实现让通信双方打开的是同一个共享内存呢?
- 给共享内存提供唯一的标识符key
- 通信双方在打开共享内存通信前就要获得共享内存的key
关于如何获取key,创建、查看、操作、删除共享内存,以及如何用共享内存进行通信,就是我们接下来要讨论的话题:
1.2 共享内存的创建
通过系统调用来创建共享内存:
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
-
key
:即共享内存在内核中的唯一标识符 -
size
:申请的共享内存的大小(单位为Byte)- 需要注意:操作系统给共享内存分配空间时,是以4096字节(4KB)为单位进行分配的。如果用户定义的size为4000字节,那么尽管系统给共享内存分配了4096字节,但是自己只能使用4000字节,相当于浪费了96字节
- 因此,size的大小应该是4096字节(4KB)的整数倍
-
shmflg
:操作选项,利用位图进行操作:IPC_CREAT
:如果共享内存不存在,就创建,存在就返回IPC_EXCL
:单独使用没意义IPC_CREAT | IPC_EXCL
:如果共享内存不存在,就创建,存在就出错返回- 权限值
-
返回值:
-
如果成功,就返回共享内存的唯一标识符
shmid
-
如果失败,就返回-1
1.2.1 key的获取
通过系统调用来获取key
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
这个系统调用通过用户给定的pathname
和proj_id
,利用特殊的算法得到唯一的key值
pathname
:这个文件应该是存在且可访问,同时应该尽量稳定,不会被轻易删除或移动proj_id
:是一个非零整数- 返回值:
- 如果成功,则返回
key
- 如果失败,则返回-1
- 如果成功,则返回
看到这里,大家可能会有一个疑惑:既然key是共享内存在内核中的唯一标识符,那为什么不由操作系统自动创建,而要由用户自己调用系统调用来创建呢?
首先我们要清楚,两个进程要通过共享内存进行通信,首先要打开相同的共享内存,而为了确保打开的是相同的共享内存,就需要在通信前这两个进程就要获得相同的
key
如果
key
由操作系统自动创建,那么就做不到在通信前就将这个key
发送给两个进程(OS怎么知道这两个进程要通信了)而如果
key
由用户通过系统调用自己创建,那么双方就可以提前约定好pathname
和proj_id
,从而生成相同的key,这样就可以在通信时根据相同的key来打开相同的共享内存,从而进行通信了
1.3 共享内存的查看
通过命令查看系统当前存在的共享内存:
ipcs -m
例如:
#include <iostream>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string>
#include <cstring>
#include <unistd.h>
//将key转换成16进制
std::string toHex(key_t key)
{
char buffer[100] = {0};
snprintf(buffer, sizeof(buffer) - 1, "0x%x", key);
return buffer;
}
int main()
{
key_t key = ftok(".", 1);
if (-1 == key)
{
printf("get key error: %s", strerror(errno));
exit(1);
}
std::cout << "key: " << toHex(key) << std::endl;
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL); //创建共享内存,如果存在就出错返回
if (-1 == shmid)
{
printf("get shm error: %s", strerror(errno));
exit(2);
}
std::cout << "shmid: " << shmid << std::endl;
std::cout << "process will exit......" << std::endl;
sleep(5);
return 0;
}
运行并查看共享内存:
从中,我们可以看到一个现象:通过ipcm -m
查看,发现虽然进程已经退出了,但是由这个进程创建的共享内存任然存在。这说明:
和管道不一样,共享内存是内核级的,并不会随着进程的退出而销毁
接下来我们来分析通过ipcs -m
能看到共性内存的信息:
-
key
:即共享内存内核中的唯一标识符 -
shmid
:共享内存的唯一标识符 -
owner
:共性内存的拥有者 -
perms
:共享内存的权限,如果要修改共享内存的权限,在shmget
的参数shmflg
后加上权限即可,例如:int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666); //设置共享内存的权限为666
-
nattch
:挂载到该共享内存的进程个数 -
status
:共享内存的状态
1.3.1 key和shmid
看到这里,可能又有小伙伴会有疑惑:key和shmid都是共享内存的唯一标识符,二者之间有什么不同吗?
事实上,尽管二者都是共享内存shm的唯一标识符,但二者之间的用途却不同:
key
:是内核级别的唯一标识符,即操作系统识别共享内存的唯一标识符shmid
:是用户层面的唯一标识符,之后**通过系统调用操控共享内存时,用的都是shmid
**而不是key
1.4 共享内存的挂载/取消挂载
前面我们说过,要利用共享内存进行通信,仅仅在物理内存申请一块内存空间是不够的,我们还需要通过操作将这块空间通过页表映射到通信进程的虚拟地址空间,这一过程就叫做挂载
通过系统调用进行挂载/取消挂载:
#include <sys/shm.h>
void *shmat(int shmid, const void *_Nullable shmaddr, int shmflg);
int shmdt(const void *shmaddr);
shmid
:共享内存的唯一标识符shmaddr
:填nullptr即可shmflg
:填0即可- 返回值:
- 如果成功,就返回共享内存被映射到虚拟地址空间的起始地址
- 如果失败,就返回
(void*)-1
int shmdt(const void *shmaddr);
:即从调用进程的地址空间中分离位于 shmaddr 指定地址的共享内存段。
1.5 共享内存的操控
利用系统调用来操控共享内存:
#include <sys/shm.h>
int shmctl(int shmid, int op, struct shmid_ds *buf);
shmid
:共享内存的唯一标识符op
:要进行操作的选项buf
:一个指向shmid_ds
结构体的指针,结构体shmid_ds
定义在<sys/shm.h
中- 返回值:失败返回-1
例如:
#include <iostream>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string>
#include <cstring>
#include <unistd.h>
//将key转换成16进制
std::string toHex(key_t key)
{
char buffer[100] = {0};
snprintf(buffer, sizeof(buffer) - 1, "0x%x", key);
return buffer;
}
int main()
{
key_t key = ftok(".", 1);
std::cout << "key: " << toHex(key) << std::endl;
int shmid = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);
std::cout << "shmid: " << shmid << std::endl;
std::cout << "shm create success\n" << std::endl;
//获取共享内存的信息
struct shmid_ds shmidDs;
int ret = shmctl(shmid, IPC_STAT, &shmidDs);
printf("key: %d, nattch: %ld\n",shmidDs.shm_perm.__key, shmidDs.shm_nattch);
//挂载共享内存
char* buffer = (char*)shmat(shmid, nullptr, 0);
if ((void*)-1 != buffer)
std::cout << "shm attach success" << std::endl;
std::cout << "shm will detattch" << std::endl;
sleep(5);
//取消挂载共享内存
ret = shmdt(buffer);
if (0 == ret)
std::cout << "shm detattch success" << std::endl;
sleep(3);
//删除共享内存
ret = shmctl(shmid, IPC_RMID, nullptr);
std::cout << "shm delete success" << std::endl;
std::cout << "process exit" << std::endl;
return 0;
}
运行结果:
除了通过系统调用shmctl(shmid, IPC_RMID, nullptr)
,同样可以通过命令来删除共享内存
ipcrm -m [shmid]
1.6 利用共享内存实现进程间的通信
代码如下:
common.hpp
#include <iostream>
#include <cstring>
#include <cstdio>
#include <string>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 1
#define SIZE 4096
#define MODE 0666
int _create_shm(key_t key, size_t size, int shmflg)
{
int shmid = shmget(key, size, shmflg);
return shmid;
}
std::string toHex(key_t key)
{
char buffer[1024] = {0};
snprintf(buffer, sizeof(buffer) - 1, "0x%x", key);
return buffer;
}
key_t get_key(const char* pathname = PATHNAME, int proj_id = PROJ_ID)
{
size_t key = ftok(pathname, proj_id);
if (-1 == key)
{
std::cerr << "get_key error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(1);
}
std::cout << "get_key success, key: " << toHex(key) << std::endl;
return key;
}
//用于服务端:
//创建共享内存,如果已经存在,就出错返回
int create_shm(key_t key, size_t size)
{
int shmid = _create_shm(key, size, IPC_CREAT | IPC_EXCL | MODE);
if (-1 == shmid)
{
std::cerr << "shmid error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(2);
}
std::cout << "create shm success, shmid: " << shmid << std::endl;
return shmid;
}
//用于客户端
//获取服务端已经创建好的共享内存
int get_shm(key_t key, size_t size)
{
int shmid = _create_shm(key, size, IPC_CREAT);
if (-1 == shmid)
{
std::cerr << "get_shm error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(3);
}
std::cout << "get shm success, shmid: " << shmid << std::endl;
return shmid;
}
void delete_shm(int shmid)
{
int ret = shmctl(shmid, IPC_RMID, nullptr);
if (-1 == ret)
{
std::cerr << "delete_shm error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(4);
}
std::cout << "delete shm success" << std::endl;
}
void* attach_shm(int shmid)
{
void* address = shmat(shmid, nullptr, 0);
if (address == (void*)-1)
{
std::cerr << "aattch_shm error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(5);
}
std::cout << "aattch shm success" << std::endl;
return address;
}
int disattach_shm(void* address)
{
int ret = shmdt(address);
if (-1 == ret)
{
std::cerr << "disattach_shm error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(6);
}
std::cout << "disattach shm success" << std::endl;
return 0;
}
shm_server.cc
#include "common.hpp"
int main()
{
std::cout << "I am server" << std::endl;
//获取key
key_t key = get_key();
//创建共享内存
int shmid = create_shm(key, SIZE);
//挂载共享内存
char* buffer = (char*)attach_shm(shmid);
std::cout<<std::endl;
while(1)
{
std::cout << "client message: " << buffer << std::endl;
sleep(1);
}
//取消挂载
disattach_shm((void*)buffer);
sleep(5);
//删除共享内存
delete_shm(shmid);
return 0;
}
shm_client
#include "common.hpp"
int main()
{
std::cout << "I am client" << std::endl;
//获取key
key_t key = get_key();
//创建共享内存
int shmid = get_shm(key, SIZE);
//挂载共享内存
char* buffer = (char*)attach_shm(shmid);
//开始通信 write
for (char ch = 'A'; ch <= 'Z'; ch++)
{
buffer[ch - 'A'] = ch;
sleep(1);
}
//取消挂载
disattach_shm((void*)buffer);
sleep(5);
return 0;
}
结果如图:
从运行的结果我们可以看出:服务端(读端)还没等客户端写,就已经开始读取共享内存的数据,客户端(写)都已经推出了,服务端(读)还没有推出。
从中我们可以得出结论:
共享内存这种进程间通信的方式并不提供同步机制(写端还没写或着还没写完,读端就要阻塞等待),这可能会导致数据错乱与不一致
为了避免出现数据错乱,不一致的问题,需要用户来实现通信的同步机制,其中一种方法就是使用管道来实现(管道天生就是同步的):
重写代码如下:
fifo.hpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PATH "FIFO"
#define MODE 0666
class fifo
{
public:
fifo(const std::string& name)
: _name(name)
{
int ret = mkfifo(_name.c_str(), MODE);
if (-1 == ret)
{
std::cerr << "mkfifo error, " << "errno: " << errno << ", errorstring: " << strerror(errno) << std::endl;
exit(-1);
}
std::cout << "fifo made success" << std::endl;
}
~fifo()
{
unlink(_name.c_str());
}
private:
const std::string _name;
};
common.hpp
#include <iostream>
#include <cstring>
#include <cstdio>
#include <string>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#include "fifo.hpp"
#define PATHNAME "."
#define PROJ_ID 1
#define SIZE 4096
#define MODE 0666
int _create_shm(key_t key, size_t size, int shmflg)
{
int shmid = shmget(key, size, shmflg);
return shmid;
}
std::string toHex(key_t key)
{
char buffer[1024] = {0};
snprintf(buffer, sizeof(buffer) - 1, "0x%x", key);
return buffer;
}
key_t get_key(const char* pathname = PATHNAME, int proj_id = PROJ_ID)
{
size_t key = ftok(pathname, proj_id);
if (-1 == key)
{
std::cerr << "get_key error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(1);
}
std::cout << "get_key success, key: " << toHex(key) << std::endl;
return key;
}
//用于服务端:
//创建共享内存,如果已经存在,就出错返回
int create_shm(key_t key, size_t size)
{
int shmid = _create_shm(key, size, IPC_CREAT | IPC_EXCL | MODE);
if (-1 == shmid)
{
std::cerr << "shmid error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(2);
}
std::cout << "create shm success, shmid: " << shmid << std::endl;
return shmid;
}
//用于客户端
//获取服务端已经创建好的共享内存
int get_shm(key_t key, size_t size)
{
int shmid = _create_shm(key, size, IPC_CREAT);
if (-1 == shmid)
{
std::cerr << "get_shm error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(3);
}
std::cout << "get shm success, shmid: " << shmid << std::endl;
return shmid;
}
void delete_shm(int shmid)
{
int ret = shmctl(shmid, IPC_RMID, nullptr);
if (-1 == ret)
{
std::cerr << "delete_shm error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(4);
}
std::cout << "delete shm success" << std::endl;
}
void* attach_shm(int shmid)
{
void* address = shmat(shmid, nullptr, 0);
if (address == (void*)-1)
{
std::cerr << "aattch_shm error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(5);
}
std::cout << "aattch shm success" << std::endl;
return address;
}
int disattach_shm(void* address)
{
int ret = shmdt(address);
if (-1 == ret)
{
std::cerr << "disattach_shm error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(6);
}
std::cout << "disattach shm success" << std::endl;
return 0;
}
//用于实现同步机制
class Syc
{
public:
//用于读端sever,等待写端的唤醒
bool wait_message()
{
_rfd = open(PATH, O_RDONLY);
if (_rfd < 0)
{
std::cerr << "open rfd error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(7);
}
int code = 0;
int n = read(_rfd, &code, sizeof(code));
if (n == sizeof(code)) //如果收到了client端发来的唤醒码,就说明写端已经写完了,读端被唤醒
{
std::cout << "server weak up" << std::endl;
return true;
}
else if (n == 0)
{
return false;
}
else
{
std::cerr << "read error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(8);
}
}
//用于写端client,用于唤醒读端server
void weak_up()
{
int _wfd = open(PATH, O_WRONLY);
if (_wfd < 0)
{
std::cerr << "open wfd error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(9);
}
//向读端server发送唤醒码
int code = 0;
int n = write(_wfd, &code, sizeof(code));
if (n != sizeof(code))
{
std::cerr << "write error, " << "errno: " << errno << ", errnostring: " << strerror(errno) << std::endl;
exit(10);
}
}
private:
int _rfd = 0;
int _wfd = 0;
};
shm_server
#include "common.hpp"
int main()
{
std::cout << "I am server" << std::endl;
//获取key
key_t key = get_key();
//创建共享内存
int shmid = create_shm(key, SIZE);
//挂载共享内存
char* buffer = (char*)attach_shm(shmid);
std::cout<<std::endl;
//开始通信 read
//引入命名管道,实现同步
fifo named_pipe(PATH);
Syc syc;
while(1)
{
//如果成功被唤醒,就输出写端信息
if (syc.wait_message())
{
std::cout << "client message: " << buffer << std::endl;
}
else
break;
}
//取消挂载
disattach_shm((void*)buffer);
sleep(5);
//删除共享内存
delete_shm(shmid);
return 0;
}
shm_client
#include "common.hpp"
int main()
{
std::cout << "I am client" << std::endl;
//获取key
key_t key = get_key();
//创建共享内存
int shmid = get_shm(key, SIZE);
//挂载共享内存
char* buffer = (char*)attach_shm(shmid);
//开始通信 write
//引入命名管道,实现同步
Syc syc;
while(1)
{
std::cout << "client message #";
memset(buffer, 0, sizeof(buffer));
fgets(buffer, 4095, stdin);
if (std::string(buffer) == "quit\n")
break;
syc.weak_up(); //写端写完,唤醒读端
sleep(1);
}
//取消挂载
disattach_shm((void*)buffer);
sleep(5);
return 0;
}
结果如图:
1.7 共享内存的优缺点
缺点:
共享内存不提供通信的同步机制,这可能会导致数据的不一致问题
优点:
共享内存是所有进程间通信方式中最快的
- 因为其直接将共享内存通过页表映射到了自己的虚拟地址空间,可以直接对其进行读和写,而不需要频繁地使用系统调用
read()、write()
,从而节省了时间开销
2. 信号量(信号灯) sem
在学习信号量之前,我们先来明确几个概念:
同步与互斥:
对共享资源进行保护,是一个多执行流场景下,一个比较常见和重要的话题(例如共享内存),保护的方式一般分为两种:
- 同步:对于同一份资源,允许不同进程在安全的前提下,以一定的顺序进行访问
- 互斥:对于同一份资源,任何时候都只允许一方来进行访问
原子性:
原子性是指:一个操作只有两种状态,要么已经执行完毕,要么还没有执行。不存在执行中的状态
临界资源与临界区:
临界资源:被保护起来的,任何时刻只允许一个执行访问流访问的公共资源
临界区:用来访问临界资源的代码就叫临界区
从程序员的角度来看,对临界资源的保护其实就是对访问临界资源的代码即临界区的保护
2.1 信号量的基本概念
信号量的本质是对资源的预定机制:
- 对于某一份资源,不一定要我持有,才是我的
- 只要我预定了这份资源,那在未来的某一段时间,这段资源就一定会被我持有
- 被我预定的资源,只允许我访问,不会被并发访问。除非我主动释放,才允许其他人访问
假设一个场景:
对于一块较大的资源,如果我们将其当作一个整体,那么当要访问这块资源进行读写操作时,由于要确保数据的一致性和稳定性,一般只同时允许一个进程读,一个进程写,这样对这块资源的利用率显然不高
为了提高资源利用率,操作系统提供了这样的办法:可以将一块较大的资源分成许多小块,这样就允许多对进程对这些小块资源进行访问,从而也就提高了资源的利用率,但是这里也面临了两个问题:
- 如何控制访问资源的进程的数量?
- 如何合理分配资源?
而信号量就是用来解决第一个问题的:
信号量实际上就是一个计数器,用来描述一块临界资源可以被访问的进程数量
- 如果一个进程要访问这块资源,就要申请信号量,如果成功,代表获得了这块资源的访问权限,同时信号量进行
--
操作- 如果一个进程要停止访问这块资源,就要释放信号量,此时信号量就要进行
++
操作
注:如果一个信号量的值只能取0/1
,那么就相当于只允许一个进程对这块临界资源进行访问,其他资源都要等待,这就实现了互斥
2.2 信号量的实现
看到这里,可能有小伙伴会想:既然信号量实际上就是一个计数器,那么我是不是可以直接在代码里用一个计数器count
来实现?
答案当然是不可以!!!:
原因有两点:
- 写时拷贝
- 首先我们要清楚:如果有多个进程要访问同一块临界资源,且这块临界资源被一个信号量所管理,那么为了确保数据的安全性,这些进程就要看到这个信号量,即:这些进程看到了信号量这一公共的资源
- 如果有用普通的代码计数器来当作信号量,那么当一个进程申请信号量时,就要对信号量(计数器)进行
--
操作,而由于这个计数器是多个进程共享的资源,那么当一个进程对其进行修改时,就会发生写时拷贝,这就会导致其他的进程的计数器并不会被影响
- 代码计数器
count
的++、——
操作不是原子性的
- 前面就说过,原子性指的是一个操作,要么已经执行完毕,要么还没执行。代码的
++
操作一般分为三步(--
同理):读取当前的值、对读取的值进行+1操作,最后返回。这是一个过程,并不符合原子性的条件- 信号量的
++、--
操作必须是原子性的,这样才能确保数据的统一
因此,信号量的++、--
操作都交给操作系统来实现,一般,我们将信号量的--
操作称为P
操作,++
操作称为V
操作,统称信号量的PV
操作
2.3 查看、获取信号量
通过命令来查看信号量:
ipcs -s
通过系统调用来获取信号量
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
key
:信号量集合的键值,可由ftok()
获取nsems
:要创建的信号量的个数semlfg
:标志参数IPC_CREAT
:如果共享内存不存在,就创建,存在就返回IPC_EXCL
:单独使用没意义IPC_CREAT | IPC_EXCL
:如果共享内存不存在,就创建,存在就出错返回- 权限值
- 返回值:
- 成功:返回信号量集和标识符
- 失败:返回-1
2.4 控制信号量
通过系统调用来控制信号量
#include <sys/sem.h>
int semctl(int semid, int semnum, int op, ...);
-
semid
:信号量集合标识符 -
semnum
:要控制哪一个信号量(信号量集和下标从0开始) -
op
:操作选项- IPC_RMID:删除信号量集合
- ………………
如果要删除信号量,也可以通过命令ipcrm -s [semid]
删除指定信号量集和
2.5 信号量的PV操作
通过系统调用来对信号量进行PV操作
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
-
semid
:信号量结合标识符 -
nsops
:要对哪一个信号量进行操作(下标) -
sops
:一个结构体,定义了操作方法,其包含的内容如下:unsigned short sem_num; /* semaphore number 及上面的nsops */ short sem_op; /* semaphore operation P操作为-1 V操作为1*/ short sem_flg; /* operation flags 0即可*/
-
返回值:失败返回-1
本篇完