第2章 内核一瞥
在我们开始步入
Linux
设备驱动的神秘世界之前,让我们先熟悉一些从驱动开发人员应该理解的基本的内核概念。我们将学习到内核定时器、同步机制以及内存分配方法,但是,先让我们从顶层视角开始探索,扫描一下内核发出的启动信息,并在感兴趣的地方设置停下来看一看。
启动过程
图
2.1
显示了基于
x86
计算机
Linux
系统的启动顺序。第一步是
BIOS
从启动设备中导入主引导记录(
MBR
),接下来
MBR
中的代码查看分区表并从活动分区读取
GRUB
、
LILO
或
SYSLINUX
等
bootloader
,之后
bootloader
会加载压缩后的内核映像并将控制权传递给它。内核取得控制权后,会将自身解压缩并投入运转。
图
2.1
基于
x86
的硬件上
Linux
的启动过程
基于
x86
的处理器有两种操作模式:实模式和保护模式。在实模式下,用户仅可以使用
1MB
内存,并且没有任何保护。保护模式则更加复杂,用户可以使用更多的高级功能(如分页)。
CPU
提供了一条由实模式通向保护模式的道路,但是,这条路只允许单向行驶,用户不能从保护模式再切换回实模式。
内核初始化的第一步是执行实模式下的汇编代码,之后执行保护模式下
init/main.c
文件(上一章我们修改了这个文件)中的
start_kernel()
函数。
start_kernel()
函数首先会初始化
CPU
子系统,之后让内存管理和进程管理系统就位,接下来启动外部总线和
I/O
设备,最后的一步是激活所有
Linux
进程的父亲
init
。
init
执行用户空间的脚本以启动必要的内核服务,它最终派生控制台终端程序并显示登录(
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 image 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-provided physical RAM map:
BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
...
BIOS-e820: 00000000ff800000 - 0000000100000000 (reserved)
实模式下的初始化代码通过使用
BIOS
的
int 0x15
服务并执行
0xe820
号函数来获得系统的内存映射信息。内存映射信息中包含了预留的和可用的内存,内核将使用这些信息创建其可用的内存池。在附录
B
《
Linux
和
BIOS
》的《实模式调用》一节,我们会对
BIOS
提供的内存映射问题进行更深入的讲解。
896MB
以内的常规的可被寻址的内存区域被称作低端内存。内存分配函数
kmalloc()
就是从该区域分配内存的。高于
896MB
被称为高端内存,只有在采用特殊的方式进行映射后才能被访问。在启动过程中,内核会计算并显示这些内存
zone
内总的页数,在本章的稍后,会对这些内存
zone
进行更深入的分析。
Linux
的
bootloader
通常会给内核传递一个命令行。命令行中的参数类似于传递给
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 image 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
,意味着你希望启动过程相对简洁,并且设置
runlevel
为
2
。因为你已经熟悉了
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
:
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
循环持续了
1
个
jiffy
以上,将使用以前的
loops_per_jiffy
值(将当前值右移
1
位)修复当前
loops_per_jiffy
的最高位;否则,该函数继续通过左移
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
的方式如下:
|
|
|
= (2394935 * 250 * 2) / (1000000)
|
|
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
被设置,在空闲的时候,内核会进行忙等待而不是通过
HLT
给
CPU
降
温。
NET: Registered Protocol Family 2
Linux
套接字(
socket
)层是用户空间应用程序访问各种网络协议的统一接口。每个协议通过
include/linux/socket.h
文件中定义的被分配给它的独一无二的家族(
family
)号注册自身。上述打印信息中的
Family 2
代表
AF_INET
(
Internet
协议)。启动过程中另一个常见的被打印的信息是
AF_NETLINK
(
Family 16
)。
Netlink socket
提供了用户进程和内核通信的方法。通过
netlink socket
可完成的功能还包括存取路由表和地址解析协议(
ARP
)表(
include/linux/netlink.h
文件给出了完整的用法列表
)。对于此类任务而言,
netlink socket
比系统调用更合适,因为前者具有采用异步机制、更易于实现和可动态连接的优点。
内核中经常使能的另一个协议家族是
AF_UNIX
或
UNIX-domain
套接字。
X Windows
等程序使用它们在同一个系统在进行进程间通信。
Initrd
是一种由
bootloader
加 载的常住内存的虚拟磁盘映像。在内核启动后,会将其挂载为初始根文件系统,这个初始根文件系统中存放着挂载实际根文件系统磁盘分区时所依赖的可动态连接的 模块。由于内核可运行于各种各样的存储控制器硬件平台上,把所有可能的磁盘驱动都直接放进基本的内核映像中并非一种灵活的方式。你所使用的系统的存储设备 的驱动被打包放入了
initrd
中,在内核启动后、实际的根文件系统被挂载之前,这些驱动才被加载。使用
mkinitrd
命令可以创建一个
initrd
映像。
2.6
内核提供了一种称为
initramfs
的新功能,它在几个方面较
initrd
更为优秀。后者模拟了一个磁盘(因而被称为
initramdisk
或
initrd
),会带来
Linux
块
I/O
子系统的开销(如缓冲),然后前者基本上如同一个被挂载的文件系统一样,由自身获取缓冲(因此被称作
initramfs
)。
不同于
initrd
,基于页缓冲建立的
initramfs
如同页缓冲一样会动态地变大和缩小,从而减少了其内存消耗。另外,
initrd
要求你的内核映像包含了
initrd
所使用的文件系统(例如,如果你的
initrd
为
EXT2
文件系统,内核必须包含
EXT2
驱动),然而
initramfs
不需要文件系统支持。再者,由于
initramfs
只是页缓冲之上的一小层,因此它的代码量很小。
用户可以将初始根文件系统打包为一个
cpio
压缩包
[2]
,并通过
initrd=
命令行参数传递给内核。当然,也可以在内核配置过程中通过
INITRAMFS_SOURCE
选项直接编译进内核。对于后一种方式而言,用户可以提供
cpio
压缩包的文件名或者包含
initramfs
的目录树。在启动过程中,内核会将文件解压缩为一个
initramfs
根文件系统,如果它找到了
/init
,它就会执行该顶层的程序。这种获取初始根文件系统的方法对于嵌入式系统而言特别有用,因为在嵌入式系统中系统资源非常宝贵。使用
mkinitramfs
可以创建一个
initramfs
映像,查看文档
Documentation/filesystems/ramfs-rootfs-initramfs.txt
可获得更多信息。
在本例中,我们使用的是通过
initrd=命令行参数向内核传递初始根文件系统
cpio压缩包的方式。在将压缩包中的内容解压为根文件系统后,内核将释放该压缩包所占据的内存(本例中为
387K
)并打印上述信息。释放后的页面会被分发给内核中的其他部分以便被申请。
在第
18
章中我们会发现,在嵌入式系统开发过程中,
initrd
和
initramfs
有时候也可被用作嵌入式设备上实际的根文件系统。
I/O
调度器的主要目标是通过减少磁盘的定位次数以增加系统的吞吐率。在磁盘定位过程中,磁头需要从当前的位置移动到感兴趣的目标位置,这会带来一定的延迟。
2.6
内核提供了
4
种不同的
I/O
调度器:
Deadline
、
Anticipatory
、
Complete 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
键盘和鼠标、软驱、
ramdisk
、
loopback
设备、
IDE
控制器(本例中为
ICH4
南桥芯片集中的一部分)、触控板、以太网控制器(本例中为
e1000
)以及
PCMCIA
控制器初始化的启动信息。图
2.3
中——
>
符号指向的为
I/O
设备的标识(
ID
)。
SCSI subsystem initialized ——>SCSI usbcore: registered new driver hub ——>USB agpgart: Detected an Intel 855 Chipset. ——>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 ——>Keyboard serial8250: ttyS0 at I/O 0x3f8 (irq = 4) is a NS16550A ——>Serial Port Floppy drive(s): fd0 is 1.44M ——>Floppy RAMDISK driver initialized: 16 RAM disks of 4096K size 1024 blocksize ——>Ramdisk loop: loaded (max 8 devices) ——>Loop back ICH4: IDE controller at PCI slot 0000:00:1f.1 ——>Hard Disk ... input: SynPS/2 Synaptics TouchPad as /class/input/input1 ——>Touchpad e1000: eth0: e1000_probe: Intel® PRO/1000 Network Connection ——>Ethernet Yenta: CardBus bridge found at 0000:02:00.0 [1014:0560] ——>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
”,名称为
ext4dev
。
EXT4
很大程度上向后兼容于
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
)的磁盘分区中。根据经验法则,交换分区的大小应该是
RAM
的
2
倍。在本例中,交换空间位于
/dev/hda6
这个磁盘分区,其大小为
1552384K
字节。
接下来,
init
开始运行
/etc/rc.d/rcX.d/
目录中的脚本,
X
是
inittab
中定义的运行级别。
Runlevel
是根据预期的工作模式所进入的执行状态。例如,多用户文本模式意味着
runlevel
为
3
,
X Windows
则意味着
runlevel
为
5
。因此,当你看到“
INIT: Entering runlevel 3”这条信息的时候,init
就已经开始执行
/etc/rc.d/rc3.d/
目录中的脚本了。这些脚本会启动动态设备命名子系统(第
4
章《打下基础》中将讨论
udev
),并加载网络、音频、存储设备等驱动所对应的内核模块:
Starting udev: [ OK ]
Initializing hardware... network audio storage [Done]
...
MS-DOS
等操作系统在单一的
CPU
模式下执行,但是一些类
UNIX
的操作系统则使用了
2
种模式。在
Linux
机器上,
CPU
将或者处于受信任的内核模式,或者处于受限制的用户模式。除了内核本身处于内核模式以外,所有的用户进程都运行在用户模式之上。
内核模式的代码可以无限制地使用完整的
CPU
指令集并访问所有的内核和
I/O
空间。但是,如果用户模式的进程要享有此特权,并必须通过系统调用向设备驱动或其他内核模式的代码请求服务。另外一个不同是,用户模式的代码允许发生缺页,而内核模式的代码则不允许。
(
1
)
它自愿放弃
CPU
(
2
)
发生中断或异常
2.6
内核引入了内核抢占,大多数内核模式的代码也可以被抢占。
内 核可以处于两种上下文:进程上下文和中断上下文。在系统调用之后,用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的代表就运行于进程上下 文。异步发生的中断会引发中断处理程序被调用,中断处理程序就运行于中断上下文。中断上下文和进程上下文不可能同时发生。
运行于进程上下文的内核代码是可抢占的,而进程上下文则不会被抢占。因此,内核会限制中断上下文的工作,不允许其执行如下操作:
(
1
)进入睡眠状态或主动放弃
CPU
(
2
)占用
mutex
(
3
)执行耗时的任务
(
4
)访问用户空间虚拟内存
内核定时器
内核中许多部分的工作都高度依赖于时间的推移。
Linux
内核使用了硬件提供的不同的定时器以支持忙等待或睡眠等待等依赖于时间的服务。
忙等待时,
CPU
会不断运转,但是睡眠等待时,进程将放弃
CPU
。因此,只有在后者不可取的情况下,才可以考虑使用前者。内核也提供了这样的便利:在特定的时间之后调度某函数运行。
我们首先来讨论一些重要的内核定时器变量(
jiffies
、
HZ
和
xtime
)的含义,接下来,我们会使用
Pentium
时间戳计数器(
TSC
)测量基于
Pentium
的系统的运行次数,之后,我们也分析一下
Linux
怎么使用实时钟(
RTC
)。
系统定时器能以可编程的频率中断
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
),它会根据系统的负载动态触发定时器中断。无节拍系统的实现超出了本章的范围。
j
iffies
变量记录了自系统启动依赖,系统定时器已经触发的次数。内核每秒钟将
jiffies
变量增加
HZ
次。因此,对于
HZ
值为
100
的系统,
1
个
jiffy
等于
10
毫秒,而对于
HZ
为
1000
的系统,
1
个
jiffy
仅为
1
毫秒。
为了更好地理解
HZ
和
jiffies
变量,请看下面的取自
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
,否则,返回
-EBUSY
。
3*HZ
是
3
秒内的
jiffies
数量。计算出来的超时
jiffies + 3*HZ
,将是
3
秒超时发生后新的
jiffies
值。
time_after()
的功能是将目前的
jiffies
值与请求的超时时间对比,检测溢出。类似函数还包括
time_before()
、
time_before_eq()
和
time_after_eq()
。
jiffies
被定义为
volatile
类型,它会告诉编译器不要优化该变量的存取代码。这样就确保了每个节拍发生的定时器中断处理程序都能更新
jiffies
值,并且循环中的每一步都会重新读取
jiffies
值。
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
就得到了秒数值。
假定
jiffies
位
1000
,
32
位的
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()
完成此功能:
schedule_timeout(timeout); /* Allow other parts of the
kernel to run */
这种延时仅仅确保超时时较低的精度,由于只有在时钟节拍引发的内核调度才会更新
jiffies
,所以超时的最大精度是
HZ
。另外,即使你的进程已经超时并可被调度,但是调度器仍然可能基于优先级策略选择运行队列的其他进程
[5]
。
用于睡眠等待的另
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 */
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);
}
本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/119950,如需转载请自行联系原作者