> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。> 目标:理解进程通信——system V(共享内存 | 消息队列 | 信号量)
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:Linux初阶
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
🌟前言
system V:同一主机内的进程间通信方案,在OS层面专门为进程间通信设计的方案
进程间通信的本质:让不同的进程看到同一份资源
system V标准下的三种通信方式
-
共享内存
-
消息队列
-
信号量
我们来看看 system V标准下的三种通信方式 。
⭐主体
学习【Linux学习】进程间通信——system V(共享内存 | 消息队列 | 信号量)咱们按照下面的图解:
🌙 共享内存
💫 共享内存的基本原理
概念:
共享内存:通过让不同的进程,看到通过一个内存块的方式就叫共享内存。
进程具有独立性:
内核数据结构包括对应的代码、数据与页表都是独立的。OS系统为了让进程间进行通信:
1.申请一块空间
2.将创建好的内存映射进进程的地址空间。
- 共享内存让不同的进程看到同一份的资源就是在物理内存上申请一块内存空间,如何将创建好的内存分别与各个进程的页表之间建立映射,然后在虚拟地址空间中将虚拟地址填充到各自页表的对应位置,建立起物理地址与虚拟地址的联系。
3.如果不想通信:取消进程和内存的映射关系,释放内存
- 而我们把创建好的内存称为共享内存,把进程和共享内存建立映射关系的操作称为挂接,把取消进程和内存的映射关系称为去关联
- 把释放内存称为释放共享内存。
- 共享内存的建立:在物理内存当中申请共享内存空间;将申请到的共享内存挂接到地址空间,即建立映射关系。
- 共享内存的释放:共享内存与地址空间去关联,即取消映射关系;释放共享内存空间,即将物理内存归还给系统。
对于共享内存的理解:
对比以前C语言的malloc也可以在物理内存申请空间,并把开辟好的空间经过页表映射到进程地址空间当中。
但是system V进程间通信是专门设计的,用来IPC;共享内存是一种通信方式,所有想通信的进程都可以进行使用;OS一定可能会同时存在很多的共享内存。
💫 共享内存的创建
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.
参数讲解:
- shmflg:通常被设置成两个选项: IPC_CREAT、 IPC_EXCL
IPC_CREAT:共享内存不存在,则创建,如果存在则获取;IPC_EXCL:无法单独使用,IPC_CREAT|IPC_EXCL:如果不存在就创建,如果存在就出错返回
- size:共享内存的大小
- key:保证看到同一份共享内存,能进行唯一性标识。如何去形成key:ftok
ftok:形成key
ftok作用:是通过存在的路径名pathname以及设置的标识符proj_id来形成一个key值,通过shmget创建共享内存时,key值会被填充维护共享内存的数据结构当中。
理解key:
OS一定会存在很多的共享内存,共享内存本质就是在内存中申请一块空间,而key能进行唯一标识。OS申请的,自然要做管理,共享内存也是如此,如何管理:先描述,在组织。所以共享内存=物理内存块+共享内存的相关属性。进程如果在内存中创建了共享内存,为了让共享内存在系统中保证唯一的,通过key来进行标识,只要让另一个进程也看到同一个key。而key是在哪?key作为创建共享内存时共享内存的相关属性集合,描述共享内存时就有一个字段struct shm中有key。
共享内存数据结构的第一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在shm_perm这个结构体变量当中,其中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 */
};
💫 共享内存的控制
shmctl
:控制共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数讲解:
-
shmid:共享内存id
-
cmd:控制方式,这里我们只使用IPC_RMID 选项,表示删除共享内存
-
buf:描述共享内存的数据结构
返回值:0表示成功,-1表示失败
共享内存的内核结构shmid_ds:
//man shmctl
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
💫 共享内存的关联
shmat:
关联共享内存
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数讲解:
-
shmid:共享内存id
-
shmaddr:挂接地址(自己不知道地址,所以默认为NULL)
-
shmflg:挂接方式,默认为0
返回值:挂接成功返回共享内存起始地址(虚拟地址),类似C语言malloc
💫 共享内存的去关联
shmdt
:去关联
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
RETURN VALUE
On success shmdt() returns 0; on error -1 is returned, and errno is set to indi‐cate the cause of the error.
参数讲解:
-
shmaddr:去关联内存地址,即shmat返回值
-
返回值:调用成功返回0,失败返回-1
💫 查看IPC资源
概念:
对于管道:进程退出,文件描述符就会自动释放,但是对于共享内存不一样:共享内存的生命周期是随OS的,而不是随进程的,这是所有System V进程间通信的共性。
查看共享内存:ipcs -m
删除:ipcsrm -m + shmid
💫 代码实现通信
1.comm.hpp:
#ifndef __COMM_HPP_
#define __COMM_HPP_
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
#define PATHNAME "."
#define PROJ_JD 0x66
#define MAX_SIZE 4096
key_t getkey()
{
key_t k = ftok(PATHNAME,PROJ_JD);
if(k <0)
{
cerr<<errno<<":"<<strerror(errno)<<endl;
exit(1);
}
return k;
}
int getShmHelper(key_t k,int flags)
{
//k是要shmget,设置进入共享内存属性中的,用来标识
//该共享难内存在内核中的唯一性
//shmid与key:
//fd inode
int shmid = shmget(k,MAX_SIZE,flags);
if(shmid<0)
{
cerr<<errno<<":"<<strerror(errno)<<endl;
exit(2);
}
return shmid;
}
//获取
int getShm(key_t k)
{
return getShmHelper(k,IPC_CREAT);
}
//创建
int createShm(key_t k)
{
return getShmHelper(k,IPC_CREAT | IPC_EXCL|0600);
}
void delShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,nullptr)==-1)
{
cerr<<errno<<":"<<strerror(errno)<<endl;
}
}
void * attachShm(int shmid)
{
void*mem = shmat(shmid,nullptr,0);
if((long long)mem==-1L)//64位系统,8个字节,L表示数字类型
{
cerr<<errno<<"shmat:"<<strerror(errno)<<endl;
exit(3);
}
return mem;
}
void detachShm(void * start)
{
if(shmdt(start)==-1)
{
cerr<<"shmdt:"<<errno<<":"<<strerror(errno)<<endl;
}
}
#endif
2.server.cc:
#include "comm.hpp"
#include <unistd.h>
using namespace std;
int main()
{
key_t k = getkey();
printf("key:%0x%x\n",k);
int shmid = createShm(k);
printf("shmid:%d\n",shmid);
//sleep(5);
char*start = (char*)attachShm(shmid);
printf("attach success,address start:%p\n",start);
//使用
while(true)
{
printf("client say:%s\n",start);
struct shmid_ds ds;
shmctl(shmid,IPC_STAT,&ds);
printf("获取属性:size:%d,pid:%d,myself:%d",ds.shm_segsz,ds.shm_cpid);
sleep(1);
}
//去关联
detachShm(start);
sleep(10);
//删除共享内存
delShm(shmid);
return 0;
}
3.client.cc:
#include "comm.hpp"
#include <unistd.h>
using namespace std;
int main()
{
key_t k = getkey();
printf("key:%0x%x\n",k);
int shmid = getShm(k);
printf("shmid:%d\n",shmid);
char*start = (char*)attachShm(shmid);
printf("attach success,address start:%p\n",start);
const char*message = "hello server,我是另一个进程,正在和你通信";
pid_t id = getpid();
int count = 1;
//char buffer[1024];
while(true)
{
sleep(5);
snprintf(start,MAX_SIZE,"%s[pid:%d][消息编号:%d]",message,id,count++);
// snprintf(buffer,sizeof(buffer),"%s[pid:%d][消息编号:%d]",message,id,count++);
// memcpy(start,buffer,strlen(buffer)+1);
}
detachShm(start);
return 0;
}
💫 共享内存的特点
概念:
共享内存的生命周期是随OS的,而不是随进程的,这是所有System V进程间通信的共性。
共享内存的优点:
共享内存是所有进程间通信速度是最快的,因为共享内存是被双方所共享,只要写入对方就能立即看到,能大大减少数据的拷贝次数。
总结:
综合考虑管道和共享内存,考虑键盘输入,和显示器输出,对于同一份数据:共享内存有几次数据拷贝,管道有几次数据拷贝。
管道:需要通过键盘输入到自己定义的缓冲区char buffer[],将数据拷贝到buffer中,调用write接口在把buffer里的数据拷贝到管道里,
另一进程也有定义buffer缓冲区,调用read读取把数据从管道里读取到buffer里,在把数据显示到显示器上:
图解:
共享内存的缺点:
不给我们进行同步和互斥的操作,没有对数据做任何保护。客户端和服务端没做保护,如果想做保护要用到信号量,对共享内存进行保护,写完通过读端进行读取。
🌙 消息队列(了解)
💫 消息队列的概念
消息队列是OS提供的内核级队列,消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法,每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。
💫 消息队列数据结构
结构如下:
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 last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
消息队列数据结构的第一个成员是msg_perm
,它和shm_perm
是同一个类型的结构体变量,ipc_perm
结构体的定义如下:
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 */
};
💫 消息队列相关函数
msgget:获取消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
RETURN VALUE
If successful, the return value will be the message queue identifier (a nonnegative integer), otherwise -1 with errno indicating the error.
参数讲解:
- key:ftok函数生成一个key值,这个key值作为msgget函数的第一个参数
- msgflg:与创建共享内存时使用的shmget函数的第三个参数相同。
返回值:msgget函数返回的一个有效的消息队列标识符
msgctl:控制消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
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
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);
- msqid:表示消息队列的用户级标识符。msgp:表示获取到的数据块,是一个输出型参数。
- msgsz:表示要获取数据块的大小
- msgtyp:表示要接收数据块的类型,msgflg:表示发送数据块的方式,一般默认为0即可
成功返回实际获取到mtext数组中的字节数,失败返回-1。
🌙 信号量(了解)
💫 信号量相关概念
概念:
信号量的本质是一个计数器,通常用来表示公共资源中,资源数的多少问题。信号量主要用于同步和互斥的。
公共资源:
能被多个进程同时访问的资源,访问没有保护的公共资源:数据不一致问题。要让不同的进程看到同一份资源是为了通信,通信是为了让进程间实现协同,而进程之间具有独立性,所以为了解决独立性问题要让进程看到同一份资源,但是会导致数据不一致的问题。
被保护起来的公共资源称为临界资源,而进程要使用资源一定是该进程有对应的代码来访问这部分临界资源称为临界区,但是多个进程看到同一份资源是少数情况,大部分申请自己的资源用自己的代码区访问。有临界区自然就有非临界区,不访问公共资源的代码。
如何保护公共资源:互斥&&同步
互斥:由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
原子性:要么不做、要么做完两态的这种情况。比如支付转账
如果用全局的整数来替代信号量?
全局的整数在父子关系的进程上都看不到,要发生写时拷贝,而不同的进程更看不到,所以进程间想看到同一个计数器得让进程看到同一个计数器。
为什么要信号量?
当我们想要某种资源的时候可以通过信号量进行预;,共享资源被使用的方式:作为一个整体使用;划分成为一个一个的资源部分
图解:
💫 信号量数据结构
结构:
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
信号量数据结构的第一个成员也是ipc_perm
类型的结构体变量,ipc_perm
结构体的定义如下:
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 */
};
💫 信号量pv操作
概念:
所有的进程在访问公共资源之前,都必须申请sem信号量,而申请sem信号量的前提是所有进程必须先看到同一个信号量,所以信号量本身就是公共资源,同时,信号量必须保证自身操作的安全性,++,–操作是原子
💫 信号量相关函数
semget:申请信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
RETURN VALUE
If successful, the return value will be the semaphore set identifier (a nonnegative integer), otherwise -1 is returned, with errno indicating the error.
参数讲解:
- key:使用ftok函数生成一个key值,这个key值作为semget函数的第一个参数。
- nsems:表示创建信号量的个数。
第三个参数,与创建共享内存时使用的shmget函数的第三个参数相同。
返回值:信号量集创建成功时,semget函数返回的一个有效的信号量集标识符
semctl:信号量的删除
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
semop:信号量操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
我们可以发现,共享内存、消息队列、信号量接口相似度非常高,获取与删除,都是system V标准的进程间通信。
OS如何管理:先描述,在组织,对相关资源的内核数据结构做管理,对于共享内存、消息队列、信号量的第一个成员都是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类型的成员变量,都可以通过key来标识唯一性。这样设计的好处:在操作系统内可以定义一个struct ipc_perm类型的数组,此时每当我们申请一个IPC资源,就在该数组当中开辟一个这样的结构。((struct shmid_ds*)perms[0],强转,此时就可以访问其他剩下的属性)
🌟结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。