本文完整阅读约需 86 分钟,如时间较长请考虑收藏后慢慢阅读~

使用过Linux/MacOS的读者一定对shutdown、poweroff、halt和reboot这四个管理系统电源状态的命令非常熟悉,但这些命令到底有什么区别?它们之间存在什么样的历史故事?本文将为读者们讲解这四个命令的起源、发展、以及它们之间的联系与区别。

0x01 起源

鉴于Linux/MacOS都从Unix操作系统借鉴了内核和Shell的设计,我们首先来看看Unix中是否存在这四个命令。

这里笔者选取最经典的两个Unix发行版:BSD 4.2和AT&T UNIX System V,分别查看它们的使用手册和源码:

1. BSD 4.2

BSD的文档可以在互联网档案馆中得到:文档链接

在文档中分别搜索这四个命令,我们会发现手册中分别提到了reboot命令与关机(Shutdown)流程,如图所示:

图1. Reboot命令

图2. 关机操作所执行的系统调用

这篇文档只是入门手册,只讲解了系统管理相关的命令和一部分的运行细节。那么除了reboot命令以外,剩余的三个命令是否同样存在呢?这里我们就要翻阅BSD 4.2的源码了。

BSD4.2发行于1983年8月,距今已有37年历史。但好在有一个GitHub仓库存储了所有的Unix发行版源码:dspinellis/unix-history-repo,我们可以clone下来之后checkout到BSD-4_2-Snapshot-Development这个Tag,再使用代码编辑器寻找我们要的代码。

早期Unix因为功能较少,且专为特定机器开发,因此源码结构也很直观。简单的翻阅后,我找到了这样三个文件:

这三个文件分别负责haltrebootshutdown三个命令的实现。

我们可以从源码中看出来,haltreboot命令源码是基本类似的,但reboot在源码的第80行使用kill()系统调用中止了PID为1的进程,这会导致系统重启(后文将会解释为什么)。详细对比可参考图3:

图3. reboot.chalt.c几乎完全一致,除了使用pause()系统调用之后的行为不同。

shutdown命令则是使用execle() 系统调用,根据用户传入的不同参数决定需要执行reboot还是halt,相关源码如下:

#ifndef DEBUG
            kill(-1, SIGTERM);  /* terminate everyone */
            sleep(5);       /* & wait while they die */
            if (reboot)
                execle(REBOOT, "reboot", 0, 0);
            if (halt)
                execle(HALT, "halt", 0, 0);
            kill(1, SIGTERM);   /* sync */
            kill(1, SIGTERM);   /* sync */
            sleep(20);
#else
            printf("EXTERMINATE EXTERMINATE\n");
#endif

BSD中的shutdown命令相对于reboothalt更加柔和,支持定时关机,而且在关机前五分钟会关闭登录入口,还能在命令执行后通知所有在线用户,可以看做是reboothalt的高级替代。

需要注意的是,BSD4.2版本尚未出现poweroff命令,不过这也不难推断——当时的计算机并不支持电源管理,又从何提起『关闭电源』?

2. AT&T UNIX System V

由于System V是一款商业软件(现在依旧是),截止到写作日期,GitHub上依旧没有相关仓库。但我在Internet Archive上找到了这款操作系统的源码:AT&T UNIX System V Source Code

在源码中搜索本文提到的四个命令,发现只有一个命令在System V中得到实现:

  • src/.adm/shutdown.sh

而且这还并不是一个C语言代码,只是一个系统实用工具。那么这个工具到底调用了什么命令让系统关机呢?

我们继续阅读这个工具的源码,会在其中找到如下代码:

echo "\nBusy out (push down) the appropriate"
echo "phone lines for this system.\n"
echo "Do you want to continue? (y or n):   \c"
read b
if [ "$b" = "y" ]
then
    /usr/lib/acct/shutacct
    echo "Process accounting stopped."
    /etc/errstop
    echo "Error logging stopped."
    echo "\nAll currently running processes will now be killed.\n"
    /etc/killall
    mount^sed -n -e '/^\/ /d' -e 's/^.* on\(.*\) read.*/umount \1/p' ^ sh -
    /etc/init s
    mount^sed -n -e '/^\/ /d' -e 's/^.* on\(.*\) read.*/umount \1/p' ^ sh -
    sync;sync;sync
    echo "Wait for \`INIT: SINGLE USER MODE' before halting."
    sync;sync;sync;
