目录
三、system V(IPC)
3.1 system V共享内存
3.1.1 共享内存的概念
3.1.2 共享内存的原理
3.1.3 创建共享内存(shmget )
3.1.4 ftok函数
3.1.5 查看共享内存资源
3.1.6 创建共享内存测试代码
3.1.7 再次理解共享内存
3.1.8 释放共享内存(shmctl)
3.1.9 关联共享内存(shmat)
3.1.10 去关联共享内存(shmdt)
3.1.11 使用共享内存实现serve&client通信
3.1.12 共享内存的优缺点
3.1.12 共享内存的内核数据结构
3.2 System V消息队列(了解)
3.3 System V信号量(了解)
三、system V(IPC)
system V 用于本地通信,通信的本质就是让不同的进程看到同一份资源
system V IPC提供的通信方式有以下三种:
- system V共享内存
- system V消息队列
- system V信号量
3.1 system V共享内存
3.1.1 共享内存的概念
让不同的进程看到同一块内存块,这块内存块就叫共享内存,并且共享内存区是最快的IPC形式
3.1.2 共享内存的原理
共享内存让不同进程看到同一份资源的方式就是,在物理内存当中申请一块内存空间,然后将这块内存空间分别与各个进程各自的页表之间建立映射,再在虚拟地址空间当中开辟空间并将虚拟地址填充到各自页表的对应位置,使得虚拟地址和物理地址之间建立起对应关系,至此这些进程便看到了同一份物理内存,这块物理内存就叫做共享内存,共享内存申请好之后会把共享内存的起始地址返回给用户
未来不想通信(即关闭通信):
- 去关联:取消进程和内存的映射关系
- 释放共享内存
对共享内存的理解:
- 共享内存,是专门设计的,是用于IPC,与malloc、new出来的内存完全不一样,这个共享内存是进程都可以看得到的,而malloc、new 出来的内存只对自己的进程开放,是私有的,其他进程看不到这块内存
- 共享内存是一种通信方式,所有想通信的进程都可以使用
- OS内一定可能同时存在大量的的共享内存
3.1.3 创建共享内存(shmget )
共享内存的建立大致包括以下两个过程:
- 在物理内存当中申请共享内存空间
- 将申请到的共享内存挂接到地址空间,即建立映射关系
创建共享内存需要用 shmget 函数(系统调用)
man 2 shmget 查看一下
解释:
函数:shmget
shm: shared memory
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数声明:
int shmget(key_t key, size_t size, int shmflg);
参数:
第一个参数key: 这个共享内存段名字,表示待创建共享内存在系统当中的唯一标识
第二个参数size: 共享内存大小
第三个参数shmflg: 表示创建共享内存的方式,用法和创建文件时使用的mode模式标志是一样的
返回值:
成功,返回一个有效的共享内存标识符(用户层标识符)
失败,返回-1,错误码被设置
第三个参数shmflg 可使用的选项:
常用的选项只有两个:IPC_CREAT 和 IPC_CREAT
- IPC_CREAT:创建新的共享内存,如果共享内存已经存在,就获取已经存在的共享内存并返回它
- IPC_CREAT:无法单独使用,通常与 IPC_CREAT 一起使用,即(IPC_CREAT | IPC_CREAT),如果共享内存已经存在则报错,也就是说如果创建成功,这个共享内存一定是新建的
第一个参数key: 表示共享内存的唯一性,这个很重要
如何获取 key? 要使用函数 ftok
3.1.4 ftok函数
ftok函数用于获取keuy
man 3 ftok 查看一下:
解释:
函数:ftok
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
函数声明:
key_t ftok(const char *pathname, int proj_id);
参数:
第一个参数pathname: 路径名
第一个参数proj_id: 一个整数标识符
注:这两个参数都可以随便给
返回值:
成功key被返回
失败返回-1,错误码被设置
3.1.5 查看共享内存资源
Linux当中,我们可以使用 ipcs 命令查看有关进程间通信的相关资源信息,单独使用 ipcs 命令时,会默认列出消息队列、共享内存以及信号量相关的信息
若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
- q:列出消息队列相关信息
- -m:列出共享内存相关信息
- -s:列出信号量相关信息
比如查看共享内存:ipcs -m
ipcs 命令输出的每列信息的含义如下:
3.1.6 创建共享内存测试代码
代码如下:
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "."
#define PROJ_ID 0x666
#define MAX_SIZE 4096
int main()
{
//获取key
key_t k = ftok(PATHNAME, PROJ_ID);
if(k < 0)//失败
{
// cin cout cerr -> stdin stdout stderr -> 对应文件描述符:0 1 2
cerr << errno << ":" << strerror(errno) << endl;
}
//打印key值
printf("key: %x\n", k);
//创建共享内存
int shm = shmget(k, MAX_SIZE, IPC_CREAT | IPC_EXCL);
if(shm < 0)//创建失败
{
cerr << errno << ":" << strerror(errno) << endl;
exit(-1);
}
//打印shm
printf("shm: %d\n", shm);
return 0;
}
运行结果
ipcs -m 查看一下
再次运行
从这里可以看出:程序结束,共享内存还没有被释放,说明共享内存的声明周期是随OS的,不随进程结束而结束
共享内存既然创建了,也需要释放(去关联),下面讲
3.1.7 再次理解共享内存
共享内存不是普通的内存(即不是malloc、new出来的),共享内存有自己的属性,OS也会存在很多的共享内存,它们都需要被OS管理
怎么管理共享内存?先描述,再组织
共享内存 = 物理内存块 + 共享内存的相关属性
创建共享内存时,如何保证共享内存的唯一性??通过 key
进行通信的时候,两个进程也要看到同一块共享内存,如何保障另一个进程也看到同一块共享内存??只有让另一个进程看到相同的一个 key 就可以保证两个进程可以看到同一块共享内存
key是要被设置进共享内存的属性里面的,用来表示唯一的共享内存
shmget 创建共享内存成功返回一个有效的共享内存标识符,这里暂且叫它 shmid,那这个shimid 和 key 有什么区别呢??
shimid 和 key 有点像 fd 和 inode
shmid -> fd
key -> inode
文件在内核里面是用 inode 标识的,而上层不使用inode,而是使用fd,所以 fd 是给上层使用的,同理shmid 和 key也是如此;
key 是在内核中表示共享内存的,上层也是不使用 key,而是使用 shmid,这么对比可以很好理解
所以,我们尝试删除共享内存
ipcrm -m 命令用于删除共享内存
从测试结果看,使用key删除不了共享内存,必须使用 shmid 才可以,这也反映了上层使用的是shmid,而不是key
ipcrm -m shmid
3.1.8 释放共享内存(shmctl)
共享内存释放,有两个方法,一就是使用命令(ipcrm)释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放,shnctl 系统调用就是用于控制共享内存(包括控制或设置共享内存的属性,共享内存的释放)
man 2 shmctl 查看一下
解释:
函数: shmctl
shmctl: shared memory control
头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
第一个参数shmid,表示所控制共享内存的用户级标识符
第二个参数cmd,表示具体的控制动作
第三个参数buf,用于获取或设置所控制共享内存的数据结构
返回值:
成功,返回0
失败返回-1,错误码被设置
第二个参数cmd的选项有:
- IPC_STAT:获取共享内存的当前关联值,此时参数buf作为输出型参数
- IPC_SET:在进程有足够权限的前提下,将共享内存的当前关联值设置为buf所指的数据结构中的值
- IPC_RMID:删除共享内存
常用的选项就是第三个:IPC_RMID ,删除共享内存
第三个参数buf 一般不使用,传nullptr 即可,它是用来控制共享内存的数据结构的
共享内存的数据结构下面谈
测试删除共享内存:
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "."
#define PROJ_ID 0x666
#define MAX_SIZE 4096
int main()
{
//获取key
key_t k = ftok(PATHNAME, PROJ_ID);
if(k < 0)//失败
{
// cin cout cerr -> stdin stdout stderr -> 对应文件描述符:0 1 2
cerr << errno << ":" << strerror(errno) << endl;
}
//打印key值
printf("key: %x\n", k);
//创建共享内存
int shmid = shmget(k, MAX_SIZE, IPC_CREAT | IPC_EXCL);
if(shmid < 0)//创建失败
{
cerr << errno << ":" << strerror(errno) << endl;
exit(-1);
}
//打印shm
printf("shm: %d\n", shmid);
//间隔10s删除共享内存
sleep(10);
//释放共享内存
int ret = shmctl(shmid, IPC_RMID, nullptr);
if(ret == -1)//删除失败
{
cerr << errno << ":" << strerror(errno) << endl;
}
return 0;
}
监控脚本
while :; do ipcs -m; sleep 2; done
运行结果,共享内存已被删除
上面所做的工作是创建和删除共享内存,但是共享内存还没有与进程关联起来,进程无法使用,所以下面谈共享内存的关联
3.1.9 关联共享内存(shmat)
shmat 函数的功能:将共享内存段连接到进程地址空间,即关联共享内存
man 2 shmat 查看一下:
解释:
函数: shmat
at:attach
头文件:
#include <sys/types.h>
#include <sys/shm.h>
函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
第一个参数shmid,表示待关联共享内存的用户级标识符
第二个参数shmaddr,指定共享内存映射到进程地址空间的某一地址,通常设置为NULL,表示让内核自己决定一个合适的地址位置
第三个参数shmflg,表示关联共享内存时设置的某些属性
返回值:
成功,返回共享内存映射到进程地址空间中的起始地址(成功返回一个指针,指向共享内存第一个节)
败,返回(void*)-1,错误码被设置
第三个参数shmflg跟读写权限有关,通常设置为0,表示默认为读写权限
进行测试:
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "."
#define PROJ_ID 0x666
#define MAX_SIZE 4096
int main()
{
//获取key
key_t k = ftok(PATHNAME, PROJ_ID);
if(k < 0)//失败
{
// cin cout cerr -> stdin stdout stderr -> 对应文件描述符:0 1 2
cerr << "ftok: " << errno << ":" << strerror(errno) << endl;
exit(-1);
}
//打印key值
printf("key: %x\n", k);
//创建共享内存
int shmid = shmget(k, MAX_SIZE, IPC_CREAT | IPC_EXCL);
if(shmid < 0)//创建失败
{
cerr << "shmget: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
//打印shm
printf("shm: %d\n", shmid);
//关联共享内存
void* men = shmat(shmid, nullptr, 0);
if(men == (void*)-1)//关联失败
{
cerr << "shmat: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
//间隔10s删除共享内存
sleep(10);
//释放共享内存
int ret = shmctl(shmid, IPC_RMID, nullptr);
if(ret == -1)//删除失败
{
cerr << "shmctl: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
return 0;
}
运行结果,在关联共享内存的时候权限被拒绝
这是为什么??
这是因为我们使用 shmget 函数创建共享内存时,并没有对创建的共享内存设置权限,所以创建出来的共享内存的默认权限为0,即什么权限都没有,因此server进程没有权限关联该共享内存
perme 便是该共享内存的权限,我们创建的共享内存的权限为0,
nattch 是关联共享内存的进程数量,n代表数量,attch代表关联
所以我们需要给创建共享内存时加上权限:
再次编译运行,共享内存关联成功
3.1.10 去关联共享内存(shmdt)
取消共享内存与进程地址空间之间的关联我们需要用 shmdt 函数
man 2 shmdt 查看:
解释:
函数:shmdt
dt: detach
头文件与shmat一致
函数原型:
int shmdt(const void *shmaddr);
参数:传入shmat所返回的指针,即共享内存的起始地址
返回值
成功,返回0
失败,返回-1,错误码被设置
进行测试:
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "."
#define PROJ_ID 0x666
#define MAX_SIZE 4096
int main()
{
//获取key
key_t k = ftok(PATHNAME, PROJ_ID);
if(k < 0)//失败
{
// cin cout cerr -> stdin stdout stderr -> 对应文件描述符:0 1 2
cerr << "ftok: " << errno << ":" << strerror(errno) << endl;
exit(-1);
}
//打印key值
printf("key: %x\n", k);
//创建共享内存
int shmid = shmget(k, MAX_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)//创建失败
{
cerr << "shmget: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
//打印shm
printf("shm: %d\n", shmid);
//关联共享内存
void* men = shmat(shmid, nullptr, 0);
if(men == (void*)-1)//关联失败
{
cerr << "shmat: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
//使用和其他操作
sleep(5);
//去关联共享内存
int n = shmdt(men);
if(n == -1)
{
cerr << "shmdt: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
//间隔5s删除共享内存
sleep(5);
//释放共享内存
int ret = shmctl(shmid, IPC_RMID, nullptr);
if(ret == -1)//删除失败
{
cerr << "shmctl: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
return 0;
}
运行结果,共享内存去关联成功
3.1.11 使用共享内存实现serve&client通信
已经知道共享内存的创建、关联、去关联以及释放后,现在可以尝试让两个进程通过共享内存进行通信
共同的头文件(comm.hpp):
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "."
#define PROJ_ID 0x666
#define MAX_SIZE 4096
//获取key
key_t getKey()
{
key_t k = ftok(PATHNAME, PROJ_ID);
if(k < 0)//失败
{
// cin cout cerr -> stdin stdout stderr -> 对应文件描述符:0 1 2
cerr << "ftok: " << errno << ":" << strerror(errno) << endl;
exit(-1);
}
}
int getShmHelper(key_t k, int flags)
{
int shmid = shmget(k, MAX_SIZE, flags);
if(shmid < 0)//创建或获取失败
{
cerr << "shmget: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
}
//创建共享内存
int createShm(key_t k)
{
return getShmHelper(k, IPC_CREAT | IPC_EXCL | 0600);
}
//获取共享内存
int getShm(key_t k)
{
return getShmHelper(k, IPC_CREAT);
}
//关联共享内存
void *attachShm(int shmid)
{
void* men = shmat(shmid, nullptr, 0);
if(men == (void*)-1)//关联失败
{
cerr << "shmat: "<< errno << ":" << strerror(errno) << endl;
exit(-1);
}
return men;
}
//去关联共享内存
void detachShm(void *start)
{
if(shmdt(start) == -1)
{
cerr << "shmdt: "<< errno << ":" << strerror(errno) << endl;
}
}
//释放共享内存
void delShm(int shmid)
{
int ret = shmctl(shmid, IPC_RMID, nullptr);
if(ret == -1)//删除失败
{
cerr << "shmctl: "<< errno << ":" << strerror(errno) << endl;
}
}
服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,获取用户端发送的消息
服务端代码如下(shm_server.cpp):
#include "comm.hpp"
//服务端
int main()
{
//获取key
key_t k = getKey();
printf("key: 0x%x\n", k);
//创建共享内存
int shmid = createShm(k);
printf("shmid: %d\n", shmid);
//关联共享内存
char *start = (char*)attachShm(shmid);
printf("attach success, address start: %p\n", start);
//使用,进行通信
while(true)
{
printf("client say : %s\n", start);
sleep(1);
}
//去关联
detachShm(start);
sleep(5);
// //释放共享内存
delShm(shmid);
return 0;
}
客户端只需要直接和服务端创建的共享内存进行关联即可,客户端给服务端发送消息
客户端代码如下(shm_client.cpp):
#include "comm.hpp"
//用户端
int main()
{
//获取key,key是与服务端一致的
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 cnt = 1;
while(true)
{
snprintf(start, MAX_SIZE, "%s[pid:%d][消息编号:%d]", message, id, cnt++);//snprintf自带 '\n'
sleep(1);
}
//去关联
detachShm(start);
//done
return 0;
}
注意:删除shm ipc资源,注意,不是必须通过手动来删除,这里只为演示相关指令,删除IPC资源是进程该做的事情
服务端先运行,然后运行客户端,通信成功
ipcs 查看一下,共享内存的链接数为2
3.1.12 共享内存的优缺点
共享内存的优点:
共享内存是所有进程间通信中速度最快的(无需缓冲区,能大大减少通信数据的拷贝次数)
共享内存的缺点:
共享内存是不进行同步和互斥的,没有对数据进行任何保护。比如:如果服务端读取速度较快,用户端发送数据较慢,就会产生同一段消息被服务端读取多遍
注意:因为系统分配共享内存是以4KB为基本单位(因为内存划分内存块的基本单位Pag),一般建议申请共享内存的大小为4KB的整数倍, 4096字节(4KB), 比如你申请的共享内存的大小为 4097字节,OS会给你申请 8KB,但是你实际上可以使用等待只有 4097字节
3.1.12 共享内存的内核数据结构
共享内存的数据结构如下:
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 */
};
在用户层,OS也暴露了大部分数据结构给用户使用
共享内存唯一性的标识key 也在这个数据结构里面
3.2 System V消息队列(了解)
消息队列实际上就是在系统当中创建了一个队列,队列当中的每个成员都是一个数据块,这些数据块都由类型和信息两部分构成,两个互相通信的进程通过某种方式看到同一个消息队列,消息队列的两端都可以读写数据(简单了解即可)
消息队列的内核数据结构中也是用 key标识队列的唯一性
消息队列使用的接口,与共享内存的接口功能基本一致,函数名字不同而已
//创建消息队列
msgget
int msgget(key_t key, int msgflg);
//消息队列的控制
msgctl
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
//向消息队列发送数据
msgsnd
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//从消息队列获取数据
msgrcv
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
3.3 System V信号量(了解)
信号量主要用于同步和互斥的,也是属于通信的范畴
信号量本质是:
信号量的本质是一个计数器,通常用来表示公共资源中,资源数量多少的问题
- 公共资源:被多个进程可以同时访问的资源(比如上面的共享内存,消息队列都是公共资源)
- 访问没有受到保护的公共资源:会产生数据不一致问题(比如上面的共享内存)
- 临界资源:未来将被保护的公共资源我们叫做临界资源
- 临界区:进程有对应的代码来访问对应的临界资源,对应的代码就叫临界区
- 非临界区:进程中不访问临界资源的代码称为非临界区
- 注意:大部分的资源是独立的
- 互斥和同步:这是用来保护公共资源的(互斥:有一个进程对该公共资源进行访问了,就不允许其他进程对该资源进行访问,同步多线程再谈)
- 原子性:要么不做,要做就做完,只有两态,称为原子性
注:上面的概念只是浅谈,有些到后面解释
为什么要有信号量???
以例子进行解释,比如一个电影院,这个电影院看作是公共资源,电影院里面的每一个座位看作是共享资源划分成的一个个子资源;
我只有买了票,这个座位在一定时间内才是归属我的,即便我买了票不去,这个座位也是我的,反过来,你没买票,这个座位就一定不是你的。买的票一定对应着一个座位。
同样道理,我想用某种资源的时候,可以对这种资源进行预订,就相当于买票,买了票我就已经预订了这个座位,我预订了这个资源,它在未来的某一时间一定属于我
假设电影院的座位只有100个,你不能卖出第101张票吧,信号量就相当于这里的票一样,票卖完了就不允许再卖了,再卖就会发生 ‘座位冲突’,也就对应进程访问该资源会发生冲突
进程想要访问某个资源,就要先申请信号量,申请到了信号量就相当于预订了共享资源的某一部分小资源,就允许该进程对该资源的访问
将一块公共的资源拆分成一个个的子资源,不同的进程可以对不同的子资源进程访问,从而实现并发
而申请信号量的前提是,所有进程要看到一个相同的信号量,所以信号量本身就是公共资源
既然信号量是公共资源,是公共资源就要保证自身的安全性,可以把信号量看作是一个整数,这个整数进行 ++ 和 -- 就要保证自身的安全性,所以 ++ 和 -- 操作是原子性的
即信号量本身也是一个临界资源,它能保护其他共享资源的同时,也需要保护自己的安全,信号量内部的加加减减具有原子性
信号量为1时,说明共享资源是一整个整体使用的,提供互斥功能(别的进程不能使用),提供互斥功能的信号量也叫二元信号量
这些浅谈一下,后序详谈
注意:IPC资源必须删除,否则不会自动清除,除非重启,system V IPC资源的生命周期随内核
----------------我是分割线---------------
文章到这里就结束了,下一篇即将更新