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

基于x86的处理器有两种操作模式:实模式和保护模式。在实模式下,用户仅可以使用1MB内存,并且没有任何保护。保护模式则更加复杂,用户可以使用更多的高级功能(如分页)。CPU提供了一条由实模式通向保护模式的道路,但是,这条路只允许单向行驶,用户不能从保护模式再切换回实模式。
内核初始化的第一步是执行实模式下的汇编代码,之后执行保护模式下init/main.c文件(上一章我们修改了这个文件)中的start_kernel()函数。start_kernel()函数首先会初始化CPU子系统,之后让内存管理和进程管理系统就位,接下来启动外部总线和I/O设备,最后的一步是激活所有Linux进程的父亲initinit执行用户空间的脚本以启动必要的内核服务,它最终派生控制台终端程序并显示登录(login)提示。
接下来,每一小节的标题都是图2.2中的一条打印信息,这些信息来源于基于x86的笔记本电脑的Linux启动过程。如果你在启动体系结构上启动Linux,消息以及语义可能会有所改变。如果本节中的一些内容读起来非常晦涩,请不要担心。目前的目的仅是从100英尺的高度给你一个视图,让你初次品尝内核甜点的味道。接下来要提到的许多概念都会在以后的章节中进行更深的论述。
2.2 内核启动信息
Linux version 2.6.23.1y ([email]root@localhost.loca[/email]ldomain) (gcc version 4.1.1 20061011 (Red
Hat 4.1.1-30)) #7 SMP PREEMPT Thu Nov 1 11:39:30 IST 2007
BIOS-provided physical RAM map:
 BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
 BIOS-e820: 000000000009f000 - 00000000000a0000 (reserved)
 ...
758MB LOWMEM available.
...
Kernel command line: ro root=/dev/hda1
...
Console: colour VGA+ 80x25
...
Calibrating delay using timer specific routine.. 1197.46 BogoMIPS (lpj=2394935)
...
CPU: L1 I cache: 32K, L1 D cache: 32K
CPU: L2 cache: 1024K
...
Checking 'hlt' instruction... OK.
...
Setting up standard PCI resources
...
NET: Registered protocol family 2
IP route cache hash table entries: 32768 (order: 5, 131072 bytes)
TCP established hash table entries: 131072 (order: 9, 2097152 bytes)
...
checking if p_w_picpath is initramfs... it is
Freeing initrd memory: 387k freed
...
io scheduler noop registered
io scheduler anticipatory registered (default)
...
00:0a: ttyS0 at I/O 0x3f8 (irq = 4) is a NS16550A
...
Uniform Multi-Platform E-IDE driver Revision: 7.00alpha2
ide: Assuming 33MHz system bus speed for PIO modes; override with idebus=xx
ICH4: IDE controller at PCI slot 0000:00:1f.1
Probing IDE interface ide0...
hda: HTS541010G9AT00, ATA DISK drive
hdc: HL-DT-STCD-RW/DVD DRIVE GCC-4241N, ATAPI CD/DVD-ROM drive
...
serio: i8042 KBD port at 0x60,0x64 irq 1
mice: PS/2 mouse device common for all mice
...
Synaptics Touchpad, model: 1, fw: 5.9, id: 0x2c6ab1, caps: 0x884793/0x0
...
agpgart: Detected an Intel 855GM Chipset.
...
Intel(R) PRO/1000 Network Driver - version 7.3.20-k2
...
ehci_hcd 0000:00:1d.7: EHCI Host Controller
...
Yenta: CardBus bridge found at 0000:02:00.0 [1014:0560]
...
Non-volatile memory driver v1.2
...
kjournald starting. Commit interval 5 seconds
EXT3 FS on hda2, internal journal
EXT3-fs: mounted filesystem with ordered data mode.
...
INIT: version 2.85 booting
...
 
