Linux 操作系统启动流程

CPU Reset → Firmware → Loader → Kernel _start() → 第一个程序 /bin/init → 程序 (状态机) 执行 + 系统调用syscal

操作系统会加载 “第一个程序”

  • RTFSC(latest Linux Kernel)
    • 如果没有指定启动选项 init=,按照 “默认列表” 尝试一遍
    • 从此以后,Linux Kernel 就进入后台,成为 “中断/异常处理程序”

操作系统为 (所有) 程序提供 API

  • 进程 (状态机) 管理
    • fork, execve, exit - 状态机的创建/改变/删除 ← 今天的主题
  • 存储 (地址空间) 管理
    • mmap - 虚拟地址空间管理
  • 文件 (数据对象) 管理
    • open, close, read, write - 文件访问管理
    • mkdir, link, unlink - 目录管理

进程(状态机)管理

虚拟化:操作系统在物理内存中保存多个状态机

  • 通过虚拟内存实现每次 “拿出来一个执行”
  • 中断后进入操作系统代码,“换一个执行”

创建状态机:fork

int fork();

  • 立即复制状态机 (完整的内存)
  • 新创建进程返回 0
  • 执行 fork 的进程返回子进程的进程号

Fork Bomb

模拟状态机需要资源——只要不停地创建进程,系统还是会挂掉的

1
2
3
4
5
6
7
8
9
:(){:|:&};:   # 刚才的一行版本

:() { # 格式化一下
: | : &
}; :

fork() { # bash: 允许冒号作为标识符……
fork | fork &
}; fork

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
int n = 2;
for (int i = 0; i < n; i++) {
fork();
printf("Hello\n");
}
for (int i = 0; i < n; i++) {
wait(NULL);
}
}

./a.out v.s. ./a.out | cat,会看到结果并不相同

原因:printf在终端输出时,会先存在LineBuffer,直到遇到\n等。输出至文件时,会存在FullBuffer,满一定额输出。fork时缓存也被复制了。

  • i=0,fork

  • i=0,printf ×2

  • i=1,fork Hello\n ×2

  • i=1,printf Hello\n ×4

  • i=2,结束 Hello\nHello\n ×4


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(int argc, char* argv[])
{
int rc = fork();
if(rc<0){
fprintf(stderr,"fork failed\n");
exit(1); //这些用于检测异常情况
}
else if (rc==0) printf("this is child process\n");
else printf("this is parent process\n");
return 0;
}

​ 如果在单CPU的系统上运行上面这段程序,程序所输出的结果是不确定的,有可能先输出 this is parent process,也有可能先输出 this is child process,因为在 int rc = fork(); 这行代码执行完之后,父进程和新创建的子进程就是操作系统当中并发的两个不同的进程了,他们两个谁先运行并输出信息就要取决于CPU调度程序了。

wait()系统调用

​ 父进程在使用fork()创建完子进程之后,可以执行wait()系统调用,或者是更完整的waitpid()接口,从而延迟自己的执行,直到子进程执行完,如果将上面fork()系统调用中的举例使用的代码进行部分修改,改为下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>


int main(int argc, char* argv[])
{
int rc = fork();
if(rc<0){
fprintf(stderr,"fork failed\n");
exit(1);
}
else if (rc==0) printf("this is child process\n");
else{
int wc = wait(NULL); //这一句代码是新添加的,父进程通过wait()等待子进程结束之后自己才继续执行
printf("this is parent process\n");
}
return 0;
}

这样,输出的结果就总是 this is child process 这句话先输出了,哪怕调度算法让父进程先于子进程执行,父进程执行到wait()处时,便会停止执行,开始等待,直至子进程运行结束。

替换状态机:execve

将当前运行的状态机重置成成另一个程序的初始状态

int execve(const char *filename, char * const argv, char * const envp);

  • 执行名为 filename 的程序
  • 允许对新状态机设置参数 argv (v) 和环境变量 envp (e)

环境变量

“应用程序执行的环境”

  • 使用 env 命令查看
    • PATH: 可执行文件搜索路径
    • PWD: 当前路径
    • HOME: home 目录
    • DISPLAY: 图形输出
    • PS1: shell 的提示符
  • export: 告诉 shell 在创建子进程时设置环境变量

终止状态机:_exit

立即摧毁状态机,void _exit(int status)

销毁当前状态机,并允许有一个返回值

结束程序执行的三种方法

exit 的几种写法 (它们是不同)

  • exit(0) - stdlib.h 中声明的 libc 函数
    • 会调用 atexit
  • _exit(0) - glibc 的 syscall wrapper
    • 执行 “exit_group” 系统调用终止整个进程 (所有线程)
      • 细心的同学已经在 strace 中发现了
    • 不会调用 atexit
  • syscall(SYS_exit, 0)
    • 执行 “exit” 系统调用终止当前线程
    • 不会调用 atexit