else
    echo "For help, call your system administrator."
fi

可以看到该脚本调用了/etc/init s命令。那么这么命令的目的是什么呢?我们同样可以在src/cmd/init.c中找到答案:

/* If the character is a digit between 0 and 6 or the letter S, */
/* fine, exit with the level equal to the new desired state. */
                if ( c == 'S' || c == 's' ) c = '7';
                if ( c >= '0' && c <= '7') exit(levels[c - '0']);
                else if (c != EOF) WRTSTR(1,"\nUsage: 0123456sS\n");
            }
        }
    }

可以看到,如果传入参数为sS,那么等同于传递一个7参数。继续阅读源码,我们会发现这个7参数的含义为单用户模式:

char level(state)
int state;
{
    register char answer;

    switch(state) {
    case LVL0 :
        answer = '0';
        break;
    case LVL1 :
        answer = '1';
        break;
    case LVL2 :
        answer = '2';
        break;
    case LVL3 :
        answer = '3';
        break;
    case LVL4 :
        answer = '4';
        break;
    case LVL5 :
        answer = '5';
        break;
    case LVL6 :
        answer = '6';
        break;
    case SINGLE_USER :
        answer = 'S';
        break;
    case LVLa :
        answer = 'a';
        break;
    case LVLb :
        answer = 'b';
        break;
    case LVLc :
        answer = 'c';
        break;
    default :
        answer = '?';
        break;
    }
    return(answer);
}

也就是说,使用shutdown所执行的并非关机(与BSD/现代操作系统均不同),而是挂断所有远程终端(因为当时的远程连接依靠的是电话线,可以参考我写过的《在Linux中建立后台任务的若干种姿势》),然后进入单用户模式(即禁止其他用户连接)。因为此时已经没有任何用户操作计算机,管理员就可以自行关闭设备(如磁盘/磁带机),清除数据(临时数据),同步磁盘(在System V中是自动的),然后手动关闭电源。init s命令的用途同样可以在这个Patch的描述中看到。

也就是说,AT&T UNIX System V只有这四个命令中的shutdown,其他三个均未实现。但它有着更灵活的init工具,可以指定操作系统的运行状态。init这一功能被后来的几乎所有UNIX/类UNIX系统所继承,后文将会详细提及。

部分兼容System V的操作系统(如微软的SCO XENIX)额外添加了如haltsys这样的命令,用于模仿BSD下的halt,实现『暴力』关闭系统的功能。感兴趣的读者可以阅读这篇手册

0x02 发展

上文我们提到了八十年代UNIX操作系统中不存在poweroff命令,这是因为当时的计算机没有控制电源的能力,而APM(高级电源管理)的首次应用是在1992年,我们目前使用的ACPI(高级配置与电源接口)则是在1997年才问世,直到2000年后才开始普及。

Linux操作系统问世于1991年底,在很长的一段时间内依旧仅包含了reboothaltshutdown命令,如图4所示:

图4. 使用Qemu运行Slackware0.99版本的截图,磁盘镜像来源于2014年『Qemu基督降临日活动』第一天:地址,请注意图中的poweroff命令存放于etc目录,这是因为当时的etc目录会存放各种各样的东西,而不只是现在的配置文件。这里有一个非常详细的帖子介绍了Linux/Unix下各目录的历史演变。

从图4中我们可以看出,Linux的第一个发行版Slackware在发行之初依旧只支持shutdownhaltreboot三个命令,与BSD基本一致。90年代的Unix与80年代在电源管理与系统命令方面区别并不大,此处不再赘述。

但随着ACPI的出现,操作系统得以通过BIOS对电源进行控制(之前只能在BIOS菜单中设置或者完全不能设置)。ACPI提供了电池信息、低功耗睡眠、风扇管理、温度管理等先进功能,但最主要的是终于支持电源管理了!

图5. Windows98及之前版本常见的『您现在可以安全的关闭计算机了』提示,通常出现在关机结束后(关闭所有进程,同步所有未保存数据到磁盘,将磁头放置在停泊区之后),随着ACPI的普及逐渐消失。但其实这一提示依旧存在于最新的Windows 10中。

