OSTEP Part1 Virtualization-虚拟化(一)

431 阅读7分钟

看知乎上很多人推荐《Operating System:Three Easy Pieces》这本书,听说写的比较易懂且主线脉络鲜明,所以我来了。打算先看完这本书再去学mit 6.848 或者 清华的ucore 的操作系统。 这本书分为三个piece,虚拟化(virtualization)、并发(concurrency)和持久性(persistence) 下面进入这本书的第一部分——虚拟化。

这一部分,我们关注的问题是:
操作系统通过哪些机制和策略来实现虚拟化?
操作系统如何有效地实现虚拟化?
需要哪些硬件支持?

Chapter2-操作系统介绍

#ifndef __common_h__
#define __common_h__

#include <sys/time.h>
#include <sys/stat.h>
#include <assert.h>

double GetTime() {
    struct timeval t;
    int rc = gettimeofday(&t, NULL);
    assert(rc == 0);
    return (double) t.tv_sec + (double) t.tv_usec/1e6;
}

void Spin(int howlong) {
    double t = GetTime();
    while ((GetTime() - t) < (double) howlong)
	; // do nothing in loop
}

#endif // __common_h__

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;
}

运行不太有趣:系统开始运行程序时,该程序会重复检查时间,直到一秒钟过去。一秒钟过去后,代码打印用户传入的字符串(在本例中为字母“A”)并继续。注意:该程序将永远运行,只有按下“Control-c”(这在基于UNIX的系统上将终止在前台运行的程序),才能停止运行该程序。

尽管我们只有一个处理器,但这4个程序似乎在同时运行!这种魔法是如何发生的?事实证明,在硬件的一些帮助下,操作系统负责提供这种假象(illusion),即系统拥有非常多的虚拟CPU的假象。将单个CPU(或其中一小部分)转换为看似无限数量的CPU,从而让许多程序看似同时运行,这就是所谓的虚拟化CPU(virtualizing the CPU),这是本书第一大部分的关注点。当然,要运行程序并停止它们,或告诉操作系统运行哪些程序,需要有一些接口(API),你可以利用它们将需求传达给操作系统。我们将在本书中讨论这些API。事实上,它们是大多数用户与操作系统交互的主要方式。你可能还会注意到,一次运行多个程序的能力会引发各种新问题。例如,如果两个程序想要在特定时间运行,应该运行哪个?这个问题由操作系统的策略(policy)来回答。在操作系统的许多不同的地方采用了一些策略,来回答这类问题,所以我们将在学习操作系统实现的基本机制(mechanism)(例如一次运行多个程序的能力)时研究这些策略。因此,操作系统承担了资源管理器(resource manager)的角色。

Chapter4-抽象:进程

本章讨论操作系统提供的基本的抽象—— 进程。进程的非正式定义非常简单:进程就是运行中的程序。程序本身是没有生命周期的,它只是存在磁盘上面的一些指令(也可能是一些静态数据)。是操作系统让这些字节运行起来,让程序发挥作用。

操作系统通过虚拟化(virtualizing)CPU来提供这种假象(illusion)。通过让一个进程只运行一个时间片,然后切换到其他进程,操作系统提供了存在多个虚拟CPU的假象。这就是时分共享(time sharing)CPU技术,允许用户如愿运行多个并发进程。潜在的开销就是性能损失(performence),因为如果CPU必须共享,每个进程的运行就会慢一点。 我们将底层的硬件机制称为mechanism.在这些机制之上,操作系统存在很多策略(policy),是在操作系统内做出某种决定的算法.

例如,给定一组可能的程序要在CPU上运行,操作系统应该运行哪个程序?操作系统中的调度策略(schedulingpolicy)会做出这样的决定,可能利用历史信息(例如,哪个程序在最后一分钟运行得更多?)、工作负载知识(例如,运行什么类型的程序?)以及性能指标(例如,系统是否针对交互式性能或吞吐量进行优化?)来做出决定。

为了理解构成进程的是什么,我们必须理解它的机器状态(machinestate),分为内存和寄存器,他们决定了这个进程的状态.(也就是状态机了)


4.2-进程API

这里先介绍一下设计API时,肯定会设计的接口,所有现代操作系统都以某种形式提供这些API:

4.3-进程创建 : more detail

我们应该揭开一个谜,就是程序如何转化为进程。具体来说,操作系统如何启动并运行一个程序?进程创建实际如何进行?

操作系统运行程序必须做的第一件事是将代码和所有静态数据(例如初始化变量)加载(load)到内存中,加载到进程的地址空间中。程序最初以某种可执行格式驻留在磁盘上(disk,或者在某些现代系统中,在基于闪存的SSD上)。因此,将程序和静态数据加载到内存中的过程,需要操作系统从磁盘读取这些字节,并将它们放在内存中的某处. 在早期的(或简单的)操作系统中,加载过程尽早(eagerly)完成,即在运行程序之前全部完成。现代操作系统惰性(lazily)执行该过程,即仅在程序执行期间需要加载的代码或数据片段,才会加载。要真正理解代码和数据的惰性加载是如何工作的,必须更多地了解分页和交换的机制,这是我们将来讨论内存虚拟化时要涉及的主题。现在,只要记住在运行任何程序之前,操作系统显然必须做一些工作,才能将重要的程序字节从磁盘读入内存。

将代码和静态数据加载到内存后,操作系统在运行此进程之前还需要执行其他一些操作。必须为程序的运行时栈(run-time stack或stack)分配一些内存。你可能已经知道,C程序使用栈存放局部变量、函数参数和返回地址。操作系统分配这些内存,并提供给进程。操作系统也可能会用参数初始化栈。具体来说,它会将参数填入main()函数,即argc和argv数组。

操作系统也可能为程序的(heap)分配一些内存。在C程序中,堆用于显式请求的动态分配数据。程序通过调用malloc()来请求这样的空间,并通过调用free()来明确地释放它。数据结构(如链表、散列表、树和其他有趣的数据结构)需要堆。起初堆会很小。随着程序运行,通过malloc()库API请求更多内存,操作系统可能会参与分配更多内存给进程,以满足这些调用。

操作系统还将执行一些其他初始化任务,特别是与输入/输出(I/O)相关的任务。例如,在UNIX系统中,默认情况下每个进程都有3个打开的文件描述符(file descriptor),用于标准输入、输出和错误。这些描述符让程序轻松读取来自终端的输入以及打印输出到屏幕。在本书的第3部分关于持久性(persistence)的知识中,我们将详细了解I/O、文件描述符等。

通过将代码和静态数据加载到内存中,通过创建和初始化栈以及执行与I/O设置相关的其他工作,OS现在(终于)为程序执行搭好了舞台。然后它有最后一项任务:启动程序,在入口处运行,即main()。通过跳转到main()例程(第5章讨论的专门机制),OS将CPU的控制权转移到新创建的进程中,从而程序开始执行。