BIOS-provided physical RAM map
内核解析从BIOS中读取到的系统内存映射,并率先将这些信息打印出来:      
BIOS-provided physical RAM map:
BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
...
BIOS-e820: 00000000ff800000 - 0000000100000000 (reserved)
实模式下的初始化代码通过使用BIOSint 0x15服务并执行0xe820号函数来获得系统的内存映射信息。内存映射信息中包含了预留的和可用的内存,内核将使用这些信息创建其可用的内存池。在附录BLinuxBIOS》的《实模式调用》一节,我们会对BIOS提供的内存映射问题进行更深入的讲解。
758MB LOWMEM Available
896MB以内的常规的可被寻址的内存区域被称作低端内存。内存分配函数kmalloc()就是从该区域分配内存的。高于896MB被称为高端内存,只有在采用特殊的方式进行映射后才能被访问。在启动过程中,内核会计算并显示这些内存zone内总的页数,在本章的稍后,会对这些内存zone进行更深入的分析。
Kernel Command Line: ro root=/dev/hda1
Linuxbootloader通常会给内核传递一个命令行。命令行中的参数类似于传递给C程序中main()函数的argv[]列表,唯一的不同是它们是传递给内核的。你可以在bootloader的配置文件中增加命令行参数,当然,也可以在运行过程中对bootloader的提示行进行修改[1]。如果你正在使用GRUB这个bootloader,归因于发行版的不同,其配置文件可能是/boot/grub/grub.conf或者是/boot/grub/menu.lst。如果你正在使用LILO,配置文件为/etc/lilo.conf。下面给出了一个grub.conf文件的例子(增加了一些注释),阅读了紧接着“title kernel 2.6.23”后的一行之后,你会发现前述打印信息的由来。\
[1] 嵌入式设备上的bootloader通常经过了“瘦身”,并不支持配置文件或类似机制。归因于此,许多非x86体系结构提供了CONFIG_CMDLINE这个内核配置选项,通过它,用户可以在编译内核时提供内核命令行。
default 0  #Boot the 2.6.23 kernel by default
timeout 5  #5 second to alter boot order or parameters
 
title kernel 2.6.23     #Boot Option 1
  #The boot p_w_picpath resides in the first partition of the first disk
  #under the /boot/ directory and is named vmlinuz-2.6.23. 'ro'
  #indicates that the root partition should be mounted read-only.
  kernel (hd0,0)/boot/vmlinuz-2.6.23 ro root=/dev/hda1
 
  #Look under section "Freeing initrd memory:387k freed"
  initrd (hd0,0)/boot/initrd
 
#...
命令行参数将影响启动过程中的代码执行路径。举一个例子,假设某命令行参数为bootmode,如果该参数被设置为1,意味着你希望在启动过程中打印一些调试信息并在启动结束时切换到runlevel的第3级(到我们分析init进程的打印信息时,会学习到runlevel的含义);如果bootmode参数被设置为0,意味着你希望启动过程相对简洁,并且设置runlevel2。因为你已经熟悉了init/main.c文件,让我们在该文件中增加如下修改:
static unsigned int bootmode = 1;
static int __init
is_bootmode_setup(char *str)
{
  get_option(&str, &bootmode);
  return 1;
}
 
/* Handle parameter "bootmode=" */
__setup("bootmode=", is_bootmode_setup);
 
if (bootmode) {
  /* Print verbose output */
  /* ... */
}
 
/* ... */
 
/* If bootmode is 1, choose an init runlevel of 3, else
   switch to a run level of 2 */
if (bootmode) {
  argv_init[++args] = "3";
} else {
  argv_init[++args] = "2";
}
 
/* ... */
       请重新编译内核并尝试新的修改。另外,本书第18章《嵌入式Linux》的《内存分布》一节也将对命令行参数进行更多的讲解。
