进程实际上是一个权利受限的执行环境,我们讲过,简单的虚拟线程有这样的问题,每个线程都能访问每个线程的内存,内存翻译机制可以保护我们可以访问的内存,即一个受保护的内存块。它被操作系统中的一个实体独占,这个实体叫做进程。它包括一个受限的地址空间和一个或多个线程,它拥有一些文件描述符和文件系统上下文。
进程提供了内存保护抽象,在保护和效率之间有一个基本的权衡,如果你在同一个进程中有一堆线程,它们之间可以很容易地通信,因为它们共享相同的内存,它们可以通过一个写入内存,另一个读取内存来通信,但是它们之间可能会互相覆盖导致并发安全问题。有时候你想要高性能,提高并行性,你会想要在一个进程中有很多线程。但是当你想要保护时,你想要限制进程之间的通信,所以进程之间的通信故意变得更加困难,这就是我们得到保护的方式。
这是一个单线程的进程还有一个多线程的进程。对于单线程的进程,只有一组寄存器还有栈内存。对于多线程的进程,代码段,数据,文件是共享的,但是每个线程有独立的寄存器还有栈。当我们从一个线程切换到另一个线程时,为了给人一种多处理的错觉,我们需要从第一个线程切换出寄存器,这样我们就能从第二个线程把它们加载回来。
线程封装了并发性,为什么进程要用多线程?
- 一个是并行性(Parallelism)。如果你有多个核,通过在同一个进程中有多个线程,可以让许多任务同时处理。
- 另一个是为了并发(Concurrency)。并发性就是大多数线程大部分时间都在休眠的情况,比如某个线程需要做一些 I/O,开始 I/O 时进入睡眠,然后在 I/O 完成时醒来,那么 CPU 不必等待 I/O 完成,而是可以做其他的事情。这就是多线程的好处。
那么为什么我们需要进程来保证可靠性、安全性和隐私性呢?
- 对于可靠性:Bug 只会覆盖一个进程的内存,恶意的或者被破坏的进程不能干扰其他进程。
- 对于安全还有隐私性:进程不能修改其他进程的内存
- 公平性:共享磁盘,CPU 等资源。
这个保护主要通过翻译机制实现的,每个进程地址空间通过翻译机制映射到物理内存,这个翻译是进程本身不可控的。那么操作系统是如何保证进程无法修改页表从而影响翻译呢?这就引入了下一个话题:双模式操作(Dual-mode Operation)
硬件至少提供了两种模式:内核模式(Kernel mode,或管理模式 Supervisor mode)和用户模式(User mode)。当你在用户模式下运行时某些操作会被禁止,比如当你在用户模式时你不能改变你使用的页表,这只有在内核模式下的操作系统才能做到。在用户模式下还不能禁止中断,这样,一个如果想计算PI最后一位的进程就不能阻止其他进程在计时器结束时获得CPU时间。在用户模式下你也被阻止直接与硬件交互等等,因此不能破坏磁盘上的文件。
我们小心控制的用户模式和内核模式之间的转换的是什么?包括系统调用(System call),中断,异常等等。如上图中的流程所示,我们有用户进程,它们对内核进行系统调用,从用户模式切换到内核模式执行系统调用中对应的操作,完成后退出内核模式,系统调用返回。
这是一个典型的Unix系统结构中各个模式分别包含什么的表格:
- 用户模式包含你所有的程序和库等等。
- 系统调用:表示可以安全访问各种资源的代码。
- 内核模式包含:信号处理,I/O 系统,文件系统,块交换 I/O 系统,磁盘驱动,CPU 调度,页交换,虚拟内存管理等等。内核通过接口访问并控制硬件。
举个例子,我们有硬件有内核模式和用户模式。硬件可能会 exec 创建一个新进程。用户模式下的系统调用会进入内核模式,执行完操作后返回用户模式。中断可能导致用户模式进入内核,然后可能检查硬件比如 I/O 就绪,最终从中断中返回。在你除以零或者发生一个页面错误,会发生一个异常,导致进入内核模式,然后最终返回。
总共有三种可能触发用户模式到内核模式转换的操作类型:
- 系统调用:
- 调用一个系统服务,例如 exit 退出进程
- 函数调用,但是涉及访问进程外的一些资源
- 目前没有系统函数需要的内存地址
- RPC 远程函数调用
- Marshall 寄存器中的系统调用id和参数并执行系统调用
- 中断:
- 外部异步事件触发上下文切换,例如,定时器,I/O设备
- Trap 或者异常:
- 内部同步事件触发上下文切换
- 例如,违反保护(segmentation fault),除以零,…
如果你注意到这里有两个进程,一个绿色的,一个黄色的。灰色的代表的是操作系统的内存。