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 的安全转变,需要许多细节来保证:
转变的例子如下图所示:
在转变的过程中,会存下 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 调用系统进程时,处理步骤如下:
定位调用参数,在寄存器或 user stack 中
复制调用参数,将参数从 user memory 移动到 kernel memory 中,保证 kernel 不被入侵
验证调用参数
执行系统调用 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