Calibrating Delay...1197.46 BogoMIPS (lpj=2394935)
在启动过程中,内核会计算处理器在一个jiffy时间内运行一个内部的delay循环的次数。jiffy的含义是系统定时器2个连续的节拍之间的间隔。如果你所期待的那样,该计算必须被校准到你的CPU的处理速度。校准的结果被存储在称为loops_per_jiffy的内核变量中。使用loops_per_jiffy的一个场合是某设备驱动希望进行小的微妙级别的延迟的时候。
为了理解delay循环校准代码,让我们看一下定义于init/calibrate.c文件中的calibrate_delay()函数。该函数机智地使用整型运算得到了浮点的精度。如下的代码片段(增加了一些注释)显示了该函数的开始部分,这部分用于得到一个粗略的loops_per_jiffy
loops_per_jiffy = (1 << 12); /* Initial approximation = 4096 */
printk(KERN_DEBUG "Calibrating delay loop... ");
while ((loops_per_jiffy <<= 1) != 0) {
ticks = jiffies;  /* As you will find out in the section, "Kernel
                     Timers," the jiffies variable contains the
                     number of timer ticks since the kernel
                     started, and is incremented in the timer
                     interrupt handler */
 
  while (ticks == jiffies); /* Wait until the start
                               of the next jiffy */
  ticks = jiffies;
  /* Delay */
  __delay(loops_per_jiffy);
 
  /* Did the wait outlast the current jiffy? Continue if
     it didn't */
  ticks = jiffies - ticks;
  if (ticks) break;
}
 
loops_per_jiffy >>= 1; /* This fixes the most significant bit and is
                          the lower-bound of loops_per_jiffy */
上述代码首先假定loops_per_jiffy高于4096,这可以转化为处理器速度大约为每秒100万条指令,即1MIPS。接下来,它等待jiffy被刷新(1个新的节拍的开始),并开始运行delay循环__delay(loops_per_jiffy)。如果这个delay循环持续了1jiffy以上,将使用以前的loops_per_jiffy值(将当前值右移1位)修复当前loops_per_jiffy的最高位;否则,该函数继续通过左移loops_per_jiffy值来探测出其最高位。在内核计算出最高位后,它开始计算低位并微调其精度:
loopbit = loops_per_jiffy;
 
/* Gradually work on the lower-order bits */
while (lps_precision-- && (loopbit >>= 1)) {
  loops_per_jiffy |= loopbit;
  ticks = jiffies;
  while (ticks == jiffies); /* Wait until the start
                               of the next jiffy */
ticks = jiffies;
 
  /* Delay */
  __delay(loops_per_jiffy);
 
  if (jiffies != ticks)        /* longer than 1 tick */
    loops_per_jiffy &= ~loopbit;
}
上述代码计算出了delay循环跨越jiffy边界时loops_per_jiffy的低位值。这个被校准的值可被用于获取BogoMIPS(其实它是一个并非科学的处理器速度指标)。你可以使用BogoMIPS作为衡量处理器运行速度的相对尺度。在1.6Ghz 基于Pentium M的笔记本电脑上,根据前述启动过程的打印信息,delay循环校准的结果趋向于loops_per_jiffy的值为2394935。获得BogoMIPS的方式如下:
BogoMIPS = loops_per_jiffy * 1秒内的jiffy * delay循环消耗的指令数(以百万为单位)
 
= (2394935 * HZ * 2) / (1 million)
 
= (2394935 * 250 * 2) / (1000000)
 
