课程主页:https://courses.cs.washington.edu/courses/cse351/16sp/

课程资料:

实验部分:https://github.com/vuquangtrong/HW-SW_Interface_Course

实验说明:https://courses.cs.washington.edu/courses/cse351/13sp/lab-0.html

课件:http://academictorrents.com/details/b63a566df824b39740eb9754e4fe4c0140306f4b

课程视频:https://www.bilibili.com/video/BV1Zt411s7Gg?from=search&seid=8781593976070799647

这次回顾第八部分,这部分介绍了进程。

第8节:虚拟内存

异常控制流

控制流
  • 处理器只做一件事:
    • 从启动到关闭,CPU只需读取并执行(解释)一系列指令,一次一条命令
    • 此序列是CPU的控制流

改变控制流
  • 到目前为止,有两种用于更改控制流的机制:
    • 跳跃和分支
    • call和return
      对程序状态的变化做出反应
  • 对于有用的系统而言不够:难以对系统状态的变化做出反应
    • 数据来自磁盘或网络适配器
    • 除以零的指令
    • 用户在键盘上按Ctrl+C
    • 系统计时器到期
  • 系统需要“异常控制流”的机制
异常
  • 一个异常是响应某些事件(即处理器状态更改)将控制权转移到OS内核。
    • 内核是操作系统的内存驻留部分
    • 事件示例:除以0,算术溢出,页面错误,$\text{I/O}$请求完成,键入Ctrl+C
  • 处理完引起异常的事件的类型,会发生以下三种情况
    • 处理程序将控制返回给当前指令$I_{\text {curr} }$,即当事件发生时正在执行的指令。
    • 处理程序将控制返同给 $I_{\text {next } },$ 如果没有发生异常将会执行的下一条指令。
    • 处理程序终止被中断的程序。

异常表
  • 每种类型的事件都有一个唯一的异常号k
  • k=异常表的索引(也称为中断向量)
  • 每次发生异常k时都调用程序k

异步异常(中断)
  • 由处理器外部的事件引起
    • 通过设置处理器的中断引脚来指示
    • 处理程序返回到“下一条”指令
  • 例子:
    • 定时器中断
      • 每隔几毫秒,一个外部计时器芯片就会触发一个中断
      • 内核用来从用户程序取回控制权
    • 来自外部设备的$\text{I/O}$中断
      • 在键盘上按Ctrl+C
      • 来自网络的数据包到达
      • 磁盘中的数据到达
同步异常
  • 由执行指令引起
    • 陷阱
      • 有意的异常
      • 示例:系统调用,断点陷阱,特殊说明
      • 将控制权返回“下一条”指令
    • 故障
      • 无意但可能可以恢复
      • 示例:页面错误(可恢复),保护错误(不可恢复),浮点异常
      • 重新执行产生故障的“当前”指令或中止
    • 终止
      • 意外和无法恢复
      • 示例:非法指令,奇偶校验错误,机器检查
      • 中止当前程序
Trap示例:打开文件
  • 用户调用:open(filename, options)

  • 函数open执行系统调用指令int

    0804d070 <__libc_open>:
     . . .
     804d082:	cd 80                	int    $0x80
     804d084:	5b                   	pop    %ebx
     . . .

  • 操作系统必须查找或创建文件,并准备读取或写入文件

  • 返回整数文件描述符

故障示例:Page Fault
int a[1000];
main ()
{
    a[500] = 13;
}
  • 用户写入内存位置

  • 用户内存的那部分(页)当前在磁盘上

    80483b7:	c7 05 10 9d 04 08 0d 	movl   $0xd,0x8049d10

  • 页面处理程序必须将页面加载到物理内存中

  • 返回错误指令:mov再次执行!

  • 第二次尝试成功

故障示例:无效的内存引用
int a[1000];
main ()
{
    a[5000] = 13;
}
80483b7:	c7 05 60 e3 04 08 0d 	movl   $0xd,0x804e360

  • 页面处理程序检测到无效地址
  • 向用户进程发送SIGSEGV信号
  • 用户进程退出时出现“分段故障”
总结
  • 异常
    • 需要非标准控制流程的事件
    • 由外部(中断)或内部(陷阱和故障)生成
    • 异常处理后,可能会出现以下三种情况之一:
      • 重新执行当前指令
      • 使用下一条指令继续执行
      • 中止导致异常的进程

什么是进程

  • 我们为什么要学习进程?
    • 进程是我们计算机系统中的另一种抽象——进程抽象提供了程序和底层CPU+内存之间的接口。
  • 进程与异常控制流有什么关系?
    • 异常控制流是操作系统用来使多个进程在同一系统上运行的机制。
  • 什么是程序?处理器?进程?
进程
  • 定义:进程是正在运行的程序的实例。
    • 计算机科学中最深刻的想法之一。
    • 与“程序”或“处理器”不同。
  • 进程为每个程序提供了两个关键的抽象:
    • 逻辑控制流程
      • 每个程序似乎都专用于CPU。
      • 由称为上下文切换的内核机制提供。
    • 专用地址空间
      • 每个程序似乎都专用于主存储器。
      • 由称为虚拟内存的内核机制提供。

进程并发
  • 每个进程都是一个逻辑控制流。
  • 如果两个进程的时间重叠,则两个进程将同时运行(并发)。
  • 否则,它们是顺序的。
  • 例子(运行在单核上):
    • 并发:$A \& B, A \& C$
    • 顺序:$B\& C$
用户视角下的并发
  • 并发进程的控制流在时间上实际上是不相交的
  • 但是,我们可以将并发进程视为彼此并行运行

用户模式和内核模式

为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常是用某个控制寄存將中的一个模式位(mode bit)来提供这种功能的,当设置了该模式,进程就运行在内核模式中,该模式下执行指令集中的任何指令;当没有设置模式位时,进程就运行用户模式中,该模式下不允许执行特权指令。

上下文切换
  • 进程由共享的内存驻留OS代码块(称为内核)管理
    • 重要提示:内核不是一个独立的进程,而是作为某些现有进程的一部分运行的。
  • 控制流通过上下文切换从一个进程传递到另一个进程

创建进程

创建新的进程
  • fork-exec模型:
    • fork()创建当前进程的副本
    • execve()将当前进程的代码和地址空间替换为其他程序的代码
  • fork()和execve()是系统调用
    • 注意:Windows中的进程创建与Linux的fork-exec模型略有不同
  • 其他系统需要的进程管理:
    • getpid()
    • exit()
    • wait()/waitpid()
fork: 创建新的进程
  • pid_t fork(void)

    • 创建一个与调用进程(父进程)相同的新进程(子进程)

    • 返回0到子进程

    • 将子进程的ID(pid)返回给父进程

      pid_t pid = fork();
      if (pid == 0) {
         printf("hello from child\n");
      } else { 
         printf("hello from parent\n");
      }
  • fork是唯一的(并且常常令人困惑),因为它被调用一次,但是返回两次

理解Fork

Fork例子
  • 父进程和子进程都运行相同的代码
    • 通过fork()的返回值区分父项与子项
    • fork()未定义之后哪个先运行
  • 以相同的状态开始,但是每个都有一个私有副本
    • 相同的变量,相同的调用堆栈,相同的文件描述符…
void fork1()
{
    int x = 1;
    pid_t pid = fork();
    if (pid == 0) {
		printf("Child has x = %d\n", ++x);
    } else {
		printf("Parent has x = %d\n", --x);
    }
    printf("Bye from process %d with x = %d\n", getpid(), x);
}

Fork-Exec

Fork-Exec
  • fork-exec模型:
    • fork()创建当前进程的副本
    • execve()将当前进程的代码和地址空间替换为其他程序的代码
