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