= 1197.46 (与启动过程打印信息中的值一致)
在本章《内核定时器》一节,将有对jiffyHZloops_per_jiffy更深入的阐述。
Checking HLT Instruction
由于Linux内核支持多种硬件平台,启动代码会检查体系结构相关的bug。其中一项工作就是验证停机(HLT)指令。
x86处理器的HLT指令会将CPU置入一种低功耗睡眠模式,直到下一次硬件中断发生之前维持不变。当内核想让CPU进入空闲状态时(查看arch/x86/kernel/process_32.c 文件中定义的cpu_idle()函数),它会使用HLT指令。对于有问题的CPU而言,命令行参数no-hlt可以禁止HLT指令。如果no-hlt被设置,在空闲的时候,内核会进行忙等待而不是通过HLTCPU温。
init/main.c中的启动代码调用include/asm-your-arch/bugs.h中定义的check_bugs()时,会打印上述信息。
NET: Registered Protocol Family 2
Linux套接字(socket)层是用户空间应用程序访问各种网络协议的统一接口。每个协议通过include/linux/socket.h文件中定义的被分配给它的独一无二的家族(family)号注册自身。上述打印信息中的Family 2代表AF_INETInternet协议)。启动过程中另一个常见的被打印的信息是AF_NETLINKFamily 16)。Netlink socket提供了用户进程和内核通信的方法。通过netlink socket可完成的功能还包括存取路由表和地址解析协议(ARP)表(include/linux/netlink.h文件给出了完整的用法列表)。对于此类任务而言,netlink socket比系统调用更合适,因为前者具有采用异步机制、更易于实现和可动态连接的优点。
内核中经常使能的另一个协议家族是AF_UNIXUNIX-domain套接字。X Windows等程序使用它们在同一个系统在进行进程间通信。
Freeing Initrd Memory: 387k Freed
Initrd是一种由bootloader加 载的常住内存的虚拟磁盘映像。在内核启动后,会将其挂载为初始根文件系统,这个初始根文件系统中存放着挂载实际根文件系统磁盘分区时所依赖的可动态连接的 模块。由于内核可运行于各种各样的存储控制器硬件平台上,把所有可能的磁盘驱动都直接放进基本的内核映像中并非一种灵活的方式。你所使用的系统的存储设备 的驱动被打包放入了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压缩文件格式,从[url]www.gnu.org/software/cpio[/url]可以下载到它。
在本例中,我们使用的是通过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)。
SCSI subsystem initialized                  rightwards ——>SCSI
usbcore: registered new driver hub          rightwards ——>USB
agpgart: Detected an Intel 855 Chipset.     rightwards ——>Video
[drm] Initialized drm 1.0.0 20040925
PS/2 Controller [PNP0303:KBD,PNP0f13:MOU]
at 0x60,0x64 irq 1,12 serio: i8042 KBD port rightwards ——>Keyboard
serial8250: ttyS0 at I/O 0x3f8 (irq = 4)
is a NS16550A                               rightwards ——>Serial Port
Floppy drive(s): fd0 is 1.44M               rightwards ——>Floppy
RAMDISK driver initialized: 16 RAM disks
of 4096K size 1024 blocksize                rightwards ——>Ramdisk
loop: loaded (max 8 devices)                rightwards ——>Loop back
ICH4: IDE controller at PCI slot
0000:00:1f.1                                rightwards ——>Hard Disk
...
input: SynPS/2 Synaptics TouchPad as
/class/input/input1                         rightwards ——>Touchpad
e1000: eth0: e1000_probe: Intel® PRO/1000
Network Connection                          rightwards ——>Ethernet
Yenta: CardBus bridge found at
0000:02:00.0 [1014:0560]                    rightwards ——>PCMCIA/CardBus
...
 
本书会以单独的章节讨论了许多个上述的驱动子系统,请注意如果驱动以模块的形式被动态连接到内核,其中的一些消息也许只有在内核启动后才会被显示。
EXT3-fs: Mounted Filesystem
EXT3文件系统已经成为Linux事实上的文件系统。EXT3在退役的EXT2文件系统基础上增添了日志层,该层可用于崩溃后文件系统的快速恢复。它的目标是不经由耗时的文件系统检查(fsck)操作即可获得一个一致的文件系统。EXT2仍然是新文件系统的工作引擎,但是EXT3层会在进行实际的磁盘改变之前记录文件交互的日志。EXT3向后兼容于EXT2,因此,你可以在你现存的EXT2文件系统上批上EXT3的大衣或者脱去EXT3的大衣以回归到EXT2文件系统。
EXT4
EXT文件系统的最新版本是EXT4,自2.6.19内核以来,EXT4已经被增加到了主线Linux内核中,但是被注明为“experimental”,名称为ext4devEXT4很大程度上向后兼容于EXT3,其主页为[url]www.bullopensource.org/ext4[/url]
EXT3会启动一个称为kjournald的内核辅助线程(在接下来的一章中将深入讨论内核线程)来完成日志功能。在EXT3投入运转以后,内核挂载根文件系统并做好“业务”上的准备:
EXT3-fs: mounted filesystem with ordered data mode
kjournald starting. Commit interval 5 seconds
VFS: Mounted root (ext3 filesystem).
INIT: Version 2.85 Booting
所有Linux进程的父进程init是内核完成启动序列后运行的第1个程序。在init/main.c的最后几行,内核会搜索一个不同的位置以定位到init
if (ramdisk_execute_command) { /* Look for /init in initramfs */
  run_init_process(ramdisk_execute_command);
}
 
