IPC对象的查看: ipcs -a
共享内存:
1> 共享内存是内核实际存在的一块物理内存
2> 共享内存是通信效率最高的通信方式
3> 共享内存的读操作主动不会阻塞(共享内存的数据不会主动消失,bzero清空后,读操作也会阻塞);
-------------------------------------
key shmid owner perms bytes nattch status
键值 id号 拥有者 权限 大小 当前使用的进程数 状态
System V 共享内存
共享内存(Shared Memory)是 System V IPC 机制中效率最高的进程间通信方式,允许多个进程直接访问同一块物理内存区域。
共享内存是内核中的一块物理内存,它由操作系统管理,可以被多个进程访问。共享内存是效率最高的通信方式,因为进程直接读写内存,无需系统调用或数据拷贝。
System V共享内存的基本概念和操作步骤:共享内存的创建通常使用 shmget
函数,连接使用 shmat
,分离使用 shmdt
,控制使用 shmctl
。本文解析了这些函数的作用与函数和使用场景;以及共享内存的特性,比如生命周期、同步问题,以及为什么读操作不会阻塞,但清空后可能阻塞。
共享内存的数据不会主动消失,这意味着数据会一直保留直到被显式删除或系统重启,但使用bzero清空共享内存后,读操作可能阻塞,这似乎矛盾。因为bzero
的作用只是将内存区域置零,但共享内存本身仍然存在。阻塞的原因则是因为进程在读取时没有数据可读,但共享内存本身并不管理数据的可用性,需要额外的同步机制,比如信号量,来协调读写操作。
共享内存是通信效率最高的通信方式,因为数据不需要在进程间复制;但其缺点在于需要手动同步,容易导致竞争条件。并且,共享内存的生命周期独立于进程,需要显式删除,否则会一直存在,可能导致资源泄漏 ——> 同步机制与异步机制不同,在使用共享内存的过程中尤其需要注意这一点,比如需要使用信号量或互斥锁来避免数据不一致。
共享内存适合大量数据交换且对速度要求高的应用,比如图像处理、科学计算等。共享内存的读操作通常不会阻塞,因为数据一旦写入就存在,但如果没有同步机制,读进程可能读到不完整的数据,或者写进程覆盖数据导致问题。
一、共享内存的核心特点
- 内核管理的物理内存
- 共享内存是内核分配的一块真实物理内存,通过系统调用映射到进程的虚拟地址空间。
- 生命周期独立于进程:即使创建它的进程终止,共享内存仍存在,需显式删除(
shmctl(..., IPC_RMID)
)。
- 通信效率最高
- 数据直接在内存中读写,无需内核中转或用户态与内核态的数据拷贝。
- 适用于高频、大数据量的通信场景(如视频处理、科学计算)。
- 读写操作的阻塞特性
- 默认不阻塞:读操作直接访问内存数据,不会因数据未就绪而阻塞。
- 清空后的阻塞:若共享内存被清空(如
bzero()
),读操作可能因无有效数据而需同步机制(如信号量)协调。
二、共享内存的操作步骤
- 创建或获取共享内存段
使用 shmget()
创建或获取共享内存标识符:
#include <sys/ipc.h>
#include <sys/shm.h>
key_t key = ftok("/tmp", 'A'); // 生成唯一键值
int shmid = shmget(key, size, IPC_CREAT | 0666);
key
:唯一标识共享内存的键值(通过ftok()
生成)。size
:共享内存大小(字节)。shmflg
:权限标志(如IPC_CREAT | 0666
)。
- 连接共享内存到进程地址空间
使用 shmat()
将共享内存映射到进程的虚拟地址空间:
char *shm_ptr = shmat(shmid, NULL, 0);
shmid
:共享内存标识符。shmaddr
:映射地址(通常设为 NULL,由内核自动选择)。shmflg
:标志(如 SHM_RDONLY 只读模式)。
- 读写共享内存
直接通过指针操作内存:
// 写入数据
sprintf(shm_ptr, "Hello Shared Memory");
// 读取数据
printf("Data: %s\n", shm_ptr);
- 分离共享内存
使用 shmdt()
断开进程与共享内存的连接:
shmdt(shm_ptr); // 分离共享内存
- 删除共享内存
使用 shmctl()
控制共享内存(如删除):
shmctl(shmid, IPC_RMID, NULL); // 标记删除,当所有进程分离后释放
三、共享内存的同步问题
由于共享内存不提供内置同步机制,需结合其他 IPC(如信号量)避免竞态条件。
使用信号量同步:
#include <sys/sem.h>
// 创建信号量
int semid = semget(key, 1, IPC_CREAT | 0666);
semctl(semid, 0, SETVAL, 1); // 初始值为1(互斥锁)
struct sembuf op;
op.sem_num = 0;
op.sem_op = -1; // P操作(获取资源)
op.sem_flg = 0;
semop(semid, &op, 1);
// 临界区:操作共享内存
sprintf(shm_ptr, "Data with semaphore");
op.sem_op = 1; // V操作(释放资源)
semop(semid, &op, 1);
四、共享内存的底层原理
- 内核数据结构
struct shmid_kernel
:内核维护每个共享内存段的信息,包括权限、大小、连接数等。- 页表映射:共享内存被映射到进程的虚拟地址空间,通过页表指向同一物理内存。
- 读写操作的原子性
- 共享内存本身不保证原子性,需用户自行同步。
- 单次内存访问(如
int
读写)通常是原子的,但复杂操作(如结构体更新)需加锁。
- 清空内存与阻塞
bzero(shm_ptr, size)
:仅将内存数据清零,不改变共享内存的可用性。- 若读操作依赖数据有效性(如等待非零值),需通过同步机制(如信号量)实现阻塞等待。
优点 | 缺点 |
---|---|
无数据拷贝,速度极快 | 需手动同步(易引发竞态条件) |
适合大数据量通信 | 生命周期需显式管理 |
多进程可并发访问 | 数据无内置结构(需约定格式) |
代码示例:进程间通信
写进程(writer.c)
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
//1- ftok()获得键值
key_t key = ftok("/",0x2);
if(key == -1)
{
perror("ftok");
return -1;
}
//2- 获得ID号
int shmid = shmget(key,1024,IPC_CREAT | 0666); //通过上一步获得键值得到共享内存的ID号
if(shmid == -1)
{
perror("shmget");
return -1;
}
//3- 映射地址
char* addr = (char *)shmat(shmid,NULL,0); //利用上一步获得的键值得到共享内存的映射地址
if(addr == (void *)-1)
{
perror("shamt");
return -1;
}
bzero(addr,1024);
/* 向共享内存中写入数据 */
while(1)
{
scanf("%s",addr); //从键盘读取数据写入共享内存
if(strncmp(addr,"quit",4) == 0)
break;
}
//4- 解除映射
if(shmdt(addr) == -1)
{
perror("shmat");
return -1;
}
}
读进程(reader.c)
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
//1- ftok()获得键值
key_t key = ftok("/",0x2);
if(key == -1)
{
perror("ftok");
return -1;
}
//2- 获得ID号
int shmid = shmget(key,1024,IPC_CREAT | 0666); //通过上一步获得键值得到共享内存的ID号
if(shmid == -1)
{
perror("shmget");
return -1;
}
//3- 映射地址
char* addr = (char *)shmat(shmid,NULL,0); //利用上一步获得的键值得到共享内存的映射地址
if((addr == (void *)-1))
{
perror("shamt");
return -1;
}
/* 读取数据 */
while(1)
{
printf("%s",addr);
fflush(stdout);
if(strncmp(addr,"quit",4) == 0)
break;
bzero(addr,1024);
sleep(1);
}
//4- 解除映射
if(shmdt(addr) == -1)
{
perror("shmat");
return -1;
}
}
运行步骤:
- 编译并运行写进程:
gcc writer.c -o writer
./writer
- 编译并运行读进程:
gcc reader.c -o reader
./reader
简而言之,重点记住共享内存是最高效的 IPC,适用于高性能数据共享;并且,必须结合同步机制(如信号量)避免数据竞争;生命周期管理关键:显式创建、分离和删除。
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
感谢!