System V提供的通信方式有三种:System V共享内存,System V消息队列,System V信号量,共享内存和消息队列主要为了传递数据,而信号量主要为了控制进程间的同步和互斥问题。System V共享内存是最快的通信形式,原因在提到原理之后就方便理解了。
一、System V共享内存
原理
之前提到,进程间通信的本质是让不同的进程看到同一块资源,共享内存是怎么看到同一份资源的呢?其实我们在进程地址空间已经提到过一次了,就是共享区,共享区是堆栈之间的一部分,动态库的加载也在里面,具体共享内存怎么做呢?
1.申请资源,也就是内存,在物理内存上开辟一块空间这块就是共享内存,通过页表映射到虚拟地址空间中的共享区中,完成挂接,返回给task_struct的首地址,对两个进程同时挂接这样两个进程就看到了同一份资源!两个进程的虚拟地址不一定顶相同但是可以映射到同一块物理内存!
其实这里是两个核心操作:1.申请物理内存 2.挂接到虚拟内存 会有对应的系统调用接口
我们发现,一旦映射成功,之后的通信都与内核无关,进程不需要执行内核的系统调用接口来从用户态到内核态,直接交换数据即可,而管道是要从用户缓冲区拷贝到内核缓冲区再从内核缓冲区拷贝到用户缓冲区的!所以,System V共享内存的通信效率一定远高于管道!
释放共享内存
建立的过程是申请 + 挂接,那么释放共享内存呢?
一定是先去关联,再释放(引用计数问题),因为一块共享内存可能由多个进程共享,比如A、B、C,A不需要了,将A虚拟地址去关联,然后将引用计数减1,发现不为0,就不释放内存,当B、C都不需要了再释放,如果先释放,可能还会有进程关联,就会触发段错误等问题。
当然也一定会有对应的系统调用接口来完成这一操作,用户调用接口,实现是操作系统完成的。
内核数据结构
一个时间可能有很多进程在通信,就会有很多块共享内存,操作系统一定要管理,怎么管理?先描述再组织。存在的对应的数据结构来维护共享内存---struct shmid_ds
cpp
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 */
};
所以申请共享内存前,一定是先创建对应的数据结构!
类似pid用于区分不同的进程,共享内存在申请时都会有一个唯一的key值在系统内标识该共享内存的唯一性,位于struct ipc_perm中,这个key值后面还会讲。
cpp
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
调用接口shmget
上面说了,建立的过程是申请 + 挂接,先来看申请的系统调用接口, shmget

先来看三个参数和返回值:
key: 保证让不同的进程看到同一个共享内存,知道共享内存存在还是不存在,这个key就是上面提到的key!
size_t size:创建共享内存的大小,单位是字节
shmflg: 选项,也是宏的形式
IPC_CREAT : 如果申请的共享内存不存在就创建,存在就获取并且返回
IPC_EXCL | IPC_CREAT:如果申请的共享内存不存在就创建,存在就出错返回
确保如果我们申请成功了一个共享内存,这个共享内存一定是一个新的!
IPC_EXCL 不单独使用!
如果要创建共享内存,要带上权限,比如IPC_EXCL | IPC_CREAT | 0666
返回值: 一个整数,成功返回一个共享内存标识符,失败返回-1
这里有两个问题了:1.key具体是干什么的,2.这个返回值shmid和key的区别
key的作用
1.key是一个数字,是多少不重要,关键在于它必须在内核中具有唯一性,能够让不同的
进程进行唯一性标识
2.第一个进程可以通过key创建共享内存,第二个之后的进程,只需要拿着同一个key就可以和第一个进程看到同一个共享内存。
3.对于一个已经创建好的共享内存,key在哪 ? key在共享内存的描述对象中,struct ipc_perm
4.第一次创建的时候,必须有一个key了--怎么有? ? ?
ftok函数,提供的接口ftok
这是一套算法,对pathname和proj_id进行了数值计算
所以,这两个由用户自由指定 ! ! 返回值就是创建的key

key和返回值shmid的区别?一个是ftok的返回值,一个是shmget的返回值
key:操作系统内标定唯一性
shmid: 只在你的进程内,来表示资源的唯一性! ! !
ipcs和ipcrm
共享内存的生命周期是随内核的!用户不主动关闭,共享内存会一直存在。
除非内核重启(用户释放) 即使关了Xshell也不会消失!
可以通过指令查看:
ipcs -m查看共享内存,可以看到perms、key、owner、shmid等信息,其中perms是权限
ipcrm -m + shmid 释放共享内存
挂接、去关联、控制接口
分别是三个函数shmat shmdt shmctl



