Linux的DMA拷贝、MMAP映射与sendfile原理详解
1. DMA拷贝(Direct Memory Access)
原理:
DMA(直接内存访问)是一种硬件机制,允许外设(如网卡、磁盘控制器)直接与内存交互,无需CPU参与数据拷贝。CPU仅需初始化传输参数(如源地址、目标地址、数据长度),后续传输由DMA控制器完成,从而释放CPU资源。
示例:
假设从磁盘读取文件到内存:
- CPU通知磁盘控制器通过DMA传输数据到内存地址
0x1000
。 - 磁盘控制器直接操作内存总线,将数据写入
0x1000
,完成后通过中断通知CPU。 - CPU无需逐字节拷贝数据,可并行处理其他任务。
图例:
传统拷贝:
磁盘 → CPU寄存器 → 内存
DMA拷贝:
磁盘 → 内存(直接操作,无需CPU)
2. MMAP映射(Memory Mapping)
原理:
MMAP将文件直接映射到进程的虚拟地址空间,使得文件操作如同访问内存。内核通过页表将虚拟地址映射到文件对应的物理页,首次访问时触发缺页中断,由操作系统将文件内容加载到内存。
优势:
- 减少用户态与内核态之间的数据拷贝(如
read()
/write()
)。 - 支持共享内存(多个进程映射同一文件)。
示例:
// 将文件映射到内存
int fd = open("large_file.dat", O_RDWR);
void *addr = mmap(NULL, file_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
// 直接通过指针操作文件
memcpy(addr + offset, data, len);
munmap(addr, file_size);
图例:
进程虚拟地址空间
│
├─── Code
├─── Data
└─── Mapped File (通过页表直接关联磁盘文件)
3. sendfile系统调用
原理:
sendfile()
用于在内核态直接将文件数据从文件描述符(如磁盘文件)传输到套接字,无需用户态参与。其过程如下:
- 文件数据通过DMA从磁盘读取到内核缓冲区(Page Cache)。
- 内核直接将缓冲区数据通过DMA拷贝到网卡缓冲区(NIC Buffer)。
- 全程无需数据经过用户态,减少2次上下文切换和1次数据拷贝。
优化:
- 结合
scatter-gather DMA
,实现“零拷贝”(Zero-Copy),无需内核缓冲区到套接字缓冲区的拷贝。
示例:
// Web服务器发送静态文件
int file_fd = open("index.html", O_RDONLY);
int socket_fd = accept(...);
sendfile(socket_fd, file_fd, NULL, file_size);
图例:
传统传输:
磁盘 → 内核缓冲区 → 用户缓冲区 → 内核Socket缓冲区 → 网卡
sendfile传输:
磁盘 → 内核缓冲区 → 网卡(直接DMA传输)
4、对比总结
技术 | 数据拷贝次数 | CPU参与度 | 典型场景 |
---|---|---|---|
DMA | 0(硬件完成) | 仅初始化 | 外设与内存交互(如磁盘I/O) |
MMAP | 1(缺页加载) | 低(内存访问) | 大文件读写、进程间共享(大视频文件) |
sendfile | 0-1 | 低(系统调用) | 网络文件传输(如HTTP服务器) |
图示补充:
- DMA拷贝:外设与内存直连,绕过CPU。
- MMAP:进程虚拟地址空间直接映射到文件页。
- sendfile:内核内部数据管道,避免用户态切换。
通过结合这些技术,Linux可显著提升I/O性能,降低系统开销。
----------分界线-----------------------------------------------------------------------------------------------
以下是 Linux 中 DMA 拷贝、MMAP 映射 和 sendfile 的具体调用方法及示例代码
5、DMA 拷贝的隐式调用
DMA 由操作系统自动管理,开发者无需直接调用 DMA 接口,但可通过标准 I/O 函数(如 read
/write
)隐式触发 DMA 操作。
示例:从磁盘读取文件到内存
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("data.bin", O_RDONLY); // 打开文件
char buffer[4096];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer)); // 触发DMA拷贝
close(fd);
return 0;
}
说明:
read
函数内部通过 DMA 将磁盘数据直接拷贝到用户态内存,无需 CPU 逐字节参与。- 数据流向:
磁盘 → 内核缓冲区(DMA) → 用户缓冲区
。
6、MMAP 映射的显式调用
使用 mmap
系统调用将文件映射到进程内存空间,直接通过指针操作文件。
示例:修改文件内容
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("example.txt", O_RDWR); // 打开可读写文件
off_t file_size = lseek(fd, 0, SEEK_END); // 获取文件大小
char *mapped = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // 映射文件
// 直接通过内存指针修改文件内容
mapped[0] = 'A'; // 修改第一个字符为'A'
msync(mapped, file_size, MS_SYNC); // 同步到磁盘(可选)
munmap(mapped, file_size); // 解除映射
close(fd);
return 0;
}
说明:
mmap
将文件映射到虚拟内存,修改内存即修改文件(需MAP_SHARED
标志)。- 数据流向:
磁盘 → 内核页缓存 → 进程虚拟内存(零拷贝)
。
7、sendfile 的高效传输
使用 sendfile
系统调用在内核态直接传输文件到网络套接字,避免用户态拷贝。
示例:Web服务器发送静态文件
#include <sys/sendfile.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main() {
int file_fd = open("index.html", O_RDONLY); // 打开文件
int sock_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
// ... 绑定(bind)、监听(listen)、接受连接(accept)等操作 ...
off_t offset = 0;
off_t file_size = lseek(file_fd, 0, SEEK_END); // 获取文件大小
sendfile(sock_fd, file_fd, &offset, file_size); // 直接发送文件到套接字
close(sock_fd);
close(file_fd);
return 0;
}
说明:
sendfile
将文件内容从file_fd
直接传输到sock_fd
,无需用户态缓冲区。- 数据流向:
磁盘 → 内核缓冲区 → 网卡缓冲区(零拷贝)
。
(望各位潘安、各位子健/各位彦祖、于晏不吝赐教!多多指正!🙏)