// Example arguments: path="/usr/bin/ls”,
//     argv[0]="/usr/bin/ls”, argv[1]="-ahl", argv[2]=NULL
void fork_exec(char *path, char *argv[])
{
    pid_t pid = fork();
    if (pid != 0) {
        printf("Parent: created a child %d\n”, pid);
    } else {
        printf("Child: exec-ing new program now\n");
        execv(path, argv);
    }
    printf("This line printed by parent only!\n");
}
Exec新程序

在Linux shell中运行命令“ls”时发生的情况的高层次图:

execve:加载和运行程序
  • ```c
    int execve(
    char *filename,
    char *argv[],
    char *envp[]
    
    )
    
    - 加载并在当前进程中运行:
    
      - 可执行filename
      - 带参数列表argv
      - 和环境变量列表envp
        - Env变量:“name=value”字符串(例如"PWD=/homes/iws/pjh")
    
    - execve不返回(除非出现错误)
    
    - 覆盖代码,数据和堆栈
    
      - 保留pid,打开的文件以及其他一些项目
    
    ![](https://github.com/Doraemonzzz/md-photo/blob/master/Hardware%20Software%20Interface/Section8/2020083006.jpg?raw=true)
    
    
    
    ##### exit:结束进程
    
    - void exit(int status)
      - 退出进程
        - 状态码:0用于正常退出,非零用于异常退出
      - atexit()注册退出时要执行的函数
    
    ```c
    void cleanup(void) {
       printf("cleaning up\n");
    }
    
    void fork6() {
       atexit(cleanup);
       fork();
       exit(0);
    }
僵死进程
  • 理念
    • 当进程终止时,它仍然消耗系统资源
      • 示例:退出状态,各种OS表
    • 被称为“僵死进程”
  • 回收
    • 由父进程对终止的子进程执行(使用wait或waitpid)
    • 父进程被给予退出状态信息
    • 内核然后删除“僵死进程”
  • 如果父进程不回收怎么办?
    • 如果任何父进程在没有回收子进程的情况下终止,那么内核会安排init进程(pid == 1)成为孤儿进程的养父,然后进行回收操作。
    • 因此,长期运行的进程中会进行显式回收,否则会消耗系统的内存资源。
      • 例如,shells和服务器
wait:与子进程同步
  • 父进程通过调用wait为子进程回收
  • int wait (int * child_status)
    • 暂停当前进程,直到其子进程之一终止
    • 返回值是终止的子进程的pid
    • 如果child_status != NULL,则它将指向的整数将设置为一个值,该值指示子进程终止的原因和退出状态:
      • 使用wait.h中定义的宏进行检查
        • WIFEXITED,WEXITSTATUS,WIFSIGNALED,WTERMSIG,WIFSTOPPED,WSTOPSIG,WIFCONTINUED
wait例子
void fork_wait() {
   int child_status;
   pid_t child_pid;

   if (fork() == 0) {
      printf("HC: hello from child\n");
   } else {
      child_pid = wait(&child_status);
      printf("CT: child %d has terminated\n”, child_pid);
   }
   printf("Bye\n");
   exit(0);
}

进程管理总结
  • fork为我们提供了同一进程的两个副本(但是fork()向两个进程返回了不同的值)
  • execve用一个新的进程替代真正运行的进程
    • 两进程程序:
      • 第一个fork()
      • if (pid == 0) { //child code } else { //parent code }
    • 两种不同的程序:
      • 第一个fork()
      • if (pid == 0) { execve() } else { //parent code }
      • 现在运行两个完全不同的程序
  • wait/waitpid用于同步父/子执行并获得子进程

总结

  • 进程
    • 在任何给定时间,系统都有多个活动进程
    • 一次只能执行一个,但是每个进程似乎都可以完全控制处理器
    • 操作系统会定期在活动进程之间“上下文切换”
      • 使用异常控制流实现
  • 进程管理
    • fork-exec模型