操作系统(二)之进程与线程

自己写的操作系统学习笔记,如有雷同不胜荣幸,如有错误敬请指正


1. 进程

在进程模型中,计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程,简称进程
在只有一个物理程序计数器时,在每个程序运行时,它的逻辑程序计数器被装入实际的程序计数器中。当该程序执行结束(或暂停执行)时,物理程序计数器被保存在内存中该进程的逻辑程序计数器中。

进程有存放程序正文和数据以及其他资源的地址空间,这些资源包括:打开的文件,子进程,即将发生的定时器,信号处理程序,账号信息等,把他们都放到进程中可以更容易的管理

4种主要事件导致进程的创建:

  • 系统初始化
  • 正在运行的程序执行了创建进程的系统调用
  • 用户请求创建一个新进程
  • 一个批处理作业的初始化

进程的终止(UNIX:exit;Windows:ExitProcess):

  • 正常退出(自愿的)
  • 出错退出(自愿的)
  • 严重错误(非自愿)
  • 被其他进程杀死(非自愿)

守护进程: 是一类在后台运行的特殊进程,用于执行特定的系统任务。守护进程是一个在后台运行并且不受任何终端控制的进程

在一个已存在的进程中创建新进程:

  • 在 UNIX 系统中,fork 可以用来创建新进程,这个系统调用会创建一个与调用进程相同的副本。在调用 fork 后,这两个进程(父和子进程)拥有相同的内存映像,同样的环境字符串和同样的打开文件。通常,子进程接着执行 execve 或一个类似的系统调用,以修改其内存映像并运行一个新的程序(之所以要安排两步创建创建进程,是为了在 fork 之后但在 execve 之前允许该子进程处理其文件描述符,这样可以完成对标准输入文件,标准输出文件和标准错误文件的重定向)
  • 在Windows中,win32函数调用 CreateProcess 既处理进程的创建,也负责把正确的程序装入新的进程。该调用有十个参数:包括① 要执行的程序 ② 输入给该程序的命令行参数 ③ 各种安全属性 ④ 有关打开的文件是否继承的控制位 ⑤ 优先级信息 ⑥ 该进程所需要创建的窗口规格 ⑦ 指向一个结构的指针 等,在该结构中新创建进程的信息被返回给调用者。

内存通过写时复制共享: 这意味着一旦两者之一想要修改部分内存,则这块内存首先被明确的复制,以确保修改发生在私有内存区域(可写的内存是不可以共享的)

进程的层序结构: 进程只有一个父进程(但可以有零个,一个或多个子进程)。在创建进程的时候,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程

进程的三种状态图:

  • 运行态(该时刻进程实际占用CPU)
  • 就绪态(可运行,但因为其他进程正在运行而占时停止)
  • 阻塞态(除非某种外部事件发生,否则进程不能运行)

这里写图片描述

  • 在操作系统发现进程不能继续运行时,发生转换1
  • 系统认为一个运行进程占用处理器的时间已经过长,决定让其他进程使用CPU时间时,会发生转换2
  • 在系统已经让所有其他进程享有了它们应有的公平待遇而重新轮到第一个进程再次占用CPU运行时,会发生转换3
  • 当进程等待的一个外部事件发生时(如一些输入到达),则发生转换4

转换2和3是由调度程序引起,调度程序的主要工作:决定应当运行哪个进程,何时运行及它应当运行多长时间

进程表: 每个进程占用一个进程表项(进程控制块),该表项包含了进程状态的重要信息
这里写图片描述

中断向量: 与每一类 I/O 类关联的是一个称作中断向量 的位置(靠近内存底部的固定区域),它包含中断服务程序的入口地址。(所有的中断都从保存寄存器开始)
中断发生后操作系统最底层的工作步骤:

  • ① 硬件压入堆栈程序计数器等
  • ② 硬件从中断向量装入新的程序计数器
  • ③ 汇编语言过程保存寄存器值
  • ④ 汇编语言过程设置新的堆栈
  • ⑤ C 中断服务例程运行(典型的读和缓冲输入)
  • ⑥ 调度程序决定下一个将运行的进程
  • ⑦ C 过程返回至汇编代码
  • ⑧ 汇编语言过程开始运行新的当前进程

