我的内核学习笔记13:x86平台linux系统重启流程跟踪

一直以来,笔者只知道重启Linux系统性使用reboot,但对其过程却无所知,涉及到哪些知识点也无概念。本文就跟踪一下重启的流程,平台为Intel x86,Linux内核版本为3.17。行文中“重启”与“复位”等价。

一、初识

在Linux命令行下输入reboot,终端出现如下信息:

1
2
3
4
5
6
7
8
9
* Stopping web server apache2         * 
Stop in my script and clean net.rules ....
* Asking all remaining proc[ OK ]to terminate...
[ OK ] * All processes ended within 1 seconds...
rpcbind: rpcbind terminating on signal. Restart with "rpcbind -w"
[ OK ]ctivating swap...
[ OK ] * Unmounting local filesystems...
* Will now restart
[ 847.054796] reboot: Restarting system

信息格式错乱,但不影响分析。系统首先做的是停止apache2,然后执行用户自定义脚本。再结束进程、卸载文件系统。最后提示“reboot: Restarting system”,便完成使命,系统重启。
从上面信息第二行看到执行了笔者自己写的脚本,它的作用是用于清除70-persistent-net.rules文件,主要解决当时的一个棘手问题,离今二年有余,由是怀念。

二、用户空间

本节抽取uClinux和busybox源码中关于重启部分函数代码,以便了解用户空间重启的过程。两者的重启代码具备一定代表。 先看一下uClinux重启代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, char *argv[])
{
kill(1, SIGTSTP);
sync();
signal(SIGTERM,SIG_IGN);
setpgrp();
kill(-1, SIGTERM);
kill(-1, SIGHUP);
sleep(1);
kill(-1, SIGKILL);
sync();
sleep(1);
#if __GNU_LIBRARY__ > 5
reboot(0x01234567);
#else
reboot(0xfee1dead, 672274793, 0x01234567);
#endif
exit(0); /* Shrug */
}

再看一下busybox重启代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
extern int bb_shutdown_system(unsigned long magic)
{
int pri = LOG_KERN|LOG_NOTICE|LOG_FACMASK;
const char *message;

/* Don't kill ourself */
signal(SIGTERM,SIG_IGN);
signal(SIGHUP,SIG_IGN);
setpgrp();

/* Allow Ctrl-Alt-Del to reboot system. */
#ifndef RB_ENABLE_CAD
#define RB_ENABLE_CAD 0x89abcdef
#endif
reboot(RB_ENABLE_CAD);

openlog(bb_applet_name, 0, pri);

message = "\nThe system is going down NOW !!";
syslog(pri, "%s", message);
printf(bb_shutdown_format, message);

sync();

/* Send signals to every process _except_ pid 1 */
message = "Sending SIGTERM to all processes.";
syslog(pri, "%s", message);
printf(bb_shutdown_format, message);

kill(-1, SIGTERM);
sleep(1);
sync();

message = "Sending SIGKILL to all processes.";
syslog(pri, "%s", message);
printf(bb_shutdown_format, message);

kill(-1, SIGKILL);
sleep(1);

sync();

reboot(magic);
return 0; /* Shrug */
}

两者处理过程类似,首先调用kill发送信号,最后调用reboot函数。

reboot是一个系统调用,man手册说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* For libc4 and libc5 the library call and the system call
are identical, and since kernel version 2.1.30 there are
symbolic names LINUX_REBOOT_* for the constants and a
fourth argument to the call: */

#include <unistd.h>
#include <linux/reboot.h>

int reboot(int magic, int magic2, int cmd, void *arg);

/* Under glibc some of the constants involved have gotten
symbolic names RB_*, and the library call is a 1-argument
wrapper around the 3-argument system call: */

#include <unistd.h>
#include <sys/reboot.h>

int reboot(int cmd);

如果不考虑影响其它服务、进程的话,直接在代码调用reboot即可完成系统重启功能。

三、内核空间

在了解用户空间的执行过程后,再看看内核空间是如何实现的。

前节提到reboot是一个系统调用。其定义位于kernel/reboot.c文件,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
void __user *, arg)
{
mutex_lock(&reboot_mutex);
switch (cmd) {
case LINUX_REBOOT_CMD_RESTART:
kernel_restart(NULL);
break;

case LINUX_REBOOT_CMD_HALT:
kernel_halt();
do_exit(0);
panic("cannot halt");

case LINUX_REBOOT_CMD_POWER_OFF:
kernel_power_off();
do_exit(0);
break;

default:
ret = -EINVAL;
break;
}
mutex_unlock(&reboot_mutex);
return ret;
}

