init

作者:Alessandro Rubini

在 UNIX 术语中,“init”这个词并非指代特定的程序,而是一类程序。 “init”这个名称通常用来称呼系统启动时执行的第一个进程——实际上,唯一在系统启动时执行的进程。当内核完成计算机硬件的设置后,它会调用 init 并放弃对计算机的控制。从那时起,内核只处理系统调用,而不再在系统操作中扮演任何决策角色。在内核挂载根文件系统后,一切都由 init 控制。

目前,有几种 init 可供选择。您可以使用 Miquel van Smoorenburg 提供的现已成为经典的 SysVinit 包中的程序、Peter Orbaek 的 simpleinit(在 util-linux 的源代码包中找到),或者一个简单的 shell 脚本(例如本文中展示的脚本,它的功能远不如任何 C 语言实现)。如果您设置嵌入式系统,您甚至可以将目标应用程序作为 init 运行。喜欢自虐且不喜欢多任务处理的人甚至可以将 command.com 移植到 Linux 并将其作为 init 进程运行,尽管在运行 Linux 内核时,您永远无法将自己限制在 640KB 以内。

无论您选择哪个程序,都需要使用路径名 /sbin/init、/etc/init 或 /bin/init 访问它,因为这些路径名已编译到内核中。如果它们都无法执行,那么系统将严重损坏,内核将生成一个 root shell 以允许交互式恢复(即,/bin/sh 用作 init 进程)。

为了实现最大的灵活性,内核开发人员提供了一种为 init 进程选择不同路径名的方法。内核接受一个命令行选项 init= 正是为了这个目的。内核选项可以在启动时以交互方式传递,或者您可以使用 /etc/lilo.conf 中的 append= 指令。Silo、Milo、Loadlin 和其他加载器也允许指定内核选项。

正如您可能想象的那样,获得 Linux 机器 root 访问权限的最简单方法是在 LILO 提示符下键入 init=/bin/sh。请注意,这本身并不是一个安全漏洞 per se,因为真正的安全漏洞是物理访问控制台。如果您担心 init= 选项,LILO 可以使用自己的密码保护来防止交互。

init 的任务

现在我们知道 init 是一个通用名称,几乎任何东西都可以用作 init。问题是,真正的 init 应该做什么?

作为内核生成的第一个(也是唯一一个)进程,init 的任务包括生成系统中所有其他进程,包括系统操作中使用的各种守护进程以及文本控制台上的任何登录会话。

init 还应该在其子进程退出后立即重启它们。这通常适用于在文本控制台上运行的登录会话。一旦您注销,系统应立即运行另一个 getty 以允许启动另一个会话。

init 还应该收集已死亡的进程并处理它们。在 UNIX 的进程抽象中,除非进程的死亡报告给其父进程(或者在父进程不再存在的情况下报告给另一个祖先进程),否则无法从系统表中删除进程。每当进程通过调用 exit 或其他方式死亡时,它都会保持僵尸进程状态,直到有人收集它为止。 init 作为任何其他进程的祖先,应该收集任何孤立僵尸进程的退出状态。请注意,每个编写良好的程序都应该回收自己的子进程——僵尸进程只在某些程序行为不当时才会存在。如果 init 不收集僵尸进程,懒惰的程序员可能会轻易消耗系统资源并通过填满进程表来挂起系统。

init 的最后一项任务是处理系统关机。当超级用户指示关机时间已到时,init 程序必须停止任何进程并卸载所有文件系统。 shutdown 可执行文件不执行任何操作,它只是告诉 init 一切都结束了。

正如我们所见,init 的任务实现起来并不太困难,shell 脚本可以执行大多数必需的任务。请注意,每个像样的 shell 都会收集其已死亡的子进程,因此这对于 shell 脚本来说不是问题。

真正 的 init 实现添加到简单 shell 脚本方法中的是,对系统活动更大的控制,从而在整体灵活性方面带来巨大的好处。

使用 /bin/sh 作为最小选择

如上所述,shell 可以用作 init 程序。使用裸 shell (init=/bin/sh) 只是会导致在完全未配置的系统中打开 root shell。本节介绍 shell 脚本如何执行最小运行系统所需的所有任务。这种微小的 init 可用于嵌入式系统或类似的简化环境,在这些环境中,您必须从系统中挤出每一个字节。嵌入式系统最激进的方法是直接将目标应用程序作为 init 进程运行;这会导致一个封闭的系统(管理员无法交互,如果出现问题),但有时它适合这种设置。大多数现代发行版的安装环境是非 init 驱动的 Linux 系统的典型示例,其中 /sbin/init 是安装程序的符号链接。

