操作系统 - 进程与线程 - 学习笔记

166 阅读32分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 12 天,点击查看活动详情

一、进程

1,进程状态

  • 活动期间:
    • 运行状态(Running):该时刻进程占用 CPU;
    • 就绪状态(Ready):可运行,由于其他进程处于运行状态而暂时停止运行;
    • 阻塞状态(Blocked):该进程正在等待某一事件发生或返回而暂时停止运行,此时即使给它CPU控制权,也无法运行;
  • 基本状态:
    • 创建状态(new):进程正在被创建时的状态;
    • 结束状态(Exit):进程正在从系统中消失时的状态;
  • 挂起状态:
    • 阻塞挂起状态:进程在外存(硬盘),并等待某个事件的出现;
    • 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;
    • 来描述进程没有占用实际的物理内存空间的情况;
    • 处于阻塞状态的进程,可能会占用物理内存空间,在虚拟内存管理的操作系统中,通常会把阻塞状态进程的物理内存空间换出到硬盘,等需要再次运行的时候,再从硬盘换入到物理内存;

2,进程控制块(PCB)

  • 用来描述进程信息的数据结构,是进程存在的唯一标识;
  • 进程描述信息:
    • 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;
    • 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;
  • 进程控制和管理信息:
    • 进程当前状态,如 new、ready、running、waiting 或 blocked 等;
    • 进程优先级:进程抢占 CPU 时的优先级;
  • 资源分配清单:
    • 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息;
  • CPU 相关信息:
    • CPU中各个寄存器的值,当进程被切换时,CPU的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行;
  • 通常通过链表的方式把具有相同状态的进程链在一起,组成各种队列:
    • 将所有处于就绪状态的进程链在一起,称为就绪队列;
    • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列;
    • 对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序;

3,进程的控制

  • 创建进程
    • 操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源;
    • 创建过程:
      • 申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息,比如进程的唯一标识等;
      • 为该进程分配运行时所必需的资源,比如内存资源;
      • 将 PCB 插入到就绪队列,等待被调度运行;
  • 终止进程
    • 进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill 掉);
    • 当子进程被终止时,其在父进程处继承的资源应当还给父进程;
    • 当父进程被终止时,该父进程的子进程就变为孤儿进程,会被 1 号进程收养,并由 1 号进程对它们完成状态收集工作;
    • 终止过程:
      • 查找需要终止的进程的 PCB;
      • 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
      • 如果其还有子进程,则应将该进程的子进程交给 1 号进程接管;
      • 将该进程所拥有的全部资源都归还给操作系统;
      • 将其从 PCB 所在队列中删除;
  • 阻塞进程
    • 当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待,而一旦被阻塞等待,它只能由另一个进程唤醒;
    • 阻塞过程:
      • 找到将要被阻塞进程标识号对应的 PCB;
      • 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
      • 将该 PCB 插入到阻塞队列中去;
  • 唤醒进程
    • 进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的;
    • 如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它;
    • 唤醒过程:
      • 在该事件的阻塞队列中找到相应进程的 PCB;
      • 将其从阻塞队列中移出,并置其状态为就绪状态;
      • 把该 PCB 插入到就绪队列中,等待调度程序调度;
      • 进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句;

4,CPU上下文

  • 定义:CPU寄存器和程序计数器是CPU在运行任何任务前,所必须依赖的环境,这些环境就叫做 CPU 上下文,寄存器存储当前指令,程序计数器下一条指令;
  • CPU上下文切换:系统内核会先把前一个任务的CPU上下文(CPU 寄存器和程序计数器)保存起来,然后CPU加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务;
  • CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换;

5,进程上下文切换

  • 进程是由内核管理和调度的,所以进程的切换只能发生在内核态;
  • 通常会把交换的信息保存在进程的PCB,当要运行另外一个进程的时候,需要从这个进程的 PCB 取出上下文,恢复到 CPU 中,使得这个进程可以继续执行;
  • 切换内容:
    • 包含了虚拟内存、栈、全局变量等用户空间的资源,以及内核堆栈、寄存器等内核空间的资源;
  • 发生切换的场景:
    • 时间片耗尽:当某个进程的时间片耗尽了,进程就从运行状态变为就绪状态,系统从就绪队列选择另外一个进程运行;
    • 系统资源不足:进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
    • 主动挂起:当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
    • 高优进程插队:当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
    • 硬件中断:发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;

