%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
%%
# 内存映射
> 参见 [^1] [^2] [^3]
mmap 的作用是**内存映射**——向**内核请求申请一块新的虚拟地址空间**,将一个文件或特殊对象**映射到该内存空间**。
### 三种使用方式
| 使用方式 | 说明 |
| ------------------------------------------- | ---------------------------------------------------------------------------- |
| (1)为 "**普通文件**" 提供 "**==内存映射 I/O==**" | 磁盘上文件映射到内存后,直接**通过操作内存来读/写文件内容** |
| (2)为 "**POSIX 共享内存对象**" 提供 "**==共享内存映射==**" | 实现任意进程间 "**共享内存**"。 |
| (3)为进程直接提供 "**==匿名内存映射==**" | 进程内直接分配一块 "**匿名内存**"(内容全为**二进制 0**),无关联文件。 <br>(只能在关联进程间实现**共享内存**,例如父子进程间) |
##### (1)映射文件——内存映射 I/O
1. 调用 `open` 打开一个磁盘文件,获取 `fd`;
2. 通过 `mmap` 将 `fd` 关联文件**映射到内存**
```c
int fd = open("file.txt", O_RDWR | O_CREAT);
void* addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
```
##### (2)映射共享内存对象——共享内存
1. 调用 `shm_open` 打开一个 POSIX 共享内存对象,获取 `fd`;
2. 通过 `mmap` 将 `fd` 关联对象**映射到内存**
```c
int shm_fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
void* addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
```
##### (3)匿名内存映射
匿名内存映射:设置`MAP_ANON`标志位, fd 参数传入为 `-1`。
```c
//
// 直接分配一块4KB的匿名内存
void* addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
```
<br><br>
## 内存映射 API
### mmap 函数
![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-CADADFAF33281153CB32F504EE9AEC69.png|706]]
参数说明:
- `addr`:被映射至的虚拟内存地址(需搭配 `MAP_FIXED`)
- 未指定 `MAP_FIXED` 时仅作为建议地址,**不强制**。故该情况下通常**设置为 `NULL`**,表示由内核自动选择合适地址(页对齐)
- `length`:映射的**字节数**;
- `prot`:保护标志,指定**对映射区域的访问权限**,可选项为:
- `PROT_READ`:可读
- `PROT_WRITE`:可写入
- `PROT_EXEC`:可执行
- `PROT_NONE`:不可访问
- `flags`:映射方式,其中 `MAP_SHARED` 与 `MAP_PRIVATE` 必须指定其一;
- `MAP_SHARED`:**==共享映射==**,对 "**映射内存**" 的修改会**同步**到 "**底层被映射的文件/对象**",**反之亦然**。
- `MAP_PRIVATE`:**==私有映射==**(**==写时拷贝==**),对 "**映射内存**" 修改只在当前进程的内存中可见,**不改变底层文件/对象**,`munmap` 后将丢弃所有内存中变更。
- `MAP_ANON`:**匿名映射**,不与文件关联;
- Linux 下定义了等价的 `MAP_ANONYMOUS` 标志[^5]
- `MAP_FIXED`:**强制按指定地址映射**,若不可行则出错返回 `MAP_FAILED`;
- `fd`:文件描述符,表示**需要被映射的文件**。若使用匿名映射,则 `fd` 应设置为-1;
- `offset`:文件中**被映射的起始位置的偏移量**,必须是页面大小的整数倍。
> [!note] mmap 成功返回后,`fd` 关联文件可被关闭,不影响已建立的映射。
> [!NOTE] mmap 映射 "普通文件"[^1]
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-25667BB1CF7B2EC3E1C75172D100A5CF.png|574]]
> [!important] fork 对 mmap 的影响
>
> fork 形成的父子进程间 **初始时共享物理内存页帧** 且受 "**写时拷贝**"(COW)机制的影响。
>
> 对于父进程**在 fork 之前通过 mmap 映射的内存区域**,取决于 `mmap` 映射方式[^2]:
>
> - `MAP_SHARED` :**父子进程对映射内存的写入直接==同步==到被映射的原文件**,不涉及写时拷贝,父子进程 **==共享映射内存区的物理页==**。
> - `MAP_PRIVATE` :父子进程对映射内存的写入**不影响原文件**,写入**触发==写时拷贝==**,变更仅在执行写入的进程内可见,如下图:
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-39D633740F0D6337BD3F5AD7777C04E5.png|722]]
>
>
<br>
#### 🚨 mmap 背后的内存分配
两点注意事项[^4]:
1. OS 以 "**==页==**" 为单位分配内存,故**实际分配给 `mmap` 的内存块大小是 =="页大小" 的整数倍==**。
2. mmap 请求大小 "**可超出文件大小所需的整数内存页**",但 **==超出部分的页==不可访问**,将触发 **SIGBUS**。
> [!example] 示例一[^4]
>
> 由于按页分配,故按 **"被映射文件大小"** 进行映射时,实际分配的内存可能大于 "**被映射文件的实际大小**"。
> 最后一页中**超过文件大小的部分,均为二进制 0**。这部分内存 **==可被读/写==**,但**不会影响到被映射的原文件**。
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-E48873AA1EDB93EB2B64D84982FA67CA.png|477]]
>
> 验证示例:
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-A086F59CB072C5AAEE188F9A8DAA3D0C.png|472]]
>
> [!example] 示例二[^4]
>
> 当 mmap 请求的映射大小 "**超出文件大小所需的整数页**" 时,尽管 mmap 成功返回,但**超出部分的页不可访问**,如下所示:
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-A2E9BD893A15609B3C6413457A90DAC3.png|565]]
> 验证示例:
> 文件大小实际只需 1 个页,但 `mmap` 请求了 5000 字节故 **OS 为其分配 2 个页**,然而访问第二个页时触发 SIGBUS。
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-E9DD45D7BB6D2B9616EFC95CAD52962B.png|526]]
>
>
> [!error] mmap 相关的错误信号: SIGSEGV 与 SIGBUS
>
> - **SIGBUS**:是在内存映射区内访问,但 "**已超出了底层支撑对象的大小**"。
> - 若 mmap 请求的内存 "**超出文件大小所需的整数页**",则**超出部分的页不可访问**,将触发 **SIGBUS**。
> - **SIGSEGV**:非法内存引用,触发原因包括两种:
> 1. 尝试**写入设置为 "==只读==" 的映射内存区**;
> 2. 尝试访问 "**映射内存区**" 之外的非法地址。
<br><br>
### munmap 函数
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-350240AFED53D9D1EBF8F1F5C2E9EE0D.png|704]]
`munmap` 用以从进程的地址空间中 "**删除一个映射关系**"。
<br>
### msync 函数
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-BFE3FE9FAE692F2B2C8618510B13D526.png|729]]
`MAP_SHARED` 映射方式下,进程对映射内存的修改会**同步**到 "**底层被映射的文件/对象**"——由内核**在稍后某个时刻**来完成对文件的更新。
`msync` 函数用以 **立即执行同步**,**将修改过的页刷入到被映射文件中**,类似于 `fsync`。
> [!NOTE] 若映射方式为 `MAP_PRIVATE`,则 `msync` 无效,不会修改被映射的文件。
<br><br><br>
# 共享内存
共享内存:多个进程的虚拟内存空间共享 "**==相同的物理页帧==**",从而实现**进程间通信**。
特点:
- **高效**:在所有 IPC 机制中最为高效,进程间通过直接访问内存来通信,**不需要经由内核进行数据传递/拷贝**。
- **需要同步机制**:保证多个进程间**读写共享内存时互斥**
### OS 提供的共享内存接口
共享内存 API 有两种标准实现:
- **POSIX 共享内存**:
- `<sys/mman.h>` 头文件中,提供了 `shm_open`, `mmap`, `shm_unlink`, `munmap` 等方法来操作共享内存
- **System V 共享内存**:
- `<sys/shm.h>` 头文件中,提供了 `shmget`, `shmat`, `shmdt`, `shmctl` 等接口来操作共享内存
![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-1B3429B758C8E43F3C0F36AC70D5146F.png|502]]
以 POSIX 共享内存为例。使用步骤大致如下
1. **创建共享内存对象**:由某个进程通过 `shm_open` 首先创建一个**共享内存对象**,其余进程则可以打开已存在的同名对象。
2. **映射共享内存对象至进程的地址空间**:各进程通过 `mmap` 以 `MAP_SHARED` 方式**将对象映射至其地址空间**。
3. **数据访问**:进程通过指针**直接读/写==映射内存==中的数据**
4. **同步管理**:需使用额外的同步机制(如互斥锁)**协调多个进程对共享内存中同一块数据的读写**;
5. **移除映射内存、删除共享内存对象**:
- `munmap(addr, size)` :从进程地址空间中**移除一段映射内存**;
- `shm_unlink(shm_name)`:删除共享内存对象
<br><br>
# POSIX 共享内存
POSIX 标准提供了两种**共享内存**方式:
1. 映射**普通文件**——由`open` 打开并由 `mmap` 映射到各进程的地址空间。
2. 映射 **POSIX ==共享内存对象==**——由 `shm_open` 打开一个 POSX IPC 名并返回 fd,再由 `mmap` 映射到各进程空间。
> [!NOTE] 两种方式差别在于 "**获取文件描述符的方式**"
>
> 获取 `fd` 后**均由 `mmap` 以 `MAP_SHARED` 方式映射到各进程的地址空间**,各进程的映射内存区**指向==相同的物理页帧==**,从而实现共享内存。
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-1D48043FD2B5CB7C146838CBE7B13AFE.png|543]]
>
<br>
## POSIX 共享内存对象
![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-F712070CAC143EA5D1D93A7493E03C49.png|763]]
- `shm_open` 用以 **新建或打开一个已存在的共享内存区对象**,返回其**文件描述符**。
- `shm_unlink` 用以删除一个共享内存对象。(**不影响已映射的共享内存**)
> [!NOTE] `shm_open` 本质上是通过 tmpfs 伪文件系统创建了一个 "**匿名内存文件**"。
>
> tmpfs 是挂载在 `/dev/shm` 的 "伪文件系统",其本身**常驻内存**中,
> 而 `shm_open` 本质上是在该伪文件系统中 **创建/打开一个内存文件**。
>
> 可通过 `ls -l /dev/shm` 查看已创建的所有 POSIX 共享内存对象:
>
> ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-C35A7C400C42E1F653ACE799D4D3B491.png|685]]
>
> 因此多个进程通过 `mmap` 映射**同一共享内存对象**时,实质上即是**将各进程的映射内存指向了 "同一物理页帧"**,从而实现共享。
<br>
### 共享内存示例
```c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <semaphore.h>
#include <string.h>
#include <sys/wait.h>
/* 使用POSIX共享内存对象与mmap实现进程间共享内存
* 1) shm_open打开POSIX共享内存对象
* 2) 各进程通过mmap将其映射至自己的虚拟内存空间.
*/
struct ShmData {
int count;
sem_t mutex;
} shm_data;
const int loops = 10;
int main() {
// 共享内存对象
const char *shm_name = "/my_shm"; // 共享内存对象的名称
const size_t shm_size = sizeof(struct ShmData); // 共享内存的大小
struct ShmData *shm_ptr; // 指向共享内存的指针
int shm_fd; // 共享内存对象的文件描述符
// 创建或打开共享内存对象, 返回文件描述符
shm_fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open");
exit(EXIT_FAILURE);
}
// 设置共享内存对象的大小
if (ftruncate(shm_fd, shm_size) == -1) {
perror("ftruncate");
exit(EXIT_FAILURE);
}
// 对shm_data结构体初始化, 而后将其作为数据写入共享内存对象.
shm_data.count = 0;
sem_init(&shm_data.mutex, 1, 1);
write(shm_fd, &shm_data, shm_size);
// 使用fork()创建子进程
if (fork() == 0) {
shm_ptr = (struct ShmData*)mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
close(shm_fd); // 关闭共享内存对象的文件描述符(完成映射后即可关闭)
printf("子进程[%d]: mmap addr: %p\n", getpid(), shm_ptr);
for (int i = 0; i < loops; ++i) {
sem_wait(&shm_ptr->mutex);
printf("子进程[%d]: count: %d\n", getpid(), shm_ptr->count++);
sem_post(&shm_ptr->mutex);
}
// 解除映射
munmap(shm_ptr, shm_size);
exit(EXIT_SUCCESS);
}
// 父进程
void* addr = (void*)0x7ffff7ffb000; // 固定地址需使用`MAP_FIXED`
shm_ptr = (struct ShmData*)mmap(addr, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, shm_fd, 0);
close(shm_fd); // 关闭共享内存对象的文件描述符(完成映射后即可关闭)
printf("父进程[%d]: mmap addr: %p\n", getpid(), shm_ptr);
for (int i = 0; i < loops; ++i) {
sem_wait(&shm_ptr->mutex);
printf("父进程[%d]: count: %d\n", getpid(), shm_ptr->count++);
sem_post(&shm_ptr->mutex);
}
// 解除映射
munmap(shm_ptr, shm_size);
// 删除共享内存对象
if (shm_unlink(shm_name) == -1) {
perror("shm_unlink");
exit(EXIT_FAILURE);
}
wait(NULL);
return 0;
}
```
%%
# System V 共享内存
```c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <string.h>
int main() {
key_t key;
int shmid;
char *shm_ptr;
// 生成一个共享内存对象的键, 用作外部名
key = ftok("shmfile", 65);
// 创建一个大小为1024字节的共享内存段
shmid = shmget(key, 1024, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
// 将共享内存段附加到当前进程的地址空间
shm_ptr = (char*) shmat(shmid, NULL, 0);
if (shm_ptr == (char*) -1) {
perror("shmat");
exit(EXIT_FAILURE);
}
// 使用fork()创建子进程
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程: 向共享内存写入数据
printf("子进程(%d) 正向共享内存写入数据...\n", getpid());
const char* msg = "Hello from child process!";
strncpy(shm_ptr, msg, strlen(msg) + 1);
printf("子进程(%d) 已完成写入\n", getpid());
exit(EXIT_FAILURE);
} else {
// 父进程: 等待子进程完成
wait(NULL);
// 读取共享内存中的数据
printf("父进程(%d) 从共享内存读取数据: %s\n", getpid(), shm_ptr);
// 分离共享内存
if (shmdt(shm_ptr) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(EXIT_FAILURE);
}
}
return 0;
}
```
%%
<br><br>
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
<br>
# ♾️参考资料
# Footnotes
[^1]: 《UNIX 环境高级编程》(P424)
[^2]: 《深入理解计算机系统》P583~P584
[^3]: 《UNIX 网络编程卷 2:进程间通信》(P247)
[^4]: 《UNIX 网络编程卷 2:进程间通信》(P257~258)
[^5]: 《UNIX 环境高级编程》(P465)