open-courses
  • 公开课笔记
  • CMU 15-445/645 Database Systems
    • Relational Data Model
    • Advanced SQL
    • Database Storage
    • Buffer Pools
    • Hash Tables
    • Tree Indexes
    • Index Concurrency Control
    • Query Processing
    • Sorting&Aggregations
    • Join Algorithms
    • Query Optimization
    • Parallel Execution
    • Embedded Database Logic
    • Concurrency Control Theory
    • Two Phase Locking
    • Timestamp Ordering Concurrency Control
    • Multi-Version Concurrency Control
    • Logging Schemes
    • Database Recovery
    • Introduction to Distributed Databases
    • Distributed OLTP Databases
    • Distributed OLAP Databases
  • UCB - CS162
    • OS intro
    • Introduction to the Process
    • Processes, Fork, I/O, Files
    • I/O Continued, Sockets, Networking
    • Concurrency: Processes & Threads
    • Cooperating Threads, Synchronization
    • Semaphores, Condition Variables, Readers/Writers
    • Scheduling
    • Resource Contention & Deadlock
    • Address Translation, Caching
    • File System (18,19,20)
    • Distributed Systems, Networking, TCP/IP, RPC (21,22)
    • Distributed Storage, Key-Value Stores, Security (23)
    • Security & Cloud Computing (24)
    • Topic: Ensuring Data Reaches Disk
  • MIT - 6.006
    • Sequence and Set Interface
    • Data Structure for Dynamic Sequence Interface
    • Computation Complexity
    • Algorithms and Computation
    • Structure Of Computation
    • Graph & Search
    • Tree & Search
    • Weighted Shortest Paths
    • String Matching, Karp-Rabin
    • Priority Queue Interface & Implementation
    • Dictionary Problem & Implementation
    • Sorting
    • Dynamic Programming
    • Backtracking
    • Self-Balancing Tree
  • MIT - 6.824
    • 2PC & 3PC
    • Introduction and MapReduce
    • RPC and Threads
    • Primary/Backup Replication
    • Lab: Primary/Backup Key/Value Service
    • Google File System (GFS)
    • Raft
    • Lab: Raft - Leader Election
    • Lab: Raft - Log Replication
  • Stanford-CS107
    • 原始数据类型及相互转化
    • 指鹿为马
    • 泛型函数
    • 泛型栈
    • 运行时内存结构
    • 从 C 到汇编
    • 函数的活动记录
    • C 与 C++ 代码生成
    • 编译的预处理过程
    • 编译的链接过程
    • 函数的活动记录续、并发
    • 从顺序到并发和并行
    • 信号量与多线程 1
    • 信号量与多线程 2
    • 复杂多线程问题
    • 函数式编程 - Scheme 1
    • 函数式编程 - Scheme 2
    • 函数式编程 - Scheme 3
    • 函数式编程 - Scheme 4
    • 函数式编程 - Scheme 5
    • Python 基础
  • MIT - 6.001 - SICP
    • 什么是程序
    • 程序抽象
    • 替代模型
    • 时间/空间复杂度
    • 数据抽象
    • 高阶函数
    • Symbol
    • 数据驱动编程与防御式编程
    • 数据抽象中的效率与可读性
    • 数据修改
    • 环境模型
    • 面向对象-消息传递
    • 面向对象 - Scheme 实现
    • 构建 Scheme 解释器
    • Eval-Apply Loop
    • Normal Order (Lazy) Evaluation
    • 通用机
    • 寄存器机器
    • 子程序、栈与递归
    • 在寄存器机器中执行
    • 内存管理
  • MIT - 6.046
    • Randomized Algorithms
    • Skip Lists
  • System Design
    • Twitter
    • Cache Consistency & Coherence
  • DDIA 笔记
    • Replication
    • Transactions
    • The Trouble with Distributed Systems
    • Consistency & Consensus
  • Papers We Love
    • Consistent Hashing and Random Trees (1997)
    • Dynamic Hash Tables (1988)
    • LFU Implementation With O(1) Complexity (2010)
    • Time, Clocks, and the Ordering of Events in a Distributed System (1978)
    • Dapper, a Large-Scale Distributed Systems Tracing Infrastructure (2010)
    • Gorilla: A Fast, Scalable, In-Memory Time Series Database (2015)
  • Release It 笔记
    • Anti-patterns & Patterns in Microservice Architecture
  • Database Design
    • Log Structured Merge (LSM) Tree & Usages in KV Stores
    • Prometheus
Powered by GitBook
On this page
  • Process Scheduling
  • Process Control Block (PCB)
  • Kernel Scheduler
  • System Call
  • Interrupt
  • Operations on Process
  • How Does Kernel Provide Services
  • UNIX System Structure
  • Key Unix I/O Design Concepts
  • I/O & Storage Layers
  • Connecting Processes, Filesystem, and Users
  • C API Standard Streams
  1. UCB - CS162

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 来选择下一个运行的进程,这个过程可以用如下代码概括:

