system V
一、system V介绍
进程间通信除了通过管道,都是基于文件的通信方式,还有一种方式是:SystemV标准的进程间通信方式。SystemV是一个在OS层面专门为进程通信设计的一个方案。这些都是由计算机科学家和程序员设计的,并且需要给用户使用。
如果要给用户用,是以什么方式给用户使用的呢?在操作系统层面上,SystemV是OS内核的一部分,是为OS中多进程提供的一种通信方案。但是OS不相信任何用户,给用户提供功能的时候,采用系统调用。所以System V进程间通信,一定会存在专门用来通信的接口:system call。
因为在早期由很多的方案,但是我们需要统一使用一个方案,所以现在诞生了在统一主机内的进程间通信方案:system V方案。
system V IPC提供的通信方式有三种: 共享内存、消息队列、信号量
二 、共享内存
2.1 共享内存的原理
前面说过两个进程要通信就需要看到同一块资源。
而我们知道进程之间具有独立性,物理内存当中代码和数据也互相独立。
那么现在我们可以在物理内存中创建一个内存块,让不同的进程都能看到这个内存块,具体的做法如下:
1️⃣ 通过某种调用,在内存中创建一份内存空间。
2️⃣ 通过某种调用,让进程”挂接“到这份内存空间上。(将创建好的内存映射进进程地址空间)
在后面可能不会共享内存了。所以在不用共享内存的时候
3️⃣ 去关联(去挂接)。
4️⃣ 释放共享内存。
对于共享内存的理解:
OS内可能存在多个进程同时使用不同的共享内存来进行进程间通信,既然有多份共享内存,那么操作系统就要管理它们,按照前面学习的经验,先描述后组织,描述就是对共享内存的一系列属性进行描述,而后用数据结构组织起来,这样对共享内存的管理变成了对数据结构的操作。
2.2 共享内存接口
2.2.1 创建共享内存shmget
shmget
:用来创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
RETURN VALUE
On success, a valid shared memory identifier is returned.
On errir, -1 is returned, and errno is set to indicate the error.
参数说明:
size
:共享内存的大小。
shmflag
:通常有两种选项:IPC_CREAT、IPC_EXCL。
IPC_CREAT: 共享内存如果不存在,则创建,不存在则获取。
IPC_EXCL: 无法单独使用
IPC_CREAT | IPC_EXCL: 如果不存在就创建,如果存在就出错返回(保证共享内存是新创建的)。
如果shmflag是0默认就是IPC_CREAT。
key
:保证看到同一份共享内存,能进行唯一性标识(就像省份证号码一样,数字不重要,只用来标识唯一性)。通过ftok函数转化。
如果创建成功返回共享内存的标识符,如果失败则返回-1。
ftok
:用来形成key
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
RETURN VALUE
On success, the generated key_t value is returned.
On failure -1 is returned, with errno indicating the error as for the stat(2) system call.
ftok第一个参数是自定义路径名,第二个参数是自定义的项目ID。最后唯一的共享内存ID就是通过路径名+项目ID来标识。最后生成的返回值并不重要,只要它能生成一个值来唯一标识这块共享内存就可以。
只要参数不变,生成的返回值就不会变。 这样就可以让两个进程拥有同一个key,就可以用key找到同一份共享内存。
key_t GetKey()
{
key_t n = ftok(PATHNAME, PROJ_ID);
if(n == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
return n;
}
int Creatshm(key_t key)
{
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
return shmid;
}
int Getshm(key_t key)
{
int shmid = shmget(key, SIZE, IPC_CREAT | 0666);
if(shmid == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
return shmid;
}
key的理解:
上面也说过了OS会把内存块管理起来,共享内存=物理内存块+共享内存的相关属性。描述共享内存时就有一个字段struct shm中有key。 一个进程创建内存块后把key值写进相关属性中,而另一个进程拿着key值遍历相关属性查找。这样就完成了两个进程共享一块内存块。
我们发现key和shmid都是标识内存块的:key是在内核标识唯一性,而shmid是在用户层标识唯一性,这样即使操作系统有什么变化也不会影响用户使用,充分的解耦。他们的关系就类似于fd与inode。
2.2.2 查看IPC资源
共享内存的生命周期不是随着进程的,而是随着OS的,这也是所有system V进程间通信的特征。
ipcs -m
查看共享内存
ipcs -q
查看消息队列
ipcs -s
查看信号量
ipcs
三个一起查看
ipcrm -m shmid
删除共享内存
2.2.3 共享内存的控制shmctl
shmctl
:控制共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
它的作用是对共享内存的控制。
参数介绍:
shmid
:控制共享内存的标识符。
cmd
:控制的种类,主要用的是IPC_RMID(立即移除共享内存)。
buf
:控制共享内存的数据结构,设置为空即可。
返回值
:0表示返回成功,-1表示失败。
void Delshm(int shmid)
{
if(shmctl(shmid, IPC_RMID, nullptr) == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
}
2.2.4 共享内存的关联shmat
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数介绍:
shmaddr
:可以指定虚拟内存,设置为nullptr即可。
shmflg
:读取权限,默认为0。
返回值
:返回共享内存的起始地址。
void* Attachshm(int shmid)
{
void* mem = shmat(shmid, nullptr, 0);
if((long long)mem == -1L)// 64位指针8字节
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
return mem;
}
2.2.5 共享内存的去关联shmdt
去关联是指把进程和共享内存之间的映射关系删掉(修改页表),并不是删除共享内存。
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
它的参数就是在shmat返回的参数。
成功返回0,失败返回-1。
void Detachshm(void* start)
{
if(shmdt(start) == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
}
2.3 进程间通信
comm.hpp
#ifndef _COMM_HPP_
#define _COMM_HPP_
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#define PATHNAME ".."
#define PROJ_ID 12345
#define SIZE 4096
key_t GetKey()
{
key_t n = ftok(PATHNAME, PROJ_ID);
if(n == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
return n;
}
int Creatshm(key_t key)
{
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
return shmid;
}
int Getshm(key_t key)
{
int shmid = shmget(key, SIZE, IPC_CREAT | 0666);
if(shmid == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
return shmid;
}
void Delshm(int shmid)
{
if(shmctl(shmid, IPC_RMID, nullptr) == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
}
void* Attachshm(int shmid)
{
void* mem = shmat(shmid, nullptr, 0);
if((long long)mem == -1L)// 64位指针8字节
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
return mem;
}
void Detachshm(void* start)
{
if(shmdt(start) == -1)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
assert(false);
}
}
#endif
shm_a.cc
#include "comm.hpp"
int main()
{
key_t k = GetKey();
// 创建共享内存
int shmid = Creatshm(k);
// 挂接
char* start = (char*)Attachshm(shmid);
//使用
while(true)
{
if(strlen(start) > 4)
{
printf("%s\n", start);
}
if(strlen(start) == 4)
{
break;
}
sleep(1);
}
// 去关联
Detachshm(start);
// 删除共享内存
Delshm(shmid);
return 0;
}
shm_b.cc
#include "comm.hpp"
int main()
{
key_t k = GetKey();
// 获取共享内存
int shmid = Getshm(k);
// 挂接
char* start = (char*)Attachshm(shmid);
// 使用
std::string str = "hello shm";
int cnt = 0;
while(cnt != 8)
{
snprintf(start, SIZE, "%s[%d]", str.c_str(), cnt);
cnt++;
sleep(1);
}
std::string cmd = "stop";
snprintf(start, SIZE, "%s", cmd.c_str());
// 去关联
Detachshm(start);
return 0;
}
2.4 共享内存的特性
1️⃣ 共享内存是所有的进程间通信速度最快的。(优点)
2️⃣ 共享内存不提供任何同步或者互斥机制,不提供不代表不需要,所以需要程序员自行保证数据的安全!这也造成了共享内存在多进程中是不太安全的。(缺点)
3️⃣ 共享内存的生命周期是随OS的,而不是随进程的,这是所有System V进程间通信的共性。
为什么速度快呢?
管道和共享内存:考虑键盘输入,和显示器输出,对于同一份数据,共享内存有几次数据拷贝,管道有几次数据拷贝:
管道:
可以看到写到管道需要拷贝两次,而另一个进程读也需要两次,所以一共四次。如果考虑到输入输出外设,那么就是六次。
共享内存:
直接写入共享内存,直接从共享内存输出,所以是两次。考虑到输入输出的话就是四次。
2.5 共享内存的大小
这里讲的是shget
的第二个参数,一般建议设置成4096(4KB)的整数倍,因为系统分享内存是以4KB为单位,假如我们申请的是4097,那么系统就会直接向上取整,也就是4096*2,但是我们只能使用其中的4097大小。
三、消息队列
3.1 消息队列的概念
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法,读端和写端公用一个队列,每个数据块就是队列的一个节点,每个数据块都会有个记录类型的数据,来判断该数据块该被哪个进程读取。
3.2 消息队列接口
3.2.1 消息队列的获取msgget
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
这里的msgflg跟共享内存的两个参数一摸一样(IPC_CREAT、IPC_EXCL)。
返回值
:msgget函数返回的一个有效的消息队列标识符
3.2.2 消息队列的控制msgctl
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数说明:
msqid
:消息队列的标识符。
其他的参数跟shmctl一样。
3.2.3 消息队列发送数据msgsnd
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数说明:
msqid
:消息队列的用户级标识符。
msgp
:表示待发送的数据块(输出型参数)。
msgsz
:表示所发送数据块的大小
msgflg
:表示发送数据块的方式,一般默认为0即可
成功返回0,失败返回-1
3.2.4 消息队列获取msgrcv
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg)
参数跟上面msgsnd一致。
四、信号量
4.1 信号量概念
信号量本质上就是个计数器,它统计的是公共资源资源还剩多少。
公共资源:可以被多个进程同时访问的资源,而如果访问没有被保护的公共资源,就会导致数据不一致问题(一个进程还在写的时候另一个进程就开始读)。所以公共资源需要保护,被保护起来的资源称为临界资源。而访问这些临界资源的那部分代码称为临界区。其他的代码就称为非临界区。
保护公共资源:同步和互斥
互斥:当有多个进程想要访问同一份资源的时候,我们只允许一个进程访问,当这个进程访问完了,下一个进程才能访问。
原子性:要么不做,要么做完,只有这两种状态的情况。
既然我们想让多个进程看到同一个计数器,那么信号量也是个公共资源。
4.2 信号量的用处
举个例子,假设我们要去看电影,而里面的座位只有我们买了票才能拥有,这里的票就是信号量,我们买了一张后,信号量就--
。
所以当我们想要某种资源的时候,我们可以进行预定(买票成功)。
共享资源可以作为一个整体使用或者划分成多个子资源部分。大部分都是整体使用,比如果管道。
如果我们申请成功,相当于我们预定了共享内存中的一小部分资源。如果不成功,就不能访问共享资源,以达到保护其他进程的目的。
在访问公共资源前要先申请信号量,而信号量本身就是个公共资源,那么信号量也带保护自己的安全。那么如何保证呢?
4.3 信号量的pv操作
信号量操作本质上就是计数器的++
或者--
,是原子性的。当我们预定资源的时候(--
)称为p操作,释放资源(++
)称为v操作。
如果信号量的初始值是1就代表了访问公共资源作为一个整体来使用,一个进程申请了别的进程就不能再申请了,我们把只有两种状态的信号量称为二元信号量。
4.4 信号量接口
4.4.1 信号量申请semget
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数介绍:
nsems
:表示申请信号量的个数
第一个和第三个参数就跟前面的shmget相同。
返回值
:信号量集创建成功时,semget函数返回的一个有效的信号量集标识符
4.4.2 信号量控制semctl
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
参数介绍:
semid
:信号量标识符
semnum
:信号量的下标,默认0
4.4.3 信号量操作semop
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数介绍:
sembuf
:一个结构体
sem_num
:信号量下标。
sem_op
:1表示++
p操作,-1表示--
v操作。
sem_flg
:选项设为0即可。
nsops
:有多少个sembuf结构体。
五、总结
我们可以发现,共享内存、消息队列、信号量接口相似度非常高,尤其是获取与删除,这些都是system V标准的进程间通信。
他们的第一个成员都是ipc_perm
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 */
};
我们可能会申请共享内存、消息队列、信号量的任意一种,那么操作系统如何组织它们呢?
因为ipc_perm
是一样的,所以可以维护一个struct ipc_perm*
的指针数组,存共享内存、消息队列、信号量它们的第一个元素的地址,也就是&ipc_perm
。
而我们知道结构体的第一个成员的地址和结构体对象的地址在数字上是相等的。
所以当我们想使用的时候直接强转成想用的结构体(共享内存、消息队列、信号量)就可以。