操作系统三个简单的部分(Operating Systems in Three Easy Pieces闲来无聊,翻译的)1,2(操作系统的介绍部分)

那啥书名具体不知道怎么说才好,反正看英文知道是那么个意思,翻译成中文就是别扭大家就简单接受一下吧,多多包涵(六级擦线而过,灰溜溜~~)

Operating Systems in Three Easy Pieces

虚拟,并发,持续

Chapter1

Some  dialogues

一些对话,对于学习该科目的兴趣引导和解惑

Chapter2 操作系统介绍

    如果你已经对系统进程有一定的了解,那么当一个计算机程序运行时,你应该已经对这个过程有一定的体会。如果没有了解,那这本书(以及相应方面的内容)对你来说可能是很难理解的。因此,你应该停止阅读这本书,或者你可以跑去最近的书店,挑选那些为本书提供必要的知识背景的书来给你后续阅读此书做铺垫。

那么问题来了,在一个程序运行时到底发生了什么?

一个正在运行中的程序做的事情很简单:执行指令,每秒钟约百万次(至今,约百亿)。处理器从存储器中取出指令,然后进行解码(即让机器知道程序要干什么——机器码),然后去执行指令(即让程序去做它应该要做的事情,比如将两个数加在一起,访问内存,状态检测,功能跳转等等)。当指令执行完毕,处理器将跳转到下一个指令,后面还有更多,直到程序最终完成。

至此,我们只是简单的描述了一下冯诺依曼模型的基本运算。听起来很简单,不是吗?但是在此,我们要学习的是许多在程序运行时我们看不见的东西,正是这些看不见的东西让操作系统更加的易于使用。

有一个软件的主体,实际上,它是为了让程序易于运行(就是让你感觉在同一时间运行

问题的关键

如何虚拟化资源

在本书中我们围绕的一个简单的中心问题就是:操作系统如何虚拟化资源?这就是我们的问题的核心。为什么这不是操作系统(OS)的主要问题,答案应该很明显:它只是让操作系统易于使用。因此,我们的焦点是:操作系统是通过什么机制和策略实现虚拟化的?操作系统是怎样运行的如此高效?又需要什么样的硬件支持呢?

我们说问题的关键,正如盒子里的阴影部分,我们正试着找到在构建操作系统中的一种解决具体问题的方法。因此,在这个特定主题的书里,你也许会发现一个或者更多高于中心问题的难题。当然,在本章的小节中,会给出目前的解决方案,或者至少是解决问题的关键点。

了很多东西),让程序共享内存,能够与设备交互,以及许多其他的功能。这个软件的主体就被称为操作系统。它负责确保操作系统以易于使用的方式来正确高效的运行。

操作系统运行的主要方式就是通过我们称之为虚拟化的一个通用技术。也就是说,操作系统掌握着物理资源(如处理器,内存,磁盘)并将它转化为更加比原来更高效,普遍,易于使用的虚拟形式。因此,我们更偏向于把操作系统当做一个虚拟机。

当然,为了让用户能告知OS做什么从而去使用虚拟机器的资源(比如运行一个程序,或分配内存,或者读取一个文件),OS提供了一些供用户调用的接口。实际上一个典型的操作系统,是开放几百个系统调用给应用程序的。因为OS提供的这些调用,沟通内存和设备,和其他一些相关的东西,有时我们也可以说OS为应用程序提供了一个标准库。

最后,因为虚拟化让许多程序得以运行(共享CPU),许多程序可以同时访问他们自己的指令和数据(共享内存),许多程序连接到设备(共享磁盘等等),所以OS有时也被认为是一个资源管理器。CPU,内存和磁盘都是系统的资源,他们也是操作系统管理的那些资源中的一部分,不停地做着高效的或者是公平的或者是其他很多预想可能会有的事情。懂得这些操作系统的成员一些会更好,来看看一些例子。

1 #include <stdio.h>

2 #include <stdlib.h>

3 #include <sys/time.h>

4 #include <assert.h>

5 #include "common.h"

6

8 int  main(int argc, char *argv[])

9 { 

10  if (argc != 2) { 

11  fprintf(stderr, "usage: cpu \n"); 

12  exit(1); 

13  } 

14  char *str = argv[1]; 

15  while (1) { 

16  Spin(1);

17  printf("%s\n", str); 

18  } 

19  return 0; 

20 }

figure_2.1:一个简单的例子:循环打印代码

2.1虚拟化CPU

