Linux 学习笔记14 共享内存
为什么要学习共享内存?
假设正在直播,此时不使用共享内存,而是使用管道,则会造成如下场景:
考虑到用户之间如果需要通信,则每次增加一个用户,所需的管道数量将成倍增加。
此时,则需要使用共享内存。
共享内存原理如下:
一部电影,采用共享内存,则可以供多个用户使用。
共享内存概念:
system V IPC 机制下的共享内存本质是一段特殊的内存区域,进程间需要共享的数据被放在该共享内存区域中,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。这样一个使用共享内存的进程可以将信息写入该空间,而另一个使用共享内存的进程又可以通过简单的内存读操作获取刚才写入的信息,使得两个不同进程之间进行了一次信息交换,从而实现进程间的通信。
共享内存允许一个或多个进程通过同时出现在它们的虚拟地址空间的内存进行通信,而这块虚拟内存的页面被每个共享进程的页表条目所引用,同时并不需要在所有进程的虚拟内存都有相同的地址。进程对象对于共享内存的访问通过 key(键)来控制,同时通过 key 进行访问权限的检查。
函数学习:
- key_t ftok(const char *pathname, int proj_id);
函数 ftok 用于创建一个关键字(key),可以用该 key 关联一个共享内存。
参数解析:
pathname 为一个全路径文件名,并且该文件必须可访问。
proj_id 通常传入一非 0 字符
通过 pathname 和 proj_id 组合可以创建唯一的 key
如果调用成功,返回一关键字,否则返回-1
//依赖当前目录下的文件生成key,一般这个接口不用,自己设置一个key即可
'代码如下:
#include <func.h>
int main(int argc,char * argv[])
{
ARGS_CHECK(argc,2);
key_t key;
key=ftok(argv[1],1);
printf("key=%d\n",key);
return 0;
}
执行效果如下:
2. int shmget(key_t key, size_t size, int shmflg);
如:int shmid=shmget(1000,4096,IPC_CREAT|0600) //0600可读可写
参数解析:
key_t key:1000//key值,可自己设置,作为自己共享内存的钥匙,也可以用 ftok 函数产生,也可以是 IPC_PRIVATE(用于创建一个只属于创建进程的共享内存,主要用于父子通信),表示总是创建新的共享内存段
size_t size:指定的共享内存大小,填4K的整数倍,因为一个物理页大小 4K
shmflg:IPC_CREAT|0600 //shmflg是一个掩码合成值,IPC_CREAT 表示如果不存在该内存段,则创建它。IPC_EXCL 表示如果该内存段存在,则函数返回失败结果(-1)。如果调用成功,返回内存段标识。
ipcs //ipcs命令查看当前系统的共享资源
ipcrm -m shmid //删除指定共享内存
shmget代码如下:
#include <func.h>
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
return 0;
}
执行效果如下:’
- void *shmat(int shmid, const void *shmaddr, int shmflg); //链接 shm attach
参数解析:
shmid:填之前定义的shmid的返回值
shmaddr:堆内存的某一个起始地址,通常填NULL,OS会自动找为我们一块可以映射的连续堆空间。注意:虚拟地址一定是连续的,但是共享的那一块物理内存不一定是连续的。
shmflg:权限(位标识),填0。默认可读可写
shmat代码如下:
#include <func.h>
//共享内存链接
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
char *p=(char*)shmat(shmid,NULL,0);//进程为共享内存分配的起始地址p
ERROR_CHECK(p,(char *)-1,"shmat");
//p[0]='H';
strcpy(p,"I am Friday\n");
return 0;
}
执行效果如下:
用户使用 shmat 读取共享内存时,代码如下:
#include <func.h>
//读取共享内存链接后的内容
//
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
char *p=(char*)shmat(shmid,NULL,0);
ERROR_CHECK(p,(char *)-1,"shmat");
puts(p);//读取共享内存中的p到屏幕上
return 0;
}
执行效果如下:
- 父子(亲缘)关系中使用共享内存。父进程作为主播(写),子进程作为用户(读)的场景,
代码如下:
#include <func.h>
//共享内存和多进程
//常用做法:先创建shm,后链接shmat,再fork()
//
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
char *p=(char*)shmat(shmid,NULL,0);
ERROR_CHECK(p,(char *)-1,"shmat");
if(!fork())
{//子进程读
printf("Child Mark1 %s\n",p);
exit(0);
}else
{//父进程写
strcpy(p,"Dad Mark1");
wait(NULL);
printf("programe over\n");
}
return 0;
}
执行效果如下:
- 父子进程同时对共享内存进行读,写操作——验证共享内存读写具有危险性。
代码如下:
#include <func.h>
#define N 10000000
//父子进程同时对共享内存做加法(写)
//测试是否能够成功等于20000000
//验证共享内存读写具有危险性
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
int *p=(int*)shmat(shmid,NULL,0);
ERROR_CHECK(p,(int *)-1,"shmat");
p[0]=0;
int i;
if(!fork())
{//子进程写
for(i=0;i<N;i++)
{
p[0]=p[0]+1;
}
exit(0);
}else
{//父进程写
for(i=0;i<N;i++)
{
p[0]=p[0]+1;
}
wait(NULL);
printf("ret=%d\n",p[0]);
}
return 0;
}
执行效果如下:
Q:为什么两个进程同时对共享内存进行写,无法加成两千万
A:因为p[0]=p[0]+1不是原子操作,要通过PV操作,保护共享内存的读写。
原理如下:
cpu在还未将加的总数回写到内存时,就结束了时间片。因此最终结果不等于2千万。
- 关于shm 中的 nattach(链接共享内存的用户数) 进行探讨
#include <func.h>
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
char *p=(char*)shmat(shmid,NULL,0);
ERROR_CHECK(p,(char *)-1,"shmat");
strcpy(p,"I am Friday\n");
while(1);//有进程链接
//ipcs时nattch为1
return 0;
}
执行效果如下:
执行之后,连接数从0 变成了 1。
- int shmdt(const void *shmaddr); //解除链接 shm detaches
参数解析: shmaddr 通常为 shmat 的成功返回值。
shmdt 代码如下:
#include <func.h>
//shmdt
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
char *p=(char*)shmat(shmid,NULL,0);
ERROR_CHECK(p,(char *)-1,"shmat");
strcpy(p,"I am Friday\n");
int ret;
sleep(10);//十秒后解除共享内存和进程的链接
ret=shmdt(p);
ERROR_CHECK(ret,-1,"shmdt");
while(1);//10秒之后,程序还在运行,但已经和共享内存断开连接了
return 0;
}
执行效果如下:
- shmdt 的参数 shmaddr 不能发生偏移。
反例,代码如下:
#include <func.h>
//shmdt错误示例
//验证将链接的地址p偏移后再shmdt将产生错误
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
char *p=(char*)shmat(shmid,NULL,0);
ERROR_CHECK(p,(char *)-1,"shmat");
strcpy(p,"I am Friday\n");
int ret;
p++;//指针不能偏移,一旦偏移shmdt会报错
ret=shmdt(p);
printf("ret=%d,errno=%d\n",ret,errno);
while(1);
return 0;
}
执行效果如下:
shmdt使用错误时,nattch 数并未减少,并且产生了错误码 errno 22。
- int shmctl(int shmid, int cmd, struct shmid_ds *buf);
cmd:主要用删除功能IPC_RMID,IPC_STAT,IPC_SET。
de *buf:当传递时,需要自己定义一个结构体,因为这个接口时shmctl(2),2代表系统调用,从内核中取信息,必须自己定义好一个空间,存放从内核取出的信息。 例子:shmctl_stat
例子:shmctl_rmid.c &&shmat_while1.c
资源(共享内存)有人在用,或者没人在用(nattach数目无论是否为0),都可以删除
区别在于, shmctl为标记删除
case1有人在用时(nattch不为0时),删除后status会改为dest(destroyed),共享内存由在用的那个进程最后删除。
case2:没有人在用(nattch为0时),会直接将共享内存删除。
IPC_RMID代码如下:
#include <func.h>
//shmctl删除示例
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
//删除
int ret=shmctl(shmid,IPC_RMID,NULL);//删除时最后一个参数填NULL
ERROR_CHECK(ret,-1,"shmctl");
return 0;
}
执行效果如下:
- 当创建一个共享内存大小为4096时,第二次想获取这个内存但是size大于4096,会获取失败
代码如下:
#include <func.h>
//当创建一个共享内存大小为4096时,第二次想获取这个内存但是size大于4096,会获取失败
//例子:获取8192会失败
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,8192,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
return 0;
}
执行效果如下:
- IPC_SET 和 IPC_STAT (较少用)
结构体
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 /
…
};
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_STAT 代码如下:
#include <func.h>
//shmctl stat例子
//
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
struct shmid_ds buf;//自己定义一个结构体buf
int ret=shmctl(shmid,IPC_STAT,&buf);//获取地址
ERROR_CHECK(ret,-1,"shmctl");
printf("cuid=%d,mode=%o,size=%ld,nattch=%ld\n",buf.shm_perm.cuid,\
buf.shm_perm.mode,buf.shm_segsz,buf.shm_nattch);
return 0;
}
执行效果如下:
IPC_SET 代码如下:
#include <func.h>
//shmctl stat例子
//
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(1000,4096,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
struct shmid_ds buf;
int ret=shmctl(shmid,IPC_STAT,&buf);//获取stat
ERROR_CHECK(ret,-1,"shmctl");
printf("cuid=%d,mode=%o,size=%ld,nattch=%ld\n",buf.shm_perm.cuid,\
buf.shm_perm.mode,buf.shm_segsz,buf.shm_nattch);
buf.shm_perm.mode=0666;//更改权限
ret=shmctl(shmid,IPC_SET,&buf);//更改mode
ERROR_CHECK(ret,-1,"shmctl");
return 0;
}
执行效果如下:
- 为了防止共享内存被多人操作,限制权限,在亲缘关系中操作,使用IPC_PRIVATE。又称作shmget私有方式创建。
代码如下:
#include <func.h>
//共享内存只让亲缘方式使用
//PRIVATE
int main(int argc,char * argv[])
{
int shmid;//存储创建的共享内存id
shmid=shmget(IPC_PRIVATE,4096,IPC_CREAT|0600);//IPC_PRIVATE私有方式创建
//private不能获取,只能创建shm
ERROR_CHECK(shmid,-1,"shmget");
printf("shmid=%d\n",shmid);
return 0;
}
执行效果如下:
由此可见,私有方式创建共享内存,只能创建,无法获取。