二、线程

1,定义

  • 线程是进程当中的一条执行流程;
  • 同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。

2,优缺点

  • 优点:
    • 一个进程中可以同时存在多个线程;
    • 各个线程之间可以并发执行;
    • 各个线程之间可以共享地址空间和文件等资源;
  • 缺点:
    • 当进程中的一个线程崩溃时,可能会导致其所属进程的所有线程崩溃;

3,线程的上下文切换

  • 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;

4,线程的实现

  • 用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
  • 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程;
  • 轻量级进程(LightWeight Process):在内核中来支持用户线程;
  • 用户线程与内核线程对应关系:多对一、一对一、多对多;

5,用户线程

  • 用户线程是基于用户态的线程管理库来实现的,线程控制块(Thread Control Block, TCB)是在库里面来实现的,操作系统只能看到整个进程的PCB,看不到TCB;
  • 用户线程的管理和调度,操作系统是不直接参与的,由用户级线程库函数来完成,包括线程的创建、终止、同步和调度等;
  • 用户线程的优点:
    • 每个进程都需要有它私有的线程控制块(TCB)列表,用来跟踪记录它各个线程状态信息(PC、栈指针、寄存器),TCB 由用户级线程库函数来维护,可用于不支持线程技术的操作系统;
    • 用户线程的切换也是由线程库函数来完成的,无需用户态与内核态的切换,所以速度特别快;
  • 用户线程的缺点:
    • 由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程都不能执行了;
    • 当一个线程开始运行后,除非它主动地交出 CPU 的使用权,否则它所在的进程当中的其他线程无法运行,因为用户态的线程没法打断当前运行中的线程,它没有这个特权,只有操作系统才有,但是用户线程不是由操作系统管理的;
    • 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会比较慢;

6,内核线程

  • 内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责
  • 内核线程的优点:
    • 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行;
    • 分配给线程,多线程的进程获得更多的 CPU 运行时间;
  • 内核线程的缺点:
    • 在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息,如 PCB 和 TCB;
    • 线程的创建、终止和切换都是通过系统调用的方式来进行,因此对于系统来说,系统开销比较大;

7,轻量级进程

  • 轻量级进程(Light-weight process,LWP)是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,LWP都是由一个内核线程支持,由内核管理并像普通进程一样被调度;
  • LWP与普通进程的区别也在于它代表程序的执行线程,只有一个最小的执行上下文和调度程序所需的统计信息,不需要像进程那样多的状态信息;

三、线程与进程的比较

1,四种维度的对比

  • 粒度:进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位,而进程只是给线程提供了虚拟内存、全局变量等资源。
  • 资源:进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
  • 状态:线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系;
  • 开销:线程能减少并发执行的时间和空间开销;

2,性能开销体现

  • 创建:线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们;
  • 终止:线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
  • 切换:同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,在切换的时候不需要切换页表,可以直接共用;而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
  • 数据传递:由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,使得线程之间的数据交互效率更高了;

四、进程的调度

  • 线程是操作系统的调度单位,进程指只有主线程时,调度主线程就等于调度了整个进程;

1,调度时机

  • 在进程的生命周期中,当进程从一个运行状态到另外一状态变化的时候,就会触发一次调度;
  • 以下状态的变化都会触发操作系统的调度:
    • 从就绪态 -> 运行态:当进程被创建时,会进入到就绪队列,操作系统会从就绪队列选择一个进程运行;
    • 从运行态 -> 阻塞态:当进程发生 I/O 事件而阻塞时,操作系统必须选择另外一个进程运行;
    • 从运行态 -> 结束态:当进程退出结束后,操作系统得从就绪队列选择另外一个进程运行;
    • 因为,这些状态变化的时候,操作系统需要考虑是否要让新的进程给 CPU 运行;
  • 如果硬件时钟提供某个频率的周期性中断,那么可以根据如何处理时钟中断 ,把调度算法分为两类:
    • 非抢占式调度算法挑选一个进程,让该进程运行直到被阻塞,或者直到该进程退出,才会调用另外一个进程,不会理时钟中断;
    • 抢占式调度算法挑选一个进程,让该进程只运行某段时间,在该时段结束时,该进程仍然在运行时,则会把它挂起,接着调度程序从就绪队列挑选另外一个进程;这种抢占式调度处理,需要在时间间隔的末端发生时钟中断,以便把 CPU 控制返回给调度程序进行调度,也就是常说的时间片机制;

