当前位置: 首页 > Linux内核研究, 嵌入式Linux > 正文

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

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

一、初识

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

* 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重启代码片段:

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重启代码:

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手册说明如下:
       /* 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文件,如下:
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文件):

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):

void machine_restart(char *cmd)
{
     machine_ops.restart(cmd);
}

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

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函数:

static void native_machine_restart(char *__unused)
{
     pr_notice("machine restart\n");

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

继续看调用的函数__machine_emergency_restart:

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):

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;
          }
     }
}
从函数实现上看,使用死循环,根据不同的重启类型做转变:

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重启):
* 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文件:
U_BOOT_CMD(
	reset, 1, 0,	do_reset,
	"Perform RESET of the CPU",
	""
);

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

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:

/*
 * 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。
这样,终于和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的研究,待日后有空闲再议。
参考资料:
6、busybox官网:https://busybox.net/
李迟 2017.1.12 周四  晚

近来经济拮据,如本文对阁下有帮助,可慷慨解囊赞助笔者以输出更多好文章。
支付宝[email protected] 或 微信fly_camel_fly 均可。感谢!
14373903313201                                  14373903313202

本文固定链接: http://www.latelee.org/embedded-linux/kernel-note-13-x86-linux-reboot-source-htm.html

目前暂无评论

发表评论

*

快捷键:Ctrl+Enter