1. 概述
1.1 什么是共享内存?
共享内存的定义:
共享内存(Shared Memory)是一种进程间通信(IPC)机制,允许多个进程访问同一块物理内存区域。这是最快的 IPC 方式,因为数据不需要在内核和用户空间之间复制。
共享内存的特点:
- 高性能:数据直接映射到进程地址空间,无需复制
- 低延迟:访问速度接近直接内存访问
- 大容量:可以共享大量数据
- 持久性:数据在进程退出后仍然存在(直到显式删除)
共享内存的用途:
- 高性能数据传输:进程间大量数据交换
- 数据库系统:共享缓存和缓冲区
- 图形系统:共享图形缓冲区
- 科学计算:共享计算结果
1.2 Linux 中的共享内存类型
Linux 提供了两种主要的共享内存机制:
-
System V 共享内存:
- API :
shmget(),shmat(),shmdt(),shmctl() - 特点:传统的 IPC 机制,基于键值(key)
- 实现 :基于
tmpfs文件系统
- API :
-
POSIX 共享内存:
- API :
shm_open(),shm_unlink(),mmap() - 特点:POSIX 标准,基于文件名
- 实现 :基于
tmpfs文件系统(通常挂载在/dev/shm)
- API :
-
内存映射文件(mmap):
- API :
mmap(),munmap() - 特点:可以映射文件或匿名内存
- 实现:直接内存映射
- API :
1.3 共享内存与普通内存的区别
| 特性 | 普通内存 | 共享内存 |
|---|---|---|
| 可见性 | 仅当前进程可见 | 多个进程可见 |
| 生命周期 | 进程退出时释放 | 显式删除或系统重启 |
| 访问方式 | 直接访问 | 需要先 attach |
| 同步 | 不需要 | 需要同步机制(信号量、锁等) |
2. System V 共享内存
2.1 System V 共享内存 API
核心系统调用:
shmget():创建或获取共享内存段shmat():将共享内存段附加到进程地址空间shmdt():从进程地址空间分离共享内存段shmctl():控制共享内存段(获取信息、设置权限、删除等)
函数原型:
c
#include <sys/ipc.h>
#include <sys/shm.h>
// 创建或获取共享内存段
int shmget(key_t key, size_t size, int shmflg);
// 附加共享内存段到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 从进程地址空间分离共享内存段
int shmdt(const void *shmaddr);
// 控制共享内存段
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
2.2 shmget() 的实现原理
shmget() 的作用:
创建新的共享内存段或获取已存在的共享内存段的标识符。
内核实现:
c
// ipc/shm.c
SYSCALL_DEFINE3(shmget, key_t, key, size_t, size, int, shmflg)
{
struct ipc_namespace *ns;
static const struct ipc_ops shm_ops = {
.getnew = newseg, // 创建新段
.associate = security_shm_associate,
.more_checks = shm_more_checks,
};
struct ipc_params shm_params;
ns = current->nsproxy->ipc_ns;
shm_params.key = key;
shm_params.flg = shmflg;
shm_params.u.size = size;
return ipcget(ns, &shm_ids(ns), &shm_ops, &shm_params);
}
newseg() 函数:
c
// ipc/shm.c
static int newseg(struct ipc_namespace *ns, struct ipc_params *params)
{
key_t key = params->key;
int shmflg = params->flg;
size_t size = params->u.size;
int error;
struct shmid_kernel *shp;
size_t numpages = (size + PAGE_SIZE - 1) >> PAGE_SIZE;
struct file *file;
char name[12];
int id;
void *user_memory;
// 1. 检查大小限制
if (size < SHMMIN || size > ns->shm_ctlmax)
return -EINVAL;
// 2. 分配 shmid_kernel 结构
shp = kvmalloc(sizeof(*shp), GFP_KERNEL);
if (!shp)
return -ENOMEM;
// 3. 创建 tmpfs 文件
sprintf(name, "SYSV%08x", key);
file = shmem_kernel_file_setup(name, size, acctflag);
if (IS_ERR(file)) {
error = PTR_ERR(file);
goto no_file;
}
// 4. 初始化 shmid_kernel 结构
shp->shm_file = file;
shp->shm_perm.key = key;
shp->shm_perm.mode = (shmflg & S_IRWXUGO);
shp->shm_perm.security = NULL;
shp->shm_segsz = size;
shp->shm_nattch = 0;
shp->shm_atim = 0;
shp->shm_dtim = 0;
shp->shm_ctim = ktime_get_real_seconds();
shp->shm_cprid = get_pid(task_tgid(current));
shp->shm_lprid = NULL;
shp->shm_ns = ns;
// 5. 注册到 IPC 命名空间
id = ipc_addid(&shm_ids(ns), &shp->shm_perm, ns->shm_ctlmni);
if (id < 0) {
error = id;
goto no_id;
}
// 6. 更新命名空间统计
ns->shm_tot += numpages;
return id;
}
关键步骤说明:
-
大小检查:
- SHMMIN:最小共享内存段大小(通常 1 字节)
- shm_ctlmax :最大共享内存段大小(可通过
sysctl配置)
-
创建 tmpfs 文件:
shmem_kernel_file_setup():在 tmpfs 文件系统中创建文件- 文件名 :
SYSV+ 8 位十六进制 key - 作用:共享内存段实际上是一个 tmpfs 文件
-
初始化结构:
shmid_kernel:内核中的共享内存段结构shm_perm:IPC 权限结构shm_file:指向 tmpfs 文件的指针
-
注册到 IPC 命名空间:
ipc_addid():分配唯一的共享内存 ID- 作用:将共享内存段添加到命名空间的 ID 表中
2.3 shmat() 的实现原理
shmat() 的作用:
将共享内存段附加到调用进程的地址空间,返回映射的虚拟地址。
内核实现:
c
// ipc/shm.c
long do_shmat(int shmid, char __user *shmaddr, int shmflg, ulong *raddr, unsigned long shmlba)
{
struct shmid_kernel *shp;
unsigned long addr = (unsigned long)shmaddr;
unsigned long size;
struct file *file;
int err;
unsigned long flags = MAP_SHARED;
unsigned long prot;
int acc_mode;
struct ipc_namespace *ns;
struct shm_file_data *sfd;
int f_flags;
unsigned long populate = 0;
// 1. 获取共享内存段
ns = current->nsproxy->ipc_ns;
shp = shm_obtain_object_check(ns, shmid);
if (IS_ERR(shp))
return PTR_ERR(shp);
// 2. 检查权限
err = -EACCES;
if (ipcperms(ns, &shp->shm_perm, acc_mode))
goto out_unlock;
// 3. 检查大小
err = -EINVAL;
size = i_size_read(file_inode(shp->shm_file));
if (size < SHMMIN || size > (unsigned long)ns->shm_ctlmax)
goto out_unlock;
// 4. 创建 shm_file_data 结构
sfd = kvmalloc(sizeof(*sfd), GFP_KERNEL);
if (!sfd) {
err = -ENOMEM;
goto out_unlock;
}
file = alloc_file_clone(shp->shm_file, f_flags,
is_file_hugepages(shp->shm_file) ?
&shm_file_operations_huge :
&shm_file_operations);
if (IS_ERR(file)) {
err = PTR_ERR(file);
goto out_freed;
}
sfd->id = shp->shm_perm.id;
sfd->ns = get_ipc_ns(ns);
sfd->file = shp->shm_file;
sfd->vm_ops = NULL;
file->private_data = sfd;
// 5. 执行内存映射
err = do_mmap_pgoff(file, addr, size, prot, flags, 0, &populate, NULL);
*raddr = addr;
if (err < 0) {
fput(file);
goto out_nattch;
}
// 6. 更新统计信息
shp->shm_nattch++;
shp->shm_atim = ktime_get_real_seconds();
ipc_update_pid(&shp->shm_lprid, task_tgid(current));
return err;
}
关键步骤说明:
-
获取共享内存段:
shm_obtain_object_check():从 IPC 命名空间获取共享内存段- 检查:验证 ID 是否有效
-
权限检查:
ipcperms():检查进程是否有权限访问共享内存段- 权限:读、写权限
-
创建文件描述符:
alloc_file_clone():为共享内存段创建文件描述符- 作用:每个进程都有独立的文件描述符,但指向同一个文件
-
内存映射:
do_mmap_pgoff():将文件映射到进程地址空间- 标志 :
MAP_SHARED表示共享映射 - 结果:返回映射的虚拟地址
-
更新统计:
shm_nattch:附加计数加 1shm_atim:更新最后附加时间shm_lprid:更新最后附加进程 ID
2.4 shm_mmap() 的实现原理
shm_mmap() 的作用:
当进程调用 mmap() 映射共享内存文件时,内核会调用 shm_mmap() 来处理映射。
内核实现:
c
// ipc/shm.c
static int shm_mmap(struct file *file, struct vm_area_struct *vma)
{
struct shm_file_data *sfd = shm_file_data(file);
int ret;
// 1. 打开共享内存段(增加引用计数)
ret = __shm_open(vma);
if (ret)
return ret;
// 2. 调用底层文件的 mmap
ret = call_mmap(sfd->file, vma);
if (ret) {
shm_close(vma);
return ret;
}
// 3. 保存原始 vm_ops,替换为 shm_vm_ops
sfd->vm_ops = vma->vm_ops;
vma->vm_ops = &shm_vm_ops;
return 0;
}
__shm_open() 函数:
c
// ipc/shm.c
static int __shm_open(struct vm_area_struct *vma)
{
struct file *file = vma->vm_file;
struct shm_file_data *sfd = shm_file_data(file);
struct shmid_kernel *shp;
// 1. 获取共享内存段
shp = shm_lock(sfd->ns, sfd->id);
if (IS_ERR(shp))
return PTR_ERR(shp);
// 2. 验证文件是否匹配
if (shp->shm_file != sfd->file) {
shm_unlock(shp);
return -EINVAL; // ID 被重用
}
// 3. 更新统计信息
shp->shm_atim = ktime_get_real_seconds();
ipc_update_pid(&shp->shm_lprid, task_tgid(current));
shp->shm_nattch++;
shm_unlock(shp);
return 0;
}
关键点:
- 引用计数 :每次映射时,
shm_nattch增加 - vm_ops 替换 :将 VMA 的
vm_ops替换为shm_vm_ops - 文件验证:确保文件没有被删除和重用
2.5 shmdt() 的实现原理
shmdt() 的作用:
从进程地址空间分离共享内存段。
内核实现:
c
// ipc/shm.c
SYSCALL_DEFINE1(shmdt, char __user *, shmaddr)
{
return ksys_shmdt(shmaddr);
}
long ksys_shmdt(char __user *shmaddr)
{
unsigned long addr = (unsigned long)shmaddr;
unsigned long size;
struct vm_area_struct *vma, *next;
int retval = -EINVAL;
// 1. 查找包含该地址的 VMA
vma = find_vma(current->mm, addr);
if (!vma || vma->vm_start > addr)
return -EINVAL;
// 2. 验证是否是共享内存映射
if (vma->vm_ops != &shm_vm_ops)
return -EINVAL;
// 3. 取消映射
size = vma->vm_end - vma->vm_start;
retval = do_munmap(current->mm, addr, size, NULL);
return retval;
}
shm_close() 函数:
当 VMA 被取消映射时,内核会调用 shm_close():
c
// ipc/shm.c
static void shm_close(struct vm_area_struct *vma)
{
struct file *file = vma->vm_file;
struct shm_file_data *sfd = shm_file_data(file);
struct shmid_kernel *shp;
struct ipc_namespace *ns = sfd->ns;
down_write(&shm_ids(ns).rwsem);
// 1. 获取共享内存段
shp = shm_lock(ns, sfd->id);
if (WARN_ON_ONCE(IS_ERR(shp)))
goto done;
// 2. 更新统计信息
ipc_update_pid(&shp->shm_lprid, task_tgid(current));
shp->shm_dtim = ktime_get_real_seconds();
shp->shm_nattch--;
// 3. 检查是否需要销毁
if (shm_may_destroy(shp))
shm_destroy(ns, shp);
else
shm_unlock(shp);
done:
up_write(&shm_ids(ns).rwsem);
}
关键点:
- 查找 VMA:根据地址查找对应的 VMA
- 验证类型:确保是共享内存映射
- 取消映射 :调用
do_munmap()取消内存映射 - 更新统计 :减少
shm_nattch,更新分离时间
2.6 shmctl() 的实现原理
shmctl() 的作用:
控制共享内存段,包括获取信息、设置权限、删除等。
内核实现:
c
// ipc/shm.c
long ksys_shmctl(int shmid, int cmd, struct shmid_ds __user *buf)
{
struct shmid_kernel *shp;
int err, version;
struct ipc_namespace *ns;
ns = current->nsproxy->ipc_ns;
if (cmd < 0 || shmid < 0)
return -EINVAL;
switch (cmd) {
case IPC_INFO:
case SHM_INFO:
case SHM_STAT:
case SHM_STAT_ANY:
// 获取信息
return shmctl_info(ns, shmid, cmd, buf);
case IPC_STAT:
case SHM_STAT:
case SHM_STAT_ANY:
// 获取共享内存段状态
return shmctl_stat(ns, shmid, cmd, buf);
case IPC_SET:
// 设置共享内存段属性
return shmctl_set(ns, shmid, cmd, buf);
case IPC_RMID:
// 删除共享内存段
return shmctl_rmid(ns, shmid, cmd);
case SHM_LOCK:
// 锁定共享内存段(防止交换)
return shmctl_lock(ns, shmid);
case SHM_UNLOCK:
// 解锁共享内存段
return shmctl_unlock(ns, shmid);
default:
return -EINVAL;
}
}
IPC_RMID 的实现:
c
// ipc/shm.c
static int shmctl_rmid(struct ipc_namespace *ns, int shmid)
{
struct shmid_kernel *shp;
shp = shm_lock(ns, shmid);
if (IS_ERR(shp))
return PTR_ERR(shp);
// 1. 检查权限
if (ns->shm_rmid_forced ||
(shp->shm_perm.mode & SHM_DEST) ||
(shp->shm_nattch == 0)) {
// 2. 标记为删除
do_shm_rmid(ns, &shp->shm_perm);
shm_unlock(shp);
} else {
// 3. 如果还有进程附加,只标记为删除
shp->shm_perm.mode |= SHM_DEST;
ipc_set_key_private(&shm_ids(ns), &shp->shm_perm);
shm_unlock(shp);
}
return 0;
}
关键点:
- IPC_STAT:获取共享内存段状态信息
- IPC_SET:设置共享内存段属性(权限、大小等)
- IPC_RMID:删除共享内存段(如果还有进程附加,标记为删除,最后一个进程分离时真正删除)
- SHM_LOCK/SHM_UNLOCK:锁定/解锁共享内存段,防止交换到磁盘
3. 底层实现原理
3.1 数据结构
shmid_kernel 结构:
c
// ipc/shm.c
struct shmid_kernel /* private to the kernel */
{
struct kern_ipc_perm shm_perm; // IPC 权限结构
struct file *shm_file; // 指向 tmpfs 文件的指针
unsigned long shm_nattch; // 附加计数
unsigned long shm_segsz; // 段大小
time64_t shm_atim; // 最后附加时间
time64_t shm_dtim; // 最后分离时间
time64_t shm_ctim; // 最后修改时间
struct pid *shm_cprid; // 创建进程 ID
struct pid *shm_lprid; // 最后附加进程 ID
struct ucounts *mlock_ucounts; // 锁定计数
struct task_struct *shm_creator; // 创建进程
struct list_head shm_clist; // 创建者列表
struct ipc_namespace *ns; // IPC 命名空间
};
shm_file_data 结构:
c
// ipc/shm.c
struct shm_file_data {
int id; // 共享内存 ID
struct ipc_namespace *ns; // IPC 命名空间
struct file *file; // 底层文件
const struct vm_operations_struct *vm_ops; // 原始 vm_ops
};
关键字段说明:
-
shm_perm:
- 作用:IPC 权限和标识
- 包含:key、ID、权限、所有者等
-
shm_file:
- 作用:指向 tmpfs 文件的指针
- 类型 :
struct file * - 作用:共享内存段实际上是一个 tmpfs 文件
-
shm_nattch:
- 作用:当前附加到该段的进程数
- 更新 :每次
shmat()时增加,shmdt()时减少
-
shm_segsz:
- 作用:共享内存段的大小(字节)
- 限制 :受
shm_ctlmax限制
3.2 tmpfs 文件系统
tmpfs 的作用:
System V 共享内存段实际上是在 tmpfs 文件系统中创建的文件。tmpfs 是一个基于内存的文件系统,数据存储在 RAM 中。
tmpfs 的特点:
- 内存存储:数据存储在 RAM 中,访问速度快
- 可交换:如果内存不足,可以交换到交换分区
- 临时性:系统重启后数据丢失
- 动态大小:可以根据需要动态增长
shmem_file_setup() 函数:
c
// mm/shmem.c
struct file *shmem_file_setup(const char *name, loff_t size, unsigned long flags)
{
int error;
struct file *file;
struct inode *inode;
struct dentry *dentry, *root;
struct qstr this;
// 1. 获取 tmpfs 挂载点
root = shm_mnt->mnt_root;
// 2. 创建 dentry
this.name = name;
this.len = strlen(name);
this.hash = 0;
dentry = d_alloc(root, &this);
if (!dentry)
return ERR_PTR(-ENOMEM);
// 3. 创建 inode
inode = shmem_get_inode(root->d_sb, NULL, S_IFREG | S_IRWXUGO, 0, flags);
if (!inode) {
dput(dentry);
return ERR_PTR(-ENOMEM);
}
// 4. 设置文件大小
inode->i_size = size;
d_instantiate(dentry, inode);
inode->i_nlink = 0;
// 5. 创建文件
file = alloc_file_pseudo(inode, shm_mnt, dentry->d_name.name,
O_RDWR | (flags & O_ACCMODE),
&shmem_file_operations);
return file;
}
关键点:
- 文件创建:在 tmpfs 中创建文件
- 大小设置:设置文件大小为共享内存段大小
- 文件操作 :使用
shmem_file_operations作为文件操作函数
3.3 内存映射机制
mmap() 的作用:
将文件映射到进程地址空间,实现共享内存的访问。
映射流程:
markdown
1. 进程调用 shmat()
↓
2. 内核调用 do_mmap_pgoff()
↓
3. 创建 VMA(虚拟内存区域)
↓
4. 设置 VMA 属性(地址、大小、权限等)
↓
5. 将 VMA 添加到进程的 mm_struct
↓
6. 返回虚拟地址
VMA 结构:
c
// include/linux/mm_types.h
struct vm_area_struct {
unsigned long vm_start; // 起始地址
unsigned long vm_end; // 结束地址
struct mm_struct *vm_mm; // 所属进程
pgprot_t vm_page_prot; // 页面保护
unsigned long vm_flags; // 标志(MAP_SHARED 等)
struct file *vm_file; // 映射的文件
void *vm_private_data; // 私有数据
const struct vm_operations_struct *vm_ops; // 操作函数
// ...
};
页面错误处理:
当进程访问共享内存时,如果页面不在内存中,会触发页面错误:
c
// mm/shmem.c
static vm_fault_t shmem_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct inode *inode = file_inode(vma->vm_file);
struct page *page;
vm_fault_t ret = VM_FAULT_LOCKED;
// 1. 获取页面
page = shmem_getpage_gfp(inode, vmf->pgoff, &vmf->page,
SGP_CACHE, vmf->gfp_mask, vma, vmf, NULL);
if (!page)
return VM_FAULT_OOM;
// 2. 如果页面在交换分区,需要换入
if (PageSwapCache(page)) {
swap_readpage(page, true);
wait_on_page_locked(page);
}
return ret;
}
关键点:
- 延迟分配:页面在首次访问时才分配
- 交换支持:如果内存不足,页面可以交换到交换分区
- 写时复制 :如果使用
MAP_PRIVATE,会使用写时复制
3.4 同步机制
为什么需要同步:
多个进程访问共享内存时,需要同步机制来避免竞争条件。
常见的同步机制:
-
信号量(Semaphore):
c#include <sys/sem.h> // 创建信号量 int semid = semget(key, 1, IPC_CREAT | 0666); // P 操作(等待) struct sembuf sop = {0, -1, 0}; semop(semid, &sop, 1); // V 操作(释放) sop.sem_op = 1; semop(semid, &sop, 1); -
互斥锁(Mutex):
c#include <pthread.h> // 在共享内存中创建互斥锁 pthread_mutex_t *mutex = (pthread_mutex_t *)shm_addr; pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(mutex, &attr); // 使用 pthread_mutex_lock(mutex); // 临界区 pthread_mutex_unlock(mutex); -
条件变量(Condition Variable):
c#include <pthread.h> pthread_cond_t *cond = (pthread_cond_t *)shm_addr; pthread_condattr_t attr; pthread_condattr_init(&attr); pthread_condattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); pthread_cond_init(cond, &attr); // 等待 pthread_cond_wait(cond, mutex); // 通知 pthread_cond_signal(cond);
4. POSIX 共享内存
4.1 POSIX 共享内存 API
核心函数:
shm_open():创建或打开共享内存对象shm_unlink():删除共享内存对象mmap():映射共享内存到进程地址空间munmap():取消映射
函数原型:
c
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
// 创建或打开共享内存对象
int shm_open(const char *name, int oflag, mode_t mode);
// 删除共享内存对象
int shm_unlink(const char *name);
// 映射共享内存
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 取消映射
int munmap(void *addr, size_t length);
4.2 shm_open() 的实现原理
shm_open() 的作用:
在 /dev/shm 目录下创建或打开共享内存文件。
内核实现:
c
// mm/shmem.c
SYSCALL_DEFINE3(shm_open, const char __user *, name, int, oflag, umode_t, mode)
{
struct path path;
struct file *file;
int err;
unsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_DIRECTORY;
if (oflag & ~(O_CREAT | O_RDWR | O_TRUNC | O_EXCL | O_CLOEXEC))
return -EINVAL;
if (oflag & O_CREAT) {
if (oflag & O_EXCL)
lookup_flags |= LOOKUP_EXCL;
lookup_flags |= LOOKUP_CREATE;
}
// 1. 在 /dev/shm 中查找或创建文件
err = user_path_create(AT_FDCWD, name, &path, lookup_flags);
if (err)
return err;
// 2. 打开文件
file = dentry_open(&path, oflag, current_cred());
path_put(&path);
if (IS_ERR(file))
return PTR_ERR(file);
// 3. 设置文件大小(如果是新创建的文件)
if (oflag & O_CREAT) {
if (oflag & O_TRUNC) {
err = do_ftruncate(file, 0, 0);
if (err)
goto out;
}
}
// 4. 返回文件描述符
return fd_install(fd, file);
}
关键点:
- 文件位置 :在
/dev/shm目录下创建文件 - 文件系统:使用 tmpfs 文件系统
- 文件描述符 :返回文件描述符,可以用于
mmap()
4.3 POSIX vs System V 共享内存
| 特性 | System V | POSIX |
|---|---|---|
| 标识方式 | key_t key | 文件名 |
| 标准 | System V IPC | POSIX |
| API | shmget/shmat/shmdt | shm_open/mmap/munmap |
| 文件系统 | tmpfs(内核内部) | tmpfs(/dev/shm) |
| 权限 | IPC 权限 | 文件系统权限 |
| 删除 | shmctl(IPC_RMID) | shm_unlink() |
| 可移植性 | 较低 | 较高 |
5. 使用示例
5.1 System V 共享内存示例
创建共享内存的进程:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#define SHM_SIZE 1024
#define SHM_KEY 1234
int main(void)
{
int shmid;
char *shm_addr;
struct sembuf sop;
int semid;
// 1. 创建信号量
semid = semget(SHM_KEY, 1, IPC_CREAT | 0666);
if (semid < 0) {
perror("semget");
exit(1);
}
// 初始化信号量为 1
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
} arg;
arg.val = 1;
semctl(semid, 0, SETVAL, arg);
// 2. 创建共享内存
shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
if (shmid < 0) {
perror("shmget");
exit(1);
}
// 3. 附加共享内存
shm_addr = (char *)shmat(shmid, NULL, 0);
if (shm_addr == (char *)-1) {
perror("shmat");
exit(1);
}
// 4. 写入数据
strcpy(shm_addr, "Hello, Shared Memory!");
// 5. 等待其他进程读取
printf("Data written. Waiting for reader...\n");
sleep(10);
// 6. 分离共享内存
shmdt(shm_addr);
// 7. 删除共享内存
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
return 0;
}
读取共享内存的进程:
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <unistd.h>
#define SHM_SIZE 1024
#define SHM_KEY 1234
int main(void)
{
int shmid;
char *shm_addr;
struct sembuf sop;
int semid;
// 1. 获取信号量
semid = semget(SHM_KEY, 0, 0);
if (semid < 0) {
perror("semget");
exit(1);
}
// 2. 获取共享内存
shmid = shmget(SHM_KEY, 0, 0);
if (shmid < 0) {
perror("shmget");
exit(1);
}
// 3. 附加共享内存
shm_addr = (char *)shmat(shmid, NULL, 0);
if (shm_addr == (char *)-1) {
perror("shmat");
exit(1);
}
// 4. 读取数据
printf("Data read: %s\n", shm_addr);
// 5. 分离共享内存
shmdt(shm_addr);
return 0;
}
5.2 POSIX 共享内存示例
创建共享内存的进程:
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SHM_NAME "/my_shm"
#define SHM_SIZE 1024
int main(void)
{
int shm_fd;
char *shm_addr;
// 1. 创建共享内存对象
shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (shm_fd < 0) {
perror("shm_open");
exit(1);
}
// 2. 设置共享内存大小
if (ftruncate(shm_fd, SHM_SIZE) < 0) {
perror("ftruncate");
exit(1);
}
// 3. 映射共享内存
shm_addr = (char *)mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
if (shm_addr == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 4. 关闭文件描述符(不影响映射)
close(shm_fd);
// 5. 写入数据
strcpy(shm_addr, "Hello, POSIX Shared Memory!");
// 6. 等待其他进程读取
printf("Data written. Waiting for reader...\n");
sleep(10);
// 7. 取消映射
munmap(shm_addr, SHM_SIZE);
// 8. 删除共享内存对象
shm_unlink(SHM_NAME);
return 0;
}
读取共享内存的进程:
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SHM_NAME "/my_shm"
#define SHM_SIZE 1024
int main(void)
{
int shm_fd;
char *shm_addr;
// 1. 打开共享内存对象
shm_fd = shm_open(SHM_NAME, O_RDONLY, 0);
if (shm_fd < 0) {
perror("shm_open");
exit(1);
}
// 2. 映射共享内存
shm_addr = (char *)mmap(NULL, SHM_SIZE, PROT_READ,
MAP_SHARED, shm_fd, 0);
if (shm_addr == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 3. 关闭文件描述符
close(shm_fd);
// 4. 读取数据
printf("Data read: %s\n", shm_addr);
// 5. 取消映射
munmap(shm_addr, SHM_SIZE);
return 0;
}
6. 性能优化和最佳实践
6.1 性能优化
1. 页面大小对齐:
共享内存段的大小应该对齐到页面大小(通常 4KB):
c
#define PAGE_SIZE 4096
size_t shm_size = (data_size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
2. 避免频繁映射/取消映射:
映射和取消映射有开销,应该保持映射直到不再需要:
c
// 不好的做法
for (int i = 0; i < 1000; i++) {
void *addr = shmat(shmid, NULL, 0);
// 使用共享内存
shmdt(addr);
}
// 好的做法
void *addr = shmat(shmid, NULL, 0);
for (int i = 0; i < 1000; i++) {
// 使用共享内存
}
shmdt(addr);
3. 使用大页面:
对于大块共享内存,可以使用大页面(Huge Pages)提高性能:
c
// 创建大页面共享内存
shmid = shmget(key, size, IPC_CREAT | SHM_HUGETLB | 0666);
6.2 最佳实践
1. 错误处理:
c
int shmid = shmget(key, size, IPC_CREAT | 0666);
if (shmid < 0) {
perror("shmget");
exit(1);
}
void *addr = shmat(shmid, NULL, 0);
if (addr == (void *)-1) {
perror("shmat");
exit(1);
}
2. 清理资源:
c
// 分离共享内存
if (shmdt(addr) < 0) {
perror("shmdt");
}
// 删除共享内存(最后一个进程)
if (shmctl(shmid, IPC_RMID, NULL) < 0) {
perror("shmctl");
}
3. 使用同步机制:
c
// 使用互斥锁保护共享内存访问
pthread_mutex_lock(mutex);
// 访问共享内存
pthread_mutex_unlock(mutex);
4. 检查共享内存状态:
c
struct shmid_ds buf;
if (shmctl(shmid, IPC_STAT, &buf) < 0) {
perror("shmctl");
exit(1);
}
printf("Size: %lu\n", buf.shm_segsz);
printf("Attached: %lu\n", buf.shm_nattch);
7. 底层实现细节
7.1 tmpfs 文件系统
tmpfs 的挂载:
System V 共享内存使用的 tmpfs 文件系统在内核启动时挂载:
c
// mm/shmem.c
static struct vfsmount *shm_mnt;
static int __init init_tmpfs(void)
{
// 注册 tmpfs 文件系统
register_filesystem(&tmpfs_fs_type);
// 挂载 tmpfs
shm_mnt = kern_mount(&tmpfs_fs_type);
if (IS_ERR(shm_mnt)) {
pr_err("Could not kern_mount tmpfs\n");
return PTR_ERR(shm_mnt);
}
return 0;
}
tmpfs 的特点:
- 内存存储:所有数据存储在 RAM 中
- 可交换:如果内存不足,可以交换到交换分区
- 动态大小:可以根据需要动态增长
- 临时性:系统重启后数据丢失
7.2 页面分配机制
延迟分配(Lazy Allocation):
共享内存段创建时,并不立即分配所有页面,而是采用延迟分配策略:
c
// mm/shmem.c
static int shmem_getpage_gfp(struct inode *inode, pgoff_t index,
struct page **pagep, enum sgp_type sgp,
gfp_t gfp, struct vm_area_struct *vma,
struct vm_fault *vmf, vm_fault_t *fault_type)
{
struct address_space *mapping = inode->i_mapping;
struct shmem_inode_info *info = SHMEM_I(inode);
struct page *page;
swp_entry_t swap;
int error;
// 1. 查找页面是否已存在
page = find_get_entry(mapping, index);
if (page && !xa_is_value(page)) {
// 页面已存在
*pagep = page;
return 0;
}
// 2. 检查是否在交换分区
swap = shmem_get_swap(info, index);
if (swap.val) {
// 从交换分区换入
error = shmem_swapin_page(inode, index, &page, sgp, gfp, vma, fault_type);
if (error)
return error;
*pagep = page;
return 0;
}
// 3. 分配新页面
page = shmem_alloc_and_acct_page(gfp, info, sgp, index);
if (IS_ERR(page))
return PTR_ERR(page);
// 4. 添加到地址空间
error = shmem_add_to_page_cache(page, mapping, index, gfp, NULL);
if (error) {
__free_page(page);
return error;
}
*pagep = page;
return 0;
}
关键点:
- 延迟分配:页面在首次访问时才分配
- 交换支持:如果页面在交换分区,需要换入
- 缓存管理:使用地址空间(address_space)管理页面
7.3 页面错误处理
页面错误的触发:
当进程访问共享内存时,如果页面不在内存中,会触发页面错误:
c
// mm/memory.c
static vm_fault_t handle_pte_fault(struct vm_fault *vmf)
{
pte_t entry;
if (!vmf->pte) {
// 页表项不存在,需要分配
return do_fault(vmf);
}
entry = ptep_get(vmf->pte);
if (!pte_present(entry)) {
// 页面不在内存中
if (pte_none(entry))
return do_fault(vmf);
if (pte_swp_swap_required(entry))
return do_swap_page(vmf);
}
return 0;
}
shmem_fault() 函数:
c
// mm/shmem.c
static vm_fault_t shmem_fault(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct inode *inode = file_inode(vma->vm_file);
struct page *page;
vm_fault_t ret = VM_FAULT_LOCKED;
int error;
// 1. 获取页面
error = shmem_getpage_gfp(inode, vmf->pgoff, &page,
SGP_CACHE, vmf->gfp_mask, vma, vmf, NULL);
if (error)
return VM_FAULT_SIGBUS;
// 2. 如果页面在交换分区,需要换入
if (PageSwapCache(page)) {
swap_readpage(page, true);
wait_on_page_locked(page);
if (!PageUptodate(page)) {
unlock_page(page);
put_page(page);
return VM_FAULT_SIGBUS;
}
}
// 3. 设置页表项
vmf->page = page;
return ret;
}
关键步骤:
- 查找页面:在地址空间中查找页面
- 交换处理:如果页面在交换分区,需要换入
- 分配页面:如果页面不存在,分配新页面
- 设置页表:将页面映射到进程地址空间
7.4 写时复制(Copy-on-Write)
MAP_PRIVATE vs MAP_SHARED:
- MAP_SHARED:多个进程共享同一物理页面,一个进程的修改对其他进程可见
- MAP_PRIVATE:每个进程有独立的页面副本,使用写时复制
写时复制机制:
c
// mm/memory.c
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *old_page = vmf->page;
struct page *new_page;
// 1. 检查是否允许写时复制
if (!(vma->vm_flags & VM_WRITE))
return VM_FAULT_SIGBUS;
// 2. 分配新页面
new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
if (!new_page)
return VM_FAULT_OOM;
// 3. 复制页面内容
copy_user_highpage(new_page, old_page, vmf->address, vma);
// 4. 设置页表项
pte_t entry = mk_pte(new_page, vma->vm_page_prot);
entry = maybe_mkwrite(pte_mkdirty(entry), vma);
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
// 5. 更新统计
update_mmu_cache(vma, vmf->address, vmf->pte);
return VM_FAULT_WRITE;
}
关键点:
- 共享内存使用 MAP_SHARED:所有进程共享同一物理页面
- 写时复制用于 MAP_PRIVATE:私有映射使用写时复制
- 性能考虑:写时复制可以减少内存使用,但首次写入时有开销
7.5 内存锁定(Memory Locking)
SHM_LOCK 的作用:
锁定共享内存段,防止其被交换到磁盘。
实现原理:
c
// ipc/shm.c
static int shmctl_lock(struct ipc_namespace *ns, int shmid)
{
struct shmid_kernel *shp;
struct file *shm_file;
int err;
shp = shm_lock(ns, shmid);
if (IS_ERR(shp))
return PTR_ERR(shp);
// 1. 检查权限
if (!ns_capable(ns->user_ns, CAP_IPC_LOCK)) {
shm_unlock(shp);
return -EPERM;
}
// 2. 锁定文件
shm_file = shp->shm_file;
if (is_file_hugepages(shm_file))
err = mlock_hugepage(shm_file);
else
err = shmem_lock(shm_file, 1, shp->mlock_ucounts);
if (!err && !(shp->shm_perm.mode & SHM_LOCKED)) {
shp->shm_perm.mode |= SHM_LOCKED;
shp->mlock_ucounts = get_ucounts(current_ucounts());
}
shm_unlock(shp);
return err;
}
关键点:
- 防止交换:锁定的内存不会被交换到磁盘
- 权限要求 :需要
CAP_IPC_LOCK权限 - 资源限制 :受
RLIMIT_MEMLOCK限制
8. 故障排查和调试
8.1 查看共享内存状态
使用 ipcs 命令:
bash
# 查看所有共享内存段
ipcs -m
# 查看详细信息
ipcs -m -l
# 查看特定进程的共享内存
ipcs -m -p
使用 /proc 文件系统:
bash
# 查看进程的共享内存映射
cat /proc/<pid>/maps | grep shm
# 查看共享内存统计
cat /proc/sysvipc/shm
8.2 常见问题
1. 共享内存段已存在:
c
// 错误处理
int shmid = shmget(key, size, IPC_CREAT | IPC_EXCL | 0666);
if (shmid < 0) {
if (errno == EEXIST) {
// 共享内存段已存在,获取它
shmid = shmget(key, 0, 0);
} else {
perror("shmget");
exit(1);
}
}
2. 权限不足:
c
// 检查权限
struct shmid_ds buf;
if (shmctl(shmid, IPC_STAT, &buf) < 0) {
perror("shmctl");
exit(1);
}
// 检查是否有读权限
if (!(buf.shm_perm.mode & S_IRUSR)) {
fprintf(stderr, "No read permission\n");
exit(1);
}
3. 内存不足:
c
// 检查系统限制
struct shminfo info;
if (shmctl(0, IPC_INFO, (struct shmid_ds *)&info) < 0) {
perror("shmctl");
exit(1);
}
printf("Max size: %lu\n", info.shmmax);
printf("Total segments: %lu\n", info.shmmni);
8.3 调试技巧
1. 使用 strace 跟踪系统调用:
bash
strace -e trace=shmget,shmat,shmdt,shmctl ./program
2. 使用 gdb 调试:
bash
gdb ./program
(gdb) break shmat
(gdb) run
(gdb) print shm_addr
3. 检查内存泄漏:
bash
# 查看未删除的共享内存段
ipcs -m | grep -v "^key"
# 检查进程的共享内存映射
for pid in $(ps -eo pid); do
echo "PID: $pid"
cat /proc/$pid/maps | grep shm
done
9. 性能优化
9.1 大页面支持
使用大页面(Huge Pages):
大页面可以减少页表项数量,提高 TLB 命中率,从而提高性能。
c
// 创建大页面共享内存
int shmid = shmget(key, size, IPC_CREAT | SHM_HUGETLB | 0666);
配置大页面:
bash
# 查看大页面信息
cat /proc/meminfo | grep Huge
# 配置大页面数量
echo 10 > /proc/sys/vm/nr_hugepages
9.2 内存对齐
页面大小对齐:
共享内存段的大小应该对齐到页面大小:
c
#define PAGE_SIZE 4096
size_t aligned_size = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
缓存行对齐:
对于频繁访问的数据结构,应该对齐到缓存行大小:
c
#define CACHE_LINE_SIZE 64
struct data {
char padding[CACHE_LINE_SIZE];
int value;
} __attribute__((aligned(CACHE_LINE_SIZE)));
9.3 预分配页面
使用 mlock() 锁定内存:
c
// 锁定共享内存,防止交换
if (mlock(shm_addr, shm_size) < 0) {
perror("mlock");
exit(1);
}
预分配页面:
c
// 访问所有页面,触发分配
for (size_t i = 0; i < shm_size; i += PAGE_SIZE) {
volatile char *p = (char *)shm_addr + i;
*p = 0; // 触发页面分配
}
10. 总结
10.1 关键点
-
System V 共享内存:
- 基于 key 的标识方式
- 使用
shmget/shmat/shmdt/shmctlAPI - 基于 tmpfs 文件系统
-
POSIX 共享内存:
- 基于文件名的标识方式
- 使用
shm_open/mmap/munmapAPI - 更符合 POSIX 标准
-
底层实现:
- 共享内存段是 tmpfs 文件
- 通过
mmap()映射到进程地址空间 - 使用 VMA 管理映射
- 延迟分配页面
-
同步机制:
- 需要额外的同步机制(信号量、互斥锁等)
- 避免竞争条件
10.2 适用场景
- 高性能数据传输:进程间大量数据交换
- 数据库系统:共享缓存和缓冲区
- 图形系统:共享图形缓冲区
- 科学计算:共享计算结果
10.3 最佳实践
- 错误处理:始终检查系统调用的返回值
- 资源清理:及时分离和删除共享内存
- 同步机制:使用适当的同步机制保护共享数据
- 性能优化:使用大页面、内存对齐等技术
文档结束
7.1 关键点
-
System V 共享内存:
- 基于 key 的标识方式
- 使用
shmget/shmat/shmdt/shmctlAPI - 基于 tmpfs 文件系统
-
POSIX 共享内存:
- 基于文件名的标识方式
- 使用
shm_open/mmap/munmapAPI - 更符合 POSIX 标准
-
底层实现:
- 共享内存段是 tmpfs 文件
- 通过
mmap()映射到进程地址空间 - 使用 VMA 管理映射
-
同步机制:
- 需要额外的同步机制(信号量、互斥锁等)
- 避免竞争条件
7.2 适用场景
- 高性能数据传输:进程间大量数据交换
- 数据库系统:共享缓存和缓冲区
- 图形系统:共享图形缓冲区
- 科学计算:共享计算结果