OSTEP
Chapter 2 操作系统介绍
操作系统的功能: 负责让程序运行更加容易(甚至允许允许同时多个程序), 允许程序共享内存, 让程序与硬件设备交互
虚拟化 : 操作系统将 物理资源 (如CPU, 内存) 转换成更加通用, 更加强大且易于使用的虚拟形式
因为虚拟化让许多程序运行(从而共享CPU), 让许多程序可以同时访问自己的指令和数据(从而共享内存), 让许多程序访问设备(从而共享磁盘等), 所以操作系统有时被称为资源管理器. 每个CPU, 内存和磁盘都是系统的资源, 操作系统扮演的主要角色就是管理这些资源, 以做到高效或公平, 或者实际上考虑其他许多可能的目标
2.1 虚拟化CPU
运行以下程序, 假设以下程序的名称为cpu
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <assert.h>
#include "common.h"
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: cpu <string>\n");
exit(1);
}
char *str = agrv[1];
while(1) {
spin(1);
printf("%s\n", str);
}
return 0;
}
试图在bash运行这个程序的多个实例
unix> ./cpu A & ; ./cpu B & ; ./cpu C & ; ./cpu D &
[1] 7353
[2] 7354
[3] 7355
[4] 7356
A
B
D
C
A
B
D
C
A
C
B
D
...
CPU只有一个, 但是程序可以同时运行, 是利用虚拟化CPU实现的
实际上, 在某些硬件的帮助下, 操作系统负责提供这种 假象 , 即系统拥有非常CPU的假象. 将单个CPU转换成看似无限数量的CPU, 从而让许多程序看似同时运行, 这就是所谓的虚拟化CPU
2.2 虚拟化内存
再来看一个程序, 它通过malloc来分配内存, 该程序的名字为mem
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
int main(int argc, char *argv[]) {
int *p = malloc(sizeof(int));
assert(p != NULL);
printf("(%d) memory address of p: %08x\n", getpid(), (unsigned)p);
*p = 0;
while (1) {
spin(1);
*p = *p + 1;
printf("(%d) p: %d\n", getpid(), *p);
}
return 0;
}
在bash上面运行
unix> ./mem
(2134) memory address of p: 00200000
(2134) p: 1
(2134) p: 2
(2134) p: 3
(2134) P: 4
(2134) p: 5
^C
同时运行多个实例
unix> ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) memory address of p: 00200000
(24114) memory address of p: 00200000
(24113) p: 1
(24114) p: 1
(24114) p: 2
(24113) p: 2
(24114) p: 3
(24113) p: 3
(24113) p: 4
(24114) p: 4
...
每个程序都在00200000处分配了内存, 但它们相互不影响, 就好像是每个正在运行的程序都有自己的私有内存而不是和其他程序共享内存
实际上, 每个进程访问自己的 私有虚拟地址空间 , 操作系统以某种方式映射到机器的物理内存上, 这叫做 操作系统虚拟化内存
2.3 并发
下面是一个多线程程序的例子
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
volatile int counter = 0;
int loops;
void *worker(void *arg) {
int i;
for (i = 0; i < loops; i++) {
counter++;
}
return NULL;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: threads<value>\n");
exit(0);
}
loops = atoi(argv[1]);
pthread_t p1, p2;
printf("Initial value: %d\n", counter);
Pthread_create(&p1, NULL, worker, NULL);
Pthread_create(&p2, NULL, worker, NULL);
Pthread_join(p1, NULL);
Pthread_join(p2, NULL);
printf("Final value: %d\n", counter);
return 0;
}
可以将线程看作是与其他函数在同一内存空间运行的函数, 并且每次都有多个线程处于活动状态
unix> ./thread 1000
Initial value: 0
Final value: 2000
2.5 设计目标
操作系统取得CPU, 内存, 磁盘等物理资源, 并对它们虚拟化, 它处理与并发有关的麻烦且棘手的问题, 同时持久地存储文件, 从而使它们长期安全
Chapter 4 进程
进程的非正式定义非常简单: 进程就是运行中的程序
操作系统通过 虚拟化CPU 提供几乎有无数个CPU可用的假象, 通过让一个进程只运行一个时间片, 然后切换到其他进程, 操作系统提供了存在多个虚拟CPU的假象. 这就是 时分共享CPU技术 , 允许用户允许多个并发进程
上下文切换, 它让操作系统能够停止运行一个程序, 并开始在给定的CPU上运行另一个程序
时分共享是操作系统共享资源所使用的最基本技术之一, 通过允许资源由一个实体使用一小段时间, 然后由另一个实体使用一小段时间, 如此下去所谓的资源就可以被所有进程共享. 时分共享技术对应的技术是空分共享技术, 资源在空间上被划分为希望使用它的人
4.1 进程
程序在运行时可以读取或更新的内容就是程序的 机器状态
进程的机器状态的组成部分包括: 内存, 寄存器, 特殊寄存器(如程序计数器, 栈指针, 帧指针)
4.2 进程API
所有的操作系统都必须以某种形式提供这些API
- 创建 : 操作系统创建新进程的接口
- 销毁 : 系统提供的强制销毁进程的接口
- 等待 : 等待进程停止运行的接口
- 其他控制 : 例如暂停进程, 恢复进程
- 状态 : 获取进程状态信息的接口, 例如运行时间, 处于什么状态
4.3 进程创建的更多细节
操作系统运行程序的第一件事就是将磁盘中的代码和所有静态数据全部加载到内存中, 加载到进程的地址空间中
加载到内存之后, 操作系统必须为程序的运行时栈分配内存. 操作系统分配内存并提供给进程, 当然它也可能会用参数初始化栈, 具体来讲就是会将参数填进main函数, 即argc和argv数组
操作系统也可能为程序的堆分配内存. 在C语言程序中, 程序通过malloc请求内存空间, 用free释放内存空间, 随着malloc库的API运行, 操作系统需要不断分配更多内存给进程以满足这些调用
操作系统还将执行一些其他初始化的任务, 特别是与输入输出(I/O)相关的任务
最后一项任务: 启动程序, 在入口(main)处运行, 操作系统将CPU的控制权转交给新创建的进程, 程序开始执行
4.4 进程状态
进程可以处于以下3种状态
- 运行 : 在运行状态, 进程正在处理器上运行
- 就绪 : 在就绪状态下, 进程已准备好运行, 但由于一些原因, 操作系统选择不在此时运行
- 阻塞 : 在阻塞状态下, 一个进程执行了某种操作, 直到发生其它事件时才会准备运行
从就绪到运行就意味着该进程已经被 调度 , 从运行转移到就绪就意味着该进程被 取消调度 , 一旦进程被阻塞(例如发生了一些IO操作), OS将保持进程的这种状态, 直到发生某种事件(例如IO操作完成), 进程再次转入就绪状态
4.5 数据结构
操作系统是一个程序, 和其他程序一样, 它有一些关键的数据结构来追踪各种相关的信息.
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};
// the diffrent states a process can be in
enum proc_state {UNSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE};
// the information xv6 tracks about each process
// including its register context and state
struct proc {
char *mem; // Start of process memory
unsigned int sz; // Size of process memory
char *kstack; // Bottom of kernel stack, for this process
enum proc_state state; // Process state
int pid; //Process ID
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
struct context context; // Switch here to run process
struct trapframe *tf // Trap frame for the current interrupt
};
对于停止的进程, 寄存器上下文将保存其寄存器的内容, 当一个进程停止时, 它的寄存器将被保存到内存位置, 通过恢复这些寄存器(把它们的值放到实际的寄存器), 操作系统可以恢复运行该进程, 这种技术称为 上下文切换
有时候系统会有一个 初始状态 , 表示进程在创建时处于的状态, 一个进程可以处于已退出但尚未清理的 最终状态
Chapter 5 进程API
(这一部分建议学习Linux系统编程时候再系统学习, 简单了解即可)
5.1 fork() 系统调用
系统调用fork()用于创建新进程
新创建的进程称为 子进程 , 原来的进程称为 父进程 . 子进程不会从main()函数开始执行, 而是直接从fork()系统调用返回, 就好像是它自己调用了fork()
在一些情况下. 子进程可能比父进程先运行, 实际上CPU调度程序决定了某个时刻哪个进程被执行
5.2 wait() 系统调用
父进程需要等待子进程执行完毕, 这项任务由wait()系统调用完成, 或者是更完整的兄弟接口waitpid()
5.3 exec() 系统调用
exec() 系统调用可以让子进程执行与父进程不同的程序
5.5 其他API
可以通过kill() 系统调用向进程发送信号, 包括要求进程睡眠, 终止或者其他有用的指令