清单 1 显示了一个可以作为 init 勉强接受的脚本。该脚本简短且不完整;特别要注意的是,它只运行一个 getty,并且在终止时不会重启。如果您尝试使用此脚本,请小心,因为每个 Linux 发行版都选择了自己的 getty 版本。键入 grep getty /etc/inittab 以了解您拥有什么以及如何调用它。

该脚本还有另一个问题:它不处理系统关机。但是,添加关机支持非常容易;只需在交互式 shell 终止后关闭一切即可。添加 清单 2 中显示的文本即可解决问题。

每当您使用纯 init=/bin/sh 启动时,您至少应该在执行任何操作之前重新挂载根文件系统;您还应该记住在按 ctrl-alt-del 之前执行 umount -a,因为 shell 不会拦截三指礼。

simpleinit,来自 util-linux

util-linux 包包含一个 C 版本的 init 程序。它比 shell 脚本具有更多功能,并且可以在大多数个人系统上良好运行,尽管它没有 SysVinit 包提供的巨大可配置性,SysVinit 包是现代发行版上的默认设置。

simpleinit(应调用 init 才能正常工作)的作用与刚刚显示的 shell 脚本非常相似,但增加了管理单用户模式和控制台会话迭代调用的功能。它还可以正确处理关机请求。

simpleinit 很有趣,并且文档也很完善,因此您可能会喜欢阅读文档。我建议使用 util-linux 的源代码发行版来获取最新信息。

正如其名称所示,simpleinit 的实现确实很简单。该程序执行一个 shell 脚本 (/etc/rc) 并解析一个配置文件以确定哪些进程需要重新生成。配置文件称为 /etc/inittab,就像功能齐全的 init 使用的文件一样;但是请注意,它的格式不同。

如果您计划在系统上安装 simpleinit(您的系统很可能已经包含 SysVinit),则必须非常小心,并准备好使用 “init=/bin/sh” 的内核参数重新启动,以从不稳定情况中恢复。

真正的 init:SysVinit

大多数 Linux 发行版都附带由 Miquel van Smoorenburg 编写的 init 版本;此版本类似于 System V UNIX 采用的方法。

主要思想是计算机系统的用户可能希望以几种不同的方式(不仅仅是单用户和多用户)操作他的机器。尽管此功能通常未被利用,但它并不像您想象的那么疯狂。当计算机由一个家庭中的两个或多个人共享时,可能需要不同的设置;网络服务器和独立游戏机可以在同一台计算机上以不同的运行级别愉快地共存。虽然我是笔记本电脑的唯一用户,但我有时需要网络服务器(通过 PLIP),有时需要在火车上工作时使用无网络环境以节省资源。

每种操作模式都称为“运行级别”,您可以选择在启动时或运行时使用的运行级别。 init 的主要配置文件称为 /etc/inittab,它定义了在启动时、进入运行级别或从一个运行级别切换到另一个运行级别时要执行的操作。它还说明了如何处理三指礼以及如何处理电源故障,尽管您需要电源守护程序和 UPS 才能从此功能中受益。

inittab 文件按行组织,其中每行由几个冒号分隔的字段组成:id:runlevel:action:command

inittab(5) 手册页写得很好且内容全面,正如手册页应该的那样,我认为值得重复其中的一个示例——一个精简的 /etc/inittab,它实现了上面显示的 shell 脚本的相同功能和缺陷

id:1:initdefault:
rc::bootwait:/etc/rc
1:1:respawn:/sbin/getty 9600 tty1

这个简单的 inittab 告诉 init 默认运行级别为 “1”,在系统启动时它必须执行 /etc/rc,并且在运行级别 1 中,它必须永远重新生成命令 /sbin/getty 9600 tty1。您不应期望测试此脚本,因为它不处理关机程序。

但在继续深入之前,我必须填补几个空白。让我们回答两个常见问题

  • “如何启动到与默认运行级别不同的运行级别?” 在内核命令行上添加运行级别;例如,如果 “Linux” 是您的内核名称,请在 LILO 提示符下键入 Linux 2

  • “如何从一个运行级别切换到另一个运行级别?” 以 root 身份,键入 telinit 5 以告诉 init 进程切换到运行级别 5。不同的数字表示不同的运行级别。

配置 init

自然地,典型的 /etc/inittab 文件具有比上面显示的三行脚本更多的功能。虽然 bootwaitrespawn 是最重要的操作,但还存在其他几种操作来处理与系统管理相关的问题。我不会在这里讨论它们。

