⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 ch15s02.html

📁 介绍Linux内核驱动编程的一本书 最主要的是有源代码,都是可用的 学习操作系统很好
💻 HTML
📖 第 1 页 / 共 3 页
字号:
 printk(KERN_NOTICE "Simple VMA close.\n");}static struct vm_operations_struct simple_remap_vm_ops = { .open = simple_vma_open, .close = simple_vma_close,};</pre><p>为使这些操作为一个特定的映射激活, 有必要存储一个指向 simple_remap_um_ops 指针在相关 VMA 的 vm_ops 成员中. 这常常在 mmap 方法中完成. 如果你回看 simple_remap_mmap 例子, 你见到这些代码行:</p><pre class="programlisting">vma-&gt;vm_ops = &amp;simple_remap_vm_ops;simple_vma_open(vma);</pre><p>注意对 simple_vma_open 的明确调用. 因为 open 方法不在初始化 mmap 时调用, 我们必须明确调用它如果我们要它运行.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="MappingMemorywithnopage.sect2"></a>15.2.4.&#160;使用 nopage 映射内存</h3></div></div></div><p>尽管 remap_pfn_range 对许多人工作得不错, 如果不是大部分人, 驱动 mmap 的实现有时有点更大的灵活性是必要的. 在这样的情况下, 一个使用 nopage VMA 方法的实现可被调用.</p><p>一种 nopage 方法有用的情况可由 mremap 系统调用引起, 它被应用程序用来改变一个被映射区的绑定地址. 如它所发生的, 当一个被映射的 VMA 被 mremap 改变时内核不直接通知驱动. 如果这个 VMA 的大小被缩减, 内核可静静地刷出不需要的页, 而不必告诉驱动. 相反, 如果这个 VMA 被扩大, 当映射必须为新页建立时, 驱动最终通过对 nopage 的调用发现, 因此没有必要进行特殊的通知. nopage 方法, 因此, 如果你想支持 mremap 系统调用必须实现. 这里, 我们展示一个简单的 nopage 实现给 simple 设备.</p><p>nopage 方法, 记住, 有下列原型:</p><pre class="programlisting">struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);</pre><p>当一个用户进程试图存取在一个不在内存中的 VMA 中的一个页, 相关的 nopage 函数被调用. 地址参数包含导致出错的虚拟地址, 向下圆整到页的开始. nopage 函数必须定位并返回用户需要的页的 struct page 指针. 这个函数必须也负责递增它通过调用 get_page 宏返回的页的使用计数.</p><pre class="programlisting"> get_page(struct page *pageptr); </pre><p>这一步是需要的来保持在被映射页的引用计数正确. 内核为每个页维护这个计数; 当计数到 0, 内核知道这个页可被放置在空闲列表了. 当一个 VMA 被去映射, 内核递减使用计数给区中每个页. 如果你的驱动在添加一个页到区时不递增计数, 使用计数过早地成为 0, 系统的整体性被破坏了.</p><p>nopage 方法也应当存储错误类型在由 type 参数指向的位置 -- 但是只当那个参数不为 NULL. 在设备驱动中, 类型的正确值将总是 VM_FAULT_MINOR.</p><p>如果你使用 nopage, 当调用 mmap 常常很少有工作来做; 我们的版本看来象这样:</p><pre class="programlisting">static int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma){ unsigned long offset = vma-&gt;vm_pgoff &lt;&lt; PAGE_SHIFT;    if (offset &gt;= __pa(high_memory) || (filp-&gt;f_flags &amp; O_SYNC)) vma-&gt;vm_flags |= VM_IO; vma-&gt;vm_flags |= VM_RESERVED; vma-&gt;vm_ops = &amp;simple_nopage_vm_ops; simple_vma_open(vma); return 0;}</pre><p>mmap 必须做的主要的事情是用我们自己的操作来替换缺省的(NULL)vm_ops 指针. nopage 方法接着进行一次重新映射一页并且返回它的 struct page 结构的地址. 因为我们这里只实现一个到物理内存的窗口, 重新映射的步骤是简单的: 我们只需要定位并返回一个指向 struct page 的指针给需要的地址. 我们的 nopage 方法看来如下:</p><pre class="programlisting">struct page *simple_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type) { struct page *pageptr; unsigned long offset = vma-&gt;vm_pgoff &lt;&lt; PAGE_SHIFT; unsigned long physaddr = address - vma-&gt;vm_start + offset; unsigned long pageframe = physaddr &gt;&gt; PAGE_SHIFT; if (!pfn_valid(pageframe)) return NOPAGE_SIGBUS; pageptr = pfn_to_page(pageframe); get_page(pageptr); if (type) *type = VM_FAULT_MINOR; return pageptr;}</pre><p>因为, 再一次, 在这里我们简单地映射主内存, nopage 函数只需要找到正确的 struct page 给出错地址并且递增它的引用计数. 因此, 事件的请求序列是计算需要地物理地址, 并且通过右移它 PAGE_SHIFT 位转换它为以页帧号. 因为用户空间可以给我们任何它喜欢的地址, 我们必须确保我们有一个有效的页帧; pfn_valid 函数为我们做这些. 如果地址超范围, 我们返回 NOPAGE_SIGBUS, 它产生一个总线信号被递交给调用进程.</p><p>否则, pfn_to_page 获得必要的 struct page 指针; 我们可递增它的引用计数(使用调用 get_page)并且返回它.</p><p>nopage 方法正常地返回一个指向 struct page 的指针. 如果, 由于某些原因, 一个正常的页不能返回(即, 请求的地址超出驱动的内存区), NOPAGE_SIGBUS 可被返回来指示错误; 这是上的简单代码所做的. nopage 也可以返回 NOPAGE_OOM 来指示由于资源限制导致的失败.</p><p>注意, 这个实现对 ISA 内存区起作用, 但是对那些在 PCI 总线上的不行. PCI 内存被映射在最高的系统内存之上, 并且在系统内存中没有这些地址的入口. 因为没有 struct page 来返回一个指向的指针, nopage 不能在这些情况下使用; 你必须使用 remap_pfn_range 代替.</p><p>如果 nopage 方法被留置为 NULL, 处理页出错的内核代码映射零页到出错的虚拟地址. 零页是一个写时拷贝的页, 它读作为0, 并且被用来, 例如, 映射 BSS 段. 任何引用零页的进程都看到: 一个填满 0 的页. 如果进程写到这个页, 它最终修改一个私有页. 因此, 如果一个进程扩展一个映射的页通过调用 mremap, 并且驱动还没有实现 nopage, 进程结束以零填充的内存代替一个段错误.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="RemappingSpecificIORegions.sect2"></a>15.2.5.&#160;重新映射特定 I/O 区</h3></div></div></div><p>所有的我们至今所见的例子是 /dev/mem 的重新实现; 它们重新映射物理地址到用户空间. 典型的驱动, 但是, 想只映射应用到它的外设设备的小的地址范围, 不是全部内存. 为了映射到用户空间只一个整个内存范围的子集, 驱动只需要使用偏移. 下面为一个驱动做这个技巧来映射一个 simple_region_size 字节的区域, 在物理地址 simple_region_start(应当是页对齐的) 开始:</p><pre class="programlisting">unsigned long off = vma-&gt;vm_pgoff &lt;&lt; PAGE_SHIFT;unsigned long physical = simple_region_start + off;unsigned long vsize = vma-&gt;vm_end - vma-&gt;vm_start;unsigned long psize = simple_region_size - off;if (vsize &gt; psize) return -EINVAL; /* spans too high */remap_pfn_range(vma, vma_&gt;vm_start, physical, vsize, vma-&gt;vm_page_prot);</pre><p>除了计算偏移, 这个代码引入了一个检查来报告一个错误当程序试图映射超过在目标设备的 I/O 区可用的内存. 在这个代码中, psize 是已指定了偏移后剩下的物理 I/O 大小, 并且 vsize 是虚拟内存请求的大小; 这个函数拒绝映射超出允许的内存范围的地址.</p><p>注意, 用户空间可一直使用 mremap 来扩展它的映射, 可能超过物理设备区的结尾. 如果你的驱动不能定义一个 nopage method, 它从不会得到这个扩展的通知, 并且额外的区映射到零页. 作为一个驱动编写者, 你可能很想阻止这种行为; 映射理由到你的区的结尾不是一个明显的坏事情, 但是很不可能程序员希望它发生.</p><p>最简单的方法来阻止映射扩展是实现一个简单的 nopage 方法, 它一直导致一个总线信号被发送给出错进程. 这样的一个方法可能看来如此:</p><pre class="programlisting">struct page *simple_nopage(struct vm_area_struct *vma, unsigned long address, int *type);{ return NOPAGE_SIGBUS; /* send a SIGBUS */}</pre><p>如我们已见到的, nopage 方法只当进程解引用一个地址时被调用, 这个地址在一个已知的 VMA 中但是当前没有有效的页表入口给这个 VMA. 如果有已使用 remap_pfn_range 来映射全部设备区, 这里展示的 nopage 方法只被调用来引用那个区外部. 因此, 它能够安全地返回 NOPAGE_SIGBUS 来指示一个错误. 当然, 一个更加完整的 nopage 实现可以检查是否出错地址在设备区内, 并且如果是这样进行重新映射. 但是, 再一次, nopage 无法在 PCI 内存区工作, 因此 PCI 映射的扩展是不可能的.</p></div><div class="sect2" lang="zh-cn"><div class="titlepage"><div><div><h3 class="title"><a name="RemappingRAM.sect2"></a>15.2.6.&#160;重新映射 RAM</h3></div></div></div><p>remap_pfn_range 的一个有趣的限制是它只存取保留页和在物理内存顶之上的物理地址. 在 Linux, 一个物理地址页被标志为"保留的"在内存映射中来指示它对内存管理是不可用的. 在 PC 上, 例如, 640 KB 和 1MB 之间被标记为保留的, 如同驻留内核代码自身的页. 保留页被锁定在内存并且是唯一可被安全映射到用户空间的; 这个限制是系统稳定的一个基本要求.</p><p>因此, remap_pfn_range 不允许你重新映射传统地址, 这包括你通过调用 get_free_page 获得的. 相反, 它映射在零页. 所有都看来正常, 除了进程见到私有的, 零填充的页而不是它在期望的被重新映射的 RAM. 这个函数做了大部分硬件驱动需要来做的所有事情, 因为它能够重新映射高端 PCI 缓冲和 ISA 内存.</p><p>remap_pfn_range 的限制可通过运行 mapper 见到, 其中一个例子程序在 misc-progs 在 O'Reilly 的 FTP 网站提供的文件. mapper 是一个简单的工具可用来快速测试 mmap 系统调用; 它映射由命令行选项指定的一个文件的只读部分, 并且输出被映射的区到标准输出. 下面的部分, 例如, 显示 /dev/mem 没有映射位于地址 64 KB的物理页 --相反, 我们看到一个页充满 0 (例子中的主机是一台 PC, 但是结果应该在其他平台上相同).</p><pre class="screen">morgana.root# ./mapper /dev/mem 0x10000 0x1000 | od -Ax -t x1mapped "/dev/mem" from 65536 to 69632000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00*001000</pre><p>remap_pfn_range 处理 RAM 的不能之处说明基于内存的设备如 scull 不能轻易实现 mmap, 因为它的设备内存是传统内存, 不是 I/O 内存. 幸运的是, 一个相对容易的方法对任何需要映射 RAM 到用户空间的驱动都可用; 它使用我们前面已见过的 nopage 方法.</p><div class="sect3" lang="zh-cn"><div class="titlepage"><div><div><h4 class="title"><a name="RemmapingRAMwiththenopagemethod.sect3"></a>15.2.6.1.&#160;使用 nopage 方法重新映射 RAM</h4></div></div></div><p>映射真实内存到用户空间的方法是使用 vm_ops-&lt;nopage 来一次一个地处理页错. 一个简单的实现是 scullp 模块的一部分, 在第 8 张介绍.</p><p>scullp 是一个面向页的字符设备. 因为它是面向页的, 它可以在它的内存上实现 mmap. 实现内存映射的代码使用一些在"Linux 中的内存管理"一节中介绍的概念.</p><p>在检查代码前, 让我们查看影响在 scullp 中的 mmap 实现的设计选择.</p><div class="itemizedlist"><ul type="disc"><li><p>scullp 只要设备被映射就不会释放设备内存. 这是策略问题而非一个需求, 并且它不同于 scull 和类似设备的行为, 它们被截短为 0 当为写而打开时. 对释放一个映射的 scullp 设备的拒绝, 允许一个进程覆盖被其他进程映射的区., 因此你可以测试并且看进程和设备内存如何交互. 为避免释放一个映射设备, 驱动必须保持一个激活映射的计数; 在设备结构中的 vmas 成员被用来作此目的.</p></li><li><p>内存映射仅当 scullp 的 order 参数(在模块加载时间设置)是 0 时进行. 这个参数控制 __get_free_pages 如何被调用( 见第 8 章"get_free_page 及其友" 一节). 0 order 的限制( 这强制一次分配一页, 而不是以大的组)被 __get_free_pages 的内部所规定, 它是 scullp 所使用的分配函数. 为最大化分配性能, Linux 内核维护一个空闲页列表给每个分配级别, 并且只有在一个簇中第一页的引用计数被 get_free_pages 递增以及被 free_pages 递减. mmap 方法对一个 scullp 设备被禁止, 如果分配级大于 0, 因为 nopage 处理单个页而不是一簇页. scullp 不知道如何正确管理是高级别分配的一部分的页的引用计数.(如果你需要重新回顾 scullp 和 内存分配级别值, 返回第 8 章的"一个使用整页的 scull: scullp"一节.)</p></li></ul></div><p>0-级的限制大部分是用来保持代码简单. 它可能正确实现 mmap 给多页分配, 通过使用页的使用计数, 但是它可能只增加了例子的复杂性而没有介绍任何有趣的信息.</p><p>打算根据刚刚概括的规则来映射 RAM 的代码, 需要实现 open, close, 和 nopage VMA 方法; 它还需要存取内存映射来调整页使用计数.</p><p>这个 scullp_mmap 的实现非常短, 因为它依赖 nopage 函数来做所有的感兴趣的工作:</p><pre class="programlisting">int scullp_mmap(struct file *filp, struct vm_area_struct *vma){ struct inode *inode = filp-&gt;f_dentry-&gt;d_inode; /* refuse to map if order is not 0 */

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -