《Essential Linux Device Drivers》第2章(上)

简介:

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 中读取到的系统内存映射,并率先将这些信息打印出来:       
BIOS-provided physical RAM map:
BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
...
BIOS-e820: 00000000ff800000 - 0000000100000000 (reserved)
实模式下的初始化代码通过使用 BIOS int 0x15 服务并执行 0xe820 号函数来获得系统的内存映射信息。内存映射信息中包含了预留的和可用的内存,内核将使用这些信息创建其可用的内存池。在附录 B Linux BIOS 》的《实模式调用》一节,我们会对 BIOS 提供的内存映射问题进行更深入的讲解。
758MB LOWMEM Available
896MB 以内的常规的可被寻址的内存区域被称作低端内存。内存分配函数 kmalloc() 就是从该区域分配内存的。高于 896MB 被称为高端内存,只有在采用特殊的方式进行映射后才能被访问。在启动过程中,内核会计算并显示这些内存 zone 内总的页数,在本章的稍后,会对这些内存 zone 进行更深入的分析。
Kernel Command Line: ro root=/dev/hda1
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
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 循环持续了 1 jiffy 以上,将使用以前的 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 ( 与启动过程打印信息中的值一致 )
在本章《内核定时器》一节,将有对 jiffy HZ loops_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 被设置,在空闲的时候,内核会进行忙等待而不是通过 HLT CPU 温。
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_INET Internet 协议)。启动过程中另一个常见的被打印的信息是 AF_NETLINK Family 16 )。 Netlink socket 提供了用户进程和内核通信的方法。通过 netlink socket 可完成的功能还包括存取路由表和地址解析协议( ARP )表( include/linux/netlink.h 文件给出了完整的用法列表 )。对于此类任务而言, netlink socket 比系统调用更合适,因为前者具有采用异步机制、更易于实现和可动态连接的优点。
内核中经常使能的另一个协议家族是 AF_UNIX UNIX-domain 套接字。 X Windows 等程序使用它们在同一个系统在进行进程间通信。
Freeing Initrd Memory: 387k Freed
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 可获得更多信息。
[2] cpio 是一种 UNIX 压缩文件格式,从 [url]www.gnu.org/software/cpio[/url] 可以下载到它。
在本例中,我们使用的是通过 initrd=命令行参数向内核传递初始根文件系统 cpio压缩包的方式。在将压缩包中的内容解压为根文件系统后,内核将释放该压缩包所占据的内存(本例中为 387K )并打印上述信息。释放后的页面会被分发给内核中的其他部分以便被申请。
在第 18 章中我们会发现,在嵌入式系统开发过程中, initrd initramfs 有时候也可被用作嵌入式设备上实际的根文件系统。
IO Scheduler Anticipatory Registered (Default)
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]
...
最后, 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 。因此,只有在后者不可取的情况下,才可以考虑使用前者。内核也提供了这样的便利:在特定的时间之后调度某函数运行。
我们首先来讨论一些重要的内核定时器变量( jiffies HZ xtime )的含义,接下来,我们会使用 Pentium 时间戳计数器( TSC )测量基于 Pentium 的系统的运行次数,之后,我们也分析一下 Linux 怎么使用实时钟( RTC )。
HZ Jiffies
系统定时器能以可编程的频率中断 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 值。
对于 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 就得到了秒数值。
假定 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() 完成此功能:
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);

}



 本文转自 21cnbao 51CTO博客,原文链接:http://blog.51cto.com/21cnbao/119950,如需转载请自行联系原作者





相关文章
|
28天前
|
Shell Linux C语言
【Shell 命令集合 磁盘管理 】Linux losetup命令使用教程 将一个文件或设备与一个回环设备(loop device)进行关联
【Shell 命令集合 磁盘管理 】Linux losetup命令使用教程 将一个文件或设备与一个回环设备(loop device)进行关联
37 0
|
6月前
|
Linux
69Linux - 解决Device xxx has different MAC address than expecte
69Linux - 解决Device xxx has different MAC address than expecte
55 1
69Linux - 解决Device xxx has different MAC address than expecte
|
存储 Linux 数据安全/隐私保护
基于Linux服务器出现“No space left on device”错误的解决简单有效方案
基于Linux服务器出现“No space left on device”错误的解决简单有效方案
2016 0
基于Linux服务器出现“No space left on device”错误的解决简单有效方案
|
Linux
LINUX虚拟机安装增强功能时报错:/sbin/mount.vboxsf: mounting failed with the error: No such device
LINUX虚拟机安装增强功能时报错:/sbin/mount.vboxsf: mounting failed with the error: No such device
624 0
|
Linux API C++
ARM Linux 3.x的设备树(Device Tree)宋宝华
1.    ARM Device Tree起源 Linus Torvalds在2011年3月17日的ARM Linux邮件列表宣称“this whole ARM thing is a f*cking pain in the ass”,引发ARM Linux社区的地震,随后ARM社区进行了一系列的重大修正。
1210 0