reboot进行LINUX_REBOOT_CMD_RESTART分支,调用的函数为kernel_restart。在终端看到的字符串“Restarting system”就是在里面打印的。它的实现如下(同样位于kernel/reboot.c文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
void kernel_restart(char *cmd)
{
kernel_restart_prepare(cmd);
migrate_to_reboot_cpu();
syscore_shutdown();
if (!cmd)
pr_emerg("Restarting system\n");
else
pr_emerg("Restarting system with command '%s'\n", cmd);
kmsg_dump(KMSG_DUMP_RESTART);
machine_restart(cmd);
}
EXPORT_SYMBOL_GPL(kernel_restart);

函数最后调用machine_restart。接着看一下machine_restart实现(位于arch/x86/kernel/reboot.c):

1
2
3
4
void machine_restart(char *cmd)
{
machine_ops.restart(cmd);
}

该函数调用了machine_ops结构体的函数指针,看一下machine_ops结构体定义(位于arch/x86/kernel/reboot.c):

1
2
3
4
5
6
7
8
9
10
struct machine_ops machine_ops = {
.power_off = native_machine_power_off,
.shutdown = native_machine_shutdown,
.emergency_restart = native_machine_emergency_restart,
.restart = native_machine_restart,
.halt = native_machine_halt,
#ifdef CONFIG_KEXEC
.crash_shutdown = native_machine_crash_shutdown,
#endif
};

真正重启的restart实际上是native_machine_restart函数:

1
2
3
4
5
6
7
8
static void native_machine_restart(char *__unused)
{
pr_notice("machine restart\n");

if (!reboot_force)
machine_shutdown();
__machine_emergency_restart(0);
}

继续看调用的函数__machine_emergency_restart:

1
2
3
4
5
static void __machine_emergency_restart(int emergency)
{
reboot_emergency = emergency;
machine_ops.emergency_restart();
}

而machine_ops.emergency_restart函数实际为native_machine_emergency_restart。

最终,重启实现的函数为native_machine_emergency_restar(位于arch/x86/kernel/reboot.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
static void native_machine_emergency_restart(void)
{
for (;;) {
/* Could also try the reset bit in the Hammer NB */
switch (reboot_type) { // 重启标志:reboot_type。
case BOOT_ACPI:
acpi_reboot();
reboot_type = BOOT_KBD; // BOOT_ACPI不成功再到BOOT_KBD
break;


case BOOT_KBD:
mach_reboot_fixups(); /* For board specific fixups */


for (i = 0; i < 10; i++) {
kb_wait();
udelay(50);
outb(0xfe, 0x64); /* Pulse reset low */
outb(0x0e, 0xcf9); /* for byatrail e3800 SOC by Late Lee*/
udelay(50);
}
if (attempt == 0 && orig_reboot_type == BOOT_ACPI) {
attempt = 1;
reboot_type = BOOT_ACPI;
} else {
reboot_type = BOOT_EFI; // BOOT_KBD不成功再到BOOT_EFI
}
break;


case BOOT_EFI:
efi_reboot(reboot_mode, NULL);
reboot_type = BOOT_BIOS; // BOOT_EFI不成功再到BOOT_BIOS
break;


case BOOT_BIOS:
machine_real_restart(MRR_BIOS);


/* We're probably dead after this, but... */
reboot_type = BOOT_CF9_SAFE;// BOOT_BIOS不成功再到BOOT_CF9_SAFE
break;


case BOOT_CF9_FORCE:
port_cf9_safe = true;
/* Fall through */


case BOOT_CF9_SAFE:
if (port_cf9_safe) {
u8 reboot_code = reboot_mode == REBOOT_WARM ? 0x06 : 0x0E;
u8 cf9 = inb(0xcf9) & ~reboot_code;
outb(cf9|2, 0xcf9); /* Request hard reset */
udelay(50);
/* Actually do the reset */
outb(cf9|reboot_code, 0xcf9);
udelay(50);
}
reboot_type = BOOT_TRIPLE; // BOOT_CF9_SAFE不成功再到BOOT_TRIPLE
break;


case BOOT_TRIPLE:
load_idt(&no_idt);
__asm__ __volatile__("int3");


/* We're probably dead after this, but... */
reboot_type = BOOT_KBD;
break;
}
}
}

从函数实现上看,使用死循环,根据不同的重启类型做转变:

1
BOOT_ACPI->BOOT_KBD->BOOT_ACPI->BOOT_EFI->BOOT_BIOS->BOOT_CF9_SAFE->BOOT_TRIPLE...

首先是ACPI复位,调用acpi_reboot函数(driver/acpi/reboot.c),在该函数中通过写PCI寄存器方式复位。这涉及ACPI相关知识,笔者未做深入研究。笔者在一台安装BIOS的机器上测试发现是使用ACPI复位的。

如果ACPI被禁止(即无法复位成功),则使用keyboad复位,主要是写0xfe到端口0x64,关于它的作用,直接引述网络资料:

“0x64端口是i8042键盘控制器的控制端口,0xfe命令字的意思是将P32-P21三个针脚拉为低电平,持续6usec。这段代码的实际效果就相当于你按下机箱上的 RESET键。”

值得一提的是BOOT_CF9_SAFE分支,当port_cf9_safe为true时,会通过写0xcf9寄存器的方式复位。

其余的暂略过不提。

(注:上面分析顺序根据笔者测试结果来描述,理论上并不严谨)

下面看看系统重启过程的调试信息(注:此时机器无法使用reset重启):

1
2
3
4
5
6
7
8
9
10
11
12
13
* Will now restart
[ 716.203870] reboot: Restarting system
[ 716.208485] reboot: reboot_type: 97(a)
[ 716.212675] reboot: 11111reboot_type: 97(a)
[ 716.217350] acpi_reboot() acpi is disable...
[ 716.222121] reboot: 11111reboot_type: 107(k)
[ 716.226900] reboot: native_machine_emergency_restart() in KBD reboot...
[ 718.637262] reboot: 11111reboot_type: 97(a)
[ 718.641936] acpi_reboot() acpi is disable...
[ 718.646707] reboot: 11111reboot_type: 107(k)
[ 718.651484] reboot: native_machine_emergency_restart() in KBD reboot...
[ 721.061846] reboot: 11111reboot_type: 101(e)
[ 721.066616] reboot: 11111reboot_type: 98(b)

四、u-boot环境reset

作为扩展,本文顺便也看看u-boot源码下重启的流程。reset命令位于cmd/boot.c文件:

1
2
3
4
5
U_BOOT_CMD(
reset, 1, 0, do_reset,
"Perform RESET of the CPU",
""
);

do_reset函数实现在arch/x86/cpu/cpu.c文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int do_reset(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
printf("resetting ...\n");


/* wait 50 ms */
udelay(50000);
disable_interrupts();
reset_cpu(0);


/*NOTREACHED*/
return 0;
}

__weak void reset_cpu(ulong addr)
{
/* Do a hard reset through the chipset's reset control register */
outb(SYS_RST | RST_CPU, IO_PORT_RESET);
for (;;)
cpu_hlt();
}

void x86_full_reset(void)
{
outb(FULL_RST | SYS_RST | RST_CPU, IO_PORT_RESET);
}

上面列出2个复位的函数:reset_cpu、x86_full_reset。它们只有细微区别,但都是往IO_PORT_RESET这个端口上写数值。接着看看这些宏定义是什么。它们的定义位于文件arch/x86/include/asm/processor.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* This register is documented in (for example) the Intel Atom Processor E3800
* Product Family Datasheet in "PCU - Power Management Controller (PMC)".
*
* RST_CNT: Reset Control Register (RST_CNT) Offset cf9.
*
* The naming follows Intel's naming.
*/
#define IO_PORT_RESET 0xcf9


enum {
SYS_RST = 1 << 1, /* 0 for soft reset, 1 for hard reset */
RST_CPU = 1 << 2, /* initiate reset */
FULL_RST = 1 << 3, /* full power cycle */
};

从注释上看,0xcf9是Intel Atom的E3800系列SOC的RST_CNT寄存器(在内核中亦出现此寄存器)。复位类型有3种:SYS_RST、RST_CPU、FULL_RST。

本着打破沙锅问到底的钻研精神,找到E3800的datasheet,查看关于复位的说明章节(7.4章节),截图如下:
复位类型说明

三种复位类型组合即为0x0e,亦即表格第一行所述操作。而RST_CNT寄存器的说明位于30.7章节,其寄存器地址正是CF9h。
PMC寄存器

这样,终于和u-boot源码对应起来了,对源码的复位函数也能解释得通了。网络上有资料表明可以在ICH手册中找到CF9寄存器,有兴趣的可以自行查阅。

五、所遇问题及解决

细心的读者可能会发现在native_machine_emergency_restart函数中,笔者在BOOT_KBD分支中写0xcf9寄存器,而不是使用BOOT_CF9_SAFE分支。因为笔者在使用u-boot引导启动(非BIOS)的x86机器上遇到无法复位的问题,当执行到BOOT_BIOS时机器死了,但没有复位——应该是板子上并没有BIOS,无法执行对应的指令吧,这方面没有深入研究。选择在BOOT_KBD中实现是因为同一份内核镜像文件需要在有BIOS的机器和有u-boot的机器上运行。——无论是设计还是维护角度,笔者希望能尽量兼容。

六、小结

要使系统重启,直接调用reboot即可,但由于系统重启还需要进行其它清理操作,一般不建议直接使用。在内核空间中实现的重启机制,首先使用ACPI,其次使用keyboad,再次使用BIOS,最后使用CF9。从u-boot源码分析看到,x86机器的重启是通过将0x0e写到0xcf9寄存器来实现的,在内核内核也有此方式,但差不多是在最后才执行,优先级较低。另外关于ACPI的研究,待日后有空闲再议。

参考资料:
1、baytrail手册:http://www.intel.com/content/www/us/en/embedded/products/bay-trail/atom-e3800-family-datasheet.html
2、内核源码官网:https://www.kernel.org
3、内核源码查询:http://lxr.free-electrons.com/source/?v=3.17
4、uClinux重启代码:https://github.com/rhuitl/uClinux/blob/master/user/sysutils/reboot.c
5、u-boot代码:ftp://ftp.denx.de/pub/u-boot/
6、busybox官网:https://busybox.net/

李迟 2017.1.12 周四 晚