现代操作系统 - 进程与线程

进程与线程

进程

对正在运行的程序的抽象, 在某个瞬间, CPU只能运行一个进程, 但在1秒内, 它可能运行多个进程

进程的创建

  • 系统初始化

    启动OS时, 通常会创建若干个进程, 其中有些事前台进程, 与用户交互的, 其它是后台进程, 具有某些特殊功能, 比如在请求到达时被唤醒一遍服务请求的, 这些是守护进程

  • 运行的程序创建新的进程. 比如一个网络应用创建一个进程取数据, 一个进程处理数据

  • 创建新的进程

    在交互式系统中, 双击图标可以启动一个程序

在UNIX系统中, 只有一个系统调用可以创建新的进程, fork, 是父子进程, 很多东西可以共享

在Windows系统中, 创建进程后, 父子进程有各自不同的空间

进程的终止

  • 正常的退出 - 自愿 (i.e. UNIX调用exit)
  • 错误退出 - 自愿(i.e. 编译错误)
  • 严重错误 - 非自愿 (i.e. 执行了非法的指令)
  • 被其它进程kill - 非自愿 (i.e. 调用kill指令)

进程的层级结构

UNIX: 在整个系统中, 所有的进程都属于以init为跟的一棵树

Windows: 没有层级的概念, 所有进程的地位一样

进程的状态

  • 运行态
  • 就绪态
  • 阻塞态

通过中断trap, 运行态的程序会变成就绪或者阻塞态; 在就绪态的程序会被scheduler调度

进程的实现

为了实现进程模型, OS维护者一张进程表, 每次进程切换的时候都需要保存进程的数据

线程

线程的使用

提供在并行实体拥有共享同一个地址空间和所有可用数据的能力; 线程比进程更轻量级, 所以所以它们比进程更容易创建; 如果有大量的计算和大量的I/O处理, 拥有多个线程允许这些活动彼此重叠进行, 从而会加快应用程序执行的速度

例子

文字处理文件

  1. 进程负责User的交互
  2. 进程负责计算
  3. 进程负责备份

web服务器

  1. 进程负责分派程序 - Dispatcher
  2. 进程负责计算处理 - Worker

如果没有线程的概念, web服务器就是Async I/O, 需要有个表格记录当前请求的状态, 然后处理下一个事件, 下一个事件如果是新的工作, 就开始工作, 如果是磁盘对直线操作的应答, 就从表格取出信息, 并处理应答. 这里就需要切换状态, 给编程增加了困难

在多线程中, 顺序进程的思想可以保存下来, 按顺序阻塞了系统调用, 但是任然实现了并行性

经典的线程模型

进程模型基于两种独立的概念: 资源分组处理和并行. 有时, 将这两种概念分开会更好, 这就引入了线程的概念

理解进程一个角度是, 用某种方法把相关资源集中在一起; 另外一个概念是, 进程拥有一个执行线程, 通常简写为线程. 在线程中有一个程序计数器, 寄存器和堆栈. 尽管线程必须在进程中执行, 但是线程和它的进程是不同的概念. 进程用于把资源集中到一起, 而线程则是在CPU上被调度的执行体

在同一个进程中运行多个线程, 实在对同一台计算机上运行的多个进程的模拟, 多个进程不能共享地址空间, 而线程可以, 所以有时候也称线程是轻量级的进程

线程概念试图实现的是, 共享一组资源的多个线程的执行能力

在用户空间中实现线程

有两种主要的方法实现线程包, 在用户空间中或者在内核中

用户空间: 每个进程需要有其专用的线程表, 来跟踪改进程中的线程.

优点: 快, 自己实现调度算法

缺点: 如何实现阻塞的系统调用. 线程的trap会让整个进程block

内核实现: 内核中有用来记录系统中所有线程的线程表

优点: 解决了用户空间实现实现线程的缺点

确定: 开销大

调度激活机制

