《操作系统导论》知识总结 第二章 操作系统介绍


程序运行时会发生什么?
一个正在运行的程序会做一件非常简单的事情:执行指令。处理器从内存中获取(fetch)一条指令,对其进行解码(decode)(弄清楚这是哪条指令),然后执行(execute)它(做它应该做的事情,如两个数相加、访问内存、检查条件、跳转到函数等)。完成这条指令后,处理器继续执行下一条指令,依此类推,直到程序最终完成。这就是冯·诺依曼(Von Neumann)计算模型的基本概念。

有一类软件负责让程序运行变得容易(甚至允许你同时运行多个程序),允许程序共享内存,让程序能够与设备交互,以及其他类似的有趣的工作。这些软件称为操作系统(Operating System,OS),因为它们负责确保系统既易于使用又正确高效地运行。

虚拟化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 = argv[1];
    while (1)
    {
        Spin(1);
        printf("%s\n", str);
    }
    return 0;
}

这个函数的作用就是每一秒打印输出的参数,并且是个死循环。

输出为:

prompt> gcc -o cpu cpu.c -Wall 
prompt> ./cpu "A" 
A 
A
A 
A 
ˆC 
prompt>

当同时执行运行 4 个程序的命令时,打印几乎是同时运行的,而不是等待第一个程序运行结束才运行下个程序。

prompt> ./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 
...

尽管我们只有一个处理器,但这 4 个程序似乎在同时运行。但对于单核的处理器,同时运行 4 个进程是不可能的,所以这里就要介绍 CPU 的虚拟化。事实证明,在硬件的一些帮助下,操作系统负责提供这种假象(illusion),即系统拥有非常多的虚拟CPU的假象。将单个 CPU(或其中一小部分)转换为看似无限数量的 CPU,从而让许多程序看似同时运行,这就是所谓的虚拟CPU(virtualizing the CPU)

虚拟化内存

内存就是一个字节数组。要读取(read)内存,必须指定一个地址(address),才能访问存储在那里的数据。要写入(write)或更新(update)内存,还必须指定要写入给定地址的数据。

让我们来看一个程序,它通过调用 malloc()来分配一些内存

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int main(int argc, char *argv[])
{
    int *p = malloc(sizeof(int)); // a1
    assert(p != NULL);
    printf("(%d) memory address of p: %08x\n",
           getpid(), (unsigned)p); // a2
    *p = 0;                        // a3
    while (1)
    {
        Spin(1);
        *p = *p + 1;
        printf("(%d) p: %d\n", getpid(), *p); // a4
    }
    return 0;
}

该程序的输出如下:

prompt> ./mem 
(2134) memory address of p: 00200000 
(2134) p: 1 
(2134) p: 2 
(2134) p: 3 
(2134) p: 4 
(2134) p: 5 
ˆC

该程序做了几件事。首先,它分配了一些内存(a1行)。然后,打印出内存的地址(a2行),然后将数字0放入新分配的内存的第一个空位中(a3行)。最后,程序循环,延迟一秒钟并递增p中保存的值。在每个打印语句中,它还会打印出所谓的正在运行程序的进程标识符(PID)(a4行)。该PID对每个运行进程是唯一的。

现在,我们再一运行同一个程序的多个实例,看看会发生什么。

prompt> ./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 
(24113) p: 3 
(24114) p: 3 
(24113) p: 4 
(24114) p: 4 
...

当同时运行多个相同的程序时,分配的内存地址竟然是相同的,先抛开虚拟化的概念,以物理内存的角度看待,这几个程序分配的内存指针指向了同一块内存空间,也就是修改其中一个程序修改内存也会导致另一个程序中的值改变。

但是从结果来看这两块内存相互独立,并不影响,就好像每个正在运行的程序都有自己的私有内存,而不是与其他正在运行的程序共享相同的物理内存。

实际上,这正是操作系统虚拟化内存(virtualizing memory)时发生的情况。每个进程访问自己的私有虚拟地址空间(virtual address space)(有时称为地址空间address space) ,操作系统以某种方式映射到机器的物理内存上。一个正在运行的程序中的内存引用不会影响其他进程(或操作系统本身)的地址空间。对于正在运行的程序,它完全拥有自己的物理内存。但实际情况是,物理内存是由操作系统管理的共享资源。

并发

并发指一系列问题,这些问题在同时(并发地)处理很多事情时出现且必须解决。
我们来看一个多线程程序的例子。

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

主程序利用Pthread_create()创建了两个线程(thread),每个线程中循环了loops次来递增全局变量counter 。也就是说,当 loops 的输入值设为 N 时,我们预计程序的最终输出为 2N。
但是当N很大时,结果却与我们的预期不符。

