Operating Systems: Three Easy Pieces 学习和翻译

早就听说这个讲操作系统讲的很好。这是自己学习的记录,同时翻译了一下,有缘人看到错误的话,请指正探讨哈.
这个是第一部分,后续会持续更新
原文链接 Operating Systems: Three Easy Pieces (wisc.edu) 如有侵权请告知。
 

part 1 Virtualization chap 4

 
概念: 进程(The Process)
 
    本章我们将讨论的是操作系统中最基础的概念之一:进程。 进程的定义十分简单,他是一个运行的程序[ V+65,BH70]。程序本身是没有生命特征的东西:它存在于硬盘之中,是等待触发的的一系列的指令。但是操作系统赋予了他们生命,调用他们组成具有实用意义的东西。
通常情况下,用户无需去关心过程中CPU是否可用,仅仅进行程序的运行,系统本身需要同时运行数十个程序,才是一个标准的可用的系统。因此,操作系统的关键难点(challenges)在于: 如何呈现出多个CPU运行的表象(物理上实际仅有一个或有限的几个),即同时运行多个程序
 
    操作系统通过 虚拟化的手段实现该功能。通过间歇性跳跃执行多个进程实现同时执行多个进程的表象,使系统看起来具有多个CPU。这项基本的技术也就是我们常说的CPU的 time sharing, 该技术是目前所有的操作系统都应用的方法,它允许了用户同时运行多个进程(无论多少个),当然这么执行的代价就是,共享CPU会导致性能的下降,也就是每个进程会跑的很慢。

Tips: 利用时间共享以及空间共享
 
时间共享是OS资源共享的基础手段,通过让不同的实体(entity)间歇性轮流使用CPU或者网络连接等资源一小会,可以实现所需求的资源共享。另一种方法(counterpart)是空间共享(space sharing),也就是将一块空间划分成不同的部分供给不同的功能使用,最常见的例子就是硬盘的空间划分,通常一块区域分配给了一个文件后,直到该文件被删除后,这块区域才会被划分给其他的文件。

    为了实现CPU的虚拟化,操作系统需要简单的体系结构(mechanisms)和高级的intelligence。简单的体系结构即实现一些功能的基础协议或方法,例如后续我们将接触到的上下文切换方法,也就是OS进行进程间跳跃的能力。
 
    在这些基础的体系结构上,OS还需要高级的智慧(intelligence),也就是决策(policies)。polices即OS内部做出决策的算法,例如,给出一系列的可以运行的程序,OS决策哪一个去运行,由OS 的调度( scheduling)策略实现该决策。
 
4.1 A Process
 
    如上文所述,进程是一个运行中的程序。我们通过对进程执行中某时刻调用或影响的资源的盘点对该进程进行总结。为了深入理解一个进程的组成,我们必须先理解他的机械状态(machine state):即他在运行时候能够读取或更新什么资源、在某时刻,该机器的哪个部分对于该进程的执行至关重要?
 
机械状态中,首先必然包含的是内存状态。指令和数据均存在于内存当中。因此,可寻址的内存是进程的组成部分之一
 
其次是寄存器,许多指令会对寄存器进行读写操作,因此,寄存器对于进程的执行至关重要
需要注意的是,这里有一些特殊的寄存器,例如程序计数器(program counter,PC)、堆栈指针、以及与之相关联的函数栈帧顶指针等用于管理函数的参数、变量以及返回地址的堆栈。

tips: 划分原则和机制
在许多OS案例中,常见的设计是将高级的polices和低级的机制分开[L+75]。
机制可以理解为,如何实现的方法( How ), 例如如何实现上下文切换,决策则是回答选择性问题( which ),例如现在执行哪一个进程。将这两个部分分开处理更容易实现软件的模块化。

    最后,程序也经常访问持久存储设备。这样的I/o信息可能包括进程当前打开的文件列表。
 
