OSTEP 介绍

第 2 章 操作系统介绍

2.1 虚拟化 CPU

cpu.cview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: cpu <string>\n");
exit(1);
}
char *str = argv[1];

while (1) {
printf("%s\n", str);
Spin(1);
}
return 0;
}

程序会重复打印传入的字符串,Spin() 函数用于暂停 1 秒。

1
2
3
4
5
6
$ ./cpu "A"
A
A
A
A
^C

如果后台运行多个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
./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
...

只有一个处理器,但 4 个程序似乎在同时运行。

将单个 CPU 转化为看似无限数量的 CPU,从而让多个程序看起来同时运行,这就是虚拟化 CPU。

2.2 虚拟化内存

mem.cview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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) address pointed to by p: %p\n",
getpid(), p);
*p = 0;
while (1)
{
Spin(1);
*p = *p + 1;
printf("(%d) p: %d\n", getpid(), *p);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) address pointed to by p: 0x200000
(24114) address pointed to by p: 0x200000
(24113) p: 1
(24114) p: 1
(24114) p: 2
(24113) p: 2
(24113) p: 3
(24114) p: 3
(24113) p: 4
(24114) p: 4

可以看到两个程序在相同地址分配了内存,并独立更新该处的值。

这就是虚拟化内存,每个进程访问自己的私有虚拟地址空间,操作系统将其映射到机器的物理内存上。

注意,直接运行是无法得到相同地址的,需要禁止地址随机化,例如使用 setarch $(uname -m) -R ./mem & setarch $(uname -m) -R ./mem & 来运行。

2.3 并发

threads.cview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
#include "common_threads.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 <loops>\n");
exit(1);
}
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;
}

两个线程都更新共享计数器 counter 的值。

编译运行:

1
2
3
4
5
6
7
8
9
10
$ gcc -o thread thread.c -Wall -pthread
$ ./thread 1000
Initial value : 0
Final value : 2000
$ ./thread 100000
Initial value : 0
Final value : 143012
$ ./thread 100000
Initial value : 0
Final value : 137298

一些结果与预期不同,这是因为 counter 自增需要 3 条指令:

  • counter 的值从内存读取到寄存器。
  • 将寄存器中的值自增、将寄存器中的值写回内存。
  • 这 3 条指令不是原子方式执行。

无法在本地复现此实例,原因不明,可能是并发数太少。

2.4 持久性

DRAM 是易失性的,需要硬件和软件来持久地保存数据。

硬件是一些 I/O 设备,如 HDD、SSD。

软件指文件系统。

与 CPU 和内存不同,操作系统不会对每个程序虚拟化磁盘。

io.cview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <string.h>

int main(int argc, char *argv[]) {
int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
assert(fd >= 0);
char buffer[20];
sprintf(buffer, "hello world\n");
int rc = write(fd, buffer, strlen(buffer));
assert(rc == (strlen(buffer)));
fsync(fd);
close(fd);
return 0;
}

该程序会创建文件 /tmp/file,在其中写入 hello world

程序向操作系统发出 3 个系统调用:

  • open() 的调用,打开文件并创建它。
  • write() 将一些数据写入文件。
  • close() 关闭文件。

这些系统调用会转到文件系统进行处理。

2.5 设计目标

  • 提供高性能,在提供虚拟化和其它 OS 功能的情况下,减少时间或空间上的开销。
  • 提供保护,即在程序之间以及在 OS 和应用程序之间提供保护,让进程彼此隔离。
  • 提供高度的可靠性。
  • 其它目标:能源效率(降低功耗)、安全性、移动性等。

2.6 简单历史

早期 OS:只是一些库

OS 基本只是一组常用函数库,程序以过程调用来访问。

OS 一次运行一个程序,计算模式通常是批处理。

超越库:保护

系统调用诞生。与过程调用不同,系统调用把控制转移到 OS 中同时提高硬件特权级别,用户程序以用户模式运行,这意味着硬件限制了应用程序的功能,例如不能直接进行磁盘 I/O、访问物理内存页、在网络上发送数据包等。

通常通过陷阱(trap)硬件指令发起系统调用,硬件将控制转移到陷阱处理程序,同时将特权级别提升到内核模式,在内核模式下,OS 可以完全访问系统硬件。OS 完成请求时,通过陷阱返回指令将控制权交还给用户,该指令返回到用户模式,同时将控制权交还给应用程序。

多道程序时代

将大量作业加载到内存中并在它们之间快速切换,避免 I/O 时占用 CPU,提高 CPU 利用率。

现代