操作系统上的进程

65 阅读4分钟
  • Q1: 操作系统启动后到底做了什么?

  • Q2: 操作系统如何管理程序 (进程)?

  • 主要内容

    • 虚拟化:操作系统上的进程
    • 进程管理 API

从系统启动到第一个进程

回顾 thread-os.c 的加载过程

  • CPU Reset → Firmware → Boot loader → Kernel 

_start()


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

  • RTFSC (latest Linux Kernel)
    • 如果没有指定启动选项 

init=,按照 “默认列表” 尝试一遍

    • 从此以后,Linux Kernel 就进入后台,成为 “中断/异常处理程序”

程序:状态机

  • C 代码视角:语句
  • 汇编/机器代码视角:指令
  • 与操作系统交互的方式:syscall

定制最小的 Linux

没有存储设备,只有包含两个文件的 “initramfs”

  • linux-minimal.zip
$ tree .
. 
├── bin 
│ └── busybox (可以在我们的Linux里直接执行)
└── init

加上 vmlinuz (内核镜像) 就可以在 QEMU 里启动了

小结:应用程序视角的操作系统

Linux 操作系统启动流程

  • CPU Reset → Firmware → Loader → Kernel 

_start() → 第一个程序 

/bin/init → 程序 (状态机) 执行 + 系统调用


操作系统为 (所有) 程序提供 API(三大系统调用)

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

fork()

image.png

执行流通过fork()分叉

操作系统:状态机的管理者

C 程序 = 状态机

  • 初始状态:

main(argc, argv)

  • 程序可以直接在处理器上执行

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

通过fork()来复制一份一样的状态机,除了fork()的返回值不一样

操作系统这时候就变成了并发程序

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

状态机管理:创建状态机

如果要创建状态机,我们应该提供什么样的 API?

UNIX 的答案: fork

  • 做一份状态机完整的复制 (内存、寄存器现场)

int fork();

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

记住 Fork 了!

因为状态机是复制的,因此总能找到 “父子关系”

  • 因此有了进程树 (pstree)
systemd-+-accounts-daemon---2*[{accounts-daemon}]
|-agetty 
|-atd 
|-automount---2*[{automount}]
|-avahi-daemon---avahi-daemon
|-cron
|-dbus-daemon
|-irqbalance---{irqbalance}
|-lxcfs---7*[{lxcfs}] ...

fork()无情的复制机器

execve()

状态机管理:替换状态机

光有 fork 还不够,怎么 “执行别的程序”?

UNIX 的答案: execve

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

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

  • 执行名为 

filename 的程序

  • 允许对新状态机设置参数 

argv (v) 和环境变量 

envp (e)

  • 刚好对应了 

main() 的参数!

环境变量

“应用程序执行的环境”

  • 使用 

env 命令查看

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

_exit()

状态机管理:终止状态机

有了 fork, execve 我们就能自由执行任何程序了,最后只缺一个销毁状态机的函数!

UNIX 的答案: _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

不妨试一试

结束当前进程执行的四种方式

  • return, exit, _exit, syscall
  • exit-demo.c
    • 用 strace 观察程序的执行

总结

  • Q1: 操作系统启动后到底做了什么?
  • Q2: 操作系统如何管理程序 (进程)?

Take-away messages

  • 对 “操作系统” 的完整理解
    • CPU Reset → Firmware → Loader → Kernel 

_start() → 执行第一个程序 

/bin/init → 中断/异常处理程序

    • 一个最小的 Linux 系统的例子
  • 进程管理 API
    • fork, execve, exit: 状态机的复制、重置、销毁
    • 理论上就可以实现 “各种功能” 了!