📄 (ldd) ch17-最新进展(转载).htm
字号:
<P>因此你实际上并不需要调用access_ok,除非你选择这样做。<BR> <BR>int get_user(lvalue, address);<BR> <BR>在2.1核心中使用的宏get_user与我们在2.0中使用的并不相同。其返回值在成功时为0,<BR>否则为一个负的错误代码(总是-EFAULT)。这个函数的净效果是将从地址address取得<BR>的数据赋给lvalue。在通常的C语言含义中,这个宏的第一个参数必须是一个lvalue*。<BR>与2.0版中的这个函数类似,数据项的实际大小依赖于address参数类型。这个函数在内<BR>部调用access_ok。<BR> <BR>int __get_user(lvalue, address);<BR> <BR>这个函数完全类似get_user,但它不内部调用access_ok。当你访问一个已经从同一核心<BR>函数内部检查过的用户地址时,你应该调用__get_user。<BR> <BR>get_user_ret(lvalue, address, retval);<BR> <BR>这个宏是调用get_user的快捷方式,如果函数失败则返回retval。<BR> <BR>int put_user(expression, address);<BR> <BR>int __put_user(expression, address);<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>put_user_ret(expression, address, retval);<BR> <BR>这些函数与它们的get_对应者非常类似,只是它们是向用户空间写,而不是读。成功时<BR>,值expression被写到地址address。<BR> <BR>unsigned long copy_from_user(unsigned long to, unsigned long from, unsigned<BR>long len);<BR> <BR>这个函数从用户空间复制数据到核心空间。它代替旧的memcpy_tofs调用。这个函数内部<BR>调用access_ok。返回值是未能传送的字节数。这样,如果发生错误,返回值必然大于0<BR>;在那种情况下,驱动程序返回-EFAULT,因为错误是由错误的内存访问引起的。<BR> <BR>unsigned long __copy_from_user(unsigned long to, unsigned long from,<BR>unsigned long len);<BR> <BR>这个函数与copy_from_user一样,但它不内部调用access_ok。<BR> <BR>caopy_from_user_ret(to, from, len, retval);<BR> <BR>这个宏是内部调用copy_from_user的快捷方式;如果失败,则从当前函数返回。<BR> <BR>unsigned long copy_to_user(unsigned long to, unsigned long from, unsigned<BR></P></FONT><FONT
color=#ffffff size=3>
<P>unsigned long copy_to_user(unsigned long to, unsigned long from, unsigned<BR>long len);<BR> <BR>unsigned long __copy_to_user(unsigned long to, unsigned long from, unsigned<BR>long len);<BR> <BR>copy_to_user(to, from, len, retval);<BR> <BR>这些函数被用来将数据复制到用户空间,它们的行为非常类似于它们的copy_from的对应<BR>者。<BR> <BR>2.1版核心还定义了其它访问用户空间的函数:clear_user,strncpy_from_user,和str<BR>len_user。我不打算讨论它们了,因为Linux2.0中没有这些函数,并且驱动程序的代码<BR>也很少用到它们。有兴趣的读者可以看看<asm/access.h>。<BR> <BR> <BR> <BR>使用新的接口<BR> <BR>访问用户空间的新的函数集初看起来可能有点令人失望,但它们的确使程序员的日子好<BR>过的多了。在Linux2.1上,不再需要显式地检查用户空间;access_ok一般不需要调用。<BR>使用新接口的代码可以直接进行数据传送。_ret函数在实现系统调用时证明是相当有用<BR>的,因为一个用户空间的失败通常导致系统调用的一个返回-EFAULT的失败。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>的,因为一个用户空间的失败通常导致系统调用的一个返回-EFAULT的失败。<BR> <BR>因此,一个典型的read实现,看起来如下:<BR> <BR> long new_read(struct inode *inode, struct file *filp, char *buf,<BR>unsigned long count);<BR> <BR> {<BR> <BR> /* identify your data (device-specific code) */<BR> <BR> if (__copy_to_user(buf, new_data, count))<BR> <BR> return -EFAULT;<BR> <BR> return count;<BR> <BR>}<BR> <BR>注意使用不进行检查的__copy_to_user是因为调用者在把数据传输分派到文件操作之前<BR>已经检查了用户空间。这就象2.0,read和write不需要调用verify_area。<BR> <BR>类似地,典型的ioctl实现看起来如下:<BR></P></FONT><FONT
color=#ffffff size=3>
<P>类似地,典型的ioctl实现看起来如下:<BR> <BR> int new_ioctl(struct inode *inode, struct file *filp, unsigned int<BR>cmd, unsigned long arg);<BR> <BR> {<BR> <BR>/* device-specific checks, if needed */<BR> <BR>switch(cmd){<BR> <BR> case NEW_GETVALUE:<BR> <BR> put_user_ret(new_value, (int *)arg, -EFAULT);<BR> <BR> break;<BR> <BR> case NEW_SETVALUE:<BR> <BR> get_user_ret(new_value, (int *)arg, -EFAULT);<BR> <BR> default:<BR> <BR></P></FONT><FONT color=#ffffff size=3>
<P><BR> return –EINVAL;<BR> <BR> }<BR> <BR>return 0;<BR> <BR>}<BR> <BR>于版本2.0的对应者不同的是,这个函数在switch语句之前并不需要检查参数,因为每个<BR>get_user或put_user会进行检查。另一种实现方式如下:<BR> <BR>(代码394 #2)<BR> <BR>另一方面,当你想写可以同时在2.0和2.1上编译的代码时,问题变得稍微复杂一些,因<BR>为在老的核心上,你不能用C预处理器伪装新的行为。你不能简单地#define一个接收两<BR>个参数的get_user宏,因为实际的get_user实现在2.0中已经是个宏。<BR> <BR>我在写既可移植有高效率的代码的选择是设置sysdep-2.1.h以提供具有下列函数的源码<BR>。下面只列出了读取数据的函数;写数据的函数行为完全一样。<BR> <BR>int access_ok(type, address, size);<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>当在2.0上编译时,这个函数以verify_area的名义实现。<BR> <BR>int verify_area_20(type, address, size);<BR> <BR>通常,当为Linux2.1写代码时,你不需调用access_ok。另一方面,当在Linux2.0上编译<BR>时,则必须调用verify_area。这个函数就是要填平这个不同:当为Linux2.1编译时,它<BR>扩展为空;而为2.0编译时,它扩展为原来的verify_area。这个函数不能被称做verify_<BR>area,因为2.1已经有一个宏叫这个名字了。在2.1中定义的verify_area宏实现了access<BR>_ok的老的语义,它的存在是为了简化源码从2.0到2.1的转换。(从理论上说,你可以在<BR>你的模块中留下verify_area,只是将函数名改一下;这种简单移植技巧的缺点是新版本<BR>不能在2.0上编译。)<BR> <BR>int GET_USER(var, add);<BR> <BR>int __GET_USER(var, add);<BR> <BR>GET_USER_RET(var, add, ret);<BR> <BR>当在2.1上编译时,这些宏扩展为实际的get_user函数,即上面解释过的那些。当在2.0<BR>上编译时,get_user的2.0实现被用来实现与2.1中同样的功能。<BR> <BR>int copy_from_user(to, from, size);<BR></P></FONT><FONT
color=#ffffff size=3>
<P>int copy_from_user(to, from, size);<BR> <BR>int __copy_from_user(to, from, size);<BR> <BR>copy_from_user_ret(to, from, size);<BR> <BR>当在2.0上编译时,这些扩展为memcpy_fromfs;而在2.1上,则使用本身的函数。_ret一<BR>类在2.0上从不会返回,因为复制函数不会失败。<BR> <BR> <BR> <BR>我个人比较喜欢这种实现兼容性的方法,但这并不是唯一的方法。在我的示例代码中,<BR>任何用户空间的访问(除了用来read或write的缓冲区,它们已经事先检查过了)之前,<BR>verify_area_20必须被调用。另一种方法更加忠实于2.1的语义,即当用2.0时,在每个g<BR>et_user 和copy_from_user之前自动生成一个verify_area。这个选择在源码级要更清晰<BR>一些,但在版本2.0上编译时效率相当低,包括代码大小和执行时间。<BR> <BR>可以同时在2.0和2.1上编译的示例代码,如scull模块,可以在目录v2.1/scull中找到。<BR>我不觉得这个代码足够有趣,因此不在这里给出。<BR> <BR> <BR> <BR>任务队列<BR></P></FONT><FONT
color=#ffffff size=3>
<P>任务队列<BR> <BR>从2.1.30开始的Linux版本不再定义函数queue_task_irq和queue_task_irq_off,因为在<BR>queue_task上的实际加速不值得花精力维护两个独立的函数。当新机制被加到核心时,<BR>这就变得明显了。<BR> <BR>在源码级,这是2.0和2.1之间唯一的区别;头文件定义了消失的函数简化了从2.0移植驱<BR>动程序。感兴趣的读者可以查看<asm/spinlock.h>以获得更多的细节。<BR> <BR> <BR> <BR>中断管理<BR> <BR>在2.1的开发中,有些Linux内部被修改了。新核心提供了对内部锁的很好的管理;通过<BR>使用几个细粒度的锁,而不是全局的锁,竞争条件被避免了,这样也获得了更好的性能-<BR>---特别是SMP配置下。<BR> <BR>更细的锁机制的一个结果是intr_count不再存在了。2.1.34抛弃了这个全局变量,而布<BR>尔函数in_interrupt可以取而代之(这个函数从2.1.30开始存在)。目前,in_interrup<BR>t是在头文件<asm/hardirq.h>中声明的宏,这个头文件又包含在<linux/interrupt.h>中<BR>。头文件sysdep-2.1.h用intr_count的名义定义了in_interrupt以获得对2.0的向后兼容<BR>性。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>注意虽然in_interrupt是个整数,intr_count却是个unsigned long,因此,如果你想打<BR>印这个值,并在2.0和2.1间可移植,你必须强制将这个值转换为一个显式的类型,并在<BR>调用printk时指定一个合适的格式。<BR> <BR>在2.1.37中中断管理又引入了一个不同:快和慢中断处理程序不再存在了。SA_INTERRUP<BR>T不被新版本的request_irq使用,但它在处理程序执行以前仍然控制着中断是否被打开<BR>。如果几个处理程序共享一个中断线,每个可以是个不同的“类型”。中断打开与否依<BR>赖于第一个被调用的处理程序。当中断处理程序存在时,下半部总是执行。<BR> <BR> <BR> <BR>位操作<BR> <BR>2.1.37稍微改变了在<asm/bitops.h>中定义的位操作的作用。现在函数set_bit及其相关<BR>者返回void,而新的类似test_and_set_bit的函数已被引入。新的函数集有如下原型:<BR> <BR> void set_bit(int nr, volatile void * addr);<BR> <BR>void clear_bit(int nr, volatile void * addr);<BR> <BR>void change_bit(int nr, volatile void * addr);<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>int test_and_set_bit(int nr, volatile void * addr);<BR> <BR>int test_and_clear_bit(int nr, volatile void * addr);<BR> <BR>int test_and_change_bit(int nr, volatile void * addr);<BR> <BR>int test_bit(nr, addr);<BR> <BR>如果你想获得与2.0的后向兼容性,你可以在你的模块中包含sysdep-2.1.h,并使用新的<BR>原型。<BR> <BR> <BR> <BR>转换函数<BR> <BR>版本2.1.10引入了一个新的转换函数,在<asm/byteorder.h>中声明。这些函数可以用来<BR>访问多字节值,只要这个值已知是小印地安字节序或大印地安字节序。因为这些函数为<BR>写驱动程序代码提供了很好的快捷方式,头文件sysdep-2.1.h在较早的版本就已经定义<BR>了它们。<BR> <BR>2.1核心源码提供的本身实现比sysdep-2.1.h提供的可移植的实现要快,因为它可以利用<BR>体系相关的功能。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>体系相关的功能。<BR> <BR>新函数对应下面的原型,其中le表示小印地安字节序,be表示大印地安字节序。注意编<BR>译器并不强制严格的数据类型化,因为大多数函数都是预处理宏;下面给出的类型仅供<BR>参考。<BR> <BR> __u16 cpu_to_le16(__u16 cpu_val);<BR> <BR> __u32 cpu_to_le32(__u32 cpu_val);<BR> <BR> __u16 cpu_to_be16(__u16 cpu_val);<BR> <BR> __u32 cpu_to_be32(__u32 cpu_val);<BR> <BR> __u16 le16_to_cpu(__u16 le_val);<BR> <BR> __u32 le32_to_cpu(__u32 le_val);<BR> <BR> __u16 be16_to_cpu(__u16 be_val);<BR> <BR> __u32 be32_to_cpu(__u32 be_val);<BR> <BR>这些函数在处理二进制数据流时很有用(例如文件系统数据或存在接口板中的信息),<BR></P></FONT><FONT
color=#ffffff size=3>
<P>这些函数在处理二进制数据流时很有用(例如文件系统数据或存在接口板中的信息),<BR>版本2.1.43又增加了两个新的转换函数集。这些集允许你用指针获取一个值,或是对参<BR>数指定的一个值进行就地转换。对应与16位小印地安字节序的函数又如下的原型;类似<BR>的函数对其它类型的整数也存在,导致一共16个函数。<BR> <BR> __u16 cpu_to_le16p(__u16 *addr)<BR> <BR> __u16 le16_to_cpup(__u16 *addr)<BR> <BR> void cpu_to_le16s(__u16 *addr)<BR> <BR> void le16_to_cpus(__u16 *addr)<BR> <BR>“p”函数类似与指针的复引用,但在需要时转换这个值;“s”函数可以在原地转换一<BR>个值的印地安字节序(例如,cpu_to_le16s(addr) 和addr=cpu_to_le16(*addr)完成的<BR>工作是一样的)。<BR> <BR>这些函数也在sysdep-2.1.h中定义了。为了避免双重解释的副作用,这个头文件用线入<BR>函数,而不是预处理宏。<BR> <BR> <BR> <BR>vremap<BR></P></FONT><FONT
color=#ffffff size=3>
<P>vremap<BR> <BR>在第七章“把握内存”中“vmalloc和朋友们”一节描述的vremap函数在版本2.1中得到<BR>一个新名字。新函数ioremap只是名字变了,它与旧的remap取同样的参数。响应的释放<BR>函数是iounmap,它代替vfree来释放被重映射的地址。<BR> <BR>这个改变是为了明确这个函数的实际作用:将I/O空间重映射到核心空间的一个虚地址。<BR>头文件sysdep-2.1.h强化了这种新规则,当在2.0版本编译时,它#define了ioremap和io<BR>unmap到它们2.0的对应者。<BR> <BR> <BR> <BR>虚拟内存<BR> <BR>在核心的版本2.1,Linux的Intel移植对虚拟内存有了一个成熟的视图。早些的版本的内<BR>存管理一直使用“分段”的方法,这是从核心生命期的开始时期继承下来的。这个改变<BR>并不影响驱动程序代码,但不管怎样,还是值得一说的。<BR> <BR>新的规则与Linux的其它移植匹配的起来。虚拟地址空间被构造成核心居于非常高的地址<BR>(从3GB往上),而用户地址空间在0-3GB范围。当一个进程运行在“管态”时,它可以<BR>访问两个空间。另一方面,当它运行在“用户态”时,它不能访问核心空间,因为属于<BR>核心的页被标记为“管理员”页,处理器阻止了对它们的访问。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>这种内存布局有助于取消旧的memcpy_to_fs一类的函数,因为已经没有FS段了。核心空<BR>间和用户空间使用同一个“段”,其区别在于CPU所在的优先级。<BR> <BR> <BR> <BR>处理核心空间错误<BR> <BR>Linux核心的2.1版本对从核心空间处理段错误的能力有一个极大的增强。本章里,我准<BR>备对其原则给一个快速的概述。新机制对源码的影响在“访问用户空间”中已经描述过<BR>。<BR> <BR>如前面所提到过的,核心的最近版本充分利用了ELF二进制格式,特别是考虑到它的在编<BR>译的文件中定义用户定义的节的能力。编译器和链接器保证属于同一节的代码段在可执<BR>行文件中一定是连续的,因此当文件被装载时,在内存中也是连续的。<BR> <BR>例外处理是通过在核心可执行映象(vmlinux)中定义两个新节实现的。每次当源码通过co<BR>py_to_user, put_user, 或其读取的对应者访问用户空间时,一些代码被加到这两个节<BR>中。尽管这看起来是不可忽略的开销,这个新机制的一个结果是不再需要使用一个昂贵<BR>的verify_area。而且,如果使用的用户地址是正确的,计算流将不会有一个跳转。<BR> <BR>当被访问的用户地址是无效的时,硬件发出一个页面错。错误处理程序(在体系结构特<BR>定的源码树中的do_page_fault)确认这个错误是一个“不正确的地址”错(与“页不存<BR></P></FONT><FONT
color=#ffffff size=3>
<P>定的源码树中的do_page_fault)确认这个错误是一个“不正确的地址”错(与“页不存<BR>在”相对),并使用下面的ELF节进行适当的动作:<BR> <BR>__ex_table<BR> <BR>这节是个指针对的表。每对的第一个指针指向一个可能因错误的用户空间地址而失败的<BR>指令,第二个值指向一个地址,处理器将在那里找到几条的指令来处理这个错误。<BR> <BR>..fixup<BR> <BR>这节包含指令,处理在__ex_table中描述的所有可能的错误。这个表中每对的第二个指<BR>针指向居于.fixup中的代码。<BR> <BR> <BR> <BR>头文件<asm/uaccess.h>负责构造所需的ELF节。访问用户空间的每个函数(如put_user<BR>)扩展为汇编指令,它将指针加到__ex_table并处理.fixup中的错。<BR> <BR>当代码运行时,实际的执行路径有一下步骤组成:用于函数“返回值”的处理器寄存器<BR>被初始化为0(也就是没有错误),数据被传送,返回知被传回调用者。一般的操作的确<BR>非常快。如果一个异常发生,do_page_fault打印一条消息,查看__ex_table,跳转到.f<BR>ixup,这里设置返回值为-EFAULT,然后跳转到访问用户空间的指令后位置。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>新的行为可以用faulty(在v2.1/misc-modules目录)模块来检验。faulty在第四章“调<BR>试技巧”中“调试系统错误”一节描述。faulty的设备结点通过读取一个短缓冲区界外<BR>来传送数据到用户空间,这样当读取一个在模块页以上的地址时,会导致一个页面错。<BR>有趣的是注意到这个错误依赖于使用核心空间中的一个不正确地址,而大多数情况下异<BR>常是有出错的用户空间地址造成的。<BR> <BR>当在PC上用cat命令读faulty时,下面的消息被打印在控制台上:<BR> <BR> read: inode c1188348, file c16decf0, buf 0804cbd0, count 4096<BR> <BR> cat: Exception at [<c2807b7>](c2807115)<BR> <BR>前一行是由faulty的read方法打印的,而后者是由错误处理程序打印的。第一个数字是<BR>错误指令的地址,而第二个是修正代码(在.fixup节中)的地址。<BR> <BR> <BR> <BR>其它改变<BR> <BR>在2.0和2.1.43之间还有其它一些不同。以我的观点,它们不需要给予特别的关注,因此<BR>我将迅速地概述一下。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>proc_register_dynamic在2.1.29中消失了。最近的核心对每个/proc文件使用proc_regi<BR>ster接口;如果结构proc_dir_entry的low_ino域是0,那么会被分配一个动态的inode号<BR>。当为2.1.29或更新的核心编译时,头文件sysdep-2.1.h象proc_register一样定义proc<BR>_register_dynamic;这个在注册的proc_dir_entry结构以0为inode号时是可行的。<BR> <BR>在网络接口驱动程序领域,rebuild_header设备方法从2.1.15起有一个新的原型。如果<BR>你只开发以太网驱动程序,你不会关心这个不同,因为以太网驱动程序不实现它们自己<BR>的方法;它们依赖于通用的以太网实现。当旧的实现需要时,头文件sysdep-2.1定义了<BR>宏__USE_OLD_REBUILD_HEADER__。示例模块snull显示了如何使用这个宏,但每必要在这<BR>里给出。<BR> <BR>网络代码的另一个改变影响了结构enet_statistics,它从2.1.25起不再存在。代替它的<BR>是一个新结构net_device_stats,它在<linux/netdevice.h>中定义,而不是<linux/if_<BR>ether.h>。新结构与旧结构类似,但是多了两个域存储字节计数器:unsigned long<BR>rx_bytes, tx_bytes;一个全特征的网络接口驱动程序应该与rx_packets和tx_packets<BR>一道增加这些计数器,尽管一个快速的计划可能要抛弃这些计数器。核心头文件将enet_<BR>statistics(老结构的名字)定义为net_device_stats(新结构的名字)以方便已有驱<BR>动程序的可移植性。<BR> <BR>最后,我需要指出current不再是个全局变量x86, Alpha,以及Sparc的核心移植使用了聪<BR>明的技巧将current存在处理器中。这样核心的开发者努力又挤出了几个CPU周期。这个<BR>技巧避免了大量的内存访问,有时还能释放一个通用目的寄存器;编译器经常分配处理<BR></P></FONT><FONT
color=#ffffff size=3>
<P>技巧避免了大量的内存访问,有时还能释放一个通用目的寄存器;编译器经常分配处理<BR>器寄存器来高速缓存几个经常访问的内存位置,而current是经常访问的。在不同的移植<BR>中使用了不同的技巧以优化访问。Alpha和Sparc版本使用一个处理器寄存器(编译器优<BR>化不使用的一个)来存储current。而Intel处理器有有限数目的寄存器,编译器可以使<BR>用它们所有;在这种情况下技巧包括将结构task_struct和核心栈页存储在连续的虚存页<BR>内。这允许current指针被“编码”在栈指针中。对每个Linux支持的平台,头文件<asm/<BR>current.h>给出了实际选择的实现。<BR> <BR>象所有重要的软件一样,Linux一直在改变着。如果你想为这个最新的、最伟大的核心写<BR>驱动程序,你需要保持跟上核心的发展。尽管处理不兼容性看起来可能很困难,我们发<BR>现两点特性:首先,主要的程序设计技巧一直在那里,不太可能改变(至少不常);第<BR>二,每次改变都变得更好了,经常使你在将来的开发中需要的工作越来越少。<BR> <BR> <BR> <BR>-----------------------------------------------------------------------------<BR> <BR>* 一个lvalue是一个可以作为赋值的左操作数的表达式。例如,count,<BR>v[34+check()], 和*((prt+offset)->field)是lvalue;i++,32, 和cli()都不是。<BR> <BR>--<BR>--<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P>现两点特性:首先,主要的程序设计技巧一直在那里,不太可能改变(至少不常);第<BR>二,每次改变都变得更好了,经常使你在将来的开发中需要的工作越来越少。<BR> <BR> <BR> <BR>-----------------------------------------------------------------------------<BR> <BR>* 一个lvalue是一个可以作为赋值的左操作数的表达式。例如,count,<BR>v[34+check()], 和*((prt+offset)->field)是lvalue;i++,32, 和cli()都不是。<BR> <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/20.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>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -