分享

《Essential Linux Device Drivers》第2章(上) --内核一瞥

 guitarhua 2012-07-29

2 内核一瞥


在我们开始步入Linux设备驱动的神秘世界之前,让我们先熟悉一些从驱动开发人员应该理解的基本的内核概念。我们将学习到内核定时器、同步机制以及内存分配方法,但是,先让我们从顶层视角开始探索,扫描一下内核发出的启动信息,并在感兴趣的地方设置停下来看一看。
启动过程
2.1显示了基于x86
计算机Linux系统的启动顺序。第一步是BIOS从启动设备中导入主引导记录(MBR),接下来MBR中的代码查看分区表并从活动分区读取GRUBLILOSYSLINUXbootloader,之后bootloader会加载压缩后的内核映像并将控制权传递给它。内核取得控制权后,会将自身解压缩并投入运转。


2.1 基于x86的硬件上Linux的启动过程


载 的常住内存的虚拟磁盘映像。在内核启动后,会将其挂载为初始根文件系统,这个初始根文件系统中存放着挂载实际根文件系统磁盘分区时所依赖的可动态连接的模 块。由于内核可运行于各种各样的存储控制器硬件平台上,把所有可能的磁盘驱动都直接放进基本的内核映像中并非一种灵活的方式。你所使用的系统的存储设备的 驱动被打包放入了initrd中,在内核启动后、实际的根文件系统被挂载之前,这些驱动才被加载。使用mkinitrd命令可以创建一个initrd
映像。


2.6内核提供了一种称为initramfs的新功能,它在几个方面较initrd更为优秀。后者模拟了一个磁盘(因而被称为initramdiskinitrd),会带来LinuxI/O子系统的开销(如缓冲),然后前者基本上如同一个被挂载的文件系统一样,由自身获取缓冲(因此被称作initramfs)。

不同于initrd,基于页缓冲建立的initramfs如同页缓冲一样会动态地变大和缩小,从而减少了其内存消耗。另外,initrd要求你的内核映像包含了initrd所使用的文件系统(例如,如果你的initrdEXT2文件系统,内核必须包含EXT2驱动),然而initramfs不需要文件系统支持。再者,由于initramfs只是页缓冲之上的一小层,因此它的代码量很小。

用户可以将初始根文件系统打包为一个cpio压缩包[2],并通过initrd=命令行参数传递给内核。当然,也可以在内核配置过程中通过INITRAMFS_SOURCE选项直接编译进内核。对于后一种方式而言,用户可以提供cpio压缩包的文件名或者包含initramfs的目录树。在启动过程中,内核会将文件解压缩为一个initramfs根文件系统,如果它找到了/init,它就会执行该顶层的程序。这种获取初始根文件系统的方法对于嵌入式系统而言特别有用,因为在嵌入式系统中系统资源非常宝贵。使用
mkinitramfs可以创建一个initramfs映像,查看文档Documentation/filesystems/ramfs-rootfs-initramfs.txt可获得更多信息。
[2] cpio是一种UNIX压缩文件格式,从www./software/cpio可以下载到它。
在本例中,我们使用的是通过initrd=命令行参数向内核传递初始根文件系统cpio压缩包的方式。在将压缩包中的内容解压为根文件系统后,内核将释放该压缩包所占据的内存(本例中为
387K)并打印上述信息。释放后的页面会被分发给内核中的其他部分以便被申请。
在第18章中我们会发现,在嵌入式系统开发过程中,initrdinitramfs有时候也可被用作嵌入式设备上实际的根文件系统。
IO Scheduler Anticipatory Registered (Default)
I/O调度器的主要目标是通过减少磁盘的定位次数以增加系统的吞吐率。在磁盘定位过程中,磁头需要从当前的位置移动到感兴趣的目标位置,这会带来一定的延迟。2.6
内核提供了4种不同的I/O调度器:DeadlineAnticipatoryComplete Fair Queuing以及NOOP。从上述内核打印信息可以看出,本例将Anticipatory 设置为了缺省的I/O调度器。在第14章《块设备驱动》中,我们将学习I/O调度的知识。

Setting Up Standard PCI Resources
启动过程的下一阶段会初始化I/O总线和外围控制器。内核会通过遍历PCI总线来探测PCI硬件,接下来再初始化其他的I/O子系统。从图2.3中中我们会看到SCSI子系统、USB控制器、视频芯片(855北桥芯片组信息中的一部分)、串口(本例中为
8250 UART)、PS/2键盘和鼠标、软驱、ramdiskloopback设备、IDE控制器(本例中为ICH4南桥芯片集中的一部分)、触控板、以太网控制器(本例中为e1000)以及PCMCIA控制器初始化的启动信息。图2.3中——>符号指向的为I/O设备的标识(
ID)。
[table]            [tr]            [td=1,1,657]            SCSI subsystem initialized                  核可以处于两种上下文:进程上下文和中断上下文。在系统调用之后,用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的代表就运行于进程上下 文。异步发生的中断会引发中断处理程序被调用,中断处理程序就运行于中断上下文。中断上下文和进程上下文不可能同时发生。运行于进程上下文的内核代码是可抢占的,而进程上下文则不会被抢占。因此,内核会限制中断上下文的工作,不允许其执行如下操作:
1)进入睡眠状态或主动放弃CPU

2)占用mutex
3)执行耗时的任务
4)访问用户空间虚拟内存
本书第4章《中断处理》一节会对中断上下文进行更深入的讨论。

内核定时器
内核中许多部分的工作都高度依赖于时间的推移。Linux内核使用了硬件提供的不同的定时器以支持忙等待或睡眠等待等依赖于时间的服务。忙等待时,CPU会不断运转,但是睡眠等待时,进程将放弃CPU。因此,只有在后者不可取的情况下,才可以考虑使用前者。内核也提供了这样的便利:在特定的时间之后调度某函数运行。
我们首先来讨论一些重要的内核定时器变量(jiffiesHZxtime)的含义,接下来,我们会使用Pentium时间戳计数器(TSC)测量基于Pentium的系统的运行次数,之后,我们也分析一下Linux怎么使用实时钟(RTC)。

HZJiffies
系统定时器能以可编程的频率中断CPU。此频率即为每秒的定时器节拍数,对应着内核变量HZ。选择合适的HZ值需要权衡。较大的HZ值将带来更小的定时器间隔时间,因此进程调度的准确性会更高。但是,更大的HZ值也会带来更大的开销和更大的电源消耗,因为更多的CPU周期将被耗费在定时器中断上下文中。

HZ的值依赖于体系结构。在x86系统上,在2.4内核上,该值缺省设置为100,在2.6内核中,该值变为1000,而在2.6.13中,它又被降低到了250.在基于ARM的平台上,2.6内核将HZ设置为100。在目前的内核中,你可以在编译内核时通过配置菜单选择一个HZ值。该选项的缺省值依赖于你的发行版。

2.6.21内核开始支持无节拍的内核(CONFIG_NO_HZ),它会根据系统的负载动态触发定时器中断。无节拍系统的实现超出了本章的范围。
jiffies变量记录了自系统启动依赖,系统定时器已经触发的次数。内核每秒钟将jiffies变量增加HZ次。因此,对于HZ值为100的系统,1jiffy等于10毫秒,而对于HZ1000的系统,1jiffy仅为1毫秒。

       为了更好地理解HZjiffies变量,请看下面的取自IDE驱动(drivers/ide/ide.c)的代码片段,该段代码会一直轮训磁盘驱动器的忙状态:
unsigned long timeout = jiffies   (3*HZ);
while (hwgroup->busy) {
  /* ... */

  if (time_after(jiffies, timeout)) {
    return -EBUSY;
  }
  /* ... */
}

return SUCCESS;
如果忙条件在3秒内被清除,上述代码将返回SUCCESS,否则,返回-EBUSY3*HZ3秒内的jiffies数量。计算出来的超时jiffies   3*HZ,将是3秒超时发生后新的jiffies 值。time_after()的功能是将目前的jiffies值与请求的超时时间对比,检测溢出。类似函数还包括time_before()time_before_eq()time_after_eq()

jiffies被定义为volatile类型,它会告诉编译器不要优化该变量的存取代码。这样就确保了每个节拍发生的定时器中断处理程序都能更新jiffies值,并且循环中的每一步都会重新读取jiffies值。
对于jiffies向秒转换,可以查看USB主机控制器驱动drivers/usb/host/ehci-sched.c中的如下代码片段:

if (stream->rescheduled) {
  ehci_info(ehci, "ep%ds-iso rescheduled " "%lu times in %lu
            seconds\n", stream->bEndpointAddress, is_in? "in":
            "out", stream->rescheduled,
            ((jiffies – stream->start)/HZ));

}
上述调试语句计算出USB端点流(见第11章《USB设备驱动》)被重新调度stream->rescheduled次所耗费的秒数。jiffies-stream->start是从开始到现在消耗的jiffies数量,将其除以HZ就得到了秒数值。

假定jiffies100032位的jiffies大约会50天的时间越界。由于系统的运行时间可以比该时间长许多倍,因此,内核提供了另一个变量jiffies_64以存放64位的jiffies。连接器讲jiffies_64的低32位与32位的jiffies指向同一个地址。在32位的机器上,为了将一个u64变量赋值给另一个,编译器需要需要2条指令,因此,读jiffies_64的操作不具备原子性。可以将drivers/cpufreq/cpufreq_stats.c文件中定义的cpufreq_stats_update()作为实例来学习。

长延时
在内核中,以jiffies为单位进行的延迟通常被认为是长延时。一种可能但非最佳的实现长延时的方法是忙等待。实现忙等待的函数有“占着茅坑不拉屎”之嫌,它本身不利用CPU进行有用的工作,同时还不让其他程序使用CPU。如下代码将占用CPU 1秒:
unsigned long timeout = jiffies   HZ;
while (time_before(jiffies, timeout)) continue;

时间长延时的更好方法是睡眠等待而不是忙等待,在这种方式中,本进程会在等待时将CPU出让给其他进程,schedule_timeout()完成此功能:
unsigned long timeout = jiffies   HZ;
schedule_timeout(timeout);  /* Allow other parts of the
                               kernel to run */

这种延时仅仅确保超时时较低的精度,由于只有在时钟节拍引发的内核调度才会更新jiffies,所以超时的最大精度是HZ。另外,即使你的进程已经超时并可被调度,但是调度器仍然可能基于优先级策略选择运行队列的其他进程[5]
[5]2.6.23内核中,睡着CFS调度器的出现,调度性质发生了改变。第19章《用户空间的设备驱动》会对进程调度进行讨论。

用于睡眠等待的另2个函数是wait_event_timeout()msleep(),此2者的实现都基于schedule_timeout()wait_event_timeout()的使用场合是:在一个特定的条件满足或者超时发生后,代码期待继续运行。msleep()则用于睡眠指定的毫秒数。
这种长延时技术仅仅实用于进程上下文。睡眠等待不能用于中断上下文,因为中断上下文不允许执行schedule()或睡眠(第4章的《中断处理》一节给出了中断上下文可以做和不能做的事情)。在中断中进行短时间的忙等待是可行的,但是进行长时间的忙等则被认为不可赦免的罪行。在中断禁止时,进行长时间的忙等待也被看作禁忌。

为了支持在将来的某时刻进行某项工作,内核也提供了定时器API。你可以通过init_timer()动态地定义一个定时器,也可以通过DEFINE_TIMER()静态创建。此后,将处理函数的地址和参数绑定给一个timer_list,并使用add_timer()注册它即可:
#include < linux/timer.h>


struct timer_list my_timer;

init_timer(&my_timer);            /* Also see setup_timer() */
my_timer.expire = jiffies   n*HZ; /* n is the timeout in number
                                     of seconds */

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多