2,调度原则

  • CPU利用率:
    • 调度程序应确保CPU始终是匆忙的状态,以提高CPU的利用率;
  • 系统吞吐量:
    • 表示的是单位时间内CPU完成进程的数量;
    • 长作业的进程会占用较长的CPU资源,降低了吞吐量,相反短作业的进程会提升系统吞吐量;
  • 周转时间:
    • 周转时间是进程运行+阻塞时间+等待时间的总和,一个进程的周转时间越小越好;
  • 等待时间:
    • 等待时间不是阻塞状态的时间,而是进程处于就绪队列的时间,等待的时间越长,用户越不满意;
  • 响应时间:
    • 用户提交请求到系统第一次产生响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准;

五、进程间的通信

  • 每个进程的用户地址空间都是独立的,不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核;

1,管道

  • 匿名管道:
    • 即 ‘|’,数据单向传输,用完即毁;
    • 通信范围是存在父子关系的进程;
    • 管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的;
    • 在 shell 里通过「|」匿名管道将多个命令连接在一起,实际上也就是创建了多个子进程;
  • 命名管道:
    • mkfifo 创建,管道有数据时写会阻塞;
    • 可以在不相关的进程间也能相互通信;
    • 提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信;
  • 所谓管道,就是内核里面的一串缓存,管道传输的数据是无格式的流且大小受限,遵循先进先出规则;
  • 生命周期与进程生命周期一致;

2,消息队列

  • 消息队列是保存在内核中的消息链表;
  • 消息体的格式由用户自定义,可以双向传递;
  • 通信不及时,数据大小受限,不适合大数据的传输;
  • 存在用户态与内核态之间的数据拷贝开销;

3,共享内存

  • 共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中;
  • 数据不需要再在内核空间与用户空间之间相互拷贝,大大提高了进程间通信的速度;
  • 竞争共享资源时,存在读写冲突;

4,信号量

  • 信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据;
  • 信号量表示资源的数量,控制信号量的方式有两种原子操作:
    • 一个是 P 操作,会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行;
    • 一个是 V 操作,会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
    • P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的;
  • 信号初始化为 1,分别执行PV信号,就代表着是互斥信号量,可以保证共享内存在任何时刻只有一个进程在访问;
  • 信号初始化为 0,A 执行 V 信号,B 执行 P 信号,就代表着是同步信号量,可以保证进程 A 应在进程 B 之前执行;

5,信号

  • 信号是进程间通信机制中唯一的异步通信机制,可以在任何时候发送信号给某一进程;
  • 用户进程对信号的处理方式:
    • 执行默认操作:Linux 对每种信号都规定了默认操作;
    • 捕捉信号:可以为信号定义一个信号处理函数,当信号发生时,就执行相应的信号处理函数;
    • 忽略信号:不希望处理某些信号的时候,就可以忽略该信号,不做任何处理,有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程;

6,Socket

  • 实现跨网络与不同主机上的进程之间通信,也可以在同主机上进程间通信;
  • socket 系统调用有三个参数分别代表:
    • domain 参数用来指定协议族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本机;
    • type 参数用来指定通信特性,比如 SOCK_STREAM 表示字节流对应 TCP、SOCK_DGRAM 表示数据报对应 UDP、SOCK_RAW 表示的是原始套接字;
    • protocal 参数原本是用来指定通信协议的,但现在基本废弃;
  • 根据创建 socket 类型的不同,通信的方式也就不同:
    • 实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
    • 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
    • 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL 和 SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM,另外,AF_UNIX 和 AF_LOCAL 是等价的,所以 AF_UNIX 也属于本地 socket;
  • TCP:
    • 监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket;
    • 成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样;
  • UDP:
    • 对于 UDP 来说,不需要要维护连接,也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念;
    • 只要有一个 socket 多台机器就可以任意通信,每一个 UDP 的 socket 都需要 bind;
    • 每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口;
  • 本地:
    • 本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是绑定一个本地文件,这也就是它们之间的最大区别;