图6. Windows 10中的『您现在可以安全的关闭计算机了』提示,可以通过修改组策略触发,也有可能出现于不支持ACPI的主板(非常少见),截图来自于YouTube:Do not turn off system power after a Windows system shutdown has occurred

由于2000年之前的很多资料已不可考证(很多Usenet站点和Mailing Lists已经关闭,很多文档也随着不断的更新,找不到最初的版本),目前互联网上能搜索到最早关于poweroff命令的时间大多集中于2000~2001年,下面我将会分别讲解BSD(2000年后BSD以外的其他Unix已经几乎不存在了)和Linux下的poweroff命令:

1. Linux下的poweroff命令

由于Linux2.2之前的内核版本对于虚拟化的支持非常差,无法在虚拟机中正常运行,我使用MacOS下的Parallels Desktop虚拟化工具安装了发布于2001年的RedHat Linux 7.1,基于Linux内核2.4版本(注意与RHEL区别开,RedHat Linux是桌面发行版),在其中找到了关于poweroff的文档,参考图7与图8:

图7. RedHat Linux 7.1中的内置文档,将haltrebootpoweroff合并在同一篇文档中,且拥有几乎完全相同的参数列表

图8. 从文档最后能看到发行日期为1999年,这是笔者能找到最早关于poweroff命令的文档。请注意截图中的Notes部分。

需要注意的是,此时RedHat中的poweroff依旧不支持ACPI。尽管Linux2.4内核已经实现了初步的ACPI兼容,但RedHat并未使用之,而手动开启ACPI需要在内核编译过程中选择对应模块,可以参考这篇文章:Red Hat 7.2で電源ボタンでshutdown、poweroffするように設定する。第一个完全支持ACPI的内核版本是2.6,想了解更多信息的读者可以阅读Intel写于2004年的The State of ACPI in the Linux Kernel,对2004年前后Linux内核在ACPI方面的支持进行了全面的评估。

笔者运行的RedHat 7.1基于Linux2.4.2(可以在Internet Archive上获取到由RedHat官方上传的全套CD与配套软件/源码:Red Hat Linux 7.1),可以清晰的在源码中看到ACPI的基础实现:

首先我们来看看SysVinit组件的源码:

// SysVinit-2.78: src/halt.c:258-266
// 此处为poweroff命令执行后走到的分支
// 细心的读者会注意到,我们又遇到了SysVinit,后文将会展开解释
        /*
         *  Halt or poweroff.
         */
        if (do_poweroff)
            init_reboot(BMAGIC_POWEROFF);
        /*
         *  Fallthrough if failed.
         */
        init_reboot(BMAGIC_HALT);
// SysVinit-2.78: src/reboot.h:9-23
// 请注意BMAGIC_POWEROFF与reboot()系统调用(来自sys/reboot.h)
#if defined(__GLIBC__)
#  include <sys/reboot.h>
#endif

#define BMAGIC_HARD    0x89ABCDEF
#define BMAGIC_SOFT    0
#define BMAGIC_REBOOT  0x01234567
#define BMAGIC_HALT    0xCDEF0123
#define BMAGIC_POWEROFF    0x4321FEDC

#if defined(__GLIBC__)
  #define init_reboot(magic) reboot(magic)
#else
  #define init_reboot(magic) reboot(0xfee1dead, 672274793, magic)
#endif

接下来我们走到Linux内核中,查看reboot()系统调用的定义:

// kernel: linux/include/linux/reboot.h:14-30
/*
 * 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.
 */

#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

可以看到之前传入的0x4321FEDC被定义为LINUX_REBOOT_CMD_POWER_OFF常量。

接下来我们再找到对这个常量的引用:

