这会是一系列文章,讲解的内容也很简单,文章的目的是让自己的知识固话和文档化,以备自己不时的复习,同时也希望能够给予初学者一些帮助。
前面的文章一系列文章有介绍了 linux 下常见的 IPC 机制,如管道、消息队列、信号量,今天这篇文章介绍一下最核心的机制,那就是共享内存,因为它是最高效的 IPC 方式。
什么是共享内存?
共享内存其实很容易理解,不同的进程共享一块内存。
我们都知道,进程间通信的难处就在于进程本身,每一个进程拥有独立的空间,所以数据交互就不方便,需要借助第三方,比如管道其实就是一个文件,消息队列也是因为放在内核中,它们都充当了中间人的角色。
共享内存也差不多,但是它有个特别的地方就是,它可以动态依附到进程空间当中,也可以动态卸载。
理解这一点是非常重要的。当一个进程加载了共享内存的时候,这一块内存就像是本身进程的一部分,和 malloc 分配一般。
共享内存的创建
和其他 IPC 手段类似,共享内存通过 shmget 获取。
#include <sys/shm.h>
int shmget (key_t key, size_t size, int shmflg)
- key 是有 ftok() 获取到的文件描述符
- size 是要分配的共享内存的大小
- shmflg 操作标志,取值有 IPC_CREAT 和 IPC_EXCL
- IPC_CREATE 如果共享内存不存在则新建,否则返回共享内存标志符
- IPC_EXCL 和 IPC_CREATE 配合使用,如果共享内存已经存在的话就返回-1,错误码 EEXIST
共享内存的添加与删除
前面讲到共享内存可以动态地添加到一个进程,也能动态卸载它。
映射
#include <sys/shm.h>
void *shmat (int shmid, const void *shmaddr, int shmflg)
- shmid 前面通过 shmget 得到的 id
- shmaddr 是共享内存的附加地址,一般为 NULL
- shmflg 操作标志位,配合 shmaddr 使用,可能取值 SHM_RAND
如果 shmaddr 为 NULL,那么 shmat 会自主选择一块空闲的内核空间,然后将地址返回
如果 shmaddr 不为 NULL, shmflg 指定为 SHM_RAND,那么共享内存的地址会附加到由 shmaddr 指定的地址的一个低端边界地址
如果 shmaddr 不为 NULL,shmflg 没有指定为 SHM_RAND,那么共享地址就是 shmaddr
shmat 的返回值就是处理后的共享内存的地址
卸载
#include <sys/shm.h>
int shmdt (const void *shmaddr)
- shamaddr 是由 shmat 返回的共享内存的地址
shmdt 返回值为 0 ,代表调用成功,返回值为 -1 代表调用失败。
共享内存的操作
#include <sys/shm.h>
int shmctl (int shmid, int cmd, struct shmid_ds *buf)
- shmid 共享内存的 id
- cmd 操作命令,取值有 IPC_RMID、IPC_SET、IPC_STAT 3 种
- buf 当 cmd 取值为 IPC_SET 时,将 buf 中的内容设置到共享内存的 shmid_ds 结构体中
cmd 取值 IPC_RMID 目的是删除内核中的共享内存
cmd 取值 IPC_SET 是为了设置共享内存的 shmid_ds 结构体
cmd 取值 IPC_STAT 查看共享内存的 shmid_ds 结构体内容
每一块共享内存都有一个对应的 shmid_ds 结构体。
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 */
};
- shm_segsz 内存的大小
- shm_nattch 多少个进程连接了这个内存
共享内存示例
一块共享内存可以供多个进程使用,但是使用不当的话,也很危险,因为共享内存本身是没有做同步控制的。
所以,经常就需要用信号量和共享内存配合使用
下面是示例代码.
需要编写 2 个代码文件,分别用于演示发送和接收。
testshmwrite.cpp
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
using namespace std;
void P(int semid)
{
struct sembuf buf = {0,-1,0};
semop(semid,&buf,1);
}
void V(int semid)
{
struct sembuf buf = {0,1,0};
semop(semid,&buf,1);
}
int get_sem_id()
{
key_t keyid = ftok("testforsem",102);
if (keyid < 0)
{
cerr << " get key failed" << endl;
return -1;
}
int semid = semget(keyid,1,IPC_CREAT|0660);
if (semid < 0)
{
cerr << "get semphore failed" << endl;
return -1;
}
return semid;
}
int main(int argc,char** argv)
{
key_t keyid = ftok ("./forshm",1);
if (keyid < 0)
{
cerr << "get keyid failed !" << endl;
return -1;
}
int shmid = shmget(keyid,1024,IPC_CREAT|0660);
if (shmid < 0)
{
cerr << "get share memory error!" << endl;
return -1;
}
cout << "shmmem keyid " << keyid;
cout << " share memory id: " << shmid << endl;
void* addr = shmat(shmid,(char*)0,0);
if (addr == (void*)-1)
{
perror("shmat");
}
cout << "shared memory addr :" << addr << endl;
int semid = get_sem_id();
if (semid < 0)
{
return -1;
}
if (semctl(semid,0,SETVAL,1) < 0)
{
cerr << "Inital sem val failed" << endl;
return -1;
}
P(semid);
cout << "I'm pid " << getpid() << " ready for writing a msg " << endl;
char* msg = "Hello shared memory!";
char* p = (char*)addr;
strcpy(p,msg);
//模拟耗时操作
sleep(10);
V(semid);
cout << "I'm pid " << getpid() << " writed a msg " << endl;
shmdt(addr);
return 0;
}
下面的是接收代码
testshmread.cpp
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
using namespace std;
void P(int semid)
{
struct sembuf buf = {0,-1,0};
semop(semid,&buf,1);
}
void V(int semid)
{
struct sembuf buf = {0,1,0};
semop(semid,&buf,1);
}
int get_sem_id()
{
key_t keyid = ftok("testforsem",102);
if (keyid < 0)
{
cerr << " get key failed" << endl;
return -1;
}
int semid = semget(keyid,1,IPC_CREAT|0660);
if (semid < 0)
{
cerr << "get semphore failed" << endl;
return -1;
}
return semid;
}
int main(int argc,char** argv)
{
key_t keyid = ftok ("./forshm",1);
if (keyid < 0)
{
cerr << "get keyid failed !" << endl;
return -1;
}
int shmid = shmget(keyid,1024,IPC_CREAT|0660);
if (shmid < 0)
{
cerr << "get share memory error!" << endl;
perror("get shmid");
return -1;
}
cout << "shmmem keyid " << keyid;
cout << " share memory id: " << shmid << endl;
char* addr = (char*)shmat(shmid,NULL,0);
if (addr == (char*)-1)
{
perror("shmat");
}
int semid = get_sem_id();
if (semid < 0)
{
return -1;
}
P(semid);
cout << "I’m pid " << getpid() << " read msg : " << addr << endl;
V(semid);
shmdt(addr);
shmctl(shmid,IPC_RMID,NULL);
semctl(semid,0,IPC_RMID);
return 0;
}
需要注意的是,我在当前目录新建了两个空文件 testforsem 和 forshm ,这是为了提供给 ftok 用的,用于获取信号量和共享内存的描述符
touch testfoorsem
touch forshm
最后,我们可以编译代码并执行
g++ testshmwrite.cpp -o shmwriter
g++ testshmread.cpp -o shmreader
./shmwriter &
./shmreader
最终结果如下:
[1] 9899
shmmem keyid 17307152 share memory id: 126156879
shared memory addr :0x7f0b165ad000
I'm pid 9899 ready for writing a msg
./shmreader
shmmem keyid 17307152 share memory id: 126156879
I'm pid 9899 writed a msg
I’m pid 10031 read msg : Hello shared memory!
[1]+ 已完成 ./shmwriter
可以看到,两个进程顺利通信了。
代码很简单,就不一一讲解了,如有疑问,可以在下方评论。
共享内存的用处
共享内存用到的地方很多,比较适合在进程间处理容量较大的数据。
在 Linux 终端运行 ipcs -m
命令,你就可以查看目前系统现存的共享内存。
ipcs -m
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 连接数 状态
0x00000000 1507328 frank 600 524288 2 目标
0x00000000 163841 frank 600 524288 2 目标
0x00000000 82345986 frank 600 393216 2 目标
0x00000000 425987 frank 600 524288 2 目标
0x00000000 819204 frank 600 524288 2 目标
0x00000000 622597 frank 600 524288 2 目标
0x00000000 720902 frank 600 524288 2 目标
0x00000000 983047 frank 700 16472 2 目标
我几年前曾在某小型手机厂商上班,当时负责 Android 系统的 Camera 应用。
系统是阿里的 YunOS,当时有一个需求,就是 Camera 要实现拍立淘的功能,这个是要调用阿里的 SDK 的。
Android 系统有自己的 Binder 通信机制,但是实时性不强,所以我当时记得阿里的拍立淘 SDK 是通过共享内存处理照片的,从而相机画面看起来不卡。