六、互斥与同步

1,概念

  • 互斥:操作共享变量的这段代码称为临界区,多个线程访问共享资源的代码片段可能会导致数据竞争;互斥就是要保证一个线程在临界区执行时,其他线程应该被阻止进入临界区;
  • 同步:就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步;

2,实现

2.1 锁

  • 忙等待锁:
    • 也称自旋锁,一直自旋,利用 CPU 周期,直到锁可用;
    • 在单处理器上,需要抢占式的调度器(即不断通过时钟中断一个线程),否则在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU;
  • 无等待锁:
    • 也称互斥锁,加锁失败后释放cpu;
    • 那当没获取到锁的时候,就把当前线程放入到锁的等待队列,然后执行调度程序,把 CPU 让给其他线程执行;

2.2 信号量

  • 信号量是操作系统提供的一种协调共享资源访问的方法,通常表示资源的数量,对应的变量是一个整型(sem)变量;
  • 还有两个原子操作的系统调用函数来控制信号量的,分别是:P 操作,V 操作;
  • PV 操作的函数是由操作系统管理和实现的,所以操作系统已经使得执行 PV 函数时是具有原子性的;

3,死锁

  • 死锁的四个必要条件
    • 互斥条件、持有并等待条件、不可剥夺条件、环路等待条件;
  • 互斥条件:多个线程不能同时使用同一个资源;
  • 持有并等待条件:线程已经持有了某个资源,并且未释放,又想申请另一资源,并等待另一资源被其他线程释放;
  • 不可剥夺条件:当线程已经持有了资源,在自己使用完之前不能被其他线程获取;
  • 环路等待条件:在死锁发生的时候,两个线程获取资源的顺序构成了环形链;
  • 避免死锁
    • 只需要破坏四个必要条件其中一个就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件;

4,锁的分类

4.1 互斥锁与自旋锁

  • 互斥锁加锁失败后,线程会释放 CPU ,给其他线程;
  • 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
  • 对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的;
  • 互斥锁加锁失败后会有两次线程上下文切换的成本:
    • 当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行;
    • 当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行;
  • 自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源;
  • 如果能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。

4.2 读写锁

  • 由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁,用于能明确区分读操作和写操作的场景;
  • 读写锁的工作原理是:
    • 当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据;
    • 当「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞;
  • 根据实现的不同,读写锁可以分为「读优先锁」和「写优先锁」,容易出现线程饥饿现象;
  • 公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象;

4.3 乐观锁与悲观锁

  • 悲观锁认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁;互斥锁、自旋锁、读写锁,都是属于悲观锁;
  • 乐观锁假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作;只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁,此时解决冲突的重试成本低;

七、调度算法

1,进程调度算法

  • 1)先来先服务(First Come First Serve, FCFS)
    • 每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行;
    • 当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业,对长作业有利;
    • 适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统;
  • 2)最短作业优先(Shortest Job First, SJF)
    • 优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量;
    • 一个长作业在就绪队列等待运行,当就绪队列有非常多的短作业,就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行;
  • 3)高响应比优先 (Highest Response Ratio Next, HRRN)
    • 每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行;
    • 优先级 = (等待时间 + 要求服务时间)/(要求服务时间),要求服务时间未知;
    • 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应比」就越高,这样短作业的进程容易被选中运行;
    • 如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会;
  • 4)时间片轮转(Round Robin, RR)
    • 每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行;
    • 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配给另外一个进程;
    • 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换;
    • 如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率;
    • 如果设得太长又可能引起对短作业进程的响应时间变长;
    • 一般来说,时间片设为 20ms~50ms 通常是一个比较合理的折中值;
  • 5)最高优先级(Highest Priority First,HPF)
    • 希望调度程序能从就绪队列中选择最高优先级的进程进行运行;
    • 进程的优先级可以分为,静态优先级和动态优先级:
      • 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化;
      • 动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级;
    • 该算法也有两种处理优先级高的方法,非抢占式和抢占式:
      • 非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程;
      • 抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行,可能会导致低优先级的进程永远不会运行;
  • 6)多级反馈队列(Multilevel Feedback Queue)
    • 是「时间片轮转算法」和「最高优先级算法」的综合和发展;
    • 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
    • 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;
    • 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
    • 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行,如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
    • 对于短作业可能可以在第一级队列很快被处理完,对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也变更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间;