多道程序设计可以提高CPU的利用率:假设一个进程等待 I/O 操作的时间与其停留在内存中时间的比为 P,当内存中同时有 n 个进程时,则所有 n 个进程都在等待 I/O (此时CPU空转)的概率为 pn p n ,则 CPU利用率 = 1 - pn p n (n 称为多道程序设计的道数)


2. 线程

进程中拥有一个执行的线程:

  • 在线程中有一个程序计数器,用来记录接着要执行哪一条指令
  • 线程拥有寄存器,用来保存线程当前的工作变量
  • 线程还拥有一个堆栈,用来记录执行历史,其中每一帧保存了一个已调用的但是还没有从中返回的过程

进程用于把资源集中到一起,而线程则是在CPU上被调度执行的实体。
在同一个进程环境中,允许彼此之间有较大独立性的多个线程执行

这里写图片描述

1. 线程的使用

  • ① 并行实体拥有共享同一个地址空间和所有可用数据的能力
  • ② 由于线程比进程更轻量级,所以它们比进程更容易创建,也更容易撤销
  • ③ 若多个线程都是 CPU 密集型的,那么并不能获得性能上的提升,但是如果存在着大量的计算和大量的 I/O 处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快应用程序执行的速度
  • ④ 在多CPU系统中,多线程是有益的,并且真正的并行有了实现的可能

例如:一个组织 web 服务器
这里写图片描述
这里写图片描述

  • 分派程序的线程从网络中读入工作请求
  • 在检查完请求之后,分派线程挑选一个空转的工作线程,提交该请求
  • 接着,分派线程唤醒睡眠的工作线程,将它从阻塞状态转换为就绪状态
  • 在工作线程被唤醒后,它检查有关的请求是否在web页面高速缓存中。如果没有,该线程开始一个从磁盘调入页面的read操作,并且阻塞直到该磁盘操作完成

有限状态机: 每个计算都有一个被保存的状态,存在一个会发生且使得相关状态发生改变的事件集合。

这里写图片描述

2. 经典线程模型

在多线程情况下,进程通常会从当前的单个线程开始,这个线程有能力通过调用一个库函数(如:thread_create,其参数专门指定了新线程要运行的过程名;thread_yeild:它允许线程自动放弃CPU而让另一个线程运行,因为线程无法利用时钟中断强制线程让出CPU)创建新的线程

为实现可移植的线程程序,IEEE在IEEE标准1003.1c 中定义了线程的标准,它定义的线程包叫:pthread
这里写图片描述

① 在用户空间中实现线程: 把整个线程包放在用户空间中,内核对线程包一无所知(从内核角度考虑,就是按正常的方式管理,即单线程进程)

  • 优点:① 用户级线程包可以在不支持线程的操作系统上实现 (保存该线程状态的过程和调度程序都只是本地过程,所以启动它们比进行内核调用效率更高;另一方面,不需要陷入内核,不需要上下文切换,也不需要对内存高速缓存刷新,这就使得线程调度非常快捷)② 允许每个进程有自己定制的调度算法(用户级线程具有较好的可扩展性)
  • 缺点:① 如何实现阻塞系统调用(在系统调用周围从事检查的代码称为包装器)② 如果一个线程开始运行,那么在该进程中的其他线程就不能运行(在一个单独的进程内部,没有时钟中断,所以不可能用轮转调度的方式调度线程)

② 在内核中实现线程: 所有能够阻塞线程的调用都以系统调用的形式实现,由于在内核中创建或撤销线程的代价比较大,当某个线程被撤销时,就把它标志为不可运行,但是其内核数据结构没有收到影响。
- 缺点:系统调用的代价比较大

这里写图片描述

在用户空间管理线程时,每个进程需要有其专用的线程表 ,用来跟踪该进程中的线程。
内核线程表保存了每个线程的寄存器,状态和其他信息。