while (true) {
    if (readyProcesses(PCBs)) {
        nextPCB = selectProcess(PCBs);
        run(nextPCB);
    } else {
        run_idle_process();
    }
}

代码中的 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 调用系统进程时,处理步骤如下:

  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,如以下程序所示:

pid.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 1024
int main(int argc, char *argv[])
{
  int c;
  
  pid_t pid = getpid();
  
  printf("My pid: %d\n", pid);
  
  c = fgetc(stdin);
  exit(0);
}

create a process from a process

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

> 0: 在 parent process 中,且返回值为 child process 的 pid
= 0: 在 child process 中
< 0: fork 失败,在原 process 中

示例代码如下:

fork1.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 1024
int main(int argc, char *argv[])
{
  char buf[BUFSIZE];
  size_t readlen, writelen, slen;
  pid_t cpid, mypid;
  pid_t pid = getpid();
  printf("Parent pid: %d\n", pid);
  cpid = fork();
  if (cpid) > 0) {
    mypid = getpid()
    printf("[%d] parent of [%d]\n", mypid, cpid);
  } else if (cpid == 0) {
    mypid = getpid();
    printf("[%d] child\n", mypid);
  } else {
    perror("Fork failed");
    exit(1);
  }
  exit(0);
}

UNIX Process Management

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

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

  • UNIX wait:等待 child process 结束

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

wait example

//
cpid = fork();
if (cpid > 0) {
  mypid = getpid();
  printf("[%d] parent of [%d]\n", mypid, cpid);
  tcpid = wait(&status);
  printf("[%d] bye %d\n", mypid, tcpid);
} else if (cpid == 0) {
  mypid = getpid();
  printf("[%d] child\n", mypid);
}
//

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 后的行为。

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>

#include <unistd.h>
#include <signal.h>

void signal_callback_handler(int signum)
{
  printf("Caught signal %d - phew!\n", signum);
  exit(1);
}

int main() {
  signal(SIGINT, signal_callback_handler);
  
  while (1) {}
}

Process races

if (cpid > 0) {
  mypid = getpid();
  printf("[%d] parent of [%d]\n", mypid, cpid);
  for (i=0; i<100; i++) {
    printf("[%d] parent: %d\n", mypid, i);
  }
} else if (cpid == 0) {
  mypid = getpid();
  printf("[%d] child\n", mypid);
  for (i=0; i>-100; i--) {
    printf("[%d] child: %d\n", mypid, i);
  }
}

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 的代码示例:

file_op.c
#include <stdio.h>
// character oriented
int fputc(int c, FILE *fp);
int fputs(const char *s, FILE *fp);
int fgetc(FILE *fp);
char *fgets(char *buf, int n, FILE *fp);
// block oriented
size_t fread(void *ptr, size_t size_of_elements,
             size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements,
              size_t number_of_elements, FILE *a_file);

// formatted
int fprintf(FILE *restrict stream, const char *restrict format, ...);
int fscanf(FILE *restrict stream, const char *restrict format, ...);

// stream positioning
int fseek(FILE *stream, long int offset, int whence);
long int ftell(FILE *stream);
void rewind(FILE *stream);

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

#include <stdio.h>

#define BUFLEN 256
FILE *outfile;
char mybuf[BUFLEN];

int storetofile() {
  char *instring;
  outfile = fopen("/usr/homes/testing/tokens", "w+");
  if (!outfile) {
    return (-1);
  }
  while (1) {
    instring = fgets(*mybuf, BUFLEN, stdin);
    
    if (!instring || strlen(instring) == 0) break;
    
    if (fputs(instring, outfile) < 0) break;
  }
  fclose(outfile);
}

C Low Level I/O

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

#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>

int open (const char *filename, int flags [, mode_t mode])
int creat (const char *filename, mode_t mode)
int close (int filedes)

ssize_t read (int filedes, void *buffer, size_t maxsize)
// returns bytes read, 0 => EOF, -1 => error
ssize_t write (int filedes, const void *buffer, size_t size)
// returns bytes written
off_t lseek (int filedes, off_t offset, int whence)

int fsync (int fildes)
// wait for i/o to finish
void sync (void)
// wait for ALL to finish

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

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

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 1024

int main(int argc, char *argv[])
{
  char buf[BUFSIZE];
  ssize_t writelen = write(STDOUT_FILENO, "I am a process.\n", 16);
  ssize_t readlen = read(STDIN_FILENO, buf, BUFSIZE);
  ssize_t strlen = snprintf(buf, BUFSIZE, "Got %zd chars\n", readlen);
  writelen = strlen < BUFSIZE ? strlen : BUFSIZE;
  write(STDOUT_FILENO, buf, writelen);
  exit(0);
}

Syscall Level

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

File System Level

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

  • 它的位置

  • 它的状态

  • 如何访问它

Lower level Driver

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

struct file_operations {
  struct module *owner;
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  //...
}

这些 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 得以实现。

PreviousIntroduction to the ProcessNextI/O Continued, Sockets, Networking

Last updated 6 years ago

before
after