// kernel: linux/kernel/sys.c:269-311
asmlinkage long sys_reboot(int magic1, int magic2, unsigned int cmd, void * arg)
{
    char buffer[256];

    /* We only trust the superuser with rebooting the system. */
    if (!capable(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))
        return -EINVAL;

    lock_kernel();
    switch (cmd) {
// ......
    case LINUX_REBOOT_CMD_POWER_OFF:
        notifier_call_chain(&reboot_notifier_list, SYS_POWER_OFF, NULL);
        printk(KERN_EMERG "Power down.\n");
        machine_power_off();
        do_exit(0);
        break;
// ......

可以看到reboot()系统调用将传入的0x4321FEDC转换为对machine_power_off()函数的调用,这一函数在不同系统架构下有不同的实现,以i386为例,这一函数会在系统支持ACPI电源管理时执行pm_power_off()函数:

// kernel: linux/arch/i386/process.c:61-64
/*
 * Power off function, if any
 */
void (*pm_power_off)(void);


// kernel: linux/arch/i386/process.c:381-385
void machine_power_off(void)
{
    if (pm_power_off)
        pm_power_off();
}

这里的pm_poweroff()默认只是一个空指针,只有在ACPI驱动成功加载(也意味着设备支持ACPI)后才会指向真正执行电源关闭的函数:

// kernel: linux/drivers/acpi/sys.c:123-146
int
acpi_sys_init(void)
{
// ......
    printk(KERN_INFO "ACPI: System firmware supports:");
// ......
    pm_power_off = acpi_power_off;
// ......
}

// kernel: linux/drivers/acpi/sys.c:79-90
/*
 * Enter soft-off (S5)
 */
static void
acpi_power_off(void)
{
    struct acpi_enter_sx_ctx ctx;

    init_waitqueue_head(&ctx.wait);
    ctx.state = ACPI_STATE_S5;
    acpi_enter_sx_async(&ctx);
}

请注意acpi_power_off()函数中的ACPI_STATE_S5,这里的S5和注释中的Soft off是一个意思,指的是ACPI的深度睡眠状态,即G2状态。这一状态只允许被特定设备(如电源键、键盘、网络、USB等)唤醒,但电源尚未完全断开。ACPI的每个状态可以参考UEFI协会的ACPI规范文档:Advanced Configuration and Power Interface (ACPI) Specification

如果从acpi_enter_sx_async()函数继续深挖,我们还能看到Linux是如何使用汇编与BIOS通信的。但限于篇幅,此处不再扩展。

从上面的源码,我们可以清晰看出Linux 2.4内核对ACPI的支持情况。尽管源码中存在很多其他『尚未支持』的ACPI特性,且电源管理功能也不成熟,但至少在本文所述的关机方面有了很大进展。操作系统在内核层面终于可以控制系统电源,而用户们也可以告别早期IBM兼容机的物理AT开关,只需执行一次命令就能完全关机!希望更详细了解Linux关机/重启流程的读者还可以阅读这篇博文:计算机是如何实现重启的

2. Unix(BSD)下的poweroff命令

由于AT&T UNIX System V在2000年之后就很少更新,且版本越新源码越难找到,此处使用BSD代指Unix。MacOS由于2000年刚刚切换到部分基于BSD的Darwin内核,且硬件为自主设计,不具有参考价值,同样不再赘述。

尽管步入新世纪之后,BSD系的操作系统逐渐式微,但在对于电源管理与控制方面,其先进程度并不逊于Linux。

这里我们选取发布于2000年的NetBSD 1.5系统,查看它的文档:reboot, poweroff, halt – stopping, powering down and restarting the system

图9. NetBSD 1.5版本手册中关于poweroffhaltreboot三个命令行为的描述。

这篇手册撰写于2000年4月,在我用红框标示的位置可以明显看到poweroff命令可以respectively, power down, halt or restart the system

那么NetBSD是如何在源码中实现关闭电源的呢?我们可以在普林斯顿大学的资源站中找到NetBSD1.5版本的内核源码:https://mirror.math.princeton.edu/pub/NetBSD-archive/

// usr: src/sbin/reboot.c:72-91
// 注意根目录名为usr,现在很多Linux用户以为usr是User的缩写
// 但其实它最开始指的是Unix System Resources,即Unix系统资源
// 此处NetBSD将其作为根目录名,某种意义上也声明了自己『正统』的Unix身份
int
main(argc, argv)
    int argc;
    char *argv[];
{
    int i;
    struct passwd *pw;
    int ch, howto, lflag, nflag, qflag, sverrno, len;
    const char *user;
    char *bootstr, **av;

    if (!strcmp(__progname, "halt") || !strcmp(__progname, "-halt")) {
        dohalt = 1;
        howto = RB_HALT;
    } else if (!strcmp(__progname, "poweroff") 
           || !strcmp(__progname, "-poweroff")) {
        dopoweroff = 1;
        howto = RB_HALT | RB_POWERDOWN;
    } else
        howto = 0;

这里的RB_HALTRB_POWERDOWN分别指代传递给reboot()系统调用的参数,如果系统不支持ACPI/APM,则会在poweroff命令执行后进入Halt状态。

遗憾的是尽管1.5版本引入了这一命令,也在源码方面给予了判断,但这一版本的源码中既不存在RB_POWERDOWN这个常量,也没有对dopoweroff这个变量进行后续的读取与操作。

在最新的NetBSD文档中查看reboot()系统调用的文档,我们可以看到RB_POWERDOWN背后的魔法数字是0x0808reboot — reboot system or halt processor

而在阅读NetBSD最新的poweroff命令文档,我们同样可以在HISTORY部分看到这一命令最先出现在2000年的NetBSD 1.5版本:

图10. 最新版本NetBSD文档中关于poweroff命令历史的介绍。

值得一提的是,笔者在阅读源码过程中发现了另一件趣事:在我使用Visual Studio Code的IntelliSense功能查看reboot()系统调用引用时,IntelliSence默认指向了XCode中关于macOS操作系统reboot()系统调用的头文件。

从头文件的define对比刚刚NetBSD的reboot()文档,我们可以发现两者除了电源管理和内核状态的微小差别,其他几乎完全一致,某种意义上也证实了『macOS/iOS是最大的Unix发行版』这一论断(尽管最新的系统镜像已经不再单独包含BSD内核)。

图11. NetBSD的reboot()系统调用参数(左图)对比macOS的reboot()系统调用参数(右图),可以看到两者几乎完全一致。


综合来看,步入新世纪后无论是Linux还是Unix都开始支持poweroff命令来对系统的电源状态进行管理。但两者在调用方式上存在显著区别:

  • Linux通过SysVinit作为代理触发reboot()系统调用
  • Unix(BSD)直接通过poweroff二进制文件触发reboot()系统调用

0x03 合并

1. BSD风格与SysV风格之争

在上一节的最后一段,我提到了Linux与Unix两者在reboot()系统调用上的差别。但其实早在上世纪八十年代,这一差别就存在了——只是当时的Linux尚未问世,取而代之与BSD分庭抗礼的是AT&T UNIX System V。

前文我们已经多次提及了initSysVinit,其实它们是同一个东西,都指的是AT&T UNIX System V风格的系统状态管理工具,大部分时候我们称之为SysV风格。

Linux作为一款『类Unix』操作系统,自然会努力向Unix靠齐以兼容Unix下的软件/用户习惯。但为什么Linux会使用init作为启动程序,又最终选择了SysV风格而非BSD风格呢?这就要提到Linux早期的历史了。

熟悉Linux历史的读者应该知道:Linus Torvalds在1991年开发Linux时参照的是塔能鲍姆教授的Minix操作系统。不过大部分人不知道的是,其实当时的Linux不止借鉴了Minix的文件系统,同样借鉴了Minix的init状态管理工具;如果大家再结合Minix出现的原因:塔能鲍姆教授需要一个功能和AT&T UNIX一致,但完全自由的操作系统用于教学,就不难理解为什么Linux最初选择使用init来管理系统状态。

但其实无论是早期的Linux还是Minix1.0,init的实现都非常暴力。相较于SysV风格,其实更像BSD风格,这里有两个系统早期init组件的源码可供对比,可以看到Linux的init除了前半段多了一些操作BIOS/CMOS的代码,main()函数的部分几乎和Minix完全一致:

既然早期的Minix也没有使用SysV init,那么Linux又是如何从这样『暴力』的init转换到SysV init的呢?这里我们就要了解一下另一位对Linux发展起到突出贡献(但远不如Linus出名)的开发者:Miquel van Smoorenburg。

他是SysVinit软件包最初的作者,第一个版本发布于1992年2月1日,最初为Minix系统设计,但在两个月后的第二个版本,该软件就宣布支持Linux。尽管Systemd(以及后文会提到的Upstart)的出现让SysVinit风光不再,但该软件并没有完全停止更新。在这里可以看到SysVinit的发行注记,可以在最下面找到最初版本的发行时间和内容:CHANGELOG : sysvinit.git

SysVInit的功能与稳定性让它在Linux社区中迅速站稳脚跟,替代了最初原始的init程序,成为当年MCC、TAMU以至后来Slackware、Debian和RedHat Linux的默认系统状态管理工具。就这样,SysVinit及其名为init的进程开始在Linux中成为第1号进程,接管Linux操作系统从开机到关机的整个生命周期,负责Linux下所有进程的创建和系统状态的管理。这一固定过程一直到二十年后才被Systemd取代。

2. shutdown的『一统江湖』

在SysV风格的init在Linux中得到广泛应用后,几乎所有的主流发行版都将shutdownpoweroffhaltreboot这四个命令合并,最终体现为对init的调用。这里以CentOS 6为例,为读者介绍这四个命令背后的联系:

首先需要注意的是:尽管CentOS 6依旧使用/etc/rc.d/存储系统各个状态的启动脚本,但其背后依赖的已经不是SysVinit,而是由Canonical公司(Ubuntu背后的公司)开发的Upstart。Upstart拥有更丰富的功能,通过异步的方式提升了脚本执行速度,并能完美向下兼容SysVinit。

CentOS 6中的shutdownpoweroffhaltreboot四个软件均存储在/sbin目录,下图解释了这四个命令之间的关联:

图12. 可以看到,在CentOS 6中,haltpoweroff指向reboot,而shutdown则是另一个独立软件,这四个命令连同init都属于Upstart软件包。

我们可以在Launchpad(可以理解为Ubuntu私有的GitHub)中获取到Upstart的源码,这里我摘录一部分源码便于读者理解这四个命令合并后的情况:

// Upstart: util/reboot.c:196-228
    /* Normally we just exec shutdown, which notifies everyone and
     * signals init.
     */
    if ((! force) && (! exit_only)) {
        char *args[5];
        int   i = 0;

        args[i++] = SHUTDOWN;

        switch (mode) {
        case REBOOT:
            args[i++] = "-r";
            break;
        case HALT:
            args[i++] = "-h";
            args[i++] = "-H";
            break;
        case POWEROFF:
            args[i++] = "-h";
            args[i++] = "-P";
            break;
        }

        args[i++] = "now";
        args[i] = NULL;

        nih_info (_("Calling shutdown"));
        execv (args[0], args);

        nih_fatal (_("Unable to execute shutdown: %s"),
               strerror (errno));
        exit (1);
    }

上面摘录的是reboot命令对应的源码,可以看到reboot命令会判断传入参数,按照参数模式构建对shutdown命令的调用。因为shutdown源码部分主要是管理倒计时、用户消息这些功能,不再赘述,我们直接来看shutdown.c调用的sysv_change_runlevel函数:

// Upstart: util/sysv.c:67-186
/**
 * sysv_change_runlevel:
 * @runlevel: new runlevel,
 * @extra_env: NULL-terminated array of additional environment.
 *
 * Returns: zero on success, negative value on raised error.
 **/
int
sysv_change_runlevel (int           runlevel,
              char * const *extra_env,
              const char *  utmp_file,
              const char *  wtmp_file)
{
// ......
    /* Make the EmitEvent call, we don't wait for the event to finish
     * because sysvinit never did.
     */
    err = NULL;
    pending_call = NIH_SHOULD (upstart_emit_event (
                       upstart, "runlevel", env, FALSE,
                       NULL,
                       (NihDBusErrorHandler)error_handler,
                       &err,
                       NIH_DBUS_TIMEOUT_NEVER));
    if (! pending_call)
        return -1;

    dbus_pending_call_block (pending_call);
    dbus_pending_call_unref (pending_call);

    if (err) {
        nih_error_raise_error (err);
        return -1;
    }

    return 0;
}

同样可以从源码和注释中清楚看到,上面的函数会调用dbus_pending_call_block(),这是另一个软件包D-Bus(可以理解为Linux下的事件总线)下的函数,然后将指定的init状态传递到D-Bus中,并指定名为upstartNihDBusProxy实例接收,继续完成关机/重启流程。

3. Systemd: 备受争议的『独裁者』

尽管SysV风格的init得到了广泛的应用,但依旧有不少开发者对它感到不满,下面摘录了Systemd开发者和支持者们对SysV风格init的一些意见:

  • 启动过程为单线程,如果一个不存在依赖的脚本耗时太长就会拖慢整个系统启动的时间
  • 使用脚本描述启动过程,不够安全也不够稳固
  • 日志管理过于分散,不能在一处查看所有应用程序的日志,日志需要频繁写入磁盘,性能也不理想
  • 进程不完全受控,比如使用两次fork这种方式产生的daemon进程就脱离了init的控制(感兴趣的读者可以参考《UNIX环境高级编程》这本书籍,里面详细的提到了如何利用这个Trick

于是在德国程序员Lennart Poettering(受雇于RedHat的同时也是Avahi和PulseAudio的作者)的推动下,Systemd应运而生。Systemd的灵感很大程度上来源于MacOS的Launchd,却又在很多方面不同于MacOS。Systemd同样不同于之前Linux/Unix中的任何状态管理工具,甚至可以说它管理的不止是状态,而是整个Linux——从内核到系统库到用户到事件到进程到服务到日志到状态到生命周期,甚至网络和时区……活脱脱的一个『独裁者』!

可想而知,尽管Systemd拥有诸多便捷之处,其在性能方面也十分优异,但不少的Linux用户、甚至部分发行版的开发者们都对Systemd心存芥蒂,甚至直接拒绝使用Systemd。这其中一部分原因在于Systemd违背了Hacker们『坚决相信自己不会做错事』的自负,托管了很多完全可以交给用户手动操作的模块;另一部分则是Systemd对于非Linux用户的不友好——Lennart甚至在访谈中大言不惭的说道:

kFreeBSD(Debian的BSD内核版本)完全就是个玩具,如果我是Debian的开发者,我会直接放弃这个玩具,然后投奔Systemd,但可惜我并不是。
——Retourner au contenu associé (dépêche : Un entretien avec Lennart Poettering)

在这里我们首先不讨论他说的是否正确,但如果我是kFreeBSD的开发者或用户,这样的讽刺与对自己的蔑视是完全不可原谅的。

但不知是RedHat号召力太大、还是真正向往『自由软件』的Hacker们越来越少,在2016年前后,几乎所有的主流发行版都将init替换成了Systemd——再怎么抗议,也无法抵挡时代的变迁与少数龙头企业越来越大的话语权。某种意义上,这也让Hacker们变得更加心酸:一个最初只是『Just For Fun』的自由操作系统,如今却升级到了『企业级』,成为了无数科技公司敛财的工具,以至于完全不再考虑最初那帮奠基者们的感受。Linus Torvalds?在全世界对Linux如此大规模的使用情况下,他的意见早已不再重要。

戏剧性的是,在Systemd席卷整个Linux生态时只有两个发行版曾有所抵抗。它们分别是1993年首发的Debian和Slackware——目前仅存最早的Linux发行版。而Debian在经过社区投票后最终还是无奈选择了Systemd,Slackware则负隅顽抗,直到最新版本也坚持使用BSD风格的init系统(甚至连SysVinit都不是)。

也许只有经历过那个时代的开发者们,才真正懂得自由软件中的自由二字是多么来之不易。谁能想到当年自由软件的领袖Richard Stallman,如今却被自己亲手构建的自由软件帝国所背叛?

我们,即参与签名的所有GNU项目维护者和开发者,非常感谢Richard Stallman于过去几十年中在自由软件运动方面的杰出工作,但我们也必须承认,他多年来的行为已经破坏了GNU计划的核心价值:将软件自由地授权给所有用户……我们觉得Richard Stallman不能代表所有GNU成员。现在到了GNU维护者们集体决定项目组织的时候了。
——GNU Hurd开发者对RMS『过分追求自由』行为的投诉,要求GNU基金会取消RMS的领导职位。摘自《A programmer explains why he’s willing to quit rather than work with industry legend Richard Stallman, who resigned from MIT after controversial remarks on Jeffrey Epstein

图13. 出现于2014年,并受到许多『反Systemd』用户支持(包括我)的boycottsystemd.org网站。六年不见,没想到域名都已经无人维护,只能从Wayback Machine中找到当时的一些存档。

鉴于笔者厌恶Systemd却又不得不在日常工作中使用Systemd的矛盾心情,很抱歉没办法给读者们进行详细的源码分析(实话实说,Systemd的源码又臭又长,为了少数几家大企业的高性能需求做了太多的提前优化,完全违背了Unix哲学),但既然本文的主题是关于关机和重启的命令,我还是会向读者介绍Systemd下这四个命令如何被进一步合并。

Systemd将电源管理进一步整合,此时无论是shutdownpoweroffhalt还是reboot都变成了/usr/bin/systemctl的符号链接:

图14. 整合后的四个命令。

由于systemctl命令接管了这四个命令背后的行为,因此我们也可以使用如下的方式替代这四个命令,这样对于刚接触Linux的用户来说可能会更直观(所有操作统一在一个命令中):

# 替代halt
systemctl halt
# 替代reboot
systemctl reboot
# 替代shutdown
systemctl shutdown
# 替代poweroff
systemctl poweroff

由于Systemd高度配置化的特性,以上四个命令背后同样都对应着各自的配置文件:

  • /usr/lib/systemd/system/systemd-halt.service
  • /usr/lib/systemd/system/systemd-reboot.service
  • /usr/lib/systemd/systemd-shutdown(是一个二进制文件,为了满足延时/用户提醒等功能)
  • /usr/lib/systemd/system/systemd-poweroff.service

Systemd的功过,笔者觉得现在谈论为时尚早(尽管这几年Systemd的确爆出过不少致命漏洞),但我相信在未来某天,一定会有一个新的方案来替代目前『独裁』的Systemd。目前看来,Systemd更多满足的少数是依靠Linux盈利的企业,使用集中的管理方式来提升培训效率,并让客户觉得『一切尽在掌控之中』,并能通过性能提升节省更多成本,而目前Systemd的流行无非是这些企业拥有更多的话语权,Systemd并不有趣。过去几年其实也出现了非常多和Systemd一样优秀的系统状态管理工具,但它们都被Systemd的风头所掩盖,只在一些小的用户群体中得到欢迎。也许再过几年,其中的某一款就会重新流行起来呢?

0x04 现状

前文我们通过源码的方式从最早期的Unix,谈论到了最新的Linux,相信读者们一定收获了不少历史知识。那么到底截止到2020年,这四个命令在使用上有什么区别呢?

答案是:基本没有区别。

实际上,除了shutdown的部分功能独一无二以外,其他任意一个命令都可以被另外某个所代替:

# 让OS进入Halt状态
halt
# 关闭电源,等同于poweroff
halt -p
# 重启机器,等同于reboot
halt --reboot
# 关闭电源
poweroff
# 让OS进入Halt状态,等同于halt
poweroff --halt
# 重启机器,等同于reboot
poweroff --reboot
# 重启机器
reboot
# 让OS进入Halt状态,等同于halt
reboot --halt
# 关闭电源,等同于poweroff
reboot -p

shutdown则有更多的功能:

# 延迟关机,可以与下面的命令结合使用
shutdown
shutdown now
shutdown 13:20  
# 关闭电源,等同于poweroff
shutdown -p now
# 让OS进入Halt状态,等同于halt
shutdown -H now
# 重启机器,等同于reboot
shutdown -r
# 取消之前设置的shutdown
shutdown -c

既然四个命令都可以相互替换,那么读者可以只选择一个命令使用,而无需记忆另外几个。笔者个人更习惯使用shutdown,因为在多用户环境下可以向其他用户发送即将关机的消息,不会直接断电导致其他用户来不及保存。

0x05 后记

本文的灵感来自于一位同学不久前问我的问题:

因为当时已经是深夜,我大概的向他解释了一下,并承诺有空会找一篇讲解这几个命令区别及其历史的文章供他参考。

第二天我在搜索引擎中搜寻很久,却完全没有找到类似的内容,无论是在百度还是Google,能搜索到的都是片面的『使用方法』或是一些无关紧要的内容。本着守信的原则,也是为了填补互联网上的这一块空缺,我觉得应该写一篇文章,通过源码、截图、引言、链接等方式,用纪录片的严谨程度和详细程度向这位同学,以及阅读本文的读者展示这四个命令的变迁,及其背后Unix/Linux操作系统六十年来的历史纪实,而某种程度上这对我自己也是一种不小的锻炼。

笔者必须得承认,这篇文章的内容实在是太长了。我大概花了一周时间,才收集到了所有的源码、资料、截图、链接等信息,只希望向读者们展示全网最全面最详细最硬核的内容。

尽管笔者在发布前已经做了非常多的校对工作,但由于部分历史内容并未亲身经历(虽然2002年就开始接触电脑,但知道2010年才开始了解Linux),在一些方面依旧存在或多或少的错漏。如果读者们在阅读过程中发现有与事实不符的部分,欢迎评论或私信指正,我会尽力修改。