Linux系统编程系列(16篇管饱,吃货都投降了!)
16、 Linux系统编程系列之线程池
一、什么是共享内存
共享内存是system-V三种IPC对象之一,是进程间通信的一种方式。
二、共享内存的特性
通过不同进程共享一段相同的内存(物理内存)来达到通信的目的,由于SHM对象不再交由内核托管,因此共享内存SHM对象是众多IPC方式最高效的一种方式,但也因为这个原因,SHM一般不能单独使用,而需要配合诸如互斥锁、信号量等协同机制使用。
三、共享内存的使用场景
1、进程间数据共享
比如单机上需要启动多个实例,每个实例又存在相同的、比较大的、只读或部分只读的资源数据,那么就可以将其放在共享内存里面,第一个启动的实例创建出该共享内存,后续实例直接挂载上去即可。
2、进程间通信
比如两个进程同时挂载一片共享内存,进程A写,进程B读,就是生产者和消费者模式,读写都是基于内存操作,效率非常高。
3、单进程的数据缓存
比如单机上只有一个实例,该实例需要再启动的时候加载一大块资源到内存,那么,如果基于共享内存,将资源加载到共享内存,在下次启动时,可以直接挂载使用,无需再次读入。
四、函数API接口
1、创建或打开SHM对象
// 创建或打开SHM对象 int shmget(key_t key, size_t size, int shmflg); // 接口说明: 返回值:SHM对象ID 参数key:SHM对象键值 参数size:共享内存大小 参数shmflg:创建模式和权限 IPC_CERAT:如果key对应的共享内存不存在,则创建SHM对象 IPC_EXCL:如果该key对应的共享内存已存在,则报错 权限与文件创建open类型,用八进制表示
2、映射/解除映射SHM对象
//映射 void *shmat(int shmid, const void *shmaddr, int shmflg); // 接口说明 返回值:共享内存映射后的虚拟地址入口 参数shmid:指定的共享内存的ID 参数shmaddr:指定映射后的地址,因为是虚拟地址,分配的原则要兼顾诸如段对齐、权限分配等问题,因此用户进程是无法指定的,只能由系统自动分配,因为该参数一般为NULL,表示交由系统来自动分配 参数shmflg:可选项 0:默认,代表共享内存可读可写 SHM_RDONLY:代表共享内存只读 // 解除映射 int shmdt(const void *shmaddr); // 接口说明 参数shmaddr:需要解除内存映射的虚拟地址入口
3、删除SHM对象
// 删除SHM对象 int shmctl(int shmid, int cmd, struct shmid_ds *buf); // 接口说明: 参数shmid:指定的共享内存的ID 参数cmd:一些命令字 IPC_STAT:获取共享内存的一些信息,放入结构体shmid_ds中 IPC_SET:将buf中指定的信息,设置到本共享内存中 IPC_RMID:删除指定的共享内存,此时第三个参数buf将被忽略 参数buf:用来存放共享内存信息的结构体
注:共享内存的删除仅仅是标记删除而已,当系统检测到没有任何进程正在使用他的时候共享内存才会真正被删除掉。
五、共享内存的使用步骤
1、使用ftok(),获取IPC通信对象的KEY值
2、使用shmget(),获取共享内存对象的ID
3、使用shmat(),将共享内存映射至本进程虚拟内存空间的某个区域
4、操作共享内存,像数组那样进行读取和写入
5、当不再使用时,使用shmdt()解除映射关系
6、当没有进程再需要这块共享内存时,使用命令或者函数删除它
六、案例
使用共享内存结合信号的方式来实现两个进程间进行相互收发信息
// 共享内存的案例 #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <signal.h> #define SHM_SIZE 4096 // 编译时,编译两个版本,一个版本直接编译,另外一个版本把发送信号和接收信号互换后再次编译 #define SEND_SIGNAL 34 // 发送信号 #define RECV_SIGNAL 35 // 接收信号 // 映射的虚拟地址 char *shm_addr = NULL; // 信号响应函数,用来接收消息 void recv_handler(int sig) { printf("recv data success: %s\n", shm_addr); memset(shm_addr, 0, SHM_SIZE); } int main(int argc, char *argv[]) { // 0、注册信号响应函数 signal(RECV_SIGNAL, recv_handler); // 打印自身PID,并获取对方PID int recv_pid; printf("my PID is %d\n", getpid()); printf("please input receiver PID:\n"); scanf("%d", &recv_pid); while(getchar() != '\n'); // 消除换行 // 1、获取KEY值 key_t shm_key = ftok("./", 1); if(shm_key == -1) { perror("ftok fail"); return -1; } // 2、指定共享内存,获取共享内存对象ID int shm_id = shmget(shm_key, SHM_SIZE, IPC_CREAT | 0666); if(shm_id == -1) { perror("shmget fail"); return -1; } // 3、映射共享内存 shm_addr = (char*)shmat(shm_id, NULL, 0); if(shm_addr == (void*)-1) { perror("shmat fail"); return -1; } // 4、操作共享内存 char data[32] = {0}; while(1) { printf("please input data:\n"); fgets(shm_addr, SHM_SIZE, stdin); kill(recv_pid, SEND_SIGNAL); printf("write data success: %s\n", shm_addr); } return 0; }
注:编译时,编译两个版本,一个版本直接编译,另外一个版本把发送信号和接收信号互换后再次编译。跟消息队列的案例一个意思。
七、总结
共享内存SHM对象是众多IPC对象中最高效的一种方式,与消息队列的使用步骤不同的是,共享内存需要进行映射,这就是高效的原因。可以通过结合案例来加深对共享内存的理解。