if (execute_command) { /* You may override init and ask the kernel
                          to execute a custom program using the
                          "init=" kernel command-line argument. If
                          you do that, execute_command points to the
                          specified program */
  run_init_process(execute_command);
}
 
/* Else search for init or sh in the usual places .. */
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
init会接受/etc/inittab的指引。它首先执行/etc/rc.sysinit中的系统初始化脚本,该脚本的一项最重要的职责就是激活交换(swap)分区,这会导致如下启动信息被打印:
Adding 1552384k swap on /dev/hda6
让我们来仔细看看上述这段话的意思。Linux用户进程拥有3GB的虚拟地址空间(见《内存分配》一节),构成“工作集”的页被保存在RAM中。但是,如果有太多程序需要内存资源,内核会释放一些被使用了的RAM页面并将其存储到称为交换空间(swap space)的磁盘分区中。根据经验法则,交换分区的大小应该是RAM2倍。在本例中,交换空间位于/dev/hda6这个磁盘分区,其大小为1552384K字节。
接下来,init开始运行/etc/rc.d/rcX.d/目录中的脚本,Xinittab中定义的运行级别。Runlevel是根据预期的工作模式所进入的执行状态。例如,多用户文本模式意味着runlevel3X Windows则意味着runlevel5。因此,当你看到“INIT: Entering runlevel 3这条信息的时候,init就已经开始执行/etc/rc.d/rc3.d/目录中的脚本了。这些脚本会启动动态设备命名子系统(第4章《打下基础》中将讨论udev),并加载网络、音频、存储设备等驱动所对应的内核模块:
Starting udev: [ OK ]
Initializing hardware... network audio storage [Done]
...
最后,init发起虚拟控制台终端,你现在就可以登录了。
内核模式和用户模式
MS-DOS等操作系统在单一的CPU模式下执行,但是一些类UNIX的操作系统则使用了2种模式。在Linux机器上,CPU将或者处于受信任的内核模式,或者处于受限制的用户模式。除了内核本身处于内核模式以外,所有的用户进程都运行在用户模式之上。
内核模式的代码可以无限制地使用完整的CPU指令集并访问所有的内核和I/O空间。但是,如果用户模式的进程要享有此特权,并必须通过系统调用向设备驱动或其他内核模式的代码请求服务。另外一个不同是,用户模式的代码允许发生缺页,而内核模式的代码则不允许。
2.4和更早的内核中,仅仅用户模式的进程可以被其他进程抢占,除非发生以下情况,否则内核模式代码可以一直独占CPU
1 它自愿放弃CPU
2 发生中断或异常
2.6内核引入了内核抢占,大多数内核模式的代码也可以被抢占。
进程上下文和中断上下文
内 核可以处于两种上下文:进程上下文和中断上下文。在系统调用之后,用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的代表就运行于进程上下 文。异步发生的中断会引发中断处理程序被调用,中断处理程序就运行于中断上下文。中断上下文和进程上下文不可能同时发生。
运行于进程上下文的内核代码是可抢占的,而进程上下文则不会被抢占。因此,内核会限制中断上下文的工作,不允许其执行如下操作:
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 */
my_timer.function = timer_func;   /* Function to execute
                                     after n seconds */
my_timer.data = func_parameter;   /* Parameter to be passed
                                     to timer_func */
add_timer(&my_timer);             /* Start the timer */
上述代码只会让定时器昙花一现。如果你想timer_func()函数被周期性地执行,你需要在timer_func()加上相关地代码指定其在下次超时后调度自身:
static void timer_func(unsigned long func_parameter)
{
  /* Do work to be done periodically */
  /* ... */
 
  init_timer(&my_timer);
  my_timer.expire   = jiffies + n*HZ;
  my_timer.data     = func_parameter;
  my_timer.function = timer_func;
  add_timer(&my_timer);
}