先来看shmat:void * shmat(int shmid,const void * shmaddr,int shmflg)
shmaddr:挂接到共享区的什么位置,设为nullptr,让系统来决定
shmflg:默认的权限,设为0即可
shmid:shmget的返回值
返回值:调用成功返回映射到当前进程地址空间中的起始地址,失败返回-1,注意,C语言中的void*表示万能类型,需要用什么类型强转即可。
shmdt(const void* shmaddr)去关联,就是把上面的返回值传入就是去关联了。
int shmctl(int shmid,int cmd,struct shmid_ds* buf)
先介绍选项cmd,一共有三个:
1.IPC_RMID,删除共享内存段,注意这里是检查引用计数是否减到0,不是直接强制删除,第三个参数给nullptr即可
2.IPC_STAT,此时buf相当于输出型参数,带出这个数据结构
3.IPC_SET,设置为自己给定的shmid_ds
通信
有了这些接口就可以进行通信了,理解了还是比较简单
一旦有了共享内存,挂接到自己的地址空间后,你直接把他当成你的内存空间来使用即可,不需要调用系统调用。
服务端
cpp
#include"comm.hpp"
int main()
{
int shmid = CreateShm();
char* shmaddr = (char*)shmat(shmid,nullptr,0);
_log(Debug,"attach shm done!,shmaddr: 0x%x",shmaddr);
while(1)
{
cout << "client say@" << shmaddr << endl;
sleep(1);
}
shmdt(shmaddr);//去关联
shmctl(shmid,IPC_RMID,nullptr);//释放空间
return 0;
}
服务端:
cpp
#include"comm.hpp"
int main()
{
int shmid = GetShm();
char* shmaddr = (char*)shmat(shmid,nullptr,0);
int pos = 0;
while(1)
{
cout << "Please Enter@ ";
fgets(shmaddr,4096,stdin);
}
shmdt(shmaddr);
return 0;
}
comm.hpp
cpp
#include"comm.hpp"
int main()
{
int shmid = GetShm();
char* shmaddr = (char*)shmat(shmid,nullptr,0);
int pos = 0;
while(1)
{
cout << "Please Enter@ ";
fgets(shmaddr,4096,stdin);
}
shmdt(shmaddr);
return 0;
}
就可以完成通信了。
特性
1.共享内存没有同步互斥之类的保护机制,管道是有的
2.共享内存是所有的进程间通信中,速度最快的!!!拷贝少,可以用管道来实现同步
3.共享内存内部的数据,由用户自己维护
二、System V消息队列
这里只简单介绍,了解即可。整体上和System V共享内存的接口异曲同工
原理
必须让不同进程看到同一份资源. 文件缓冲区/内存块/队列
1.必须让不同进程看到同一个队列
2.允许不同的进程,向内核中发送带类型的数据块
让A进程<--以数据块的形式发送数据-->B进程
接口
操作系统要管理消息队列,也有对应的结构体!先描述再组织
int msgget(key_t key,int msgflg)--获取消息队列
msgflg: IPC_CREAT IPC_EXCL 同理和shmget()
key: 让不同的进程看到同一份资源! key的创建---- ftok函数
返回值: 消息队列标识符 和 shmget()的返回值类似
msgctl --- shmctl
描述的结构体 里面都有struct ipc_perm -里面有key
int msgctl(int msgid,int cmd,struct msqid_ds*buf); ----shmid_ds
IPC_RMID 删除 IPC_STAT 获取属性
int msgsnd(int msgid,const void* msgp,size_t msgsz,int msgflg)
struct msgbuf{
long mytype;类型
char mtext[1];
}
msgrcv ---接收
ssize_t msgrcv(int msgid,void *msgp,size_t msgsz,long msgtyp,int msgflg)
ipcs -q 查看消息队列 ipcs -m 查看共享内存
ipcrm -q msgid 删除消息队列
三、System V信号量
这里也是先涉及一些概念,先了解,线程部分还会提到这个问题,并且会有对应代码的编写。
共享内存的问题
当A进程正在写入了一部分,就被B进程读取了,导致双方发和收的数据不完整
--数据不一致 共享内存没有任何保护机制
一些概念:
1.A B看到的同一份资源--共享资源,如果不加保护,会导致数据不一致
2.加锁--互斥访问 --任何时刻,只允许一个执行流访问共享资源-互斥
3.共享的,任何时刻只允许一个执行流访问的资源--临界资源--一般是内存空间
4.访问临界资源的代码--临界区
例子
信号量(也称为信号灯的本质)一把计数器,类似int cnt,不真的是int,用cnt来举例
描述资源中资源数量的多少!
可实现N个执行流同步访问,对于临界资源时N = 1!
int cnt = 15; int number = cnt--;申请资源,实际上--cnt不是线程安全的,在底层汇编是三句语句。
cnt <= 0 资源被申请完了! 再有执行流申请,失败!
1.申请计数器成功,就表示具有访问资源的权限了
2.申请了计数器资源,没有访问,申请了计数器资源是对资源的预定机制
3.计数器可以有效保证进入共享资源的执行流的数量!
4.每一个执行流,想访问共享资源中的一部分的时候,不是直接访问,而是
先申请计数器资源, 这个计数器称为"信号量"
我们把值只能为1,0两态的计数器叫做二元信号量--本质就是一个锁!
其实就是将资源不要分成很多块了,而是当作一个整体,整体申请,整体释放。
PV操作
申请信号量:对计数器--,P操作
释放资源,释放信号量,本质是对计数器进行++,V操作
申请和释放PV操作---原子的
原子的:要么不做,要做就做完--两态的
没有"正在做"的概念
信号量本质是一把计数器,PV操作,原子的
执行流申请资源,必须先申请信号量资源,得到信号量之后,才能访问临界资源
信号量值1,0两态的,二元信号量,就是互斥功能
申请信号量的本质:是对临界资源的预定机制!
接口
System V信号量
信号量的接口和上面的类似 semget semctl struct semid_ds{};
XXXid_ds{
struct ipc_perm XXX_perm
}
int semget(key_t key,int nsems,int semflg)
semctl...
semop(int semid,struct sembuf* spos,unsigned nsops);
信号量凭什么是进程间通信的一种?
1.通信不仅仅是通信数据,互相协同也是
2.要协同,本质也是通信,信号量首先要被所有的通信进程看到