操作系统启动后到底做了什么
-
CPU Reset → Firmware → Loader → Kernel
_start()
→ 第一个程序/bin/init
→ 程序 (状态机) 执行 + 系统调用 -
操作系统会加载 “第一个程序”
-
寻找启动程序代码
-
if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/admin-guide/init.rst for guidance.");
-
linux中的pstree的systemd的来历
-
fork()
- 操作系统:状态机的管理者
- C 程序 = 状态机
- 初始状态:
main(argc, argv)
- 程序可以直接在处理器上执行
- 初始状态:
- 虚拟化:操作系统在物理内存中保存多个状态机
- 通过虚拟内存实现每次 “拿出来一个执行”
- 中断后进入操作系统代码,“换一个执行”
- C 程序 = 状态机
- int fork();
- 立即复制状态机 (完整的内存)
- 新创建进程返回 0
- 执行 fork 的进程返回子进程的进程号
- 因为状态机是复制的,因此总能找到 “父子关系”
- 因此有了进程树 (pstree)
execve()
- 状态机管理:替换状态机
- int execve(const char *filename, char * const argv, char * const envp);
- 执行名为
filename
的程序 - 允许对新状态机设置参数
argv
(v) 和环境变量envp
(e) - 刚好对应了
main()
的参数!
- 执行名为
- int execve(const char *filename, char * const argv, char * const envp);
- 环境变量:“应用程序执行的环境”
- 使用env命令查看
PATH
: 可执行文件搜索路径PWD
: 当前路径HOME
: home 目录DISPLAY
: 图形输出PS1
: shell 的提示符
export
: 告诉 shell 在创建子进程时设置环境变量
- 使用env命令查看
- _exit()
- 状态机管理:终止状态机
- void _exit(int status)
- 销毁当前状态机,并允许有一个返回值
- 子进程终止会通知父进程
- void _exit(int status)
- 结束程序执行的三种方法
exit(0)
-stdlib.h
中声明的 libc 函数- 会调用
atexit
(清理空间,安全退出)
- 会调用
_exit(0)
- glibc 的 syscall wrapper- 执行 “
exit_group
” 系统调用终止整个进程 (所有线程) - 不会调用
atexit
- 执行 “
- syscall(SYS_exit, 0)
- 执行 “
exit
” 系统调用终止当前线程 - 不会调用
atexit
- 执行 “
- 状态机管理:终止状态机
- 程序的创建执行和销毁过程
系统初始化的程序,通常是init
(在一些现代系统如 Fedora、Ubuntu 上通常是systemd
),负责进一步初始化操作系统并启动其他服务或进程。下面,我们将详细探讨在操作系统启动第一个程序后,如何使用fork
,execve
,_exit
来创建、执行和销毁程序的过程。
程序的创建、执行和销毁
-
初始化和启动首个进程
- 操作系统通过加载并执行
init
程序(或者在一些系统中是systemd
)开始。这个程序成为系统中的第一个进程(通常是进程号为1)。
- 操作系统通过加载并执行
-
进程的创建 (使用
fork
)- 当系统需要创建一个新的进程时,
init
(或任何正在运行的进程)会调用fork()
系统调用。fork()
创建一个与父进程几乎完全相同的子进程,拥有相同的内存映像和运行状态,但有一个新的唯一进程标识符。 - 父进程中
fork()
返回新创建的子进程的进程ID,而在子进程中fork()
返回0。
- 当系统需要创建一个新的进程时,
-
进程的执行 (使用
execve
)- 通常在
fork()
之后,子进程需要运行与父进程不同的代码。为此,子进程会调用execve()
系统调用,这个调用加载一个新的程序到当前进程的地址空间,并开始执行这个程序,从其main
函数开始。 execve()
需要指定程序的路径、传递给程序的参数列表(argv
),以及环境变量列表(envp
)。这意味着执行后,子进程的原有代码和数据将被新程序替换。
- 通常在
-
进程的终止 (使用
_exit
)- 当程序执行完成后,它可以通过调用
_exit()
系统调用来终止。这个调用立即结束进程的执行,并将一个状态码返回给操作系统,这个状态码可以被父进程通过wait()
系列的调用来检索。 - 使用
_exit()
而不是标准的exit()
函数,因为后者还会执行标准库注册的各种清理函数(如由atexit()
注册的函数),这在某些情况下可能不是必需或期望的。
- 当程序执行完成后,它可以通过调用
实例:简单的 shell
假设 init
系统已经启动,并且我们需要从一个简化的 shell 启动一个程序,如 ls
命令。以下是这个过程的概述:
- Shell 进程调用
fork()
,创建一个子进程。 - 子进程使用
execve()
调用来加载并运行ls
命令。 ls
命令执行完成后,子进程调用_exit()
来结束执行,返回执行结果状态。- Shell 进程使用
wait()
等待子进程结束,并获取其终止状态。
这样,我们就看到了一个完整的程序生命周期:创建、执行和终止,都是通过操作系统提供的系统调用来管理的。