2,内存页面置换算法

  • 页面置换算法的功能是,当出现缺页异常,需调入新页面而内存已满时,选择被置换的物理页面,也就是说选择一个物理页面换出到磁盘,然后把需要访问的页面换入到物理页。

  • 1)最佳页面置换算法

    • 最佳页面置换算法基本思路是,置换在「未来」最长时间不访问的页面,通过「未来」的使用情况来推测要淘汰的页面;
    • 该算法实现需要计算内存中每个逻辑页面的「下一次」访问时间,然后比较,选择未来最长时间不访问的页面;
    • 实际系统中无法实现,因为程序访问页面时是动态的,无法预知每个页面在「下一次」访问前的等待时间;
    • 所以最佳页面置换算法作用是为了衡量你的算法的效率,算法效率越接近该算法的效率,那么说明你的算法是高效的;
  • 2)先进先出置换算法

    • 选择在内存驻留时间很长的页面进行中置换;
  • 3)最近最久未使用的置换算法

    • 最近最久未使用(LRU)的置换算法的基本思路是,发生缺页时,选择最长时间没有被访问的页面进行置换,通过「历史」的使用情况来推测要淘汰的页面;
    • 该算法假设已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用;
  • 4)时钟页面置换算法

    • 时钟页面置换算法的思路是,把所有的页面都保存在一个类似钟面的「环形链表」中,一个表针指向最老的页面;
    • 当发生缺页中断时,算法首先检查表针指向的页面:
    • 如果它的访问位位是 0 就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置;
    • 如果访问位是 1 就清除访问位,并把表针前移一个位置,重复这个过程直到找到了一个访问位为 0 的页面为止;
  • 5)最不常用算法

    • 最不常用(LFU)算法,是当发生缺页中断时,选择「访问次数」最少的那个页面,并将其淘汰;
    • 它的实现方式是,对每个页面设置一个「访问计数器」,每当一个页面被访问时,该页面的访问计数器就累加 1;
    • 在发生缺页中断时,淘汰计数器值最小的那个页面;
    • 计数器的硬件成本是比较高,链表长度很长的话是非常耗时的,只考虑了频率问题,没考虑时间的问题;

3,磁盘调度算法

  • 1)先来先服务
    • 按照要访问的磁道序列的顺序进行访问;
    • 算法简单粗暴,但是如果大量进程竞争使用磁盘,请求访问的磁道可能会很分散
  • 2)最短寻道时间优先
    • 优先选择从当前磁头位置所需寻道时间最短的请求;
    • 这个算法可能存在某些请求的饥饿,产生饥饿的原因是磁头在一小块区域来回移动;
  • 3)扫描算法
    • 磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向;
    • 中间部分相比其他部分响应的频率会比较多,也就是说每个磁道的响应频率存在差异;
  • 4)循环扫描算法
    • 只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最靠边缘的磁道,也就是复位磁头;
    • 复位过程是很快的,并且返回中途不处理任何请求,特点就是磁道只响应一个方向上的请求;
    • 相比于扫描算法,对于各个位置磁道响应频率相对比较平均;
  • 5)LOOK 与 C-LOOK算法
    • LOOK:磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,反向移动的途中会响应请求;
    • C-LOOK:磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,反向移动的途中不会响应请求;

其他

1,一个进程可以创建多少线程

  • 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
  • 64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。