请注意,SysVinit 可以处理 ctrl-alt-del,而早期版本的 init 没有捕获三指礼(即,如果您按下组合键,机器将重新启动)。那些对如何完成此操作感兴趣的人可以查看 /usr/src/linux/kernel/sys.c 中的 sys_reboot。(如果您查看代码,您会注意到使用了幻数 672274793:您能想象 Linus 为什么选择这个数字吗?我想我知道答案,但您会喜欢自己发现它。)

让我们看看一个相当完整的 /etc/inittab 如何处理系统生命周期中所需的一切,包括不同的运行级别。尽管游戏的魔力始终显示在 /etc/inittab 中,但可以采用几种不同的系统配置方法,最简单的是上面显示的三行 inittab。在我看来,有两种方法值得详细讨论;我将从选择遵循它们的两个著名的 Linux 发行版中将它们称为 “Slackware 方式” 和 “Debian 方式”。

Slackware 方式

虽然我已经有一段时间没有安装 Slackware 了,但 SysVinit-2.74 中包含的文档告诉我,它仍然以相同的方式工作。它的功能较少,但比 Debian 方式快得多。我的个人 486 机器运行类似 Slackware 的 /etc/inittab 只是为了速度优势。

Slackware 系统使用的 /etc/inittab 的核心如 清单 3 所示。请注意,运行级别 0、1 和 6 具有预定义的含义。这是硬编码到 init 命令中的,或者更好的是,硬编码到同一软件包的 shutdown 命令部分中。每当您想要停止或重新启动系统时,都会告诉 init 切换到运行级别 0 或 6,从而执行 /etc/rc.d/rc.0 或 /etc/rc.d/rc.6。

这可以完美地工作,因为每当 init 切换到不同的运行级别时,它都会停止重新生成未为新运行级别定义的任何任务;实际上,它会杀死任务的正在运行的副本。在这种情况下,活动任务是 /sbin/agetty。

配置此设置非常简单,因为不同文件的角色很明确

  • /etc/rc.d/rc.S 在系统启动时运行,与运行级别无关。将您想要在启动时立即执行的任何内容添加到此文件中。

  • /etc/rc.d/rc.M 在 rc.S 结束后运行,仅当系统将要进入运行级别 2 到 5 时运行。如果您以运行级别 1(单用户)启动,则不会执行此脚本。将您仅在多用户模式下运行的任何内容添加到此文件中。

  • /etc/rc.d/rc.K 处理从多用户模式切换到单用户模式时终止进程。如果您向 rc.M 添加了任何内容,您可能希望从 rc.K 中停止它。

  • /etc/rc.d/rc.0 和 /etc/rc.d/rc.6 分别关闭和重新启动计算机。

  • /etc/rc.d/rc.4 仅在进入运行级别 4 时执行。此文件运行 “xdm” 进程,以允许图形登录。请注意,在运行级别 4 中,/dev/tty1 上没有运行 getty(如果您愿意,可以更改此设置)。

这种设置很容易理解,您可以通过添加适当的 wait(在等待终止时执行一次)和 respawn(永远执行)条目来区分运行级别 2、3 和 5。

顺便说一句,如果您还没有猜到 “rc” 的含义,它是 “run command”(运行命令)的缩写形式。多年来,我一直在编辑我的 .cshrc 和 .twmrc 文件,直到有人告诉我这个神秘的 “rc” 后缀是什么意思——UNIX 世界中的一些东西只能通过口头传统流传下来。我觉得我现在正在将某人从多年的黑暗中拯救出来——我希望我不会因为以书面形式定义它而受到惩罚。

Debian 方式

虽然很简单,但在向系统添加新的软件包时,Slackware 设置 /etc/inittab 的方式无法很好地扩展。

例如,让我们想象一下,有人将 ssh 软件包作为 Slackware 附加组件分发(并非不可能,因为由于美国关于密码学的非逻辑规则,ssh 无法在官方磁盘上分发)。程序 sshd 是一个独立的服务器,必须在系统启动时调用;这意味着软件包应修补 /etc/rc.d/rc.M 或它调用的脚本之一,以添加 ssh 支持。这在软件包通常是文件存档的世界中显然是一个问题。此外,您不能假设 rc.local 始终与库存发行版保持不变,因此即使是在典型的用户配置计算机中运行的修补文件的安装后脚本也会惨败。

您还应该考虑到,添加新的服务器程序只是工作的一部分;服务器还必须在 rc.K、rc.0 和 rc.6 中停止。事情现在变得非常棘手。

这个问题的解决方案既简洁又精细。其思想是,每个包含服务器的软件包都必须为系统提供一个脚本来启动和停止服务;然后,每个运行级别将启动或停止与该运行级别关联的服务。关联服务和运行级别可以像在特定于运行级别的目录中创建文件一样容易。此设置在 Debian 和 Red Hat 以及我从未运行过的其他发行版中很常见。

