引子:当内核完成初始化,一个“空壳”系统是如何变成可用的交互环境的?
在前面的代码分析中,我们见证了 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()执行完毕时,系统实际上已经运行在第一个用户进程——进程 0(idle 进程)的上下文中了。
随后,进程 0 调用了fork(),诞生了进程 1(init 进程)。本书中的 Linux 0.11 让进程 1 直接执行execve去加载/bin/sh,这是非常粗糙的做法。
在真实的类 UNIX 操作系统中,PID 1(Init 进程)是系统所有用户进程的“始祖”。它肩负着一项极为神圣且繁重的使命:统筹管理系统中所有(除内核线程外)的用户空间进程。
这意味着,PID 1 绝对不应该是一个简单的 Shell。在典型的 Linux 发行版中,PID 1 通常是著名的/sbin/init程序(在现代 Linux 中,可能会被systemd代替,但核心功能一致)。这个init程序就是用户态环境初始化的“总指挥”。
观察上面的流程图:
内核只负责“生”出进程 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-512,Linux 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(用户默认使用的 Shell)USER=root(用户名)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 流程图:
结语:从内核到用户的漫长接力
在 Linux 0.11 中,虽然为了精简,作者让 init 进程直接execve了/bin/sh,但这仅仅是为了演示目的。在真实的操作系统中,必须经历init->getty->login->shell这样层层递进的“接力赛”,才能在保证系统安全性(密码验证)、隔离性(进程组与控制终端)、**可配置性(/etc/inittab 和配置文件)**的基础上,为用户提供一整套完整的交互环境。
这一层层的守护机制,让 Linux 能够从一个纯粹的内核态程序,顺滑地过渡到支持多用户、多终端、安全隔离的通用操作系统。现在,我们的 Linux 0.11 试验场终于正式向用户开放了!