本文讲述reboot命令在用户态的执行过程,halt,poweroff,shutdown与其类似。本文适用于使用systemd作为系统1号进程的场景。另补充一点,reboot,halt,poweroff,shutdown是传统Unix SysV init初始化系统提供的命令,目前的Linux发行版大多使用systemd作为init进程,为了保持对sysvinit的兼容,systemd也提供了reboot等命令,严格来说,systemd本身提供的重启命令是systemctl reboot。
当我们在命令行中敲了reboot
之后,发生了些什么
reboot
,halt
,poweroff
,shutdown
都是符号链接,这些链接都指向systemctl
。具体如下:
# cd /usr/sbin/
# ll {reboot,halt,poweroff,shutdown}
lrwxrwxrwx. 1 root root 16 Feb 4 08:00 halt -> ../bin/systemctl
lrwxrwxrwx. 1 root root 16 Feb 4 08:00 poweroff -> ../bin/systemctl
lrwxrwxrwx. 1 root root 16 Feb 4 08:00 reboot -> ../bin/systemctl
lrwxrwxrwx. 1 root root 16 Feb 4 08:00 shutdown -> ../bin/systemctl
这里就会有个问题,当我们输入reboot
,poweroff
,halt
,shutdown
命令的时候,都是执行的systemctl
,那么systemctl
怎么知道我们想执行的是reboot
,poweroff
,halt
,还是shutdown
呢?这就不得不说一个源于Unix
的机制了,当加载符号链接指向的可执行文件时,会把符号链接的名字作为该可执行文件的第一个参数传给进程,即 argv[0],进程可以通过判断 argv[0] 来判断是哪个符号链接。一般情况下,argv[0]
都是可执行文件的名字,如执行systemctl list-units
时,systemctl
的第一个参数argv[0]
就是systemctl
,但使用符号链接的时候,如reboot
,这时候systemctl
的argv[0]
就是reboot
。
所以,当在命令行里敲了reboot
后,系统(或者说shell
)会加载systemctl
,并把它的第一个参数——argv[0]
设置为reboot
。halt
,poweroff
,shutdown
同理。
systemctl
中的reboot
上面说到,执行reboot
会加载systemctl
,并把它的argv[0]
设置为reboot
。接下来讲讲reboot
在systemctl
里的具体执行过程。
systemd
包的src/systemctl/systemctl.c:1214行
使用宏定义了systemctl
的main
函数(源码都在systemd
包中,后面的篇幅省略systemd
)。具体如下:
DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
这个宏展开之后是这样的:
int main(int argc, char *argv[])
{
int r;
save_argc_argv(argc, argv);
r = run(argc, argv);
if (r < 0)
(void) sd_notifyf(0, "ERRNO=%i", -r);
ask_password_agent_close();
polkit_agent_close();
pager_close();
mac_selinux_finish();
static_destruct();
return r < 0 ? EXIT_FAILURE : r;
}
该宏定义了main
函数,main
函数先保存了一把argc
,argv
,然后将其作为参数,传给了run
函数(第5
行),很明显,run
函数实现了主要的逻辑。
run
函数如下:
static int run(int argc, char *argv[]) {
int r;
pid_t ppid;
char *filter[] = {
"status", "show", "cat",
"is-active", "is-failed", "is-enabled", "is-system-running",
"list-units", "list-sockets", "list-timers", "list-dependencies",
"list-unit-files", "list-machines", "list-jobs",
"get-default", "show-environment", NULL
};
setlocale(LC_ALL, "");
log_parse_environment();
log_open();
/* The journal merging logic potentially needs a lot of fds. */
(void) rlimit_nofile_bump(HIGH_RLIMIT_NOFILE);
sigbus_install();
r = systemctl_dispatch_parse_argv(argc, argv);
if (r <= 0)
goto finish;
ppid = getppid();
(void) print_process_cmdline_with_arg(ppid, argc, argv, filter);
if (arg_action != ACTION_SYSTEMCTL && running_in_chroot() > 0) {
if (!arg_quiet)
log_info("Running in chroot, ignoring request.");
r = 0;
goto finish;
}
/* systemctl_main() will print an error message for the bus connection, but only if it needs to */
switch (arg_action) {
case ACTION_SYSTEMCTL:
r = systemctl_main(argc, argv);
break;
/* Legacy command aliases set arg_action. They provide some fallbacks, e.g. to tell sysvinit to
* reboot after you have installed systemd binaries. */
case ACTION_HALT:
case ACTION_POWEROFF:
case ACTION_REBOOT:
case ACTION_KEXEC:
r = halt_main();
break;
case ACTION_RUNLEVEL2:
case ACTION_RUNLEVEL3:
case ACTION_RUNLEVEL4:
case ACTION_RUNLEVEL5:
case ACTION_RESCUE:
r = start_with_fallback();
break;
case ACTION_RELOAD:
case ACTION_REEXEC:
r = reload_with_fallback();
break;
case ACTION_CANCEL_SHUTDOWN:
r = logind_cancel_shutdown();
break;
case ACTION_RUNLEVEL:
r = runlevel_main();
break;
case ACTION_TELINIT:
r = exec_telinit(argv);
break;
case ACTION_EXIT:
case ACTION_SUSPEND:
case ACTION_HIBERNATE:
case ACTION_HYBRID_SLEEP:
case ACTION_SUSPEND_THEN_HIBERNATE:
case ACTION_EMERGENCY:
case ACTION_DEFAULT:
/* systemctl verbs with no equivalent in the legacy commands. These cannot appear in
* arg_action. Fall through. */
case _ACTION_INVALID:
default:
assert_not_reached("Unknown action");
}
finish:
release_busses();
/* Note that we return r here, not 0, so that we can implement the LSB-like return codes */
return r;
}
该函数也很简单,初始化了一堆进程的运行环境后,调了systemctl_dispatch_parse_argv()
,该函数处理了argc
和argv
。之后直接根据arg_action
的值,用switch case
进行处理,case
有ACTION_REBOOT
,ACTION_HALT
,ACTION_POWEROFF
等,systemctl
根据arg_action
值的不同,采取了不同的行为。arg_action
是一个全局变量,很明显,在前面有函数对其进行了处理,决定了systemctl
将要执行的动作。
systemctl_dispatch_parse_argv()
函数处理了argc
,argv
,也就是shell
传过来的命令行参数,arg_action
就是在这里被处理的。我们接下来分析systemctl_dispatch_parse_argv()
函数。该函数如下:
int systemctl_dispatch_parse_argv(int argc, char *argv[]) {
assert(argc >= 0);
assert(argv);
if (invoked_as(argv, "halt")) {
arg_action = ACTION_HALT;
return halt_parse_argv(argc, argv);
} else if (invoked_as(argv, "poweroff")) {
arg_action = ACTION_POWEROFF;
return halt_parse_argv(argc, argv);
} else if (invoked_as(argv, "reboot")) {
if (kexec_loaded())
arg_action = ACTION_KEXEC;
else
arg_action = ACTION_REBOOT;
return halt_parse_argv(argc, argv);
} else if (invoked_as(argv, "shutdown")) {
arg_action = ACTION_POWEROFF;
return shutdown_parse_argv(argc, argv);
} else if (invoked_as(argv, "init")) {
/* Matches invocations as "init" as well as "telinit", which are synonymous when run
* as PID != 1 on SysV.
*
* On SysV "telinit" was the official command to communicate with PID 1, but "init" would
* redirect itself to "telinit" if called with PID != 1. We follow the same logic here still,
* though we add one level of indirection, as we implement "telinit" in "systemctl". Hence,
* for us if you invoke "init" you get "systemd", but it will execve() "systemctl"
* immediately with argv[] unmodified if PID is != 1. If you invoke "telinit" you directly
* get "systemctl". In both cases we shall do the same thing, which is why we do
* invoked_as(argv, "init") here, as a quick way to match both.
*
* Also see redirect_telinit() in src/core/main.c. */
if (sd_booted() > 0) {
arg_action = _ACTION_INVALID;
return telinit_parse_argv(argc, argv);
} else {
/* Hmm, so some other init system is running, we need to forward this request to it.
*/
arg_action = ACTION_TELINIT;
return 1;
}
} else if (invoked_as(argv, "runlevel")) {
arg_action = ACTION_RUNLEVEL;
return runlevel_parse_argv(argc, argv);
}
arg_action = ACTION_SYSTEMCTL;
return systemctl_parse_argv(argc, argv);
}
该函数最主要的作用就是根据argv
来给arg_action
赋值。invoke_as()
函数的主要逻辑是取出argv[0]
参数中的基本文件名,判断其是否包含第二个实参子串。argv[0]
是可执行文件名,可能是绝对路径,也可能是相对路径,invoke_as()
会去除其路径,仅保留基本文件名,然后使用strstr()
函数,判断是否包含第二个形式参数子串。如invoked_as(argv, "reboot")
,invoke_as
函数就会取出argv[0]
的基本名,比如是reboot
,然后和第二个入参"reboot"
比较,看看argv[0]
的基本名中是否包含reboot
,如果包含,就返回true
,否则为false
。具体可查看其源码。
于是,在systemctl_dispatch_parse_argv()
函数中,我们在命令行敲的reboot
被识别,arg_action
被赋值成了ACTION_REBOOT
(systemctl_dispatch_parse_argv()::17行
),然后走到了halt_main()
函数(run()::48行
),在halt_main()
函数中,因为arg_dry_run
,arg_force
都是0
,所以来到了start_with_fallback()
函数(halt_main()::38行
)。在start_with_fallback()
中,会先尝试基于systemd
的关机方式,如果定义了HAVE_SYSV_COMPAT
宏,也会针对sysv
(传统Unix
Init
进程)进行一些处理。
halt_main()
和start_with_fallback()
函数如下:
int halt_main(void) {
int r;
r = logind_check_inhibitors(arg_action);
if (r < 0)
return r;
/* Delayed shutdown requested, and was successful */
if (arg_when > 0 && logind_schedule_shutdown() == 0)
return 0;
/* No delay, or logind failed or is not at all available */
if (geteuid() != 0) {
if (arg_dry_run || arg_force > 0) {
(void) must_be_root();
return -EPERM;
}
/* Try logind if we are a normal user and no special mode applies. Maybe polkit allows us to
* shutdown the machine. */
if (IN_SET(arg_action, ACTION_POWEROFF, ACTION_REBOOT, ACTION_KEXEC, ACTION_HALT)) {
r = logind_reboot(arg_action);
if (r >= 0)
return r;
if (IN_SET(r, -EOPNOTSUPP, -EINPROGRESS))
/* Requested operation is not supported on the local system or already in
* progress */
return r;
/* on all other errors, try low-level operation */
}
}
/* In order to minimize the difference between operation with and without logind, we explicitly
* enable non-blocking mode for this, as logind's shutdown operations are always non-blocking. */
arg_no_block = true;
if (!arg_dry_run && !arg_force)
return start_with_fallback();
assert(geteuid() == 0);
if (!arg_no_wtmp) {
if (sd_booted() > 0)
log_debug("Not writing utmp record, assuming that systemd-update-utmp is used.");
else {
r = utmp_put_shutdown();
if (r < 0)
log_warning_errno(r, "Failed to write utmp record: %m");
}
}
if (arg_dry_run)
return 0;
r = halt_now(arg_action);
return log_error_errno(r, "Failed to reboot: %m");
}
int start_with_fallback(void) {
/* First, try systemd via D-Bus. */
if (start_unit(0, NULL, NULL) == 0)
return 0;
#if HAVE_SYSV_COMPAT
/* Nothing else worked, so let's try /dev/initctl */
if (talk_initctl(action_to_runlevel()) > 0)
return 0;
#endif
return log_error_errno(SYNTHETIC_ERRNO(EIO),
"Failed to talk to init daemon.");
}
start_with_fallback
针对systemd
的处理函数是start_unit()
。在start_unit()
中,会根据arg_action
选中一个target
,对于reboot
,就是reboot.target
,然后调用start_unit_one()
函数,该函数使用
bus_call_method(bus, bus_systemd_mgr, "EnqueueUnitJob", &enqueue_error, &reply, "sss", name, job_type, mode)
通过DBUS
给systemd
发送信息,让systemd
运行reboot.target
。在这里,name
是reboot.target
,mode
是start
。
接下来,reboot
流程就来到了systemd
。
对systemctl
中的reboot
做一个总结:首先处理argv[0]
,赋值arg_action
为ACTION_REBOOT
,然后根据arg_action
的值,通过DBUS
总线,向systemd
发送start reboot.target
的信息,将reboot
流程推进到systemd
。
一定程度上来说,执行reboot
等同于执行systemctl start reboot.target
。
systemd
中的reboot
systemd
进程在内核启动末期加载。内核在启动晚期会加载initramfs
中的/init
进程,/init
是一个符号链接,链向systemd
,于是内核会加载initramfs
中的systemd
作为1号进程
。initramfs
中的systemd
在运行中会switch root
到真正的根文件系统,在这个过程中,initramfs
中的systemd
会杀死所有从initramfs
中启动的进程,并使用execve
装载真正的根文件系统中的systemd
,再进行一次初始化,重新加载各个服务(initramfs
中的systemd
和根文件系统中的systemd
加载的服务并不一样,有较大差别)。
根文件系统中的systemd
启动后,会进行一系列的设置,进行一系列的初始化,最后会进入src/core/main.c::invoke_main_loop()
函数的死循环。invoke_main_loop()
函数的循环主体是manager_loop()
,manager_loop()