Processes, Fork, I/O, Files

从第二课的讨论中,我们已经了解:

  • 在 user processes 与 kernel 之间切换

  • kernel 可以切换不同的用户进程,实现并发

  • 保护 user processes 之间,以及 user processes 与 kernel 之间互不干扰

但你可能会有很多疑问:

  • OS 如何表示进程?

  • OS 如何选取下一个运行的进程?

  • OS 在切换进程时,如何记录进程的运行时信息?

  • kernel 的 stack、heap 是怎样的?

  • 大量切换进程,同时记录它们的运行时信息,会不会浪费内存?

本节我们就将进入这些细节中了解 OS 中的进程。

Process Scheduling

Process Control Block (PCB)

在 Kernel 中用于表示用户进程的数据结构叫作 PCB。PCB 包含进程的重要信息,如:

  • 进程状态 (running, ready, blocked, ...)

  • 寄存器状态

  • Process Id (PID)、用户、优先级……

  • 执行时间……

  • 内存空间、地址转化信息……

这些信息足够用来开始、恢复一个程序的执行。

Kernel Scheduler

Kernel Scheduler 将这些 PCB 组织起来,利用一些 scheduling algorithms 来选择下一个运行的进程,这个过程可以用如下代码概括:

代码中的 selectProcess,就隐藏着五花八门的调度策略。但不论是哪种调度策略,都需要考虑公平与效率,这在现实生活中也是如此。

System Call

Safe Kernel Mode Transfers

  • kernel 拥有自己的 kernel stack,kernel stack 与 user stack 不可以有公用的部分

  • 转变的过程必须完全可控,包括用户进程运行时环境的保存,system call 的参数移动(从 user space 到 kernel space)、参数检查以及运行结果从 kernel space 返回到 user space 的过程

为了防止恶意程序利用 kernel mode 或者破坏 kernel 本身,操作系统必须要在进程从 user mode 转变成 kernel mode 的过程中做足安保工作。实现 user mode 到 kernel mode 的安全转变,需要许多细节来保证:

转变的例子如下图所示:

before
after

在转变的过程中,会存下 user-level process 的相关信息,如寄存器中的数据,然后将参数复制到 kernel stack,切换到 kernel mode 执行系统调用,完毕后再返回 user mode。

Kernel System Call Handler

操作系统定义了一系列 subroutines (system call vector)提供给 user-level process 调用。操作系统记录着 system call number 到 handler 地址的映射关系,每当 user-level process 调用系统进程时,处理步骤如下:

  1. 定位调用参数,在寄存器或 user stack 中

  2. 复制调用参数,将参数从 user memory 移动到 kernel memory 中,保证 kernel 不被入侵

  3. 验证调用参数

  4. 执行系统调用 subroutine,并将结果复制回 user memory

Interrupt

System Call vs. Interrupt

system call 和 interrupt 都是进入 kernel operation 的机制。每当 user-level process 想要执行一些需要更高权限的操作时,就需要请求执行系统调用;每当外部装置(CPU 的外部)需要请求处理时,则通过制造一个 Interrupt 来请求系统处理。前者为主动、同步调用,后者为被动、异步调用,前者通常与当前程序息息相关,而后者通常与当前程序无关。

Interrupt Control

Interrupt 的处理应当对 user process 无感知。 interrupt handler 执行时操作系统会先 disable interrupts,等待 interrupt handler 执行完毕后才 re-enable interrupts,每个 interrupt 处理的过程都是一次从头执行到尾,无中断,无等待。只有 kernel 有权限 enable/disable interrupts,否则就没有王法了。Interrupt Controller 结构如下图所示:

Interrupts Safely

类似 system call:

  • 操作系统维护 interrupt vector,后者将 interrupt number 映射到 kernel 中相应的 subroutine

  • Kernel 维护 interrupt stack,与 user stack 完全独立

  • Interrupt Masking:保证 interrupt 能够不被中断

  • Atomic transfer of control

  • Transparent restartable execution,user process 对此无感知

Operations on Process

之前我们一直在讨论 process 中执行的内容,那么是否有一些操作是针对 process 本身的?

  • 创建 process

  • 杀死 process

  • ...

pid

操作系统根据 pid 唯一确定一个 process,如以下程序所示:

create a process from a process

系统调用 fork 可以创建复制当前 process,创建一个新的 process。fork 之后的程序实际上会被两个 processes 执行,每个 process 根据 fork 的返回值来判断自己是在 parent process 还是 child process:

示例代码如下:

