## 第 15 章 内存映射和 DMA
本章研究 Linux 内存管理的部分, 重点在对于设备驱动作者有用的技术. 许多类型的驱动编程需要一些对于虚拟内存子系统如何工作的理解; 我们在本章涉及到的材料来自手头, 而不是象我们曾进入更加复杂和性能关键的子系统一样. 虚拟内存子系统也是 Linux 内核核心的非常有趣的部分, 并且因而, 值得一见.
本章的材料分为 3 个部分:
-
第一部分涉及 mmap 系统调用的实现, 它允许设备内存直接映射到一个用户进程地址空间. 不是所有的设备需要 mmap 支持, 但是, 对一些, 映射设备内存可产生可观的性能提高.
-
我们接着看从其他的方向跨过边界, 用对直接存取用户空间的讨论. 相对少驱动需要这个能力; 在大部分情况下, 内核做这种映射而驱动甚至不知道它. 但是了解如何映射用户空间内存到内核(使用 get_user_pages)会有用.
-
最后一节涵盖直接内存存取( DMA ) I/O 操作, 它提供给外设对系统内存的直接存取.
当然, 所有这些技术需要一个对 Linux 内存管理如何工作的理解, 因此我们从对这个子系统的总览开始.
### 15.1. Linux 中的内存管理
不是描述操作系统的内存管理理论, 本节试图指出 Linux 实现的主要特点. 尽管你不必是一位 Linux 虚拟内存专家来实现 mmap, 一个对事情如何工作的基本了解是有用的. 下面是一个相当长的对内核使用来管理内存的数据结构的描述. 一旦必要的背景已被覆盖, 我们就进入使用这个结构.
### 15.1.1. 地址类型
Linux 是, 当然, 一个虚拟内存系统, 意味着用户程序见到的地址不直接对应于硬件使用的物理地址. 虚拟内存引入了一个间接层, 它允许了许多好事情. 有了虚拟内存, 系统重运行的程序可以分配远多于物理上可用的内存; 确实, 即便一个单个进程可拥有一个虚拟地址空间大于系统的物理内存. 虚拟内存也允许程序对进程的地址空间运用多种技巧, 包括映射成员的内存到设备内存.
至此, 我们已经讨论了虚拟和物理地址, 但是许多细节被掩盖过去了. Linux 系统处理几种类型的地址, 每个有它自己的含义. 不幸的是, 内核代码不是一直非常清楚确切地在每个情况下在使用什么类型地地址, 因此程序员必须小心.
下面是一个 Linux 中使用的地址类型列表. 图 [Linux 中使用的地址类型](# "图 15.1. Linux 中使用的地址类型")显示了这个地址类型如何关联到物理内存.
**图 15.1. Linux 中使用的地址类型**
![Linux 中使用的地址类型](https://box.kancloud.cn/2015-09-02_55e6d9e89c256.png)
User virtual addresses
这是被用户程序见到的常规地址. 用户地址在长度上是 32 位或者 64 位, 依赖底层的硬件结构, 并且每个进程有它自己的虚拟地址空间.
Physical addresses
在处理器和系统内存之间使用的地址. 物理地址是 32- 或者 64-位的量; 甚至 32-位系统在某些情况下可使用更大的物理地址.
Bus addresses
在外设和内存之间使用的地址. 经常, 它们和被处理器使用的物理地址相同, 但是这不是必要的情况. 一些体系可提供一个 I/O 内存管理单元(IOMMU), 它在总线和主内存之间重映射地址. 一个 IOMMU 可用多种方法使事情简单(例如, 使散布在内存中的缓冲对设备看来是连续的, 例如), 但是当设定 DMA 操作时对 IOMMU 编程是一个必须做的额外的步骤. 总线地址是高度特性依赖的, 当然.
Kernel logical addresses
这些组成了正常的内核地址空间. 这些地址映射了部分(也许全部)主存并且常常被当作它们是物理内存来对待. 在大部分的体系上, 逻辑地址和它们的相关物理地址只差一个常量偏移. 逻辑地址使用硬件的本地指针大小并且, 因此, 可能不能在重装备的 32-位系统上寻址所有的物理内存. 逻辑地址常常存储于 unsigned long 或者 void * 类型的变量中. 从 kmalloc 返回的内存有内核逻辑地址.
Kernel virtual addresses
内核虚拟地址类似于逻辑地址, 它们都是从内核空间地址到物理地址的映射. 内核虚拟地址不必有逻辑地址空间具备的线性的, 一对一到物理地址的映射, 但是. 所有的逻辑地址是内核虚拟地址, 但是许多内核虚拟地址不是逻辑地址. 例如, vmalloc 分配的内存有虚拟地址(但没有直接物理映射). kmap 函数(本章稍后描述)也返回虚拟地址. 虚拟地址常常存储于指针变量.
如果你有逻辑地址, 宏 __pa() ( 在 <asm/page.h> 中定义)返回它的关联的物理地址. 物理地址可被映射回逻辑地址使用 __va(), 但是只给低内存页.
不同的内核函数需要不同类型地址. 如果有不同的 C 类型被定义可能不错, 这样请求的地址类型是明确的, 但是我们没有这样的好运. 在本章, 我们尽力对在哪里使用哪种类型地址保持清晰.
### 15.1.2. 物理地址和页
物理内存被划分为离散的单元称为页. 系统的许多内部内存处理在按页的基础上完成. 页大小一个体系不同于另一个, 尽管大部分系统当前使用 4096-字节的页. 常量 PAGE_SIZE (定义在 <asm/page.h>) 给出了页大小在任何给定的体系上.
如果你查看一个内存地址 - 虚拟或物理 - 它可分为一个页号和一个页内的偏移. 如果使用 4096-字节页, 例如, 12 位低有效位是偏移, 并且剩下的, 高位指示页号. 如果你丢弃偏移并且向右移动剩下的部分 offset 位, 结果被称为一个页帧号 (PFN). 移位来在页帧号和地址之间转换是一个相当普通的操作. 宏 PAGE_SHIFT 告诉必须移动多少位来进行这个转换.
- Linux设备驱动第三版
- 第 1 章 设备驱动简介
- 1.1. 驱动程序的角色
- 1.2. 划分内核
- 1.3. 设备和模块的分类
- 1.4. 安全问题
- 1.5. 版本编号
- 1.6. 版权条款
- 1.7. 加入内核开发社团
- 1.8. 本书的内容
- 第 2 章 建立和运行模块
- 2.1. 设置你的测试系统
- 2.2. Hello World 模块
- 2.3. 内核模块相比于应用程序
- 2.4. 编译和加载
- 2.5. 内核符号表
- 2.6. 预备知识
- 2.7. 初始化和关停
- 2.8. 模块参数
- 2.9. 在用户空间做
- 2.10. 快速参考
- 第 3 章 字符驱动
- 3.1. scull 的设计
- 3.2. 主次编号
- 3.3. 一些重要数据结构
- 3.4. 字符设备注册
- 3.5. open 和 release
- 3.6. scull 的内存使用
- 3.7. 读和写
- 3.8. 使用新设备
- 3.9. 快速参考
- 第 4 章 调试技术
- 4.1. 内核中的调试支持
- 4.2. 用打印调试
- 4.3. 用查询来调试
- 4.4. 使用观察来调试
- 4.5. 调试系统故障
- 4.6. 调试器和相关工具
- 第 5 章 并发和竞争情况
- 5.1. scull 中的缺陷
- 5.2. 并发和它的管理
- 5.3. 旗标和互斥体
- 5.4. Completions 机制
- 5.5. 自旋锁
- 5.6. 锁陷阱
- 5.7. 加锁的各种选择
- 5.8. 快速参考
- 第 6 章 高级字符驱动操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 异步通知
- 6.5. 移位一个设备
- 6.6. 在一个设备文件上的存取控制
- 6.7. 快速参考
- 第 7 章 时间, 延时, 和延后工作
- 7.1. 测量时间流失
- 7.2. 获知当前时间
- 7.3. 延后执行
- 7.4. 内核定时器
- 7.5. Tasklets 机制
- 7.6. 工作队列
- 7.7. 快速参考
- 第 8 章 分配内存
- 8.1. kmalloc 的真实故事
- 8.2. 后备缓存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的变量
- 8.5. 获得大量缓冲
- 8.6. 快速参考
- 第 9 章 与硬件通讯
- 9.1. I/O 端口和 I/O 内存
- 9.2. 使用 I/O 端口
- 9.3. 一个 I/O 端口例子
- 9.4. 使用 I/O 内存
- 9.5. 快速参考
- 第 10 章 中断处理
- 10.1. 准备并口
- 10.2. 安装一个中断处理
- 10.3. 前和后半部
- 10.4. 中断共享
- 10.5. 中断驱动 I/O
- 10.6. 快速参考
- 第 11 章 内核中的数据类型
- 11.1. 标准 C 类型的使用
- 11.2. 安排一个明确大小给数据项
- 11.3. 接口特定的类型
- 11.4. 其他移植性问题
- 11.5. 链表
- 11.6. 快速参考
- 第 12 章 PCI 驱动
- 12.1. PCI 接口
- 12.2. 回顾: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 总线
- 12.5. SBus
- 12.6. NuBus 总线
- 12.7. 外部总线
- 12.8. 快速参考
- 第 13 章 USB 驱动
- 13.1. USB 设备基础知识
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 编写一个 USB 驱动
- 13.5. 无 urb 的 USB 传送
- 13.6. 快速参考
- 第 14 章 Linux 设备模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低级 sysfs 操作
- 14.3. 热插拔事件产生
- 14.4. 总线, 设备, 和驱动
- 14.5. 类
- 14.6. 集成起来
- 14.7. 热插拔
- 14.8. 处理固件
- 14.9. 快速参考
- 第 15 章 内存映射和 DMA
- 15.1. Linux 中的内存管理
- 15.2. mmap 设备操作
- 15.3. 进行直接 I/O
- 15.4. 直接内存存取
- 15.5. 快速参考
- 第 16 章 块驱动
- 16.1. 注册
- 16.2. 块设备操作
- 16.3. 请求处理
- 16.4. 一些其他的细节
- 16.5. 快速参考
- 第 17 章 网络驱动
- 17.1. snull 是如何设计的
- 17.2. 连接到内核
- 17.3. net_device 结构的详情
- 17.4. 打开与关闭
- 17.5. 报文传送
- 17.6. 报文接收
- 17.7. 中断处理
- 17.8. 接收中断缓解
- 17.9. 连接状态的改变
- 17.10. Socket 缓存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 统计信息
- 17.14. 多播
- 17.15. 几个其他细节
- 17.16. 快速参考
- 第 18 章 TTY 驱动
- 18.1. 一个小 TTY 驱动
- 18.2. tty_driver 函数指针
- 18.3. TTY 线路设置
- 18.4. ioctls 函数
- 18.5. TTY 设备的 proc 和 sysfs 处理
- 18.6. tty_driver 结构的细节
- 18.7. tty_operaions 结构的细节
- 18.8. tty_struct 结构的细节
- 18.9. 快速参考