Linux Grub 启动引导过程介绍


GNU GRUB是一个来自GNU项目的启动引导程序,是多启动规范的实现,它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。GRUB可用于选择操作系统分区上的不同内核,也可用于向这些内核传递启动参数。
BIOS是英文"Basic Input Output System"的缩略语,直译过来后中文名称就是"基本输入输出系统"。其实它是一组固化到计算机内主板上一个ROM芯片上的程序,它保存着计算机最重要的基本输入输出的程序、系统设置信息、开机后自检程序和系统自启动程序。 其主要功能是为计算机提供最底层的、最直接的硬件设置和控制。
因为BIOS很小,功能有限,为了完成加载操作系统的功能,就产生了mbr;bios检测到一个硬盘后,将硬盘的0柱面、0磁头、1扇区的内容经过简单判断后,至内存中的指定位置,然后跳转至这个位置,开始从这个位置运行;MBR全称为Master Boot Record,即硬盘的主引导记录。为了便于理解,一般将MBR分为广义和狭义两种:广义的MBR包含整个扇区(引导程序、分区表及分隔标识),也就是上面所说的主引导记录;而狭义的MBR仅指引导程序而言。硬盘的0柱面、0磁头、1扇区称为主引导扇区(也叫主引导记录MBR)。bios可设置系统启动的方式,比如可以设置从硬盘,光驱或U盘启动。
MBR是硬盘的主引导记录,属于引导区。
内核(kernel),是一个操作系统的核心。它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。“内核”指的是一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。一个内核不是一套完整的操作系统。一套基于Linux内核的完整操作系统叫作Linux操作系统,或是GNU/Linux。
内核是操作系统最基本的部分。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问是有限的,并且内核决定一个程序在什么时候对某部分硬件操作多长时间。直接对硬件操作是非常复杂的,所以内核通常提供一种硬件抽象的方法来完成这些操作。硬件抽象隐藏了复杂性,为应用软件和硬件提供了一套简洁,统一的接口,使程序设计更为简单。
严格地说,内核并不是计算机系统中必要的组成部分。程序可以直接地被调入计算机中执行,这样的设计说明了设计者不希望提供任何硬件抽象和操作系统的支持,它常见于早期计算机系统的设计中。最终,一些辅助性程序,例如程序加载器和调试器,被设计到机器核心当中,或者固化在只读存储器里。这些变化发生时,操作系统内核的概念就渐渐明晰起来了。
BootLoader,系统引导文件,在根目录下。
引导装载程序实际上是引出更高级的功能,以允许用户装载一个特定的操作系统。所以有多种引导装载程序,grub只是其中一种。这里强调指出,grub只是mbr的升级版,补充完成mbr所做不了的事情,其实他们的本质都一样,都是引导程序。
Linux系统启动过程
1、主机开机后,就是硬件检测(POST),通过后再根据BIOS里面设置的启动顺序找到启动驱动器(如硬盘,光驱等)。
2、读取硬盘MBR,启动系统引导程序(如grub、lilo),再由系统引导程序加载Linux的核心(kernel)
系统引导程序引导并运行核心可以分两个阶段:
一阶段:即BIOS从MBR中读入IPL(initial program loaderI),就是启动系统引导程序如grub。
二阶段:加载boot loader的所有配置文件和相关的环境参数;由于MBR只有512字节,所以系统引导工具还要从其它地方读入数据( /boot目录下的文件)。
注:MBR(Master Boot Record 512 字节,0头0道1扇区),前446字节存放的是 stage1,后面存放硬盘分区表信息。
3、Kernel会立即初始化系统中各种设备并做相关配置工作,其中包括CPU、I/O、存储设备等,也就是加载驱动程序啦。
4、驱动加载后,会创建一个根设备,然后将根文件系统 / 以只读的方式挂载,结束后,执行 switchroot,转到真正的根 / 上面去,同时运行 /sbin/init 程序,运行linux系统的第一号进程(init进程,也就是所有进程的父进程,PID为1)。
5、读取 /etc/inittab 配置文件。
6、执行系统初始化脚本 (/etc/rc.d/rc.sysinit )对系统进行基本配置,以读写方式挂载根文件系统及其其它文件系统;
主要工作有:取得网络环境与主机类型(/etc/sysconfig/network)、挂载 /proc 及 /sys、配置selinux、系统时钟、内核参数(/etc/sysctl.conf)、加载用户自定义的模块( /etc/sysconfig/modules/*.modules)、hostname、使能swap分区、根文件系统的检查和二次挂载(读写)、激活RAID和LVM设备、使能磁盘quota、检查并挂载其它文件系统等等。
根据运行级别启动相应服务,具体的每个运行级别的服务状态是放在 /etc/rc.d/rcX.d (X= 0 ~ 6)目录下,所有的文件均链接到 /etc/init.d下的相应文件/etc/rc.d/rcX.d/ 里面的文件介绍:(里面的文件都是链接文件,都是指向 /etc/init.d目录下)
以 S 为开头的文件,为开机时需要启动的服务;
以 K 为开头的文件,为关机时需要关闭的服务的档案连结;
在 S 与 K 后面接的数字,代表该档案被执行的顺序。
7、读取 /etc/rc.d/rc.local 文件,就是启动用户自定义的一些脚本程序,所以说如果你有什么东西需要在系统启动时启的的话就往这个文件里面写就可以了。
8、执行 /bin/login 程序,并等待用户登入
9、系统启动完成。
下图为上面描述,感谢原作者。

操作系统的启动流程总的来说可以分成三个阶段:加电、BIOS自检、OS启动,相关流程图如下:

第一步:上电
在 x86 系统中,将 1M 空间最上面的 0xF0000 到 0xFFFFF 这 64K 映射给 ROM。
当电脑刚加电的时候,会做一些重置的工作,将 CS 设置为 0xFFFF,将 IP 设置为 0x0000,所以第一条指令就会指向 0xFFFF0,正是在 ROM 的范围内。
在这里,有一个 JMP 命令会跳到 ROM 中做初始化工作的代码,于是,BIOS 开始进行初始化的工作

第二步:BIOS启动
固件初始化:计算机开机后,UEFI固件会进行初始化,包括硬件初始化、自检和加载UEFI固件驱动程序等。
启动设备选择:UEFI固件会检测并识别可启动的设备,如硬盘、光盘、USB设备等。它会根据预设的启动顺序或用户设置的启动选项,选择一个可启动的设备作为启动介质。
UEFI固件驱动程序加载:UEFI固件会加载设备上的UEFI固件驱动程序,这些驱动程序负责与硬件设备进行交互,以便后续的启动过程能够正常进行。
UEFI应用程序加载:UEFI固件会加载位于启动介质上的UEFI应用程序,如引导加载程序(Bootloader)或操作系统的引导管理器。这些应用程序通常位于EFI系统分区中,以.efi文件格式存在。
引导加载程序执行:加载的引导加载程序会接管控制权,负责加载操作系统内核或其他引导组件。常见的引导加载程序有UEFI Shell、GRUB、rEFInd等。

第三步:Linux启动

Linux启动-引导
可以通过BIOS界面选择硬盘启动项进入OS,那BIOS是怎么发现这个硬盘里有OS?
答案就是MBR(Master Boot Record),
MBR是放在硬盘的第一个扇区,一共512字节,
可以分成两部分:
主引导记录:安装启动引导程序的地方,446字节,
分区表:记录整个硬盘分区的的状态此外,64字节
Linux启动-引导EBR/VBR
找到MBR后下一步的操作:
(1)如果查找分区表时发现操作系统装在主分区,然后执行已载入的MBR中的boot loader代码,加载该激活主分区的VBR中的boot loader,至此控制权就交给了VBR的boot loader
(2)如果操作系统不是装在主分区,那么肯定是装在逻辑分区中,所以查找完主分区表后会继续查找扩展分区表,直到找到EBR所在的分区,然后MBR中的boot loader将控制权交给该EBR的boot loader

Linux启动-引导GRUB2介绍
GNU GRUB(GRand Unified Bootloader简称“GRUB”)是一个来自GNU项目的多操作系统启动程序。GRUB是多启动规范的实现,它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。GRUB可用于选择操作系统分区上的不同内核,也可用于向这些内核传递启动参数。
生成配置文件:grub2-mkconfig -o /boot/grub2/grub.cfg
安装:grub2-install /dev/sda
Linux启动-引导GRUB2加载
Grub2 第一个安装的就是 boot.img,BIOS 完成任务后,会将 boot.img 从硬盘加载到内存中的 0x7c00 来运行。boot.img会加载 core.img。如果从硬盘启动的话,这个扇区里面是 diskboot.img,diskboot.img 的任务就是将 core.img 的其他部分加载进来,先是解压缩程序 lzma_decompress.img,再往下是 kernel.img,最后是各个模块 module 对应的映像。这里需要注意,它不是 Linux 的内核,而是 grub 的内核。
随着加载的东西越来越大,实模式这 1M 的地址空间实在放不下了,所以在真正的解压缩之前,lzma_decompress.img 做了一个重要的决定,就是调用 real_to_prot,切换到保护模式,这样就能在更大的寻址空间里面,加载更多的东西。

Linux启动-0/1号进程
set_task_stack_end_magic(&init_task)。这里面有一个参数 init_task,它的定义是 struct task_struct init_task = INIT_TASK(init_task)。
它是系统创建的第一个进程,也被称为 0 号进程;这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程,是进程列表的第一个。
1.trap_init()中断初始化
2.mm_init()内存初始化
3.sched_init()调度策略初始化
4.vfs_caches_init()基于内存文件系统rootfs初始化
5.start_kernel()->rest_init()其他方面的初始化
rest_init 的第一大工作是,用 kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是 1 号进程。1 号进程对于操作系统来讲,有“划时代”的意义。
Linux启动-ramdisk
init 程序是在文件系统上的,文件系统一定是在一个存储设备上的,例如硬盘。Linux 访问存储设备,要有驱动才能访问。如果存储系统数目很有限,那驱动可以直接放到内核里面,但是文件系统的格式有很多,全都放进内核那内核就太大了。这该如何办呢?
只好先弄一个基于内存的文件系统。内存访问是不需要驱动的,这个就是 ramdisk。这个时候,ramdisk 是根文件系统。运行 ramdisk 上的 /init,等它运行完了就已经在用户态了。/init 这个程序会先根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了。有了真正的根文件系统,ramdisk 上的 /init 会启动文件系统上的 init。
Linux启动-init介绍
前面0/1进程都属于内核线程,ps pid=1的是init进程
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
...... }
......
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh")) return 0
它会尝试运行 ramdisk 的“/init”,或者普通文件系统上的“/sbin/init”“/etc/init”“/bin/init”“/bin/sh”。不同版本的 Linux 会选择不同的文件启动,但是只要有一个起来了就可以。
Init类型:
SysV:CentOS 5之前, 配置文件/etc/inittab
Upstart:CentOS 6,配置文件/etc/inittab,/etc/init/*.conf
Systemd:CnetOS7, 配置文件/usr/lib/system/syste,/etc/systemd/system
Linux启动-运行级别
Linux系统有7个运行级别(runlevel):
运行级别0:系统停机状态,系统默认运行级别不能设为0,否则不能正常启动
运行级别1:单用户工作状态,root权限,用于系统维护,禁止远程登录
运行级别2:多用户状态(没有NFS)
运行级别3:完全的多用户状态(有NFS),登陆后进入控制台命令行模式
运行级别4:系统未使用,保留
运行级别5:X11控制台,登陆后进入图形GUI模式
运行级别6:系统正常关闭并重启,默认运行级别不能设为6,否则不能正常启动
Linux启动-fstab
任何硬件设备连接后,操作系统使用硬件,即需要挂载。windows只不过是自动“挂载”了,linux需要手动自己搞。在Linux系统下,例如每次挂载/dev/sdb1(例如U盘设备文件)需要手动使用命令mount。当然,每次重启时,硬盘一般也是被自动挂载的,而自动挂载的信息,就记录在/etc/fstab文件中。系统每次启动都会读取/etc/fstab中的配置内容,自动挂载该文件中被记录的设备和分区。
第一列:设备文件或UUID或label(三者的区别看下面)
第二列:设备的挂载点(空目录)
第三列:该分区文件系统的格式(可以使用特殊的参数auto,自动识别分区的分区格式)
第四列:文件系统的参数,设置格式的选项
第五列:dump备份的设置(0表示不进行dump备份,1代表每天进行dump备份,2代表不定日期的进行dump备份)
第六列:磁盘检查设置(其实是一个检查顺序,0代表不检查,1代表第一个检查,2后续.一般根目录是1,数字相同则同时检查)
Linux启动-用户登录
一般来说用户登录方式有三种:
1.命令行登录
2.ssh登录
3.图形登录
Linux是多任务多用户的操作系统,它允许多人同时在线工作。但每个人都必须要输入用户名和密码并通过身份验证通过才能登录。但登录时是以图形界面的方式给用户使用,还是以纯命令行模式给用户使用呢?这由终端(类型)来决定的,也就是说在登录前需要先加载终端。现代Linux上,console终端已经和原始的意义不太一样了,其设备映射在/dev/console上,所有内核输出的信息都输出到console终端,而其他用户程序输出的信息则输出到虚拟终端或伪终端:
/dev/console:控制台终端
/dev/ttyN:虚拟终端,ctrl+alt+f[1-6]切换的就是虚拟终端
/dev/ttySN:串行终端
/dev/pts/N:伪终端,ssh等工具连接过去的活着图形终端下开启的命令行终端就是伪终端。
Linux启动-用户切换
Linux预设提供了六个命令窗口终端机来登录。默认登录的就是第一个窗口,也就是tty1,这个六个窗口分别为tty1,tty2 … tty6,你可以按下Ctrl + Alt + F1 ~ F6 来切换它们
通常Ctrl + Alt + F1是图形终端,Ctrl + Alt + F2~F6命令行终端。
Linux启动流程思维导图(图源于C语言编程的公众号,感谢原作者)

Linux Reboot过程简介
在Linux系统中,reboot命令是一个用于重启计算机的命令行工具。这个命令对于系统管理员来说非常实用,因为它允许用户无需通过图形界面直接在终端下执行重启操作。下面是对reboot命令的一些基本理解和使用说明:
基本用法
最简单的使用方式就是直接输入reboot,然后回车。执行这个命令后,系统会开始一个重启过程,类似于用户通过图形界面选择“重启”一样。
选项与参数
usage: reboot [-n] [-w] [-d] [-f] [-h] [-i]
-n: don't sync before halting the system
-w: only write a wtmp reboot record and exit.
-d: don't write a wtmp record.
-f: force halt/reboot, don't call shutdown.
-h: put harddisks in standby mode.
-i: shut down all network interfaces.
虽然reboot命令本身在不带任何选项或参数时就可以工作,但它也支持一些额外的选项来执行特定类型的重启操作或者获取更多信息。以下是一些常用的选项示例:
-h 或 --help:显示帮助信息,列出所有可用的选项和它们的作用。
-f 或 --force:强制立即重启,不进行正常的关机流程,这可能会导致未保存的数据丢失。
-p 或 --poweroff:在重启之后关闭电源(如果系统支持)。
-w 或 --wtmp-only:不实际重启系统,但会在wtmp文件中记录一个重启事件,这通常用于测试目的。
特殊情况
权限要求:由于重启系统是一个敏感操作,通常只有具有超级用户权限(root用户或使用sudo)的用户才能执行reboot命令。
计划重启:虽然基础的reboot命令不直接支持计划重启,但可以通过与其他命令结合,如使用at或cron作业安排在未来某个时间执行reboot命令。
系统状态:在某些情况下,如果系统处于严重错误状态,reboot命令可能无法正常工作,此时可能需要硬重启(例如,长按电源按钮)。
支持的reboot方式
kernel支持的reboot方式定义include/uapi/linux/reboot.h中,主要用于定义与系统重启相关的魔术值(Magic Values)和命令代码(Commands),这些常量被用来执行不同类型的重启或关机操作。下面是详细的解析:
Magic Values
LINUX_REBOOT_MAGIC1 至 LINUX_REBOOT_MAGIC2C:这些定义了一系列魔术值,用于系统调用reboot()时的身份验证。应用程序在调用reboot系统调用时,必须提供正确的魔术值组合,以证明其是有意执行重启操作,而非因错误或恶意攻击引起的。
重启命令
LINUX_REBOOT_CMD_RESTART 至 LINUX_REBOOT_CMD_KEXEC:定义了不同的重启命令码,用于指定重启操作的类型,包括:
RESTART:使用默认命令和模式重启系统。
HALT:停止操作系统,并将控制权交给ROM监控程序(如果存在)。
CAD_ON:启用Ctrl+Alt+Del组合键来执行重启命令。
CAD_OFF:Ctrl+Alt+Del组合键发送SIGINT信号给init进程。
POWER_OFF:停止操作系统,并尽可能从系统中移除所有电源。
RESTART2:使用给定的命令字符串重启系统。
SW_SUSPEND:如果支持,使用软件挂起功能暂停系统。
KEXEC:使用之前加载的Linux内核重启系统。
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _UAPI_LINUX_REBOOT_H
#define _UAPI_LINUX_REBOOT_H
/*
* Magic values required to use _reboot() system call.
*/
#define LINUX_REBOOT_MAGIC1 0xfee1dead
#define LINUX_REBOOT_MAGIC2 672274793
#define LINUX_REBOOT_MAGIC2A 85072278
#define LINUX_REBOOT_MAGIC2B 369367448
#define LINUX_REBOOT_MAGIC2C 537993216
/*
* Commands accepted by the _reboot() system call.
*
* RESTART Restart system using default command and mode.
* HALT Stop OS and give system control to ROM monitor, if any.
* CAD_ON Ctrl-Alt-Del sequence causes RESTART command.
* CAD_OFF Ctrl-Alt-Del sequence sends SIGINT to init task.
* POWER_OFF Stop OS and remove all power from system, if possible.
* RESTART2 Restart system using given command string.
* SW_SUSPEND Suspend system using software suspend if compiled in.
* KEXEC Restart system using a previously loaded Linux kernel
*/
#define LINUX_REBOOT_CMD_RESTART 0x01234567
#define LINUX_REBOOT_CMD_HALT 0xCDEF0123
#define LINUX_REBOOT_CMD_CAD_ON 0x89ABCDEF
#define LINUX_REBOOT_CMD_CAD_OFF 0x00000000
#define LINUX_REBOOT_CMD_POWER_OFF 0x4321FEDC
#define LINUX_REBOOT_CMD_RESTART2 0xA1B2C3D4
#define LINUX_REBOOT_CMD_SW_SUSPEND 0xD000FCE2
#define LINUX_REBOOT_CMD_KEXEC 0x45584543
#endif /* _UAPI_LINUX_REBOOT_H */
reboot调用流程
在Linux系统中,当执行reboot命令时,它最终会触发一个系统调用(system call)来请求内核重新启动系统。这一过程涉及用户空间到内核空间的转换,以及内核内部的一系列处理。

用户空间到内核空间
用户在终端输入reboot命令后,shell通过库函数(如glibc)调用reboot()函数。
这个库函数通过系统调用接口(如SWI指令在较旧的ARM体系结构或svc指令在ARMv7及以后版本)向内核发出请求,携带必要的参数。
系统调用sys_reboot
asmlinkage long sys_reboot(int magic1, int magic2, unsigned int cmd, void __user *arg);
static __attribute__((unused))
ssize_t sys_reboot(int magic1, int magic2, int cmd, void *arg) {
return my_syscall4(__NR_reboot, magic1, magic2, cmd, arg);
}
reboot系统调用核心函数
这段代码是Linux内核中处理reboot系统调用的核心实现部分,位于内核源码的系统调用处理模块中。
开头的注释说明了reboot系统调用的限制条件和安全措施,强调了只有具有CAP_SYS_BOOT能力的用户(通常是root用户)才能调用此功能,并且调用时需要提供特定的幻数以避免误操作导致的意外重启。
/*
* Reboot system call: for obvious reasons only root may call it,
* and even root needs to set up some magic numbers in the registers
* so that some mistake won't make this reboot the whole machine.
* You can also set the meaning of the ctrl-alt-del-key here.
*
* reboot doesn't sync: do that yourself before calling this.
*/
SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd, void __user *, arg) {
struct pid_namespace *pid_ns = task_active_pid_ns(current);
char buffer[256];
int ret = 0;
/* We only trust the superuser with rebooting the system. */
if (!ns_capable(pid_ns->user_ns, CAP_SYS_BOOT))
return -EPERM;
/* For safety, we require "magic" arguments. */
if (magic1 != LINUX_REBOOT_MAGIC1 ||
(magic2 != LINUX_REBOOT_MAGIC2 &&
magic2 != LINUX_REBOOT_MAGIC2A &&
magic2 != LINUX_REBOOT_MAGIC2B &&
magic2 != LINUX_REBOOT_MAGIC2C))
return -EINVAL;
/*
* If pid namespaces are enabled and the current task is in a child
* pid_namespace, the command is handled by reboot_pid_ns() which will
* call do_exit().
*/
ret = reboot_pid_ns(pid_ns, cmd);
if (ret)
return ret;
/* Instead of trying to make the power_off code look like
* halt when pm_power_off is not set do it the easy way.
*/
if ((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !pm_power_off)
cmd = LINUX_REBOOT_CMD_HALT;
mutex_lock(&system_transition_mutex);
switch (cmd) {
case LINUX_REBOOT_CMD_RESTART:
kernel_restart(NULL);
break;
case LINUX_REBOOT_CMD_CAD_ON:
C_A_D = 1;
break;
case LINUX_REBOOT_CMD_CAD_OFF:
C_A_D = 0;
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;
case LINUX_REBOOT_CMD_RESTART2:
ret = strncpy_from_user(&buffer[0], arg, sizeof(buffer) - 1);
if (ret < 0) {
ret = -EFAULT;
break;
}
buffer[sizeof(buffer) - 1] = '\0';
kernel_restart(buffer);
break;
#ifdef CONFIG_KEXEC_CORE
case LINUX_REBOOT_CMD_KEXEC:
ret = kernel_kexec();
break;
#endif
#ifdef CONFIG_HIBERNATION
case LINUX_REBOOT_CMD_SW_SUSPEND:
ret = hibernate();
break;
#endif
default:
ret = -EINVAL;
break;
}
mutex_unlock(&system_transition_mutex);
return ret;
}
1.函数签名:SYSCALL_DEFINE4(reboot, ...) 定义了系统调用的接口,接受四个参数:两个魔法数 (magic1, magic2),一个命令码 (cmd),以及一个用户空间缓冲区指针 (arg)。
2.权限检查:首先,代码检查当前进程所在的命名空间是否具有CAP_SYS_BOOT权限,如果没有,则返回-EPERM(权限错误)。
3.魔法数验证:然后,代码验证提供的魔法数是否正确,确保调用者确实是按照预期的方式调用此功能,否则返回-EINVAL(无效参数)。
4.PID命名空间处理:如果系统启用了PID命名空间并且当前任务在一个子命名空间中,函数会调用reboot_pid_ns()来处理重启命令,可能直接导致调用do_exit()退出进程。
5.命令处理:接下来,根据传入的cmd参数,执行不同的重启操作。如果请求的是断电关机且系统未配置pm_power_off,则将其转换为普通的关机命令。
6.同步与执行:使用互斥锁system_transition_mutex来保护系统重启过程中的并发访问。根据cmd的值,调用不同的内核函数来执行重启、设置Ctrl+Alt+Del行为、关机、断电、使用特定命令行重启(RESTART2)、执行kexec加载新内核、进入休眠状态等操作。如果命令不被识别,则返回-EINVAL。
7.错误处理与解锁:在每种操作完成后,释放互斥锁,并根据操作结果返回相应的错误码或成功标志。
kernel restart
/**
* kernel_restart - reboot the system
* @cmd: pointer to buffer containing command to execute for restart of %NULL
* Shutdown everything and perform a clean reboot
* This is not safe to call in interrupt context
**/
void kernel_restart( char *cmd) {
kernel_restart_prepare(cmd);
migrate_to_reboot_cpu();
syscore_shutdown();
if(!cmd)
pr_emerg("Restarting systrem\n");
else
pr_emerg("Restarting system with command '%s'\n", cmd);
kmsg_dump(KMSG_DUMP_RESTART);
machine_restart(cmd);
}
EXPORT_SYMBOL_GPL(kernel_restart);
kernel_restart函数的功能是重启系统。它接受一个参数cmd,该参数是指向字符串的指针,用于在重启后执行特定命令,如果不需要执行特定命令,则该参数为NULL。此外,注释还指出该函数会关闭所有服务并执行干净重启,且不应该在中断上下文中被调用,因为那可能会引发不稳定情况。
1.准备重启:调用kernel_restart_prepare函数,进行重启前的准备工作,这可能包括但不限于停止系统服务、关闭驱动程序、清理资源等,如果cmd不为NULL,也可能包括设置重启后的命令执行环境。
2.migrate_to_reboot_cpu迁移至重启CPU:在多处理器系统中,这个函数会将当前进程迁移到一个特定的CPU上,这个CPU通常是用于执行重启操作的CPU,这样做可以避免在重启过程中干扰到其他CPU上的任务。
3.系统核心关闭:调用syscore_shutdown来关闭系统核心服务,确保所有核心组件都已正确关闭,以便于进行干净的重启。
4.日志输出:根据cmd是否为空,打印紧急消息到系统日志,表明系统正在重启,并且如果提供了重启命令,则会显示该命令。
5.内核消息转储:调用kmsg_dump函数,参数KMSG_DUMP_RESTART指示在重启前将内核消息缓冲区的内容转储(可能到控制台或日志文件),这对于故障诊断非常有用。
6.硬件重启:最后,调用machine_restart函数,根据提供的cmd参数(如果有)执行特定的硬件重启操作。这个函数是体系结构相关的,会根据当前系统的硬件平台调用相应的重启代码。
7.导出符号:使用EXPORT_SYMBOL_GPL宏将kernel_restart函数导出为内核模块或其它内核代码可访问的符号。标记为GPL意味着任何使用该符号的模块或代码也必须遵循GPL许可协议。
基于ARM的machine restart
该函数定义在arch/arm/kernel/reboot.c中。
/*
* Restart requires that the secondary CPUs stop performing any activity
* while the primary CPU resets the system. Systems with a single CPU can
* use soft_restart() as their machine descriptor's .restart hook, since that
* will cause the only available CPU to reset. Systems with multiple CPUs must
* provide a HW restart implementation, to ensure that all CPUs reset at once.
* This is required so that any code running after reset on the primary CPU
* doesn't have to co-ordinate with other CPUs to ensure they aren't still
* executing pre-reset code, and using RAM that the primary CPU's code wishes
* to use. Implementing such co-ordination would be essentially impossible.
*/
void machine_restart(char *cmd) {
local_irq_disable();
smp_send_stop();
if (arm_pm_restart)
arm_pm_restart(reboot_mode, cmd);
else
do_kernel_restart(cmd);
/* Give a grace period for failure to restart of 1s */
mdelay(1000);
/* Whoops - the platform was unable to reboot. Tell the user! */
printk("Reboot failed -- System halted\n");
while (1);
}
这段代码是ARM架构下用于执行系统重启操作的核心函数machine_restart的实现。以下是对其功能和步骤的详细解析:
该函数的主要目的是确保系统能够安全重启,特别是对于多处理器(SMP,Symmetric Multi-Processing)系统而言,需要确保所有CPU在重启过程中协同工作,避免重启期间的不一致状态。
函数流程:
1.禁用中断(local_irq_disable):
调用local_irq_disable来禁用当前CPU的中断,防止在重启过程中被中断打断,保证重启操作的原子性和完整性。
2.通知其他CPU停止(smp_send_stop):
对于多CPU系统,调用smp_send_stop来通知所有其他CPU停止当前活动。这是重启协调的关键,确保在主CPU执行重启操作时,其他CPU不再执行任何任务,避免内存访问冲突等问题。
3.执行硬件重启或内核重启:
判断arm_pm_restart函数指针是否有效。如果有平台特定的硬件重启实现(arm_pm_restart),则调用它,并传入重启模式和命令行参数。这通常涉及到直接与硬件交互,执行硬件级别的复位操作。
如果没有平台特定的硬件重启函数,则调用do_kernel_restart(cmd),这通常是基于软件的重启实现,可能包括跳转到特定的重启地址或执行特定的复位指令。
4.等待重启成功或超时处理:
通过mdelay(1000)函数等待1秒,给予系统时间完成重启操作。这是给重启过程的一个宽容期,希望在这段时间内系统能够成功重启。
5.重启失败处理:
如果等待后系统仍未重启,函数通过printk打印错误信息“Reboot failed -- System halted”,表明重启尝试失败,并通过while (1);进入一个无限循环,实质上是让系统停留在一个已知的挂起状态,不再继续执行任何有意义的操作。
小结
在Linux系统中,reboot、halt和poweroff命令都是用于控制系统关机或重启操作的重要工具,它们通过与内核的交互来完成各自的任务,满足了不同场景下的系统管理需求。
BIOS是英文"Basic Input Output System"的缩略语,直译过来后中文名称就是"基本输入输出系统"。其实它是一组固化到计算机内主板上一个ROM芯片上的程序,它保存着计算机最重要的基本输入输出的程序、系统设置信息、开机后自检程序和系统自启动程序。 其主要功能是为计算机提供最底层的、最直接的硬件设置和控制。
因为BIOS很小,功能有限,为了完成加载操作系统的功能,就产生了mbr;bios检测到一个硬盘后,将硬盘的0柱面、0磁头、1扇区的内容经过简单判断后,至内存中的指定位置,然后跳转至这个位置,开始从这个位置运行;MBR全称为Master Boot Record,即硬盘的主引导记录。为了便于理解,一般将MBR分为广义和狭义两种:广义的MBR包含整个扇区(引导程序、分区表及分隔标识),也就是上面所说的主引导记录;而狭义的MBR仅指引导程序而言。硬盘的0柱面、0磁头、1扇区称为主引导扇区(也叫主引导记录MBR)。bios可设置系统启动的方式,比如可以设置从硬盘,光驱或U盘启动。
MBR是硬盘的主引导记录,属于引导区。
内核(kernel),是一个操作系统的核心。它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。“内核”指的是一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。一个内核不是一套完整的操作系统。一套基于Linux内核的完整操作系统叫作Linux操作系统,或是GNU/Linux。
内核是操作系统最基本的部分。它是为众多应用程序提供对计算机硬件的安全访问的一部分软件,这种访问是有限的,并且内核决定一个程序在什么时候对某部分硬件操作多长时间。直接对硬件操作是非常复杂的,所以内核通常提供一种硬件抽象的方法来完成这些操作。硬件抽象隐藏了复杂性,为应用软件和硬件提供了一套简洁,统一的接口,使程序设计更为简单。
严格地说,内核并不是计算机系统中必要的组成部分。程序可以直接地被调入计算机中执行,这样的设计说明了设计者不希望提供任何硬件抽象和操作系统的支持,它常见于早期计算机系统的设计中。最终,一些辅助性程序,例如程序加载器和调试器,被设计到机器核心当中,或者固化在只读存储器里。这些变化发生时,操作系统内核的概念就渐渐明晰起来了。
BootLoader,系统引导文件,在根目录下。
引导装载程序实际上是引出更高级的功能,以允许用户装载一个特定的操作系统。所以有多种引导装载程序,grub只是其中一种。这里强调指出,grub只是mbr的升级版,补充完成mbr所做不了的事情,其实他们的本质都一样,都是引导程序。
Linux系统启动过程
1、主机开机后,就是硬件检测(POST),通过后再根据BIOS里面设置的启动顺序找到启动驱动器(如硬盘,光驱等)。
2、读取硬盘MBR,启动系统引导程序(如grub、lilo),再由系统引导程序加载Linux的核心(kernel)
系统引导程序引导并运行核心可以分两个阶段:
一阶段:即BIOS从MBR中读入IPL(initial program loaderI),就是启动系统引导程序如grub。
二阶段:加载boot loader的所有配置文件和相关的环境参数;由于MBR只有512字节,所以系统引导工具还要从其它地方读入数据( /boot目录下的文件)。
注:MBR(Master Boot Record 512 字节,0头0道1扇区),前446字节存放的是 stage1,后面存放硬盘分区表信息。
3、Kernel会立即初始化系统中各种设备并做相关配置工作,其中包括CPU、I/O、存储设备等,也就是加载驱动程序啦。
4、驱动加载后,会创建一个根设备,然后将根文件系统 / 以只读的方式挂载,结束后,执行 switchroot,转到真正的根 / 上面去,同时运行 /sbin/init 程序,运行linux系统的第一号进程(init进程,也就是所有进程的父进程,PID为1)。
5、读取 /etc/inittab 配置文件。
6、执行系统初始化脚本 (/etc/rc.d/rc.sysinit )对系统进行基本配置,以读写方式挂载根文件系统及其其它文件系统;
主要工作有:取得网络环境与主机类型(/etc/sysconfig/network)、挂载 /proc 及 /sys、配置selinux、系统时钟、内核参数(/etc/sysctl.conf)、加载用户自定义的模块( /etc/sysconfig/modules/*.modules)、hostname、使能swap分区、根文件系统的检查和二次挂载(读写)、激活RAID和LVM设备、使能磁盘quota、检查并挂载其它文件系统等等。
根据运行级别启动相应服务,具体的每个运行级别的服务状态是放在 /etc/rc.d/rcX.d (X= 0 ~ 6)目录下,所有的文件均链接到 /etc/init.d下的相应文件/etc/rc.d/rcX.d/ 里面的文件介绍:(里面的文件都是链接文件,都是指向 /etc/init.d目录下)
以 S 为开头的文件,为开机时需要启动的服务;
以 K 为开头的文件,为关机时需要关闭的服务的档案连结;
在 S 与 K 后面接的数字,代表该档案被执行的顺序。
7、读取 /etc/rc.d/rc.local 文件,就是启动用户自定义的一些脚本程序,所以说如果你有什么东西需要在系统启动时启的的话就往这个文件里面写就可以了。
8、执行 /bin/login 程序,并等待用户登入
9、系统启动完成。
下图为上面描述,感谢原作者。

操作系统的启动流程总的来说可以分成三个阶段:加电、BIOS自检、OS启动,相关流程图如下:

第一步:上电
在 x86 系统中,将 1M 空间最上面的 0xF0000 到 0xFFFFF 这 64K 映射给 ROM。
当电脑刚加电的时候,会做一些重置的工作,将 CS 设置为 0xFFFF,将 IP 设置为 0x0000,所以第一条指令就会指向 0xFFFF0,正是在 ROM 的范围内。
在这里,有一个 JMP 命令会跳到 ROM 中做初始化工作的代码,于是,BIOS 开始进行初始化的工作

第二步:BIOS启动
固件初始化:计算机开机后,UEFI固件会进行初始化,包括硬件初始化、自检和加载UEFI固件驱动程序等。
启动设备选择:UEFI固件会检测并识别可启动的设备,如硬盘、光盘、USB设备等。它会根据预设的启动顺序或用户设置的启动选项,选择一个可启动的设备作为启动介质。
UEFI固件驱动程序加载:UEFI固件会加载设备上的UEFI固件驱动程序,这些驱动程序负责与硬件设备进行交互,以便后续的启动过程能够正常进行。
UEFI应用程序加载:UEFI固件会加载位于启动介质上的UEFI应用程序,如引导加载程序(Bootloader)或操作系统的引导管理器。这些应用程序通常位于EFI系统分区中,以.efi文件格式存在。
引导加载程序执行:加载的引导加载程序会接管控制权,负责加载操作系统内核或其他引导组件。常见的引导加载程序有UEFI Shell、GRUB、rEFInd等。

第三步:Linux启动

Linux启动-引导
可以通过BIOS界面选择硬盘启动项进入OS,那BIOS是怎么发现这个硬盘里有OS?
答案就是MBR(Master Boot Record),
MBR是放在硬盘的第一个扇区,一共512字节,
可以分成两部分:
主引导记录:安装启动引导程序的地方,446字节,
分区表:记录整个硬盘分区的的状态此外,64字节
Linux启动-引导EBR/VBR
找到MBR后下一步的操作:
(1)如果查找分区表时发现操作系统装在主分区,然后执行已载入的MBR中的boot loader代码,加载该激活主分区的VBR中的boot loader,至此控制权就交给了VBR的boot loader
(2)如果操作系统不是装在主分区,那么肯定是装在逻辑分区中,所以查找完主分区表后会继续查找扩展分区表,直到找到EBR所在的分区,然后MBR中的boot loader将控制权交给该EBR的boot loader

Linux启动-引导GRUB2介绍
GNU GRUB(GRand Unified Bootloader简称“GRUB”)是一个来自GNU项目的多操作系统启动程序。GRUB是多启动规范的实现,它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。GRUB可用于选择操作系统分区上的不同内核,也可用于向这些内核传递启动参数。
生成配置文件:grub2-mkconfig -o /boot/grub2/grub.cfg
安装:grub2-install /dev/sda
Linux启动-引导GRUB2加载
Grub2 第一个安装的就是 boot.img,BIOS 完成任务后,会将 boot.img 从硬盘加载到内存中的 0x7c00 来运行。boot.img会加载 core.img。如果从硬盘启动的话,这个扇区里面是 diskboot.img,diskboot.img 的任务就是将 core.img 的其他部分加载进来,先是解压缩程序 lzma_decompress.img,再往下是 kernel.img,最后是各个模块 module 对应的映像。这里需要注意,它不是 Linux 的内核,而是 grub 的内核。
随着加载的东西越来越大,实模式这 1M 的地址空间实在放不下了,所以在真正的解压缩之前,lzma_decompress.img 做了一个重要的决定,就是调用 real_to_prot,切换到保护模式,这样就能在更大的寻址空间里面,加载更多的东西。

Linux启动-0/1号进程
set_task_stack_end_magic(&init_task)。这里面有一个参数 init_task,它的定义是 struct task_struct init_task = INIT_TASK(init_task)。
它是系统创建的第一个进程,也被称为 0 号进程;这是唯一一个没有通过 fork 或者 kernel_thread 产生的进程,是进程列表的第一个。
1.trap_init()中断初始化
2.mm_init()内存初始化
3.sched_init()调度策略初始化
4.vfs_caches_init()基于内存文件系统rootfs初始化
5.start_kernel()->rest_init()其他方面的初始化
rest_init 的第一大工作是,用 kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是 1 号进程。1 号进程对于操作系统来讲,有“划时代”的意义。
Linux启动-ramdisk
init 程序是在文件系统上的,文件系统一定是在一个存储设备上的,例如硬盘。Linux 访问存储设备,要有驱动才能访问。如果存储系统数目很有限,那驱动可以直接放到内核里面,但是文件系统的格式有很多,全都放进内核那内核就太大了。这该如何办呢?
只好先弄一个基于内存的文件系统。内存访问是不需要驱动的,这个就是 ramdisk。这个时候,ramdisk 是根文件系统。运行 ramdisk 上的 /init,等它运行完了就已经在用户态了。/init 这个程序会先根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了。有了真正的根文件系统,ramdisk 上的 /init 会启动文件系统上的 init。
Linux启动-init介绍
前面0/1进程都属于内核线程,ps pid=1的是init进程
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
...... }
......
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh")) return 0
它会尝试运行 ramdisk 的“/init”,或者普通文件系统上的“/sbin/init”“/etc/init”“/bin/init”“/bin/sh”。不同版本的 Linux 会选择不同的文件启动,但是只要有一个起来了就可以。
Init类型:
SysV:CentOS 5之前, 配置文件/etc/inittab
Upstart:CentOS 6,配置文件/etc/inittab,/etc/init/*.conf
Systemd:CnetOS7, 配置文件/usr/lib/system/syste,/etc/systemd/system
Linux启动-运行级别
Linux系统有7个运行级别(runlevel):
运行级别0:系统停机状态,系统默认运行级别不能设为0,否则不能正常启动
运行级别1:单用户工作状态,root权限,用于系统维护,禁止远程登录
运行级别2:多用户状态(没有NFS)
运行级别3:完全的多用户状态(有NFS),登陆后进入控制台命令行模式
运行级别4:系统未使用,保留
运行级别5:X11控制台,登陆后进入图形GUI模式
运行级别6:系统正常关闭并重启,默认运行级别不能设为6,否则不能正常启动
Linux启动-fstab
任何硬件设备连接后,操作系统使用硬件,即需要挂载。windows只不过是自动“挂载”了,linux需要手动自己搞。在Linux系统下,例如每次挂载/dev/sdb1(例如U盘设备文件)需要手动使用命令mount。当然,每次重启时,硬盘一般也是被自动挂载的,而自动挂载的信息,就记录在/etc/fstab文件中。系统每次启动都会读取/etc/fstab中的配置内容,自动挂载该文件中被记录的设备和分区。
第一列:设备文件或UUID或label(三者的区别看下面)
第二列:设备的挂载点(空目录)
第三列:该分区文件系统的格式(可以使用特殊的参数auto,自动识别分区的分区格式)
第四列:文件系统的参数,设置格式的选项
第五列:dump备份的设置(0表示不进行dump备份,1代表每天进行dump备份,2代表不定日期的进行dump备份)
第六列:磁盘检查设置(其实是一个检查顺序,0代表不检查,1代表第一个检查,2后续.一般根目录是1,数字相同则同时检查)
Linux启动-用户登录
一般来说用户登录方式有三种:
1.命令行登录
2.ssh登录
3.图形登录
Linux是多任务多用户的操作系统,它允许多人同时在线工作。但每个人都必须要输入用户名和密码并通过身份验证通过才能登录。但登录时是以图形界面的方式给用户使用,还是以纯命令行模式给用户使用呢?这由终端(类型)来决定的,也就是说在登录前需要先加载终端。现代Linux上,console终端已经和原始的意义不太一样了,其设备映射在/dev/console上,所有内核输出的信息都输出到console终端,而其他用户程序输出的信息则输出到虚拟终端或伪终端:
/dev/console:控制台终端
/dev/ttyN:虚拟终端,ctrl+alt+f[1-6]切换的就是虚拟终端
/dev/ttySN:串行终端
/dev/pts/N:伪终端,ssh等工具连接过去的活着图形终端下开启的命令行终端就是伪终端。
Linux启动-用户切换
Linux预设提供了六个命令窗口终端机来登录。默认登录的就是第一个窗口,也就是tty1,这个六个窗口分别为tty1,tty2 … tty6,你可以按下Ctrl + Alt + F1 ~ F6 来切换它们
通常Ctrl + Alt + F1是图形终端,Ctrl + Alt + F2~F6命令行终端。
Linux启动流程思维导图(图源于C语言编程的公众号,感谢原作者)

Linux Reboot过程简介
在Linux系统中,reboot命令是一个用于重启计算机的命令行工具。这个命令对于系统管理员来说非常实用,因为它允许用户无需通过图形界面直接在终端下执行重启操作。下面是对reboot命令的一些基本理解和使用说明:
基本用法
最简单的使用方式就是直接输入reboot,然后回车。执行这个命令后,系统会开始一个重启过程,类似于用户通过图形界面选择“重启”一样。
选项与参数
usage: reboot [-n] [-w] [-d] [-f] [-h] [-i]
-n: don't sync before halting the system
-w: only write a wtmp reboot record and exit.
-d: don't write a wtmp record.
-f: force halt/reboot, don't call shutdown.
-h: put harddisks in standby mode.
-i: shut down all network interfaces.
虽然reboot命令本身在不带任何选项或参数时就可以工作,但它也支持一些额外的选项来执行特定类型的重启操作或者获取更多信息。以下是一些常用的选项示例:
-h 或 --help:显示帮助信息,列出所有可用的选项和它们的作用。
-f 或 --force:强制立即重启,不进行正常的关机流程,这可能会导致未保存的数据丢失。
-p 或 --poweroff:在重启之后关闭电源(如果系统支持)。
-w 或 --wtmp-only:不实际重启系统,但会在wtmp文件中记录一个重启事件,这通常用于测试目的。
特殊情况
权限要求:由于重启系统是一个敏感操作,通常只有具有超级用户权限(root用户或使用sudo)的用户才能执行reboot命令。
计划重启:虽然基础的reboot命令不直接支持计划重启,但可以通过与其他命令结合,如使用at或cron作业安排在未来某个时间执行reboot命令。
系统状态:在某些情况下,如果系统处于严重错误状态,reboot命令可能无法正常工作,此时可能需要硬重启(例如,长按电源按钮)。
支持的reboot方式
kernel支持的reboot方式定义include/uapi/linux/reboot.h中,主要用于定义与系统重启相关的魔术值(Magic Values)和命令代码(Commands),这些常量被用来执行不同类型的重启或关机操作。下面是详细的解析:
Magic Values
LINUX_REBOOT_MAGIC1 至 LINUX_REBOOT_MAGIC2C:这些定义了一系列魔术值,用于系统调用reboot()时的身份验证。应用程序在调用reboot系统调用时,必须提供正确的魔术值组合,以证明其是有意执行重启操作,而非因错误或恶意攻击引起的。
重启命令
LINUX_REBOOT_CMD_RESTART 至 LINUX_REBOOT_CMD_KEXEC:定义了不同的重启命令码,用于指定重启操作的类型,包括:
RESTART:使用默认命令和模式重启系统。
HALT:停止操作系统,并将控制权交给ROM监控程序(如果存在)。
CAD_ON:启用Ctrl+Alt+Del组合键来执行重启命令。
CAD_OFF:Ctrl+Alt+Del组合键发送SIGINT信号给init进程。
POWER_OFF:停止操作系统,并尽可能从系统中移除所有电源。
RESTART2:使用给定的命令字符串重启系统。
SW_SUSPEND:如果支持,使用软件挂起功能暂停系统。
KEXEC:使用之前加载的Linux内核重启系统。
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _UAPI_LINUX_REBOOT_H
#define _UAPI_LINUX_REBOOT_H
/*
* Magic values required to use _reboot() system call.
*/
#define LINUX_REBOOT_MAGIC1 0xfee1dead
#define LINUX_REBOOT_MAGIC2 672274793
#define LINUX_REBOOT_MAGIC2A 85072278
#define LINUX_REBOOT_MAGIC2B 369367448
#define LINUX_REBOOT_MAGIC2C 537993216
/*
* Commands accepted by the _reboot() system call.
*
* RESTART Restart system using default command and mode.
* HALT Stop OS and give system control to ROM monitor, if any.
* CAD_ON Ctrl-Alt-Del sequence causes RESTART command.
* CAD_OFF Ctrl-Alt-Del sequence sends SIGINT to init task.
* POWER_OFF Stop OS and remove all power from system, if possible.
* RESTART2 Restart system using given command string.
* SW_SUSPEND Suspend system using software suspend if compiled in.
* KEXEC Restart system using a previously loaded Linux kernel
*/
#define LINUX_REBOOT_CMD_RESTART 0x01234567
#define LINUX_REBOOT_CMD_HALT 0xCDEF0123
#define LINUX_REBOOT_CMD_CAD_ON 0x89ABCDEF
#define LINUX_REBOOT_CMD_CAD_OFF 0x00000000
#define LINUX_REBOOT_CMD_POWER_OFF 0x4321FEDC
#define LINUX_REBOOT_CMD_RESTART2 0xA1B2C3D4
#define LINUX_REBOOT_CMD_SW_SUSPEND 0xD000FCE2
#define LINUX_REBOOT_CMD_KEXEC 0x45584543
#endif /* _UAPI_LINUX_REBOOT_H */
reboot调用流程
在Linux系统中,当执行reboot命令时,它最终会触发一个系统调用(system call)来请求内核重新启动系统。这一过程涉及用户空间到内核空间的转换,以及内核内部的一系列处理。

用户空间到内核空间
用户在终端输入reboot命令后,shell通过库函数(如glibc)调用reboot()函数。
这个库函数通过系统调用接口(如SWI指令在较旧的ARM体系结构或svc指令在ARMv7及以后版本)向内核发出请求,携带必要的参数。
系统调用sys_reboot
asmlinkage long sys_reboot(int magic1, int magic2, unsigned int cmd, void __user *arg);
static __attribute__((unused))
ssize_t sys_reboot(int magic1, int magic2, int cmd, void *arg) {
return my_syscall4(__NR_reboot, magic1, magic2, cmd, arg);
}
reboot系统调用核心函数
这段代码是Linux内核中处理reboot系统调用的核心实现部分,位于内核源码的系统调用处理模块中。
开头的注释说明了reboot系统调用的限制条件和安全措施,强调了只有具有CAP_SYS_BOOT能力的用户(通常是root用户)才能调用此功能,并且调用时需要提供特定的幻数以避免误操作导致的意外重启。
/*
* Reboot system call: for obvious reasons only root may call it,
* and even root needs to set up some magic numbers in the registers
* so that some mistake won't make this reboot the whole machine.
* You can also set the meaning of the ctrl-alt-del-key here.
*
* reboot doesn't sync: do that yourself before calling this.
*/
SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd, void __user *, arg) {
struct pid_namespace *pid_ns = task_active_pid_ns(current);
char buffer[256];
int ret = 0;
/* We only trust the superuser with rebooting the system. */
if (!ns_capable(pid_ns->user_ns, CAP_SYS_BOOT))
return -EPERM;
/* For safety, we require "magic" arguments. */
if (magic1 != LINUX_REBOOT_MAGIC1 ||
(magic2 != LINUX_REBOOT_MAGIC2 &&
magic2 != LINUX_REBOOT_MAGIC2A &&
magic2 != LINUX_REBOOT_MAGIC2B &&
magic2 != LINUX_REBOOT_MAGIC2C))
return -EINVAL;
/*
* If pid namespaces are enabled and the current task is in a child
* pid_namespace, the command is handled by reboot_pid_ns() which will
* call do_exit().
*/
ret = reboot_pid_ns(pid_ns, cmd);
if (ret)
return ret;
/* Instead of trying to make the power_off code look like
* halt when pm_power_off is not set do it the easy way.
*/
if ((cmd == LINUX_REBOOT_CMD_POWER_OFF) && !pm_power_off)
cmd = LINUX_REBOOT_CMD_HALT;
mutex_lock(&system_transition_mutex);
switch (cmd) {
case LINUX_REBOOT_CMD_RESTART:
kernel_restart(NULL);
break;
case LINUX_REBOOT_CMD_CAD_ON:
C_A_D = 1;
break;
case LINUX_REBOOT_CMD_CAD_OFF:
C_A_D = 0;
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;
case LINUX_REBOOT_CMD_RESTART2:
ret = strncpy_from_user(&buffer[0], arg, sizeof(buffer) - 1);
if (ret < 0) {
ret = -EFAULT;
break;
}
buffer[sizeof(buffer) - 1] = '\0';
kernel_restart(buffer);
break;
#ifdef CONFIG_KEXEC_CORE
case LINUX_REBOOT_CMD_KEXEC:
ret = kernel_kexec();
break;
#endif
#ifdef CONFIG_HIBERNATION
case LINUX_REBOOT_CMD_SW_SUSPEND:
ret = hibernate();
break;
#endif
default:
ret = -EINVAL;
break;
}
mutex_unlock(&system_transition_mutex);
return ret;
}
1.函数签名:SYSCALL_DEFINE4(reboot, ...) 定义了系统调用的接口,接受四个参数:两个魔法数 (magic1, magic2),一个命令码 (cmd),以及一个用户空间缓冲区指针 (arg)。
2.权限检查:首先,代码检查当前进程所在的命名空间是否具有CAP_SYS_BOOT权限,如果没有,则返回-EPERM(权限错误)。
3.魔法数验证:然后,代码验证提供的魔法数是否正确,确保调用者确实是按照预期的方式调用此功能,否则返回-EINVAL(无效参数)。
4.PID命名空间处理:如果系统启用了PID命名空间并且当前任务在一个子命名空间中,函数会调用reboot_pid_ns()来处理重启命令,可能直接导致调用do_exit()退出进程。
5.命令处理:接下来,根据传入的cmd参数,执行不同的重启操作。如果请求的是断电关机且系统未配置pm_power_off,则将其转换为普通的关机命令。
6.同步与执行:使用互斥锁system_transition_mutex来保护系统重启过程中的并发访问。根据cmd的值,调用不同的内核函数来执行重启、设置Ctrl+Alt+Del行为、关机、断电、使用特定命令行重启(RESTART2)、执行kexec加载新内核、进入休眠状态等操作。如果命令不被识别,则返回-EINVAL。
7.错误处理与解锁:在每种操作完成后,释放互斥锁,并根据操作结果返回相应的错误码或成功标志。
kernel restart
/**
* kernel_restart - reboot the system
* @cmd: pointer to buffer containing command to execute for restart of %NULL
* Shutdown everything and perform a clean reboot
* This is not safe to call in interrupt context
**/
void kernel_restart( char *cmd) {
kernel_restart_prepare(cmd);
migrate_to_reboot_cpu();
syscore_shutdown();
if(!cmd)
pr_emerg("Restarting systrem\n");
else
pr_emerg("Restarting system with command '%s'\n", cmd);
kmsg_dump(KMSG_DUMP_RESTART);
machine_restart(cmd);
}
EXPORT_SYMBOL_GPL(kernel_restart);
kernel_restart函数的功能是重启系统。它接受一个参数cmd,该参数是指向字符串的指针,用于在重启后执行特定命令,如果不需要执行特定命令,则该参数为NULL。此外,注释还指出该函数会关闭所有服务并执行干净重启,且不应该在中断上下文中被调用,因为那可能会引发不稳定情况。
1.准备重启:调用kernel_restart_prepare函数,进行重启前的准备工作,这可能包括但不限于停止系统服务、关闭驱动程序、清理资源等,如果cmd不为NULL,也可能包括设置重启后的命令执行环境。
2.migrate_to_reboot_cpu迁移至重启CPU:在多处理器系统中,这个函数会将当前进程迁移到一个特定的CPU上,这个CPU通常是用于执行重启操作的CPU,这样做可以避免在重启过程中干扰到其他CPU上的任务。
3.系统核心关闭:调用syscore_shutdown来关闭系统核心服务,确保所有核心组件都已正确关闭,以便于进行干净的重启。
4.日志输出:根据cmd是否为空,打印紧急消息到系统日志,表明系统正在重启,并且如果提供了重启命令,则会显示该命令。
5.内核消息转储:调用kmsg_dump函数,参数KMSG_DUMP_RESTART指示在重启前将内核消息缓冲区的内容转储(可能到控制台或日志文件),这对于故障诊断非常有用。
6.硬件重启:最后,调用machine_restart函数,根据提供的cmd参数(如果有)执行特定的硬件重启操作。这个函数是体系结构相关的,会根据当前系统的硬件平台调用相应的重启代码。
7.导出符号:使用EXPORT_SYMBOL_GPL宏将kernel_restart函数导出为内核模块或其它内核代码可访问的符号。标记为GPL意味着任何使用该符号的模块或代码也必须遵循GPL许可协议。
基于ARM的machine restart
该函数定义在arch/arm/kernel/reboot.c中。
/*
* Restart requires that the secondary CPUs stop performing any activity
* while the primary CPU resets the system. Systems with a single CPU can
* use soft_restart() as their machine descriptor's .restart hook, since that
* will cause the only available CPU to reset. Systems with multiple CPUs must
* provide a HW restart implementation, to ensure that all CPUs reset at once.
* This is required so that any code running after reset on the primary CPU
* doesn't have to co-ordinate with other CPUs to ensure they aren't still
* executing pre-reset code, and using RAM that the primary CPU's code wishes
* to use. Implementing such co-ordination would be essentially impossible.
*/
void machine_restart(char *cmd) {
local_irq_disable();
smp_send_stop();
if (arm_pm_restart)
arm_pm_restart(reboot_mode, cmd);
else
do_kernel_restart(cmd);
/* Give a grace period for failure to restart of 1s */
mdelay(1000);
/* Whoops - the platform was unable to reboot. Tell the user! */
printk("Reboot failed -- System halted\n");
while (1);
}
这段代码是ARM架构下用于执行系统重启操作的核心函数machine_restart的实现。以下是对其功能和步骤的详细解析:
该函数的主要目的是确保系统能够安全重启,特别是对于多处理器(SMP,Symmetric Multi-Processing)系统而言,需要确保所有CPU在重启过程中协同工作,避免重启期间的不一致状态。
函数流程:
1.禁用中断(local_irq_disable):
调用local_irq_disable来禁用当前CPU的中断,防止在重启过程中被中断打断,保证重启操作的原子性和完整性。
2.通知其他CPU停止(smp_send_stop):
对于多CPU系统,调用smp_send_stop来通知所有其他CPU停止当前活动。这是重启协调的关键,确保在主CPU执行重启操作时,其他CPU不再执行任何任务,避免内存访问冲突等问题。
3.执行硬件重启或内核重启:
判断arm_pm_restart函数指针是否有效。如果有平台特定的硬件重启实现(arm_pm_restart),则调用它,并传入重启模式和命令行参数。这通常涉及到直接与硬件交互,执行硬件级别的复位操作。
如果没有平台特定的硬件重启函数,则调用do_kernel_restart(cmd),这通常是基于软件的重启实现,可能包括跳转到特定的重启地址或执行特定的复位指令。
4.等待重启成功或超时处理:
通过mdelay(1000)函数等待1秒,给予系统时间完成重启操作。这是给重启过程的一个宽容期,希望在这段时间内系统能够成功重启。
5.重启失败处理:
如果等待后系统仍未重启,函数通过printk打印错误信息“Reboot failed -- System halted”,表明重启尝试失败,并通过while (1);进入一个无限循环,实质上是让系统停留在一个已知的挂起状态,不再继续执行任何有意义的操作。
小结
在Linux系统中,reboot、halt和poweroff命令都是用于控制系统关机或重启操作的重要工具,它们通过与内核的交互来完成各自的任务,满足了不同场景下的系统管理需求。