UNIX Process Management

  • UNIX fork:复制当前 process,并运行复制后的 process

  • UNIX exec:改变当前运行的 process,抛弃复制的内容,直接执行新的程序

  • UNIX wait:等待 child process 结束

  • UNIX signal:向其它 process 发送 notification 的系统调用,如 ctrl+c

wait example

shell example

shell 就是一个 job control system,每次执行命令 shell 会 fork 一个 child process,然后立即 exec 相应的程序,同时 shell process 会 wait child process。

signal example

下面的例子通过 signal 调用用自定义的 SIGINT handler 替换默认 handler,使得 process 能够控制用户按下 ctrl+c 后的行为。

Process races

How Does Kernel Provide Services

UNIX System Structure

虽然应用需要通过 syscall 获取操作系统提供的服务,但我们通常看不到 syscall 本身,因为它被包装在编程语言的 runtime library 中,如下图所示:

同样的程序要在不同平台上兼容,依赖于 system call interface 的统一,有点类似网络层在 OSI 网络模型中的 narrow waist 地位,如下图所示:

Key Unix I/O Design Concepts

Unix I/O 接口的设计有自己的一套哲学,总结如下:

  • Uniformity

    • 文件操作、设备 I/O、进程间通信都通过 open、read/write、close 来实现

    • 允许程序之间 I/O 的 composition:如 "find | grep | wc" ...

  • Open before use

    • 提供访问控制和仲裁逻辑

    • 初始化内部结构

  • Byte-oriented

    • 尽管大部分数据都是以 block 为单位传输,但可以按 byte 寻址

  • Kernel buffered reads

    • Streaming and block devices looks the same

    • read blocks process, yielding processor to other task

  • Kernel buffered writes

    • Completion of out-going transfer decoupled from the application, allowing it to continue

  • Explicit close

I/O & Storage Layers

读文件的过程如下图所示:

应用通过 High Level I/O api 访问程序语言的 library,后者通过 syscall 访问 file system,file system 访问 I/O Driver,然后程序进入等待状态。数据读取完毕后,经由 I/O driver、file system 返回到 syscall 中缓存,最后经过 low level I/O 和 high level I/O 最终返回到应用。

写文件与读文件的主要不同在于,write 操作通常只把数据写到 library 的缓冲区,必须执行 flush 操作后数据才会写入 kernel 的缓冲区,但这实际上还未落盘,kernel 会在合适的时期或者接受到 sync 命令后执行落盘操作。

C High Level I/O

以下是 C 语言中 high level file api 的代码示例:

利用这些 api 读写文件的代码示例如下:

C Low Level I/O

用户通过 file handle 来操作 file,实际上 file handle 就是一个 int。

当 write 执行完毕后,data 只是处于落盘的过程中,但已经可以被读取,但此时它并不一定已经持久化。

使用 Low Level I/O 读写数据的例子如下:

Syscall Level

low level api 的参数准备完毕后,调用 syscall,进入 kernel mode 中的 subroutine 运行,后者调用 file system 层 api 后进入等待状态。

File System Level

File System 内部维护者描述文件的数据结构,这些描述信息包括:

  • 它的位置

  • 它的状态

  • 如何访问它

Lower level Driver

与计算机所使用的存储硬件本身相关,在启动时将这些 api 注册到 kernel 中:

这些 api 需要复合 kernel 与 driver 之间的标准协议。Drivers 通常可以被分成两部分:

  • 上半部分:向 system calls 提供接口支持

    • 实现一些 standard, cross-device 调用,如:open(), close(), read(), write(), ioctl(), strategy() 等

    • 上半部分会启动硬件 I/O 操作,然后将线程置于 sleep 状态

  • 下半部分:interrupt routine,当硬件 I/O 完毕后,发送中断信号,系统处理后唤醒相关线程。

一个 I/O 生命周期如下图所示:

Connecting Processes, Filesystem, and Users

默认每个 Process 都可以通过绝对路径(Absolute Paths)访问文件,同时每个 Process 都有一个 current working directory,因此也支持相对路径(Relative Paths)访问文件;此外由于 Process 属于执行用户,因此也支持相对该用户的 home 路径的相对路径访问。访问相对路径访问的过程很简单,就是把 current working directory/home directory 与相对路径拼接得到绝对路径,再访问。

C API Standard Streams

每个程序执行时,都有 3 个 streams 默认被打开:

  • FILE *stdin:normal source of input, can be redirected

  • FILE *stdout:normal source of output, can too

  • FILE *stderr: diagnostics and errors

其中 STDIN 和 STDOUT 的存在使得 UNIX 中的 composition 得以实现。

Last updated