③ 混合实现: 一种方法是使用内核级线程,然后将用户级线程与某些或者全部内核线程多路复用起来。在这种模型中,每个内核级线程有一个可以轮流使用的用户级线程集合。
这里写图片描述

④ 调度程序激活机制: 当使用调度程序激活机制时,内核给每个进程安排一定数量的虚拟机,并且让(用户空间)运行时系统将线程分配到处理器上。

目标: 是模拟内核线程的功能,但是为线程包提供通常在用户空间中才能实现的更好的性能和更大的灵活性。
上行调用: 内核通过在一个已知的起始地址启动运行时系统,从而发出了通知,这是对unix中信号的一种粗略模拟。

⑤ 弹出式线程:

对于服务请求:

  • 传统的方法是将进程或线程阻塞在一个 receive 系统调用上,等待消息的到来。当消息到达时,该系统调用接收消息,并打开消息检查其内容,然后进行处理。
  • 在该处理方式中,一个消息的到达导致系统创建一个处理该消息的线程(弹出式线程)。弹出式线程的结果是,消息到达与处理开始之间的时间非常短。

3. 进程间通信

竞争条件: 两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
互斥: 以某种手段确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的操作。
临界区域: 对共享内存进行访问的程序片段

保证使用共享数据的并发进程能够正确和高效的运行协作的解决方案:

  • ① 任何两个进程不能同时处于其临界区
  • ② 不应对CPU的速度和数量做任何假设
  • ③ 临界区外运行的进程不得阻塞其他进程
  • ④ 不得使进程无限期等待进入临界区

这里写图片描述

互斥方案的实现:

① 忙等待的互斥

  • 屏蔽中断: 使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开中断。屏蔽中断后,时钟中断也被屏蔽,CPU只有发生时钟中断或其他中断时才会进程进程切换。于是,一旦某个进程屏蔽中断后,就可以检查和修改共享内存,而不必担心其他进程介入。

    屏蔽中断对于操作系统来说是很有用的,但对于用户进程则不是一种合适的通用互斥机制

    • 如果系统是多处理器,则屏蔽中断仅仅对执行 disable 指令的那个CPU有效,其他CPU仍将继续运行,并可以访问共享内存。
    • 对内核来说,它在更新变量或列表的几条指令期间将中断屏蔽是很方便的
  • 锁变量

  • 严格轮换法
    忙等待: 连续测试一个变量直到某个值出现为止
    自旋锁: 只有在有理由认为等待时间是非常短的情况下,才使用忙等待,用于忙等待的锁称为自旋锁。
    这里写图片描述

  • Peterson解法
    这里写图片描述
    在使用共享变量之前,各个进程使用其进程号0或1作为参数来调用enter_region。该调用在需要时将使进程等待,直到能安全的进入临界区。在完成对共享变量的操作后,进程将调用 leave_region,表示操作已完成,若其他的进程希望进入临界区,则现在就可以进入。

  • TSL 指令: 执行 TSL 指令的CPU将锁住内存总线,以禁止其他CPU在本指令结束前访问内存。

    TSL RX,LOCK 称为测试并加锁,它将一个内存字 lock 读到寄存器 RX 中,然后在该内存地址上存一个非零值。读字和写字操作保证是不可分割的,即该指令结束之前其他处理器均不允许访问该内存字。
    当 lock 为 0 时,任何进程都可以使用 TSL 质量将其设置为 1,并读写共享内存。当操作结束时,进程用一条普通的 move 指令将 lock 的值重新设置为 0.
    这里写图片描述
    XCHG 原子性的交换了两个位置的内容
    这里写图片描述

② 睡眠与唤醒

优先级反转问题: 调度规则规定,只要 H 处于就绪状态就可以运行。在某一时刻,L 处于临界区中,此时 H 变到就绪状态,准备运行。现在 H 开始忙等待,但由于当 H 就绪时 L 不会被调度,也就无法离开临界区,所以 H 将永远忙等待下去。
sleep: 是一个将引起调用进程阻塞的系统调用,即被挂起,直到另外一个进程将其唤醒
wakeup: 调用一个参数,即要被唤醒的进程。

