引子当内核完成初始化一个“空壳”系统是如何变成可用的交互环境的在前面的代码分析中我们见证了 Linux 0.11 内核如何从冰冷的硬件端口读取时间如何利用fork()和exec()创建进程并通过setsid()建立全新的会话期。然而在init/main.c的最后我们看到的是这样一个简单的动作exit(execve(/bin/sh, argv, envp));你或许会问难道这就是 Linux 的全部初始化吗直接启动一个 Shell 外壳剩下的都交给用户自己折腾事实上Linux 0.11 这种直截了当的方式更像是一个**“为了演示内核已正常工作的技术展示”。在真实的生产环境和现代 Linux 系统中从内核态切入到用户态交互有一场漫长的“接力赛”**。参与这场接力的选手包括init进程、getty程序、login程序以及最终的shell。今天我们就来深入探讨这个从“内核向用户移交控制权”的经典流程。一、 内核的“终极任务”与 PID 1 的诞生在init/main.c中内核完成了硬件初始化、中断设置、内存分页管理和进程调度器的初始化。当move_to_user_mode()执行完毕时系统实际上已经运行在第一个用户进程——进程 0idle 进程的上下文中了。随后进程 0 调用了fork()诞生了进程 1init 进程。本书中的 Linux 0.11 让进程 1 直接执行execve去加载/bin/sh这是非常粗糙的做法。在真实的类 UNIX 操作系统中PID 1Init 进程是系统所有用户进程的“始祖”。它肩负着一项极为神圣且繁重的使命统筹管理系统中所有除内核线程外的用户空间进程。这意味着PID 1 绝对不应该是一个简单的 Shell。在典型的 Linux 发行版中PID 1 通常是著名的/sbin/init程序在现代 Linux 中可能会被systemd代替但核心功能一致。这个init程序就是用户态环境初始化的“总指挥”。渲染错误:Mermaid 渲染失败: Parse error on line 4: ...核] -- Fork1[创建进程 1 (PID1)] For -----------------------^ Expecting SQE, DOUBLECIRCLEEND, PE, -), STADIUMEND, SUBROUTINEEND, PIPE, CYLINDEREND, DIAMOND_STOP, TAGEND, TRAPEND, INVTRAPEND, UNICODE_TEXT, TEXT, TAGSTART, got PS观察上面的流程图内核只负责“生”出进程 1。而进程 1 的子进程们通过层层替换execve最终才演化成了用户面前的命令行环境。这就是现代 Linux 登录流程的核心骨架。二、 司令官的职责Init 进程如何管理终端与孤儿进程书中的描述非常精彩它说明了init进程需要承担以下几个关键角色2.1 为每个终端“种下”一个守护进程在 Linux 0.11 那个年代计算机往往连接着多个物理串口终端TTY。init进程会读取位于/etc/inittab的配置文件。这个文件告诉init“在这个系统上哪个终端例如/dev/tty1,/dev/tty2或串口ttyS0是允许用户登录的。”对于每一个允许登录的终端init都会进行一次fork()在子进程中调用execve启动getty或agetty程序。而init本身则在一个无限循环中调用wait()系统调用挂起自己随时等待子进程的“死讯”。2.2 孤儿进程的“灵魂归宿”书中特别强调了一个概念孤儿进程。“在 Linux 中所有的进程必须属于单棵进程树所以孤立进程必须被收取。”这个设定非常精妙。试想一个场景你在终端启动了 A 进程A 进程又fork()出了 B 进程然后 A 进程突然因为某种原因崩溃退出了。此时B 进程的父进程A已经消失B 变成了“无父无母”的孤儿进程。如果内核不干预B 进程退出时将无人替它“收尸”回收其 PCB这就是“僵尸进程”的隐患。Linux 内核的解决方案是当一个进程的父进程先于它退出内核会立刻将这个孤儿进程的“养父”修改为init进程PID 1。因此只要init进程一直活着系统里就不会有真正无法被回收的进程。每当这些“过继”过来的孤儿进程终止init的wait()就会感知到并回收它们占用的资源维持操作系统的稳态。2.3 系统关闭的“清道夫”当管理员执行shutdown命令时实际上是通过向init发送特定的信号。init接收到关机信号后会反序遍历它的子进程树向所有属于它的子孙进程发送SIGTERM信号要求它们优雅地终止。在确保所有用户进程都已退出后它会卸载所有文件系统并通知内核停止 CPU 的运行。三、 迎宾员getty的使命当init为某个终端fork()了一个子进程并且execve加载了getty之后getty就开始上岗了。getty的核心职责有三项硬件驯化它需要设置串口或者虚拟控制台的通信参数例如波特率对于串口终端、数据位、停止位等确保物理链路通畅。武装亮相getty会读取/etc/issue文件这个文件里通常写着 Linux 发行版的名称和版本号如Welcome to Linux 0.11并将其打印在终端屏幕上。抛砖引玉它打印出最重要的提示信息login: 。此时getty进程会阻塞在read()系统调用上等待键盘的输入。一旦用户输入了用户名例如root并按下了回车键getty的任务就圆满结束了。它立刻调用execve(/bin/login, ...)将自己完全替换为login程序。关键点getty进程并没有退出而是直接通过execve变成了login进程。进程的 PID 没变但程序的“灵魂”被替换了。这种方法避免了不必要的进程销毁和创建开销。四、 验票员login程序的检查与切换login接手过来后它绝对不会让用户直接进入系统。它要做一次非常严格的“验票”。4.1 密码验证login会提示用户输入密码。为了安全它通常不会直接把明文密码传给内核而是调用getpass()函数屏蔽终端的回显用户敲键盘时屏幕上不显示任何字符。随后login会打开/etc/passwd文件根据刚才getty传来的用户名查找到该用户的记录。它把用户输入的明文密码通过某种单向哈希加密算法早期 Linux 使用 DES现代使用 SHA-512Linux 0.11 时代可能使用传统的crypt函数进行加密然后与/etc/passwd文件中存储的加密密文进行比对。如果解密失败或者密码不匹配login会退出返回错误码 1。此时父进程init的wait()捕获到退出信号会再次fork()一个子进程再次运行getty让用户重新输入用户名和密码形成一个循环。4.2 构建用户环境一旦验证通过login会立刻开始为用户构建专属的环境切换目录把当前进程的工作目录切换到用户在/etc/passwd中指定的家目录例如/home/root。设置权限根据口令文件设置进程的组 ID 和用户 ID。设置环境变量它会初始化最基本的变量例如HOME/home/root家目录SHELL/bin/bash用户默认使用的 ShellUSERroot用户名PATH/bin:/usr/bin查找命令的路径“欢迎信息”它会在屏幕上打印/etc/motd每日消息Message Of The Day的内容并检查/var/spool/mail/root文件告诉用户是否有未读的新邮件。4.3 执行登录 Shell当环境准备好后login会调用execve执行口令文件中指定的 Shell如果没有指定就会使用默认的/bin/sh。它传递的参数 argv 中有一个非常重要的细节argv[0]的第一个字符必须是-。五、 最终的主角登录 Shell 与配置文件当 Shell 程序启动后它会首先检查参数argv[0][0]。如果发现它是一个减号-Shell 就会知道自己是被作为一个“登录 Shell”运行的。这与普通用户在图形终端里打开一个xterm运行的 Shell 不同登录 Shell 肩负着“初始化整个交互环境”的重任。5.1 脚本加载链登录 Shell 会先后执行以下脚本/etc/profile这是系统级别的全局配置文件。在这里系统管理员可以设置对所有用户生效的环境变量如PATH、umask等。~/.profile这是当前用户在自己家目录下的个人配置文件。用户可以将自己的个性化别名、自定义 PATH 配置写在这里。ENV环境变量配置如果在~/.profile中定义了ENV环境变量Shell 下一步会加载该变量指定的配置文件。5.2 用户交互的起点一旦这些初始化脚本运行完毕Shell 就会打印出命令提示符如[plinux root]# _。从此用户与计算机的正式对话开始了。用户在终端上输入的每一条命令例如ls -l、grep实际上都是 Shell 通过fork()和execve()创建出一个子进程并等待其完成的过程。六、 流程总览Mermaid 图解登录接力赛为了直观地展示从init到 Shell 的整个执行流我们将书中图 4-5 进行重构结合上面分析的进程替换与状态转移绘制出如下 Mermaid 流程图渲染错误:Mermaid 渲染失败: Parse error on line 3: ...rkIdle[任务0 fork出 任务1(PID1)] -- Exe -----------------------^ Expecting SQE, DOUBLECIRCLEEND, PE, -), STADIUMEND, SUBROUTINEEND, PIPE, CYLINDEREND, DIAMOND_STOP, TAGEND, TRAPEND, INVTRAPEND, UNICODE_TEXT, TEXT, TAGSTART, got PS结语从内核到用户的漫长接力在 Linux 0.11 中虽然为了精简作者让 init 进程直接execve了/bin/sh但这仅仅是为了演示目的。在真实的操作系统中必须经历init-getty-login-shell这样层层递进的“接力赛”才能在保证系统安全性密码验证、隔离性进程组与控制终端、**可配置性/etc/inittab 和配置文件**的基础上为用户提供一整套完整的交互环境。这一层层的守护机制让 Linux 能够从一个纯粹的内核态程序顺滑地过渡到支持多用户、多终端、安全隔离的通用操作系统。现在我们的 Linux 0.11 试验场终于正式向用户开放了