prompt> ./thread 100000 
Initial value : 0 
Final value : 143012 // huh?? 
prompt> ./thread 100000 
Initial value : 0 
Final value : 137298 // what the??

当我们再一运行该程序时,不仅再一次得到了错误的值,还与上一的值不同。事实上,如果你一遍又一遍地使用较高的 loops 值运行程序,可能会发现有时甚至可以得到正确的答案。

事实证明,这些奇怪的、不寻常的结果与指令如何执行有关,指令每一执行一条。遗憾的是,上面的程序中的关键部分是增加共享计数器的地方,它需要 3 条指令:

  • 一条将计数器的值从内存加载到寄存器
  • 一条将其递增
  • 一条将其保存回内存

因为这 3 条指令不是以原子方式(atomically)执行(所有的指令一次性执行)的,所以奇怪的事情可能会发生。

持久性

在系统内存中,数据容易丢失,因为像DRAM 这样的设备以易失(volatile)的方式存储数值。如果断电或系统崩溃,那么内存中的所有数据都会丢失。因此,我们需要硬件和软件来持久地(persistently)存储数据。

操作系统中管理磁盘的软件通常称为文件系统(file system)。因此它负责以可靠和高效的方式,将用户创建的任何文件(file)存储在系统的磁盘上。

我们来看一些代码。

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

int main(int argc, char *argv[])
{
    int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
    assert(fd > -1);
    int rc = write(fd, "hello world\n", 13);
    assert(rc == 13);
    close(fd);
    return 0;
}

为了完成这个任务,该程序向操作系统发出 3 个调用。第一个是对 open()的调用,它打开文件并创建它。第二个是 write(),将一些数据写入文件。第三个是 close(),只是简单地关闭文件,从而表明程序不会再向它写入更多的数据。这些系统调用(system call)被转到称为文件系统(file system)的操作系统部分,然后该系统处理这些请求,并向用户返回某种错误代码。

首先确定新数据将驻留在磁盘上的哪个位置,然后在文件系统所维护的各种结构中对其进行记录。这样做需要向底层存储设备发出 I/O 请求,以读取现有结构或更新(写入)它们。所有写过设备驱动程序(device driver)的人都知道,让设备现表你执行某项操作是一个复杂而详细的过程。它需要深入了解低级别设备接口及其确切的语义。幸运的是,操作系统提供了一种通过系统调用来访问设备的标准和简单的方法。因此,OS 有时被视为标准库(standard library)。

出于性能方面的原因,大多数文件系统首先会延迟这些写操作一段时间,希望将其批量分组为较大的组。为了处理写入期间系统崩溃的问题,大多数文件系统都包含某种复杂的写入协议,如日志(journaling)或写时复制(copy-on-write),仔细排序
写入磁盘的操作,以确保如果在写入序列期间发生故障,系统可以在之后恢复到合理的状态。为了使不同的通用操作更高效,文件系统采用了许多不同的数据结构和访问方法,从简单的列表到复杂的B树。

设计目标

操作系统实际上做了什么:它取得 CPU、内存或磁盘等物理资源(resources),甚对它们进行虚拟化(virtualize)。它处理与并发(concurrency)有关的麻烦且棘手的问题。它持久地(persistently)存储文件,从而使它们长期安全。

一个最基本的目标,是建立一些抽象(abstraction),让系统方便和易于使用。抽象对我们在计算机科学中做的每件事都很有帮助。抽象使得编写一个大型程序成为可能,将其划分为小而且容易理解的部分。

设计和实现操作系统的一个目标,是提供高性能(performance)。换言之,我们的目标是最小化操作系统的开销(minimize the overhead)。但是虚拟化的设计是为了易于使用,无形之中会增大开销,比如虚拟页的切换,cpu 的调度等等,所以尽可能的保持易用性与性能的平衡至关重要。

另一个目标是在应用程序之间以及在 OS 和应用程序之间提供保护(protection)。因为我们希望让许多程序同时运行,所以要确保一个程序的恶意或偶然的不良行为不会损害其他程序。保护是操作系统基本原理之一的核心,这就是隔离(isolation)。让进程彼此隔离是保护的关键,因此决定了 OS 必须执行的大部分任务

操作系统也必须不间断运行。当它失效时,系统上运行的所有应用程序也会失效。由于这种依赖性,操作系统往往力求提供高度的可靠性(reliability)。

其他目标。在我们日益增长的绿色世界中,能源效率(energy-efficiency)非常重要;安全性(security)(实际上是保护的扩展)对于恶意应用程序至关重要,特别是在这高度联网的时现。随着操作系统在越来越小的设备上运行,移动性(mobility)变得越来越重要。

简单历史

  • 早期操作系统:只是一些库
  • 超越库:保护
  • 多道程序时代
  • 摩登时代

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值