4.2 进程API (Application Program Interface)
   
    尽管将API的详细介绍放在了后文,但是在这里我们首先还是要简单了解一下一个操作系统的交互接口必须要包含哪些内容。
 
            1. 创建:创建一个新的进程。当双击某程序或者输入某指令到shell时,OS要创建执行指定程序的进程
            2. 终止: 强制终止进程。
            3. 等待:等待某进程停止运行。
            4. 多样控制:除了等待和终止进程,也需要其他的控制,例如挂起(suspend)后恢复。
            5. 状态:读取某进程的状态信息,例如某进程处于什么状态,还要运行多久。
 
4.3 更详细的进程创建介绍
    
    我们想要了解的还有 : 一段程序是如何转换为进程的,也就是OS是如何让一段程序调度运行的?进程是如何创建的?
 
    首先OS需要加载代码和静态数据(比如初始化过的变量)到内存(memory)中,也就是可寻址的空间。程序一开始以可执行文件的形式存在于硬盘disk中(或者flash-based SSD),所以第一步是从硬盘读取这些字节然后放置在内存中的某一段,如下图所示

Operating Systems: Three Easy Pieces 学习和翻译_第1张图片

 
    早期的OS中,在运行程序之前,数据被一次性加载,现代的OS则是在运行过程中逐渐加载所需要的数据,这也就涉及到了分页和切换调度技术( paging and swapping ),在后续的内存虚拟化部分我们会进行详细的讲解。
 
    完成代码和静态数据的加载之后,部分内存被划分为 stack),在C语言中,堆存放的是局部变量、函数参数和返回地址。
    OS还会划分一部分内存作为 栈(heap), 在C程序中,用于动态分配数据;程序通过调用malloc和free实现。常用到堆的包括链接表、哈希表、树等数据结构。堆的大小随着程序的运行,会发生动态变化,由OS进行调度分配。
    OS也会进行其他的初始化任务,尤其是与I/O相关的。例如,在Unix系统中,每个进程默认包含三个文件描述:标准输入输出和错误,这些描述使得程序更容易实现输入输出。
    完成以上工作后, OS就做好了执行程序的 准备,最后的任务就是:从main()函数入口开始运行程序。通过跳到main函数中,CPU的控制由OS交给新建的进程,然后实现运行。
    
 
4.4 进程的状态
 
 
进程的状态,可以简单总结为:
        - 运行(running):处理器运行进程,执行指令
        - 就绪(ready): 进程可以运行,但是OS在当前没有执行
        - 锁(blocked):进程锁住,等待触发事件。例如,当初始化硬盘的IO时,该进程被锁,这样其他的进程可以使用处理器。
 

Operating Systems: Three Easy Pieces 学习和翻译_第2张图片

状态转换图如上图所示,下面给出多个进程的运行状态的例子
    假设有两个进程正在运行,每个都只用到CPU。那么他们的状态转换可以总结为:
                                Operating Systems: Three Easy Pieces 学习和翻译_第3张图片
        假设进程涉及到IO的操作,那么:

Operating Systems: Three Easy Pieces 学习和翻译_第4张图片

 
 
在上述过程中,由于第一个进程需要初始化IO, 进入了blocked状态,OS识别到该进程没有用到cpu, 所以在ready状态的进程2得以执行,当初始化完成之后,再决策是否转回第一个进程运行。
 
4.4 数据结构
 
    OS本身是一个程序,正如所有其他的程序一样,他也有一些用于追踪不同相关信息的关键数据结构。例如,为了记录每个进程的状态,OS会保存所有的就绪状态中的进程清单,以及用于追踪当前正在运行的进程的附加信息。同时,OS也需要保存blocked的状态的进程,当IO事件结束后,OS要确保唤醒当前的进程进入ready状态然后再次运行。
    figure 4.5 描述了xv6 kernal [CK+08] 架构的OS的处理过程。该架构目前也被用在Linux、Mac OS、linux等操作系统中。
Operating Systems: Three Easy Pieces 学习和翻译_第5张图片
 
     如图所示,寄存器上下文(register context)会保存进程的寄存器目录,用于停止进程。当进程停止时,他的寄存器会被存在这个地址的内存中,通过重新存储这些寄存器的内容,OS可以重新恢复运行该进程。在后续的章节中我们会更详细的介绍这项上下文切换的技术。
    从图中也可以看到除了上述三个状态之外的进程的状态,有时候系统创建某进程时它涉及到初始化状态(initial),类似的,当进程退出但是还没有被完全清除的时候,会有终止态(final)(unix系统中叫zombie)。这种final状态允许其他的进程(通常是创建该进程的父进程)来测试返回码判断是否执行成功,当结束判断后,父进程会执行最后的调用(例如wait())来等待子进程的完成,同时通知OS清除该进程的相关数据。
 
4.6 总结
    我们介绍了关于OS最基本的概念:进程。通过简单了解后,我们进一步接触本质: 执行进程的低级的机制(mechanism)和规划他们的高级的决策(policy)。通过这二者的结合,我们可以深入了解OS是怎么虚拟化CPU的。
    
 
 
References
 [BH70] “The Nucleus of a Multiprogramming System” by Per Brinch Hansen. Communications of the ACM, Volume 13:4, April 1970. This paper introduces one of the first microkernels in operating systems history, called Nucleus. The idea of smaller, more minimal systems is a theme that rears its head repeatedly in OS history; it all began with Brinch Hansen’s work described herein. 
[CK+08] “The xv6 Operating System” by Russ Cox, Frans Kaashoek, Robert Morris, Nickolai Zeldovich. From: https://github.com/mit-pdos/xv6-public. The coolest real and little OS in the world. Download and play with it to learn more about the details of how operating systems actually work. We have been using an older version (2012-01-30-1-g1c41342) and hence some examples in the book may not match the latest in the source.
 [DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis, Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966 . This paper defined many of the early terms and concepts around building multiprogrammed systems. [L+75] “Policy/mechanism separation in Hydra” by R. Levin, E. Cohen, W. Corwin, F. Pollack, W. Wulf. SOSP ’75, Austin, Texas, November 1975. An early paper about how to structure operating systems in a research OS known as Hydra. While Hydra never became a mainstream OS, some of its ideas influenced OS designers.
 [V+65] “Structure of the Multics Supervisor” by V.A. Vyssotsky, F. J. Corbato, R. M. Graham. Fall Joint Computer Conference, 1965. An early paper on Multics, which described many of the basic ideas and terms that we find in modern systems. Some of the vision behind computing as a utility are finally being realized in modern cloud systems.
 
 
 

过渡章节(更多实际操作)

 

在本章,我们讨论的是Unix系统的进程创建,unix利用的是一对系统调用:fork()和exec()。当父进程希望等待它创建的子进程完成执行的时候,也会调用到wait()。本章节的重点:


                                                        如何创建和控制进程

OS需要什么接口用于创建和控制进程?如何设计这些接口使其具备强大的功能和性能并且易于使用?


 

5.1 fork()

 

    fork()用于创建新的进程[C63],然而需要注意的是,它是你调用的最奇怪的程序之一。以下面的程序为例:

Operating Systems: Three Easy Pieces 学习和翻译_第6张图片

 

运行上述程序得到输出:

Operating Systems: Three Easy Pieces 学习和翻译_第7张图片

 

详细分析:程序第6行输出hello world以及程序的进程id(缩写PID) , 然后第7行fork函数被调用,此时OS创建了一个新的进程,奇怪的地方来了:新创建的进程是该进程的复制品。这意味着,对于操作系统来说,现在又两个相同的程序p1在运行,但是他并没有输出子进程的pid,而是先完成了父进程的输出

5.2 wait()

下面,我们在程序中加入wait():

 

Operating Systems: Three Easy Pieces 学习和翻译_第8张图片

 

运行的输出为:

Operating Systems: Three Easy Pieces 学习和翻译_第9张图片

这时候我们再看,子进程并非是完全的父进程的copy,fork的返回值不同,父进程的返回值是大于0 的值,子进程是0.

我们注意到,该程序(p1.c)的输出并非是确定值,假设我们只有一个cpu,在我们的例子。首先父进程执行了,然后跳到了子进程,但是,相反的状况也可能存在。我们之后会重点讨论这个问题,也就是CPU调度。使用wait()函数可以使执行的顺序得以确定.

 

5.3  exec()

 

当想要运行与调用的程序不同的程序的时候,exec()至关重要。

Operating Systems: Three Easy Pieces 学习和翻译_第10张图片

注:strdup()是字符串复制函数,返回新配制字符串的地址;wc是计数字母程序

 

执行结果为:

Operating Systems: Three Easy Pieces 学习和翻译_第11张图片

 

上例中,execvp中传入一个可执行程序的名称(wc),然后运行输出p3.c的行数(第二个参数传入第一个可执行函数)、单词数和字母数。程序的堆、栈以及其他的内存空间被重新初始化。这意味着,他没有创建新的进程,而是将现在正在运行的程序直接转到调用的程序,在exec在子进程中执行之后,仿佛p3.c没有运行过。成功运行的exec不会返回任何值

 

5.4 why ? Motivating The API

 

        你可能会问,为什么创建子进程这么简单,我们还需要exec函数呢?然而,在unix shell中,fork和exec的不同之处至关重要,因为这能让shell在fork之后、exec之前运行代码。这样能实现切换即将运行的程序的环境,可以构建更多的特性。

 

        shell也是一个用户程序,他给你提供了一个提示接口,然后等待命令输入。通常情况下,输入一个命令后,shell会首先弄清楚命令所在文件的位置,然后fork创建一个子进程来运行该命令,调用exec来执行命令,然后通过wait来等待该程序运行完成。当子进程完成后,shell从wait中返回,然后重新输出提示符,等待下一个命令。

 

例如:

 

        prompt> wc p3.c > newfile.txt

 

 

上述命令中,wc的输出重定向到输出文件newfile.txt中,shell的执行过程:子进程创建完毕后,在调用exec之前,shell关闭了标准输出,然后打开newfile.txt,这样即将运行的wc程序的所有的输出都会被定向到文件中,而不是输出屏幕。

 

        图5.4是显示了重定向的例子,Unix系统从0开始找可用的文件描述,在下例中 STDOUT FILENO是第一个可用的,所以调用open函数的时候,首先被分配。子进程后续所创造的标准输出,会被导向到新打开的文件,而不是屏幕。

Operating Systems: Three Easy Pieces 学习和翻译_第12张图片

上述程序的输出为:

 

Operating Systems: Three Easy Pieces 学习和翻译_第13张图片

有两个值得注意的点:1.)p4运行的是偶仿佛没有任何事情发生,shell马上给出了下个指令的prompt。但是事实上,p4调用了fork创建了子进程,然后将wc程序传入exec运行,并且输出全部被重定向到p4.output里面了。2)我们用cat指令可以打开输出文件找到输出内容。

 

    Unix的pipes也是类似的方式,但是调用了系统函数pipe(),在这种情况下,进程的输出被连接到内核管道,另一个进程的输入也连接到了同一个管道,因此,实现了直接对接,建立起了又长又实用的命令链。例如,在一个文件中,查找一个单词,然后数出现了多少次,可以使用pipe、grep、wc实现,输入指令:

grep -o foo file | wc -l

就可以实现。

 

    我们揭开了process api的面纱,但是还有很多调用细节还需要进一步学习。目前来说,fork和exec的组合是一个创建和操控进程有力的工具。

 

    5.5 进程控制和用户

 

 

            除了上述接口,Unix系统还有很多的调用接口,比如说kill用于进程的暂停、死亡以及其他有用的命令。为了便于使用,也会有快捷键,例如ctrl+c用于终止进程,ctrl+z用于暂停,后续可以使用fg 等内置命令进行恢复。

tips 善于使用manual pages,man  指令能给你几乎所需的所有信息

            进程间的通信,利用信号量实现,进程应该调用siganal()来获取不同的信号。这样做能让进程在运行的时候,如果收到了特殊的信号可以挂起。【SR05】中有关于信号量的详细介绍。

            这也就引入了一个问题,谁可以发送信号量?通常来说,我们的系统有多个用户在同时使用,如果其中一个用户可以任意发送信号量(例如SIGINT来中断进程,很有可能终止进程),会影响到系统的可用性和安全性能。因此,有密码的用户登录机制被利用。不同的用户有不同的权限,通常用户只有控制自己的进程的权限,操作系统负责将系统的资源调度实现整个系统的运行。

            

5.6 常见指令和工具

        有很多常见的指令,例如 ps 可以查看目前正在运行的进程,top可以显示进程占用的系统资源。通常情况下,我们会保持一些窗口显示,例如,我们始终保持 MenuMeters (from Raging Menace software) r运行在我们的工具栏上,所以我们可以看到有多少CPU正在被利用,一般来说,关于正在发生的事情的信息越多越好。

                    tips: the superviser root

通常一个系统会有个root用户,可以管理系统,并且操作其他用户的进程。

 

 

5.7 summary

 

        我们简单介绍了unix进程处理的一些接口,进一步的了解请阅读 Stevens and Rago [SR05],。当然,上述进程控制、进程间的关系和信号量章节可以提取出很多有用的信息。近期的一篇论文 [B+19]指出了fork()的一些问题,并且推崇了spawn()来创建APIs,所以本文也是带着作者的主观认知的,尽可能多阅读相关文献。

 

 

要点总结

 

  •     每个进程有一个PID

  • fork()用于创建子进程

  • wait() 让父进程等待子进程完成

  • exec类别的函数允许子进程可以做与父进程不同的事情

  • 信号量用于进程间通信。

  • 用户机制指定了进程的控制权限

  • 超级用户可以控制整个系统

 

 

 

 

5.8 Refs

[B+19] “A fork() in the road” by Andrew Baumann, Jonathan Appavoo, Orran Krieger, Timothy Roscoe. HotOS ’19, Bertinoro, Italy. A fun paper full of fork()ing rage. Read it to get an opposing viewpoint on the UNIX process API. Presented at the always lively HotOS workshop, where systems researchers go to present extreme opinions in the hopes of pushing the community in new directions. 

 

[C63] “A Multiprocessor System Design” by Melvin E. Conway. AFIPS ’63 Fall Joint Computer Conference, New York, USA 1963. An early paper on how to design multiprocessing systems; may be the first place the term fork() was used in the discussion of spawning new processes. 

 

[DV66] “Programming Semantics for Multiprogrammed Computations” by Jack B. Dennis and Earl C. Van Horn. Communications of the ACM, Volume 9, Number 3, March 1966. A classic paper that outlines the basics of multiprogrammed computer systems. Undoubtedly had great influence on Project MAC, Multics, and eventually UNIX. 

 

[J16] “They could be twins!” by Phoebe Jackson-Edwards. The Daily Mail. March 1, 2016.. This hard-hitting piece of journalism shows a bunch of weirdly similar child/parent photos and is frankly kind of mesmerizing. Go ahead, waste two minutes of your life and check it out. But don’t forget to come back here! This, in a microcosm, is the danger of surfing the web.

 

 [L83] “Hints for Computer Systems Design” by Butler Lampson. ACM Operating Systems Review, Volume 15:5, October 1983. Lampson’s famous hints on how to design computer systems. You should read it at some point in your life, and probably at many points in your life. 

 

[QI15] “With Great Power Comes Great Responsibility” by The Quote Investigator. Available: https://quoteinvestigator.com/2015/07/23/great-power. The quote investigator concludes that the earliest mention of this concept is 1793, in a collection of decrees made at the French National Convention. The specific quote: “Ils doivent envisager qu’une grande responsabilit est la suite insparable d’un grand pouvoir”, which roughly translates to “They must consider that great responsibility follows inseparably from great power.” Only in 1962 did the following words appear in Spider-Man: “...with great power there must also come–great responsibility!” So it looks like the French Revolution gets credit for this one, not Stan Lee. Sorry, Stan. 

 

[SR05] “Advanced Programming in the UNIX Environment” by W. Richard Stevens, Stephen A. Rago. Addison-Wesley, 2005. All nuances and subtleties of using UNIX APIs are found herein. Buy this book! Read it! And most importantly, live it.

 

 

 

 

 

 

 

 

 
 
 
 
 
 
 
 
 
 
 

你可能感兴趣的:(操作系统)