生产者-消费者问题:
这里写图片描述

问题的实质在于发给一个未睡眠进程的 wakeup 信号丢失了,如果它没有丢失,则一切都很正常。快速弥补法则是修改规则,加上一个唤醒等待位;当一个 wakeup 信号发给一个清醒的进程信号时,将该位置 1,随后,当该进程要睡眠时,如果唤醒等待位为 1,则将该位清除,而该进程仍然保持清醒。(如果有更多的进程,那将会有更多的唤醒等待位,所以。。。)

解决方法:

① 信号量: 使用一个整形变量来累计唤醒次数,供以后使用。一个信号量的取值可以为 0(表示没有保存下来的唤醒操作) 或者为正值(表示有一个或多个唤醒操作)。

对一个信号量执行down 操作与 up 操作(分别为一般化后的 sleep 和 wakeup):

  • down 操作,则是检查其值是否大于 0,若大于 0,则将其值减 1(即用掉一个保存的唤醒信号)并继续;若该值为 0,则进程将睡眠,而且此时 down 操作并未结束。检查数值,修改变量值以及可能发生的睡眠操作均作为一个单一的,不可分割的原子操作完成(保证一旦一个信号量操作开始,则在该操作完成或阻塞之前,其他进程均不允许访问该信号量)
  • up 操作,与 down 操作相反

原子操作: 是指一组相关联的操作要么都不断的执行,要么都不执行

该解决方案使用了三个信号量:

  • 一个称为 full,用来记录充满的缓冲槽数目
  • 一个称为 empty, 记录空的缓冲槽数目
  • 一个称为 mutex, 用来确保生产者和消费者不会同时访问缓冲区

(full 的初值为 0,empty 的初值为缓冲区中槽的数目,mutex 初值为 1)

供两个或多个进程使用的信号量,其初值为 1,保证同时只有一个进程可以进入临界区,称作二元信号量

这里写图片描述

② 互斥量(信号量的简化版本): 互斥量是一个可以处于两态之一的变量:解锁加锁 (互斥量仅仅适用于管理共享资源或一小段代码)

互斥量使用两个过程:

  • 当一个线程(或进程)需要访问临界区时,它调用 mutex_lock。如果该互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可以自由进入该临界区。
  • 如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用 mutex_unlock。如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。

互斥量分类:

  • 快速用户区互斥量 futex: futex 是 Linux 的一个特性,它实现了基本的锁,但避免了陷入内核。futex 包含两个部分:内核服务用户库

    内核服务提供一个等待队列,它允许多个进程在一个锁上等待。它们将不会运行,除非内核明确的对他们解除阻塞(将一个进程放到等待队列需要系统调用)。
    为了避免系统调用,在没有竞争时,futex 完全在用户空间工作。
    在线程等待时,futex 库不自旋,而是使用一个系统调用把这个线程放在内核的等待队列上。

  • pthread 中的互斥量: 使用一个可以被锁定和解锁的互斥量来保护每个临界区。除互斥量外,pthead 提供另一个同步机制:条件变量

    • 互斥量在允许或阻塞对临界区的访问上市很有用的
    • 条件变量则允许线程由于一些未达到的条件而阻塞(条件变量不会存在内存中)
      这里写图片描述

这里写图片描述

③ 管程: 一个管程是由一个过程,变量及数据结构等组成的一个集合,他们组成一个特殊的模块或软件包(管程是语言概念,但 C 语言不支持)

典型处理方法: 当一个进程调用管程过程时,该过程中的前几条指令将检查在管程中是否有其他的活跃进程。如果有,调用进程将被挂起,直到另一个进程离开管程将其唤醒。如果没有活跃进程在使用管程,则该调用进程进入(进入管程时的互斥有编译器负责)

为了避免管程同时又两个活跃进程:

  • 让新唤醒的进程运行,而挂起另一个进程
  • 执行 signal 的进程必须推出管程,即 signal 语句之可能作为一个管程过程的最后一跳语句
  • 让发信号者继续运行,并且只有在发信号者推出管程之后,才允许等待进程开始运行。

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值