OSTEP 介绍

2021-02-05

第 2 章 操作系统介绍

2.1 虚拟化 CPU

intro/cpu.c
#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 秒。

$ ./cpu "A"
A
A
A
A
^C

如果后台运行多个实例:

./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 虚拟化内存

intro/mem.c
#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;
}

$ ./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 并发

intro/threads.c
#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 的值。

编译运行:

$ 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 和内存不同,操作系统不会对每个程序虚拟化磁盘。

intro/io.c
#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 利用率。

现代

OSTEP操作系统读书笔记

本作品根据 署名-非商业性使用-相同方式共享 4.0 国际许可 进行授权。

OSTEP CPU 虚拟化

RDM 无法切换 HiDPI 分辨率的一种临时解决办法