当一个内核了解线程被阻塞之后, 内核upcall通知上层runtime, runtime运行另外一个线程

进程间通信

通信最好使用一种结构良好的方式而不要使用中断

竞争条件 Race Condition

多个进程访问共享资源, 不同的顺序 → 会有不同的效果成为竞争条件

临界区

为了避免竞争, 凡涉及共享内存, 共享文件, 以及共享任何资源的情况都会引发竞争条件, 要避免关键找出某种途径阻止多个进程同时读写共享的数据, 换而言之, 互斥

把共享内存访问的片段叫做临界区

解法

  • 屏蔽中断: 这个方法不好, 因为这个权利不能交给用户

  • 严格轮换法: 使用自旋锁, 但是这个是忙等待

  • TSL指令: 硬件支持, test and set lock

  • sleep and wake up: sleep的进程状态变成阻塞态, wake up进程状态变成就绪态. 生产者消费者模型使用sleep and wake up

  • 信号量: 使用一个整型变量来累计唤醒次数, 在生产者消费者模型中

    producer:
    	while true:
    		down(&empty) ## 空槽数据-1
    		insert_item() ## 如果有空槽 添加数据
    		up(&full) ## 满槽数据+1
    
    consumer: 
    	while true:
    		down(&full) ## 满槽数据-1
    		remove_item()
    		up(&empty) ## 空槽数据+1
    
  • 互斥量: 就是不带信号量的计数能力的版本

  • 管程: 一次只有一个活跃的进程单独access. Java的Synchronized就是管程

  • 消息传递: 通过消息传递而非共享内存, 在生产者消费者中, consumer发N个空messages给producer, producer会填充信息, 然后再send back to consumer, consumer把信息取走, 再把空message发回producer

  • 读-复制-跟新: 是一种同步机制, 主要应用于读取操作远多于写入操作的场景. RCU的核心思想是允许读取操作在没有锁的情况下安全地并行执行, 而对共享数据的更新操作通过创建数据副本并在适当的时候替换原数据来实现

调度

何时调度

  1. 在创建新进程之后
  2. 进程退出时候
  3. I/O阻塞
  4. I/O中断, 比如其它设备完成工作了, 被阻塞的设备可以开始工作

调度算法

批处理系统

  1. 先来先服务
  2. 短作业优先
  3. 最短时间优先

交互式系统 (PC, 服务器)

  1. 轮转调度 - 每个程序分配一个时间片, 在时间片内运行, 这个时间一般在20-50ms之间
  2. 优先调度 - 添加一个外部因素, 就是哪些程序比较有高优先级
  3. 多级队列 - 不同优先级有不同的队列, 不同队列分配不同的时间片
  4. 最短进程优先 - 这里需要通过过去的行为推断是否是最短进程
  5. 彩票调度 - 系统可以掌握50次的一种彩票, 作为奖励每个获得者可以得到20ms的CPU时间. 对于更重要的进程, 可以多给彩票, 然后这些进程更容易获奖

线程调度

  1. 用户级线程

    用户自定义调度算法, 一般轮转调度和优先调度比较多.

    • 然后如果线程阻塞在I/O上, 需要将整个进程挂起
    • 线程调度只能在进程内的, 所以在一段时间内, 都是这个进程的线程被调度
  2. 内核级线程

    内核负责调度, 跟调度进程类似

    • 不过开销大, 因为需要
    • 但是可以做到,一段时间内不同进程的的线程可以被调度

小结

进程是对CPU运行程序的抽象, 是的CPU可以不断切换进程, 然后看起来像是同时处理多个程序. 线程是轻量级的进程, 每个线程都可以共享内存资源, 实现线程可以在用户空间实现, 也可以在内核上实现, 不过内核上实现开销会比较大

进程之间通信会涉及到锁, 保证共享的资源在同一个时间, 只有一个进程使用

还讲了调度的算法, 常见的有轮转调度, 优先调度

你可能感兴趣的:(linux,java,现代操作系统,OS)