📄 (ldd) ch13-mmap和dma(转载).htm
字号:
color=#ffffff size=3>
<P>上面给出的实现的主要问题在于驱动程序没有维护一个与被映射区域的连接。这对/dev/<BR>mem来说并不是个问题,它是核心的一个完整的部分,但对于模块来说必须有一个办法来<BR>保持它的使用计数是最新的。一个程序可以对文件描述符调用close,并仍然访问内存映<BR>射的区段。然而,如果关闭文件描述符导致模块的使用计数降为零,那么模块可能被卸<BR>载,即使它们仍被通过mmap使用着。<BR> <BR>试图关于这个问题警告模块的使用者是不充分的解决办法,因为可能使用kerneld装载和<BR>卸载你的模块。这个守护进程在模块的使用计数降为零时自动地去除它们,你当然不能<BR>警告kerneld去留神mmap。<BR> <BR>这个问题的解决办法是用跟踪使用计数的操作取代缺省的vma->vm.ops。代码相当简单—<BR>—用于模块化的/dev/mem的一个完全的mmap实现如下所示:<BR> <BR>(代码278)<BR> <BR>这个代码依赖于一个事实,即核心在调用f_op->mmap之前将新产生区域中的vm_ops域初<BR>始化为NULL。为安全起见以防止在将来的核心发生什么改变,给出的代码检查了指针的<BR>当前值。<BR> <BR>给出的实现利用了一个概念,即open(vma)和close(vma)都是缺省实现的一个补充。<BR>驱动程序的方法不须复制打开和关闭的内存区域的标准代码;驱动程序只是实现额外的<BR>管理。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>有趣的是注意到,VMA的swapin和swapout方法以另外的方式工作——驱动程序定义的vm_<BR>ops->swap*不是添加而是用完全不同的东西取代了缺省实现。<BR> <BR>支持mremap系统调用<BR> <BR>mremap系统调用被应用程序用来改变映射区段的边界地址。如果驱动程序希望能支持mre<BR>map,以前的实现就不能正确地工作,因为驱动程序没有办法知道映射的区域已经改变了<BR>。<BR> <BR>Linux的mremap实现不提醒驱动程序关于映射区域的改变。实际上,它到是通过unmap方<BR>法在区域减小时提醒驱动程序,但在区域变大时没有回调发出。<BR> <BR>将减小告诉驱动程序隐含的基本思想法是驱动程序(或是将常规文件映射到内存的文件<BR>系统)需要知道区段什么时候被取消映射了,从而采取适应的动作,如将页面刷新到磁<BR>盘上。另一方面,映射区域的增大对驱动程序来说意义不大。除非调用mremap的程序访<BR>问新的虚地址。在实际情况中,映射从未使用的区段是很常见的(如未使用过的某些程<BR>序代码段)。因此,Linux核心在映射区段增大时并不告诉驱动程序,因为nopage方法将<BR>会照管这些页。如果它们确实被访问了。<BR> <BR>换句话说,当映射区段增大时,驱动程序未被提醒是因为nopage后来会这样做;从而不<BR>必在需要前使用内存。这个优化主要是针对常规文件的,它们使用真正的RAM进行映射。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR> <BR>因此,如果你想支持mremap系统调用,就必须实现nopage。不过,一旦有了nopage,你<BR>可选择广泛地使用它,从而避免从fops->mmap调用remap_page_range;这在下一个代码<BR>段中给出。在这个mmap的实现中,设备方法只取代了vma->vm_fops。nopage方法负责一<BR>次重映射一个页并返回其地址。<BR> <BR>一个支持mremap(为节省空间,不支持使用计数)的/dev/mem实现如下所示:<BR> <BR>(代码279)<BR> <BR>(代码280)<BR> <BR>如果nopage方法被留为NULL,处理页面错的核心代码就将零页映射到出错虚地址。零页<BR>是一个写时 贝页,被当作零来读,可以用来映射BSS段。因此,如果一个进程通过调用<BR>mremap扩展一个映射区段,并且驱动程序没有实现nopage,你最终会得到一些零页,而<BR>不是段错。<BR> <BR>注意,给出的实现远远不是最优的;如果内存方法能绕过remap_page_range而直接返回<BR>物理地址会更好。不幸的是,这个技术的正确实现牵涉到一些细节,只能在本章晚些时<BR>候搞清楚。而且上面给出的实现在核心1.2中并不能工作,因为nopage的原型在版本1.2<BR>和2.0之间做了修改。在本节中我不打算管1.2核心。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>重映射特定的I/O区段<BR> <BR>到目前为止,我们所看到的所有例子都是/dev/mem的再次实现;它们将物理地址重映射<BR>到用户空间——或者至少这是它们认为它们所做的。然而,典型的驱动程序只想映射应<BR>用于它的外围设备的小地址区间,并非所有内存。<BR> <BR>为了能为一个特定的驱动程序自定义/dev/mem的实现,我们需要进一步来研究一下remap<BR>_page_range的内部。这个函数的完整原型是:<BR> <BR>int remap_page_range(unsigned long virt_add,unsigned long phy_add,unsigned<BR>long size, pgprot_t prot);<BR> <BR>这个函数的返回值通常为零或为一个负的错误代码。让我们看看它的参数的确切含义。<BR> <BR>unsigned long virt_add<BR> <BR>重映射开始处的虚拟地址。这个函数为虚地址空间virt_add和virt_add+size之间的范围<BR>构造页表。<BR> <BR>unsigned long phys_add<BR> <BR>虚拟地址应该映射到的物理地址。这个地址在上面提到的意义下是“物理的”这个函数<BR></P></FONT><FONT
color=#ffffff size=3>
<P>虚拟地址应该映射到的物理地址。这个地址在上面提到的意义下是“物理的”这个函数<BR>影响phys_add到phys_add+size之间的物理地址。<BR> <BR>unsigned long size<BR> <BR>被重映射的区域的大小,以字节为单位。<BR> <BR>pgprot_t prot<BR> <BR>为新页所请求的“保护”。驱动程序不必修改保护,而且在vma->vma_page_prot中找到<BR>的参数可以不加改变地使用。如果你好奇,你可以在<Linux/mm.h>中找到更多的信息。<BR> <BR>为了向用户空间映射整个内存区间的一个子集,驱动程序需要处理偏移量。下面几行为<BR>映射了从物理地址simple_region_start开始的simple_region_size字节大小的区段的驱<BR>动程序完成了这项工作:<BR> <BR>(代码281)<BR> <BR>除了计算偏移量,上面的代码还为错误条件引入了两个检查。第一个检查拒绝将一个在<BR>物理空间未对齐的位置映射到用户空间。由于只有完整的页能被重映射,因此映射的区<BR>段只能偏移页面大小的整数倍。ENXIO是这种情况下通常返回的错误代码,它被展开为“<BR>无此设备或地址”。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>第二个检查在程序试图映射多于目标设备I/O区段可获得内存的空间时报告一个错误。代<BR>码中psize是在偏移被确定后剩下的物理I/O大小,vsize是请求的虚存大小;这个函数拒<BR>绝映射超出允许内存范围的地址。<BR> <BR>注意,如果进程调用mremap,它便可以扩展其映射。一个“非常炫耀”的驱动程序可能<BR>希望阻止这个发生;达到目的的唯一办法是实现一个vma->nopage方法。下面是这个方法<BR>的最简单的实现:<BR> <BR>unsigned long simple_pedantic_nopage(struct vm_area_struct *vma,unsigned<BR>long address, int write_access);<BR> <BR>{return 0;} /*发送一个SIGBUS*/<BR> <BR>如果nopage方法返回0而不是一个有效的物理地址,一个SIGBUS(总线错)被发送到当前<BR>进程(即发生页面错的进程)。如果驱动程序没有实现nopage,进程在请求的虚地址处<BR>得到一个零页;这通常可以接受,因为mremap是个非常少用的系统调用,而且将零页映<BR>射到用户空间也没有安全问题。<BR> <BR>重映射RAM<BR> <BR>在Linux中,物理地址的一页被标记在内存映象中是“保留的”,表明不被内存管理系统<BR>使用。例如在PC上,640KB到1MB之间的部分被称为“保留的”,它被用来存放核心代码<BR></P></FONT><FONT
color=#ffffff size=3>
<P>使用。例如在PC上,640KB到1MB之间的部分被称为“保留的”,它被用来存放核心代码<BR>。<BR> <BR>remap_page_range的一个有趣的限制是,它只能给予对保留的页和物理内存之上的物理<BR>地址的访问。保留页被锁在内存中,是仅有的能安全映射到用户空间的页;这个限制是<BR>系统稳定性的基本要求*。<BR> <BR>因此,remap_page_range不允许你重映射常规地址——包括你通过调用get_free_page所<BR>获得的那些。不过,这个函数做了所有一个硬件驱动程序希望它做的,因为它可以重映<BR>射高PCI缓冲和ISA内存——包括第1兆内存和15MB处ISA洞,如果在第八章“1M以上的ISA<BR>内存”中提到的改变发生了的话。另一方面,当对非保留的页使用remap_page_range时<BR>,缺省的nopage处理程序映射被访问的虚地址处的零页。<BR> <BR>这个行为可以通过运行mapper看到。mapper是在O’Reilly的FTP站点上提供的文件中mis<BR>c_programs里的一个示例程序。它是个可以快速测试mmap系统调用的简单工具。mapper<BR>根据命令行选项映射一个文件中的只读部分,并把映射的区段输出到标准输出上。例如<BR>,下面这个交互过程表明/dev/mem不映射位于64KB地址处的物理页(本例中的宿主机是<BR>个PC,但在别的平台上结果应该是一样的):<BR> <BR>(代码283)<BR> <BR>remap_page_range对处理RAM的无能为力说明象scullp这样的设备不能简单地实现mmap,<BR>因为它的设备内存是常规RAM,而不是I/O内存。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>因为它的设备内存是常规RAM,而不是I/O内存。<BR> <BR>有两个办法可以绕过remap_page_range对RAM的不可用性。一个是“糟糕”的办法,另一<BR>个是干净的。<BR> <BR>使用预定位<BR> <BR>糟糕的办法要为你想映射到用户空间的页在mem_map[MAP_NR(page)]中置PG_reserved位<BR>。这样就预定了这些页,而一旦预定了,remap_page_range就可以按期望工作了。设置<BR>标志的代码很短很容易,但我不想在这儿给出来,因为另一个方法更有趣。不用说,不<BR>释放页面之前,预定的位必须被清除。<BR> <BR>有两个原因说明为什么这是个好办法。第一,被标为预定的页永远不会被内存管理所动<BR>。核心在数据结构初始化之前系统引导时确定它们,因此不能用在任何其它用途上。而<BR>另一方面,通过get_free_page,vmalloc或其它一些方式分配的页都是由内存子系统处理<BR>的。即使2.0核心在你运行时预定额外的页并不崩溃,这样做可能在将来会产生问题。因<BR>而是不鼓励的。不过你可以尝试这种快且脏的技术看看它是任何工作的。<BR> <BR>预定页不是个好办法的第二个原因是被预定的页不被算做是整个系统内存的一部分,有<BR>的用户在系统RAM发生变化时可能会很在意——用户经常留意空闲内存数量,而总的内存<BR>量一般总和空闲内存一道显示。<BR> <BR>实现nopage方法<BR></P></FONT><FONT
color=#ffffff size=3>
<P>实现nopage方法<BR> <BR>将实际RAM映射到用户空间的一个较好的办法是用vm_ops->nopage来一次处理一个页面错<BR>。作为scullp模块一部分的一个示例实现在第七章“把握内存”中介绍过。<BR> <BR>scullp是面向页的字符设备。因为它是面向页的,所以可以在它的内存中实现mmap。实<BR>现内存映射的代码用了一些以前在“Linux的内存管理”中介绍过的概念。<BR> <BR>在查看代码之前,让我们看一下影响scullp中mmap实现的设计选择。<BR> <BR>l 设备为模块更新使用计数<BR>在卸载模块时为了避免发生问题,内存区域的open和close方法被实现去跟踪模块的使用<BR>。<BR> <BR>l 设备为页更新使用计数<BR>这是为保证系统稳定是一个严格要求;不能更新这个计数将导致系统崩溃。每个页有其<BR>自己的使用计数;当它降为零时,该页被插入到空闲页表。当一个活动映象被破坏掉时<BR>,核心会将相关RAM页的使用计数减小。因此,驱动程序必须增加它所映射的每个页的使<BR>用计数(注意,这个计数在nopage增加它时不能为零,因为该页已经被fops->write分配<BR>了)。<BR> <BR>l 只要设备是被映射的,scullp就不能释放设备内存<BR>这与其说是个要求,不如说是项政策,这与scull及类似设备的行为不同,因为它们在被<BR></P></FONT><FONT
color=#ffffff size=3>
<P>这与其说是个要求,不如说是项政策,这与scull及类似设备的行为不同,因为它们在被<BR>因写而打开时长度被截为0。拒绝释放被映射的scullp设备允许一个设备一个进程重写正<BR>被另一个进程映射的区段,这样你就可以测试并看到进程与设备内存之间是如何交互的<BR>。为避免释放一个被映射的设备,驱动程序必须保存一个活动映射的计数;设备结构中<BR>的vma域被用于这个目的。<BR> <BR>l 只有在scullp的序号order参数为0时才进行内存影射<BR>这个参数控制get_free_pages是如何调用的(见第七章中“get_free_pages和朋友们”<BR>一节)。这个选择是由get_free_pages的内部机制决定的——scullp利用的分配机制。<BR>为了最大化分配性能,Linux核心为每个分配的序号(order)维护一个空闲页的列表,<BR>在一个簇中只有第一页的页计数由get-free_pages增加和free_pages减少。如果分配序<BR>号大于0,那么对一个scullp设备来说mmap方法是关闭的,因为nopage只处理单项,而不<BR>是一簇页。<BR> <BR> <BR> <BR>最后一个选择主要是为了保证代码的简单。通过处理页的使用计数,也有可能为多页分<BR>配正确地实现mmap,但那样只能增加例子的复杂性,而不能带来任何有趣的信息。<BR> <BR>如果代码想按照上面提到的规则来映射RAM,它需要实现open,close和nopage,还要访<BR>问mem_map。<BR> <BR>scullp_mmap的实现非常短,因为它依赖于nopage来完成所有有趣的工作:<BR></P></FONT><FONT
color=#ffffff size=3>
<P>scullp_mmap的实现非常短,因为它依赖于nopage来完成所有有趣的工作:<BR> <BR>(代码285 #1)<BR> <BR>开头的条件语句是为了避免映射未对齐的偏移和分配序号不为0的设备。最后,vm_ops-><BR>open被调用以更新模块的使用计数和设备的活动映射计数。<BR> <BR>open和close就是为了跟踪这些计数,被定义如下:<BR> <BR>(代码285 #2)<BR> <BR>由于模块生成了4个scullp设备并且也没有内存区域可用的private_data指针,所以open<BR>和close取得与vma相关联的scullp设备是通过从inode结构中抽取次设备号。次设备号被<BR>用来从设备结构的scullp_devices数组取偏移后得到指向正确结构的指针。<BR> <BR>大部分工作是由nopage完成的。当进程发生页面错时,这个函数必须取得被引用页的物<BR>理地址并返回给调用者。如果需要,这个方法可以计算address参数的页对齐。在scullp<BR>的实现中,address被用来计算设备里的偏移;偏移又被用来在scullp的内存树上查找正<BR>确的页。<BR> <BR>(代码286 #1)<BR> <BR>最后一行增加页计数;这个计数在atomic_t中生命,因此可以由一个原子操作更新。事<BR></P></FONT><FONT
color=#ffffff size=3>
<P>最后一行增加页计数;这个计数在atomic_t中生命,因此可以由一个原子操作更新。事<BR>实上,在这种特定的情况下,原子更新并不是严格要求的,因为该页已经在使用中,并<BR>且没有与中断处理程序或别的异步代码的竞争条件。<BR> <BR>现在scullp可以按预期的那样工作了,正如你在工具mapper的示例输出中所看到的:<BR> <BR>(代码286 #2)<BR> <BR>(代码287 #1)<BR> <BR> <BR> <BR>重映射虚地址<BR> <BR>尽管很少需要重映射虚地址,但看看驱动程序如何用mmap将虚地址映射到用户空间是很<BR>有趣的。这里虚地址指的是由vmalloc返回的地址,也就是被映射到核心页表的虚地址。<BR>本节的代码取自scullv,这个模块与scullp类似,只是它通过vmalloc分配存储。<BR> <BR>scullv的大部分实现与我们刚刚看到的scullp完全类似,除了不需要检查分配序号。原<BR>因是vmalloc一次只分配一页,因为单页分配比多页分配容易成功的多。因此,使用计数<BR>问题在通过vmalloc分配的空间中不适用。<BR> <BR>scullv的主要工作是构造一个页表,从而可以象连续地址空间一样访问分配的页。而另<BR></P></FONT><FONT
color=#ffffff size=3>
<P>scullv的主要工作是构造一个页表,从而可以象连续地址空间一样访问分配的页。而另<BR>一方面,nopage必须向调用者返回一个物理地址。因此,scullv的nopage实现必须扫描<BR>页表以取得与页相关联的物理地址。<BR> <BR>这个函数与我们在scullp中看到的一样,除了结尾。这个代码的节选只包括了nopage中<BR>与scullp不同的部分。<BR> <BR>(代码287 2#)<BR> <BR> atomic_inc(&mem_map[MAP_NR(page)]).count;<BR> <BR> return page;<BR> <BR> }<BR> <BR>页表由本章开始时介绍的那些函数来查询。用于这个目的的页目录存在核心空间的内存<BR>结构init_m中。<BR> <BR>宏VMALLOC_VMADDR(pageptr)返回正确的unsigned long值用于vmalloc地址的页表查询。<BR>注意,由于一个内存管理的问题,这个值的强制类型转换在早于2.1的X86核心上不能工<BR>作。在X86的2.1.1版中内存管理做了改动,VMALLOC_VMADDR被定义为一个实体函数,与<BR>在其它平台上一样。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>最后要提到的一点是init_mm是如何被访问的,因为我前面提到过,它并未引出到模块中<BR>。实际上,scullv要作一些额外的工作来取得init_mm的指针,解释如下。<BR> <BR>实际上,常规模块并不需要init_mm,因为它们并不期望与内存管理交互;它们只是调用<BR>分配和释放函数。为scullv的mmap实现很少见。本小节中介绍的代码实际上并不用来驱<BR>动硬件;我介绍它只是用实际代码来支持关于页表的讨论。<BR> <BR>不过,既然谈到这儿,我还是想给你看看scullv是如何获得init_mm的地址的。这段代码<BR>依赖于这样的事实:0号进程(所谓的空闲任务)处于内核中,它的页目录描述了核心地<BR>址空间。为了触到空闲任务的数据结构,scullv扫描进程链表直到找到0号进程。<BR> <BR>(代码288)<BR> <BR>这个函数由fops->mmap调用,因为nopage只在mmap调用后运行。<BR> <BR>基于上面的讨论,你也许还想将由vremap(如果你用Linux2.1,就是ioremap)返回的地<BR>址映射到用户空间。这很容易实现,因为你可以直接使用remap_page_range,而不用实<BR>现虚拟内存区域的方法。换句话说,remap_page_range已经可用以构造将I/O内存映射到<BR>用户空间的页表;并不需要象我们在scullv中那样查看由vremap构造的核心页表。<BR> <BR>直接内存访问<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>直接内存访问,或DMA,是我们内存访问方面讨论的高级主题。DMA是一种硬件机制,它<BR>允许外围组件将I/O数据直接从(或向)主存中传送。<BR> <BR>为了利用硬件的DMA能力,设备驱动程序需要能正确地设置DMA传送并能与硬件同步。不<BR>幸的是,由于DMA的硬件实质,它非常以来于系统。每种体系结构都有它自己管理DMA传<BR>送的技术,编程接口也互不相同。核心也不能提供一个一致的接口,因为驱动程序很难<BR>将底层硬件机制适当地抽象。本章中,我将描述DMA在ISA设备及PCI外围上是如何工作的<BR>,因为它们是目前最常用的外围接口体系结构。<BR> <BR>不过,我不想讨论ISA太多的细节。DMA的ISA实现过于复杂,在现代外围中并不常用。目<BR>前ISA总线主要用在哑外围接口上,而需要DMA能力的硬件生产商倾向于使用PCI总线。<BR> <BR>DMA数据传送的概况<BR> <BR>在介绍编程细节以前,我们先大致看看DMA传送是如何工作的。为简化讨论,只介绍输入<BR>传送。<BR> <BR>数据传送有两种方式触发:或者由软件请求数据(通过一个函数如read),或者由硬件<BR>将数据异步地推向系统。<BR> <BR>在第一中情况下,各步骤可如下概括:<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>l 当一个进程调用一个read,这个驱动程序方法分配一个DMA缓冲区,并告诉硬<BR>件去传诵数据。进程进入睡眠。<BR> <BR>l 硬件向DMA缓冲区写数据,完成时发出一个中断。<BR> <BR>l 中断处理程序获得输入数据,应答中断,唤醒进程,它现在可以读取数据。<BR> <BR>有时DMA被异步地使用。例如,一些数据采集设备持续地推入数据,即使没有人读它。这<BR>种情况下,驱动程序要维护一个缓冲区,使得接下来的一个read调用可以将所有累积的<BR>数据返回到拥护空间。这种传送的步骤稍有不同:<BR> <BR>l 硬件发出一个中断,表明新的数据到达了。<BR> <BR>l 中断处理程序分配一个缓冲区,告诉硬件将数据传往何处。<BR> <BR>l 外围设备将数据写入缓冲区;当写完时,再次发出中断。<BR> <BR>l 处理程序派发新数据,唤醒所有相关进程,处理一些杂务。<BR> <BR> <BR> <BR>上面这两种情况下的处理步骤都强调:高效的DMA处理以来于中断报告。尽管可以用一个<BR></P></FONT><FONT
color=#ffffff size=3>
<P>上面这两种情况下的处理步骤都强调:高效的DMA处理以来于中断报告。尽管可以用一个<BR>轮询驱动程序来实现DMA,这样做并无意义,因为轮询驱动程序会将DMA相对于简单的处<BR>理器驱动I/O获得的性能优势都抵消了。<BR> <BR>这里介绍的另一个相关问题是DMA缓冲区。为利用直接内存访问,设备驱动程序必须能分<BR>配一个特殊缓冲区以适合DMA。注意大多数驱动程序在初始化时分配它们的缓冲区,一直<BR>使用到关机——因此,上面步骤中“分配”一词指的是“获得以前已分配的缓冲区”。<BR> <BR>分配DMA缓冲区<BR> <BR>DMA缓冲区的主要问题是当它大于一页时,它必须占据物理内存的连续页,因为设备使用<BR>ISA或PCI总线传送数据,它都只携带物理地址。有趣的是注意到,这个限制对Sbus并不<BR>适用(见第15章“外围总线概览”中“Sbus”一节),它在外围总线上适用虚地址。<BR> <BR>尽管DMA缓冲区可以在系统引导或运行时分配,模块只能在运行时分配其缓冲区。第七章<BR>介绍了这些技术:“Playing Dirty”讲述在系统引导时分配;“kmalloc的真实故事”<BR>和“get_free_page和朋友们”讲述运行时分配<BR>--<BR><FONT
color=#00ff00>※ 来源:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 202.38.196.234]</FONT><BR>--<BR><FONT
color=#00ffff>※ 转寄:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 211.80.41.106]</FONT><BR>--<BR><FONT
color=#0000ff>※ 转寄:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 211.80.41.106]</FONT><BR>--<BR><FONT
color=#ffff00>※ 转载:.南京大学小百合站 bbs.nju.edu.cn.[FROM: 211.80.41.106]</FONT><BR>--<BR><FONT
color=#ff0000>※ 转载:·饮水思源 bbs.sjtu.edu.cn·[FROM: 211.80.41.106]</FONT><BR></P></FONT>
<P align=center><A href="http://joyfire.net/lsdp/index.htm"><FONT
color=#ffffff size=2>目录页</FONT></A> | <A
href="http://joyfire.net/lsdp/15.htm"><FONT color=#ffffff
size=2>上一页</FONT></A> | <A href="http://joyfire.net/lsdp/17.htm"><FONT
color=#ffffff size=2>下一页</FONT></A></P></SPAN></TD></TR></TBODY></TABLE>
<TABLE cellSpacing=0 cellPadding=0 width="90%" align=center border=0>
<TBODY>
<TR>
<TD colSpan=3 height=2>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -