【Operating Systems:Three Easy Pieces 操作系统导论 】 4 ~ 6 章 (进程 | 进程 API | 受限直接执行)

【读书笔记】 Operating Systems:Three Easy Pieces 操作系统导论

hua

第四章、 抽象 : 进程

4.1 什么是进程 ?

  • 操作系统为正在运行的程序提供的抽象
  • 进程可以访问的内存(称为地址空间,address space) 是该进程的一部分。
  • 进程的机器状态的另一部分是寄存器。
  • 例如,程序计数器(Program Counter,PC)(有时称为指令指针,Instruction Pointer 或 IP)告诉我们程序当前 正在执行哪个指令;类似地,栈指针(stack pointer)和相关的帧指针(frame pointer)用于 管理函数参数栈、局部变量和返回地址。

4.2进程API :

 创建(create):操作系统必须包含一些创建新进程的方法。在 shell 中键入命令 或双击应用程序图标时,会调用操作系统来创建新进程,运行指定的程序。
 销毁(destroy):由于存在创建进程的接口,因此系统还提供了一个强制销毁进 程的接口。当然,很多进程会在运行完成后自行退出。但是,如果它们不退出, 用户可能希望终止它们,因此停止失控进程的接口非常有用。
 等待(wait):有时等待进程停止运行是有用的,因此经常提供某种等待接口。  其他控制(miscellaneous control):除了杀死或等待进程外,有时还可能有其他4.3 进程创建:更多细节 21
控制。例如,大多数操作系统提供某种方法来暂停进程(停止运行一段时间), 然后恢复(继续运行)。
 状态(statu):通常也有一些接口可以获得有关进程的状态信息,例如运行了多 长时间,或者处于什么状态。
 其他控制(miscellaneous control):除了杀死或等待进程外,有时还可能有其他控制。例如,大多数操作系统提供某种方法来暂停进程(停止运行一段时间), 然后恢复(继续运行)

4.3 进程创建:更多细节

  • 操作系统如何启动并运 行一个程序?进程创建实际如何进行 ?
    在这里插入图片描述
  • 操作系统运行程序必须做的第一件事是将代码和所有静态数据(例如初始化变量)加载(load)到内存中,
  • 程序最初以某种可执行格式驻留在磁盘上(disk,或者在某些现代系统中,在基于闪存的 SSD 上)。因此,将程序和静态数据加载到 内存中的过程,需要操作系统从磁盘读取这些字节,并将它们放在内存中的某处 .

4.4 进程状态

 运行(running):在运行状态下,进程正在处理器上运行。这意味着它正在执行 指令。
 就绪(ready):在就绪状态下,进程已准备好运行,但由于某种原因,操作系统 选择不在此时运行。
 阻塞(blocked):在阻塞状态下,一个进程执行了某种操作,直到发生其他事件 时才会准备运行。一个常见的例子是,当进程向磁盘发起 I/O 请求时,它会被阻塞, 因此其他进程可以使用处理器。

在这里插入图片描述

  • 有IO 会造成进程的堵塞
    在这里插入图片描述
  • 操作系统必须作出许多决定来让CPY繁忙来繁忙来提高资源利用率。

4.5 数据结构

在这里插入图片描述

  • state 状态还有 init zomibe …

第 5 章 插叙:进程 API

5.1 fork()系统调用

系统调用fork()用于创建新进程

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

int main(int argc, char *argv[])
{
    
    
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) {
    
    
        // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
    
    
        // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else {
    
    
        // parent goes down this path (original process)
        printf("hello, I am parent of %d (pid:%d)\n",
	       rc, (int) getpid());
    }
    return 0;
}
运行这段程序(p1.c),将看到如下输出: 
prompt> ./p1
hello world (pid:29146) hello, I am parent of 29147 (pid:29146) 
hello, I am child (pid:29147) 
  • 子进程并我是完全拷贝了父进程。具体来说,虽然它拥有自己的 地址空间(即拥有自己的私有内存)、寄存器、程序计数器等,但是它从 fork()返回的值是不同的。父进程获得的返回值是新创建子进程的 PID,而子进程获得的返回值是 0。
  • 在其他情况下,子进程可能先运行 , 会有不同的情况 , 取决于cpu调度

5.2 wait()系统调用

  • 详细请使用 man 手册
  • 进程一旦调用了 wait,就 立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait 就会收集这个子进程的信息, 并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    
    
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) {
    
    
        // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
    
    
        // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
	sleep(1);
    } else {
    
    
        // parent goes down this path (original process)
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
	       rc, wc, (int) getpid());
    }
    return 0;
}

在这里插入图片描述

  • 该系统调用会谁子进程运行结束后才返回①。因此,即使父进程先运 行,它也会礼貌地等待子进程运行完毕,然后 wait()返回,接着父进程才输出自己的信息。

5.3 最后是 exec()系统调用

  • 这个系统调用可以让子进程执行与父进程我同的程序。例如,谁 p2.c 中调用 fork(),这只是谁你想运行相同程序 的拷贝谁有用。但是,我我常常想运行我同的程序,exec()正好做这样的事(
  • exec()有几种变体:execl()、execle()、execlp()、execv()和 execvp()。请阅读 man 手册以了解更多信息。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int
main(int argc, char *argv[])
{
    
    
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();
    if (rc < 0) {
    
    
        // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
    
    
        // child (new process)
        printf("hello, I am child (pid:%d)\n", (int) getpid());
        char *myargs[3];
        myargs[0] = strdup("wc");   // program: "wc" (word count)
        myargs[1] = strdup("p3.c"); // argument: file to count
        myargs[2] = NULL;           // marks end of array
        execvp(myargs[0], myargs);  // runs word count
        printf("this shouldn't print out");
    } else {
    
    
        // parent goes down this path (original process)
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
	       rc, wc, (int) getpid());
    }
    return 0;
}
  • 用 fork()、wait()和 exec()(p3.c)

5.4 为什么这样设计 API

在这里插入图片描述

  • fork()和 exec()的分离,让 shell 可以方便地实现很多有用的功能。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    
    
    int rc = fork();
    if (rc < 0) {
    
    
        // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) {
    
    
	// child: redirect standard output to a file
	close(STDOUT_FILENO); 
	open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);

	// now exec "wc"...
        char *myargs[3];
        myargs[0] = strdup("wc");   // program: "wc" (word count)
        myargs[1] = strdup("p4.c"); // argument: file to count
        myargs[2] = NULL;           // marks end of array
        execvp(myargs[0], myargs);  // runs word count
    } else {
    
    
        // parent goes down this path (original process)
        int wc = wait(NULL);
	assert(wc >= 0);
    }
    return 0;
}
prompt> wc p3.c > newfile.txt
prompt> ./p4
prompt> cat p4.output 
		32  109  846 p4.c

-- p4 我实调用了 fork 来创建新的子进程,之后调用 execvp()来执行 wc。
-- 屏幕上谁有看到输出, 是由于结果被重我向到文件 p4.output。

  • 补充:RTF(Friendly)M——阅读 man 手册

job

在这里插入图片描述

第 6 章 机制:受限直接执行

  • 操作系统需要以某种方式让许多任务共享物理 CPU
  • 运行一个进程一段时间,然后运行另一个进程,如此轮 换。通过以这种方式时分共享(time sharing)CPU,就实现了虚拟化 。 然而存在问题是 性能(不增加额外开销)与控制权(权限)。

6.1 基本技巧:受限直接执行

  • 为了使程序尽可能快地运行的技术 称之为受限的 直接执行(limited direct execution) LDE , 只需直接在CPU 上运行程序即可 。

  • 使用正常的调用并返回跳转到程序的 main(),并在稍后回到内核。
    在这里插入图片描述

  • 实际并没有怎么简单 ,如果对运行程序没有限制,操作系统将无 法控制任何事情,因此会成为“仅仅是一个库”

6.2 问题 1:受限制的操作(特权问题 )

  • 硬件与操作系统存在的问题 : 关键问题:如何执行受限制的操作??

提示:采用受保护的控制权转移
硬件通过提供不同的执行模式来协助操作系统。在用户模式(user mode)下,应用程序不能完全访问硬件资源。在内核模式(kernel mode)下,操作系统可以访问机器的全部资源。还提供了陷入(trap)内核和从陷阱返回(return-from-trap)到用户模式程序的特别说明,以及一些指令,让操作系统告诉硬件陷阱表(trap table)在内存中的位置。

我们采用的方法是引入新的处理器模式:

用户模式(user mode)

在用户模式下运行的代码会受到限制。例如,在用户模式下运行时,进程不能发出 I/O 请求。这样做会导致处理器引发异常,操作系统可能会终止进程。

内核模式(kernel mode)

操作系统(或内核)就以这种模式运行。在此模式下,运行的代码可以做它喜欢的事,包括特权操作,如发出 I/O 请求和执行所有类型的受限指令。

系统调用

系统调用允许内核小心地向用户程序暴露某些关键功能,例如访问文件系统、创建和销毁进程、与其他进程通信,以及分配更多内存。。
如果用户希望执行某种特权操作(如从磁盘读取),可以借助硬件提供的系统调用功能。
要执行系统调用,程序必须执行特殊的陷阱(trap)指令。该指令同时跳入内核并将特权级别提升到内核模式。一旦进入内核,系统就可以执行任何需要的特权操作(如果允许),从而为调用进程执行所需的工作。完成后,操作系统调用一个特殊的从陷阱返回(return-from-trap)指令,如你期望的那样,该指令返回到发起调用的用户程序中,同时将特权级别降低,回到用户模式。
执行陷阱时,硬件需要小心,因为它必须确保存储足够的调用者寄存器,以便在操作系统发出从陷阱返回指令时能够正确返回

陷阱如何知道在 OS 内运行哪些代码? 内核通过在启动时设置陷阱表(trap table)来实现.

陷阱表(trap table)

内核通过在启动时设置陷阱表(trap table)来实现陷阱地址的初始化。

当机器启动时,系统在特权(内核)模式下执行,因此可以根据需要自由配置机器硬件。操作系统做的第一件事,就是告诉硬件在发生某些异常事件时要运行哪些代码。例如,当发生硬盘中断,发生键盘中断或程序进行系统调用时,应该运行哪些代码?操作系统通常通过某种特殊的指令,通知硬件这些陷阱处理程序的位置。一旦硬件被通知,它就会记住这些处理程序的位置,直到下一次重新启动机器,并且硬件知道在发生系统调用和其他异常事件时要做什么(即跳转到哪段代码)。 提高安全性!!

在这里插入图片描述

问题 2:在进程之间切换

关键问题:如何重获 CPU 的控制权
操作系统如何重新获得 CPU 的控制权(regain control),以便它可以在进程之间切换?

协作方式:等待系统调用

  • 运行时间过长的进程被假定会定期放弃 CPU
  • 系统调用 eg、 yield

非协作方式:时钟中断

时钟中断(timer interrupt)。时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupt handler)会运行。此时,操作系统重新获得 CPU 的控制权,因此可以做它想做的事:停止当前进程,并启动另一个进程。

请注意,硬件发生中断时有一定的责任,尤其是在中断发生时,要为正在运行的程序保存足够的状态,以便随后从陷阱返回指令能够正确恢复正在运行的程序。该操作可以视为隐式的操作,与显式的系统调用很相似。

保存和恢复上下文

  • 当操作系统通过上述两种方式获取控制权后,就可以决定是否切换进程,这个决定是由调度程序(scheduler)做出

  • 当操作系统决定切换进程时,需要首先进行上下文切换(context switch),就是为当前正在执行的进程保存一些寄存器的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)。这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程,而是继续执行另一个进程。

上下文切换并不仅仅保存和恢复寄存器,还包含了其他操作,如页表的切换等。

  • 操作系统决定从正在运行的进程 A 切换到进程 B。此时,它调用 switch()例程, 该例程仔细保存当前寄存器的值(保存到A的进程结构),恢复寄存器进程 B(从它的进程 结构),然后切换上下文(switch context),具体来说是通过改变栈指针来使用 B的内核栈(而 不是A的)。最后,操作系统从陷阱返回,恢复 B 的寄存器并开始运行它。
    在这里插入图片描述
    xv6 的上下文切换代码 :
OS_CPU_PendSVHandler:
    CPSID   I                                                   @ Prevent interruption during context switch
    MRS     R0, PSP                                             @ PSP is process stack pointer

    CMP     R0, #0
    BEQ     OS_CPU_PendSVHandler_nosave                         @ equivalent code to CBZ from M3 arch to M0 arch
                                                                @ Except that it does not change the condition code flags

    SUBS    R0, R0, #0x10                                       @ Adjust stack pointer to where memory needs to be stored to avoid overwriting
    STM     R0!, {
    
    R4-R7}                                        @ Stores 4 4-byte registers, default increments SP after each storing
    SUBS    R0, R0, #0x10                                       @ STM does not automatically call back the SP to initial location so we must do this manually

    LDR     R1, =OSTCBCur                                       @ OSTCBCur->OSTCBStkPtr = SP;
    LDR     R1, [R1]
    STR     R0, [R1]                                            @ R0 is SP of process being switched out
                                                                @ At this point, entire context of process has been saved

问题原因:

代码优化时将 rbuf_len 保存在了寄存器 r8 上,在进行上下文切换时,r8 寄存器没有被保存,导致 r8 寄存器的值被其他进程修改,切换回本进程后,r8 的值也无法恢复。

思考:并发对中断的影响

处理一个中断时发生另一个中断,会发生什么?
一种方法是,在中断处理期间禁止中断(disable interrupt)。这样做可以确保在处理一个中断时,不会将其他中断交给 CPU。当然,操作系统这样做必须小心。禁用中断时间过长可能导致丢失中断,这(在技术上)是不好的。

提示:重新启动是有用的 , 重启后 OS 首先(在启动时)设置陷阱处理程序并启动时钟中断,然后仅在受限 模式下运行进程.操作系统能确信进程可以高效运行, 只在执行特权操作,或者当它们独占CPU时间过长并因此需要切换时,才需要操作系统干预。

猜你喜欢

转载自blog.csdn.net/weixin_49486457/article/details/130178790
今日推荐