Figure_2.1呈现的是我们的第一个程序,它所做的事情并不多。实际上,它所做的所有的事情只有一件,就是调用spin(),它是一个重复的判断时间并且每隔一秒执行一次,然后,他打印出来的字符就是用户在命令行里面传入的,并且一直执行下去。

我们以cpu.c保存该代码并且在一个系统上用单处理器(也就是所谓的CPU)编译执行,这里就是我们将会看到的。

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

这个允许过程也不是很有趣——重复的检查时间,每当一秒钟过去,系统就开始执行程序。每当一秒钟过去,代码便打印用户输入的字符(如字母“A”)。这个程序会一直执行这,只有按下“Control-c”Unix类系统前台运行程序的终止方法)才能终止。

现在,让我们再跑一次程序,但是这次我们用相同的程序运行一些不同的例子。

Figure 2.2呈现了稍微复杂的例子。

 

Figure2.2:同时运行多个程序

现在,开始变得有点意思了。尽管我们只有一个处理器,但是不知道为什么,这四个进程看起来似乎时同时进行的!这个神奇的过程是怎么产生的?

实际上,由于硬件的支持,操作系统让我们产生了一种错觉——系统拥有大量的虚拟CPU。将单个CPU(或者小的一组)转变成似乎时无限多个CPU,从而让多个程序似乎可以同步运行,这就是我们所说的虚拟化CPU,也是本书第一个主要部分的重点。

当然,启动进程,终止进程,或者告诉操作系统运行哪个进程,我们需要有一些接口能让我们将我们的目的告知OS。我们会在整本书中讨论这些接口;的确,它们就是用户与操作系统交互的主要途径。

也行你也注意到,这种一次运行多个进程的能力带来了一系列新的问题。比如,如果两个程序要在一个特定的时间运行,应该运行哪个?OS的一种策略回答了这个问题;操作系统中采用各种策略来应对很多不同场景的不同类型的问题,因此,我们将要通过学习这些策略来了解操作系统所采用的基础机制(比如同步运行多个程序的能力)。因此,OS的角色就是作为一个资源管理器。

1 #include <unistd.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include "common.h"
5
6 int
7 main(int argc, char *argv[])
8 {
9 int *p = malloc(sizeof(int)); // a1
10 assert(p != NULL);
11 printf("(%d) memory address of p: %08x\n",
12 getpid(), (unsigned) p); // a2
13 *p = 0; // a3
14 while (1) {
15 Spin(1);
16 *p = *p + 1;
17 printf("(%d) p: %d\n", getpid(), *p); // a4
18 }
19 return 0;
20 } 

Figure 2.3: 一个访问内存的程序(mem.c

2.2虚拟内存

现在,让我们来考虑内存。现代化机器呈现的物理内存的模型是非常简单的。内存就是一个字节数组;读取内存,必须要一个指定地址能够访问存储在内存中数据;要写入(更新)内存,还需要将特定的数据写入到指定的地址。

一个进程运行的所有时间内,内存都是可以被访问的。程序将它的所有数据结构都存放在内存中,并通过一系列指令来访问它们,就像在它们工作的时候,用载入、存储或者其他明确的指令访问内存。不要忘了每个指令也都是存放在内存中的,因此,在每个指令的取出时内存也是可以访问的。

让我们来看一个通过malloc()函数分配内存的程序(figure2.3)。这里是程序的输出:

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

这个程序做了几个事情。首先,它分配了一下内存(第一步)。然后,它打印了内存的地址(第二步),接着将数字0写入新分配的内存的最开始的位置(第三步)。最终,它进行循环,每隔一秒就将存储在指针p指向的地址的数值增加1。每次状态打印的时候,它也打印出来当前运行进程的进程标识符(即PID)。这个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
...

Figure 2.4:多次运行该内存分配程序

再一次运行,这次最先输出的结果就没那么有意思了。新分配的内存地址是00200000。程序运行是,它缓慢的更新数值并打印结果。

现在,我们再次运行多个实例来看看发生了什么(Figure2.4)。我们从这个例子中可以看到每个进程都是从相同的地址(00200000)开始分配内存,而且似乎是独立地去更新00200000处的数值的。似乎每个进程运行时,它们都拥有各自私有的内存,而不是与其他正在运行的进程共享相同的物理内存。

实际上,操作系统中真正发生的事情是虚拟内存。每个进程都访问它们自己的私有的虚拟地址空间(有时候就直接被称为地址空间),操作系统将这些虚拟地址空间以某种方式映射到机器的物理内存。一个运行的程序引用的内存不会影响其他进程的地址空间(或者操作系统本身);就正在运行的程序而言,它独自占有所有的物理内存。然而,事实却是——物理内存是一个由操作系统管理的共享资源。这究竟是如何实现的也是本书第一部分的主体——虚拟化。

2.3并发

本书的另一个主题就是并发。我们通过这个概念来指代在同一个程序中一次性进行多个工作给主机带来而且必须得到解决的问题。并发的问题最初是操作系统自身范围内出现的,你可以从上面的例子中看到,操作系统同时做着许多事情,首先运行一个进程,然后另一个……。事实证明,这样做带来了一些更深层次的而且很好玩的问题。

 1 #include <stdio.h>
2 #include <stdlib.h>
3 #include "common.h"
4
5 volatile int counter = 0;
6 int loops;
7
8 void *worker(void *arg) {
9 int i;
10 for (i = 0; i < loops; i++) {
11 counter++;
12 }
13 return NULL;
14 }
15
16 int
17 main(int argc, char *argv[])
18 {
19 if (argc != 2) {
20 fprintf(stderr, "usage: threads <value>\n");
21 exit(1);
22 }
23 loops = atoi(argv[1]);
24 pthread_t p1, p2;
25 printf("Initial value : %d\n", counter);
26
27 Pthread_create(&p1, NULL, worker, NULL);
28 Pthread_create(&p2, NULL, worker, NULL);
29 Pthread_join(p1, NULL);
30 Pthread_join(p2, NULL);
31 printf("Final value : %d\n", counter);
32 return 0;
33 }

Figure 2.5:一个多线程程序(thread.c

很不幸,并发的问题已经不再仅仅局限于操作系统本身。的确,现在的多线程程序也存在相同的问题。现在让我们来演示一个多线程程序(Figure2.5)。

也许你现在不能完全理解这个程序(我们将在后续的章节中了解更多——本书的并发部分),但是它的基本思想很简单。主程序用Pthread create()函数创建了两个线程。你可以把一个线程当做是在相同内存的功能函数,就和其它功能函数一样,这里的多线程就是一次执行不止一个功能函数。在这个例子中,每个线程从函数worker()函数开始,worker函数作为循环的计数器。

我们输入1000来设定循环,然后运行这个程序,下面就是输出结果。循环值会确定两个worker函数循环中共享的计数器的增长次数。

问题的关键

如何构建正确的并发程序

在同一个内存空间有多个并发执行的线程,我们应该如何构建一个正确的工作程序?需要什么操作系统原语?硬件应该提供什么机制?我们怎样使用它们来解决并发的问题?


 

当程序运行是输入10,你希望最后的计数器数值是什么?

prompt> gcc -o thread thread.c -Wall -pthread
prompt> ./thread 1000
Initial value : 0
Final value : 2000

正如你可能猜到的,当两个线程结束,最终计数器的值是2000,每个线程都给计数器增加了1000次。的确,输入循环的值为N,我们运行程序得到最终输出就是2N。但是事情不会这么简单,正如程序自己给出的结果所示。让我们输入一个更大的值来运行相同的程序,看看发生的结果:

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

这次运行,我们输入的值时10000,程序最终给出的结果不是我们想要的20000,而是143012。于是,我们又一次运行,我们不仅得到了一个错误的结果,而且还输出了一个和上次不同的数值。事实上,如果你输入很大的数值一遍又一遍的运行程序循环,你也行会发现你有时候会得到正确的结果!为什么会这样?

事实证明这些奇怪的未预期的结果跟每次如何执行指令相关。不幸的是,上面程序共享的计数器递增的关键部分包涵三条指令:一个是从内存加载计数器的值到寄存器,一个时增加,一个是将它存入内存。因为这三条指令不是原子执行的(即一次执行所有),所有发生了奇怪的事情。并发的问题我们将在本书的第二部分详细讨论。

 1 #include <stdio.h>
2 #include <unistd.h>
3 #include <assert.h>
4 #include <fcntl.h>
5 #include <sys/types.h>
6
7 int
8 main(int argc, char *argv[])
9 {
10 int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
11 assert(fd > -1);
12 int rc = write(fd, "hello world\n", 13);
13 assert(rc == 13);
14 close(fd);
15 return 0;
16 }

Figure2.6:I/O操作函数(io.c)

2.4持续性

这门学科的第三个主体就是持续性。在系统内存里面,数据很容易丢失,比如动态存储器的可变存储;在系统崩溃或者断电,内存上的所有数据都会丢失。因此,我们需要软硬件可以持久的存储数据。用户很关心他们自己的数据因而对于任何系统来说持久性存储非常重要。

硬盘自带输入输出接口或者I/O设备;在现代系统中,硬盘驱动器是常见的存储长久信息的容器,尽管固态硬盘也在这方面取得了一些进展。

操作系统上通常用来管理磁盘的软件被称为文件系统;它是负责以可靠且有效的方式存储用户在系统磁盘上创建的所有文件。

与操作系统对CPU和内存的抽象不同的是,系统不会为每个应用程序创建一个私有,虚拟化的磁盘。相反,它假定用户很多时候想要共享文件中的信息。举个例子,你写一个C程序,首先你要使用一个编辑器(比如Emacs)来创建和编辑这个c文件(命令是emacs -nw main.c)。代码写完之后,你需要用编译器来将源代码转化为一个可执行文件(gcc -o main main.c)。当你完成这些工作之后,你就可以运行这个新生成的可执行文件(./main)。因此,你可以看到在不同的进程中文件是如何共享的。首先,Emacs创建一个文件作为编译器的输入,编译器使用这个输入的文件创建一个新的可执行文件(这里面更多的步骤详见编译原理);最后,可执行文件被运行。到此,一个新的进程生成了。

想要更好的理解,让我们来看一下代码。Figure2.6展现的代码是一个输出“hello world”的程序。

问题的关键

如何构持久存储数据

文件系统是操作系统负责管理持久数据的一部分。保证正确执行需要什么技术?高性能的运行需要什么机制和政策?在面对软硬件故障的时候,如何实现可靠性?


为了完成这个任务,程序用了三个系统调用。第一个时open(),创建和打开文件,第二个是write(),向文件中写入数据,第三个时close(),简单的关闭文件防止写入更多的数据。这些系统调用直接与文件系统连,然后处理请求并返回错误码给用户。

你也可能想只想操作系统在写入磁盘时做了什么。我们会告诉你,但是首先你应该闭上眼睛;这是不愉快的。文件系统做了一些均等的事情:首先确定新的数据在磁盘上的存储位置,然后保持它在文件系统结构中轨迹。这些工作需要发出I/O请求到底层存储设备,然后读取现有的结构或者更新它们。任何一个写过设备驱动程序的人都知道,让一个设备按照你的意愿去做某件事是一个繁复的过程。这需要有对设备底层接口和它的精确语义的深入了解。幸运的是,OS通过自身的系统调用提供了一个标准化的简单方法来访问设备。因此,OS有时也被看做是一个标准库。

当然,对于访问设备和文件系统如何持久地在上述设备上管理数据,其实还有更多的详细内容。由于性能的原因,大多数文件系统首先延迟处理写操作,希望能批量处理。为了处理系统在写入数据时出现崩溃的问题,大多数文件系统采用某种错综复杂的写入协议,如日志,写复制,仔细地顺序写入磁盘来确保如果在写失败发生时,系统可以在此之后回复到一个合理的状态。为了不同寻常的操作效率,文件系统采用许多不同的数据结构和访问方法——从简单的列表到复制的B树。如果这些也都没理解,那么好!我们将在本书的第三部分(持续性)针对这些进行很多详细的讨论。我们会总体的讨论设备,I/O,然后是磁盘,RAIDs和文件系统。

2.5设计目标

2.6一些历史

2.7总结

至此,我们已经对操作系统做了一个介绍。如今的操作系统已经相对易于使用了,我们在整本书中会讨论到现今几乎所有的操作系统,也许会给你今后使用操作系统带来深远的影响。

很不幸的是,由于时间的限制,还有一部分的操作系统本书不会涉及。比如,还有操作系统的许多网络部分,我们将会让您在学习网络的时候去了解更多。同样的,图像设备是特别重要的,可以学习显卡方面的知识来扩展这方面的知识。最后,有些操作系统一大堆安全有关的东西;我们在程序运行和给用户提供交互能力的时候谈论一下OS必须提供的保护文件的保护机制,但我们不会深入到更深层次的安全问题,那些时属于安全方面的。

不过,我们也涵盖了很多重要的主题,包括并发、虚拟化CPU和内存的基本知识,还有设备文件的持久性。不要担心!这里还是涵盖了很多方面的,很多知识内容很酷,而且在学习结束的时候,你将会对电脑系统正在的工作有一个新的认识。现在开工!

原文链接:http://pages.cs.wisc.edu/~remzi/OSTEP/intro.pdf

本文word文件如果需要请留言 




评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值