Debian 1.3 使用的 /etc/inittab 的核心如 清单 4 所示。Red Hat 设置具有完全相同的系统初始化结构,但使用不同的路径名;您将能够将一个结构映射到另一个结构。让我们列出不同文件的角色

  • /etc/init.d/boot 是 rc.S 的确切对应物。它通常检查本地文件系统并挂载它们,但实际情况具有更多功能。

  • /sbin/sulogin 允许 root 登录到单用户工作站。仅在清单 4 中显示,因为单用户模式对于系统维护非常重要。

  • /etc/init.d/rc 是一个脚本,它运行属于正在进入的运行级别的任何启动/停止脚本。

最后一项,rc 程序,是此环境的主要角色:它的任务是扫描目录 /etc/rc$runlevel.d 并调用位于该目录中的任何脚本。精简版的 rc 看起来像这样

#!/bin/sh
level=$1
cd /etc/rc.d/rc$level.d
for i in K*; do
        ./$i stop
done
for i in S*; do
        ./$i start
done
这是什么意思?这意味着 /etc/rc2.d(例如)包含名为 K*S* 的文件;前者标识必须终止(或停止)的服务,后者标识必须启动的服务。

好的,但我没有解释 K* 和 S* 文件来自哪里。每个必须为特定运行级别运行的软件包都会将自身添加到所有 /etc/rc?.d 目录中,作为启动条目或终止条目。为了避免代码重复,软件包会在 /etc/init.d 中安装一个脚本,并在各种 /etc/rc?.d 目录中安装多个符号链接。

为了展示一个真实的示例,让我们看看 Debian 的两个 rc 目录中包含的内容

rc1.d:
K11croni        K20sendmail
K12kerneld      K25netstd_nfs
K15netstd_init  K30netstd_misc
K18netbase      K89atd
K20gpm          K90sysklogd
K20lpd          S20single
K20ppp
rc2.d:
S10sysklogd     S20sendmail
S12kerneld      S25netstd_nfs
S15netstd_init  S30netstd_misc
S18netbase      S89atd
S20gpm          S89cron
S20lpd          S99rmnologin
S20ppp

这两个目录的内容显示了进入运行级别 1(单用户)如何终止所有服务并启动 “single” 脚本,以及进入运行级别 2(默认级别)如何启动所有服务。出现在 K 或 S 附近的数字用于对各种服务的启动或终止进行排序,因为 shell 以字母数字顺序扩展出现在 /etc/init.d/rc 中的通配符。调用 ls -l 命令确认所有这些文件都是符号链接,例如以下内容

rc2.d/S10sysklogd -> ../init.d/sysklogd
rc1.d/K90sysklogd -> ../init.d/sysklogd
总而言之,在此环境中添加新的软件包意味着在 /etc/init.d 中添加一个文件,并从每个 /etc/rc?.d 目录添加适当的符号链接。为了使不同的运行级别表现不同(默认情况下,运行级别 2、3、4 和 5 的配置方式相同),只需在适当的 /etc/rc?.d 目录中删除或添加符号链接即可。

如果这看起来太困难和令人沮丧,那么一切都没有丢失。如果您使用 Red Hat(或 Slackware),您可以将 /etc/rc.d/rc.local 视为 autoexec.bat——如果您足够老,还记得 Linux 之前的时代。如果您运行 Debian,您可以创建 /etc/rc2.d/S95local 并将其用作您自己的 rc.local;但是请注意,Debian 在系统设置方面非常干净,我宁愿不建议这种异端邪说。强大和微不足道很少匹配——您已被警告。

Debian 2.0

在撰写本文时,Debian 2.0 正在向公众发布,我怀疑当您阅读本文时,它将被广泛使用。

尽管系统初始化的结构相同,但有趣的是,开发人员设法使其更快。脚本 /etc/init.d/rc 现在可以源(读取)/etc/rc2.d 中的文件,而无需生成另一个 shell,而不是执行它们。是否执行或源它们由文件名控制:名称以 .sh 结尾的可执行文件被源化,其他文件被执行。技巧如下面几行所示

case "$i" in
        *.sh)
            # Source shell script for speed.
            (
            trap - INT QUIT TSTP
            set start; . $i
            ) ;;
        *)
            # No sh extension, so fork subprocess.
            $i start ;;
   esac

速度提升非常明显。

是一个石器时代的人,他运行旧硬件,骑旧自行车,开旧车。他喜欢(使用 grep)在他的(旧)文件系统中搜索可以从 C 语言转换为英语或意大利语的信息。如果他没有在 rubini@linux.it 阅读电子邮件,那么他正在做其他事情。

加载 Disqus 评论