现代操作系统阅读笔记

376 阅读10分钟

阅读笔记不说无用的

进程与线程

进程

  1. 操作系统中最核心的概念是进程.
  2. 严格地说,CPU在某一瞬间只能运行一个进程.
  3. 在当代计算机中,所有进程均被组成若干顺序进程,包括操作系统自己的进程服务.

  1. 如果一个程序执行了两遍,那么算两个进程.
  2. 进程创建有以下4中情形:
1.系统初始化
2.正在运行的程序执行创建进程的系统调用
3.用户请求创建一个进程(如双击运行一个程序)
4.一个批处理作业的初始化
  1. 停留在后台的进程称为"守护进程".
  2. 在Unix和Win系统中,父进程和子进程有不同的地址空间(也就是内存空间);且进程之间的地址空间不可见,为进程私有.
  3. 注意,在Unix系统中,子进程和父进程共享不可写内存块;如果父进程或子进程想要对这样的共享区域写入,应先复制出自己的内存块,再写入.
  4. 在Win中,父子进程内存空间在一开始就完全不同,故没法共享.
  5. 进程终止有以下4种原因:
1.正常退出(自愿)
2.出错退出(自愿)
3.严重错误(非自愿)
4.被其他进程杀死(非自愿)
  1. 在进程组里,每个进程都可以捕获新的信号
  2. 在Unix系统里,所有进程组成一个以init进程为根的一棵进程树.
  3. 在Win里,没有进程层次概念,父进程拥有一个称为"句柄"的令牌控制子进程.
  4. 进程的3种状态及其相互关系:

1.运行态(此时程序正在使用CPU)
2.就绪态(可运行,但是没有CPU使用权)
3.阻塞态(除非某种外部事件发生(比如输入流到达),否则进程不能运行)
  1. 调度程序的主要工作就是决定应当运行哪个进程,何时运行,运行多久.
  2. 进程在操作系统中以进程表的形式存在.

  1. 在操作系统里,有一个称为"中断向量"的东西,它包含中断服务例程的相关信息.中断服务例程用于处理被中断程序的收尾工作;所有的中断都是从保存寄存器开始的.
  2. 假设某个进程等待IO操作的时间/其停留在内存中的时间=P(P<1),若此时内存中有n个进程,那么CPU利用率=1-P^n.

线程

  1. 每个进程有一个地址空间和一个控制线程.
  2. 使用线程的原因:
1.多线程拥有共享同一个地址空间和所有可用数据的能力,此能力在文件交互上很有用,因为不用频繁地打开关闭文件.
2.创建和销毁的代价相比较于进程低很多.
3.在存在大量的计算和I/O处理时,多线程更加得心应手.
4.在多CPU系统里,使真正的并行计算成为了可能
  1. 进程模型基于两种独立的概念:资源分组处理与执行(就是每个进程独立运行且共享CPU)
  2. 线程与进程的区别:进程用于把资源集中到一起,而线程则是在CPU上被调度执行的实体.
  3. 线程之间天生无保护,需要程序员手动添加
  4. 所有线程共享同一个打开文件集,子进程,定时器以及相关信号等.

  1. 线程有四种状态:
1.运行
2.阻塞
3.就绪
4.终止
  1. 每个线程都有自己的堆栈,用来保存它所运行的,过程调用,的结果(函数返回值);可以理解为为某一过程调用预留了返回值的位置;这是一个很重要的特性

  1. 一个进程执行的典型线程运行情况:
1.主线程(进程的第一个线程)调用create创建新的线程;
2.若某个线程完成了工作,调用exit退出,此线程不再可用;
3.若A线程需要B线程的执行结果,则调用join进入阻塞状态,直到B退出;
4.线程可调用yield来让出CPU使用权,让其他线程可以运行,这是很重要的,因为线程之间不存在时钟中断来轮换使用CPU.(时钟中断仅限于进程)
  1. POSIX是一个典型的可移植线程程序.
  2. 有两种方法实现线程包:一是用户态,一是内核态
1.用户态
    优点:
    线程切换快;
    每个线程可以有自己的调度算法;
    缺点:
    某个线程的阻塞(被动的)会导致整个进程阻塞,也就是阻塞当前进程的所有线程;
    页面故障会导致全部阻塞;
    同一进程中的线程之间的调度问题;
2.内核态
    优点:
    不会因为某个线程阻塞而停下所有线程(此时线程由内核托管);
    不会出现某个线程吃死CPU的情况;
    缺点:
    线程的创建,销毁,切换代价高昂;
    线程在父子进程中的继承问题;
    多个线程对同一信号处理问题;
  1. 某些机制使用了混合实现.
  2. 调度程序激活机制:其工作目标是模拟内核线程的功能,避免了在内核态和用户态之间的切换.其工作原理简述如下:
1.若发生阻塞,内核通知运行时系统,让它标记此线程为阻塞并启动另一就绪线程;
2.若刚刚阻塞的线程获得了可用事件(比如数据到达),则内核通知运行时系统,把刚刚那个线程设置成可运行的.
3.若发生中断,若被中断的进程对引起中断的原因不感兴趣,那么在中断处理程序处理完中断后,把被中断的线程恢复到中断前的状态;
4.若对此感兴趣,则被中断的线程不再启动,被挂起.
5.再之后,运行时系统开始新的调度(可能包括刚刚被中断的线程).
  1. 弹出式线程,这种线程相当新,没有必须存储的寄存器,堆栈等诸如此类的内容.每个线程从全新开始,每一个线程彼此之间都完全一样,所以,可以快速地创建这类线程.

进程间通信

  1. 进程间通信主要存在3个问题:
1.一个进程如何把信息传递给另一个
2.两个或更多进程在关键活动中不会出现交叉
3.进程之间通信的正确顺序

后两个更像是同步问题;以上问题及其解决方案同样适用于线程. 2. 竞争条件是指两个或多个进程对同一共享数据进行读写,而实际解决取决于精确时序的情况. 3. 互斥是指通过某种手段确保同一时刻只有一个进程对共享数据进行操作. 4. 临界区域或临界区指的是对共享内存进行访问的程序片段. 5. 一个好的解决竞争条件的解决方案应满足以下4点:

1.任何两个进程不能同时处于其临界区.
2.不应对CPU的速度和数量作任何假设
3.临界区外运行的进程不得阻塞其他进程进入临界区
4.不得使进程无限期等待进入临界区
  1. 来看几种解决方案:
1.屏蔽中断法:在某个进程进入临界区后,屏蔽一切中断.
中断是指:某种硬件/软件信号,使CPU停止运行当前程序转去运行另一程序
不足:无法用于多核心或多CPU系统
2.锁变量
不足:本身也需要同步,没有太大意义
3.严格轮换法:会导致程序忙等待
用于忙等待的锁称为自旋锁
4.Peterson解法:优化版的严格轮换法
5.TSL/XCHG指令:给内存总线加锁,确保此指令执行结束之前,其他CPU不得访问内存

Peterson解法和TSL/XCHG指令均会造成忙等待(空占CPU而不干活),且TSL和XCHG还可能造成优先级反转问题

严格轮换法

Peterson解法

TSL指令法

XCHG指令法:

  1. 以生产者-消费者模型为例.对于睡眠/唤醒(sleep/wakeup)机制,可能会发生wakeup信号丢失的可能,比如说,调度程序尝试唤醒消费者,但此时消费者是醒着的,于是此唤醒信号就丢失了,进而导致读写不一致,于是引入一个唤醒等待位,用来tashiyige记录唤醒了几次;还是上面的情况,在下次消费者打算睡眠时,因为唤醒等待位的存在,强迫它此次不睡眠,这样就可以做到信号不丢失
  2. 不可以无限地添加唤醒等待位,于是引入了信号量这个概念.他是一个整型;实际上对他的操作,是使用up()和down()来进行的.
  3. 对信号量的操作:
  • 操作系统在进行测试信号量,更新信号量,以及在需要时使某个进程休眠时采取屏蔽全部中断;或使用TSL/XCHG来进行原子性的信号量操作.

up()对信号量增加1;加之前看看信号量是不是为<=0;如果不是就+1;如果是,就说明此时有进程在这个信号量上睡眠(言外之意就是这个信号量为0了),就随机唤醒一个进程,但继续保持0(废话不然不守恒)

  • down()操作,如果信号量>0,则-1;否则休眠此进程,但是此时进程并没终止,在它下次被唤醒时继续进行down()操作,因为人家也要访问临界区, 也就是-1.

啊啊啊啊!艹!看原文终于看懂了,就是,一个或多个进程绑定到一个信号量,然后信号量被初始化临界区的数量,每一个进程访问临界区,就把信号量-1,出来后+1,就这么简单,但是这个操作事原子性的。然后呢进程访问之前需要检查信号量的值,如果>0说明它还有得访问,就-1,访问,出来+1,如果不是,其实也就是0,那就阻塞,说明此时原本为正的信号量被别的进程-1成0了,并且它还在临界区,还没出来,那当然现在的进程就得休眠,等到下次唤醒时再-1信号量,访问临界区。每个进程会在+1时会检查信号量是否=0,如果是就唤醒在它上面睡眠的进程,此时信号量+1,很快就因为刚刚被唤醒的进程的-1操作而又变成了0。

注意,检查信号量的值,修改其值或休眠进程,这个操作必须是原子性的,所以up()和down()由操作系统提供.

  1. 信号量还能用于实现同步,安排程序的执行顺序.
  2. 如果仅仅需要实现互斥,而不需要统计次数,那么可以使用互斥量,互斥量在实现用户空间线程包时很有用.
  3. 互斥量仅有两个操作,加锁和释放锁,直接使用TSL和XCHG就可以实现相应的操作

有一点需要注意,和直接使用TSL不同的是,由于时钟超时的存在,所以即使当前线程拿不到锁,也会在后面一会儿后让另一个线程尝试进入临界区;而在互斥量这里,因为是用户线程,所以不存在时钟,所以只能在尝试失败后,手动转移让其他线程来尝试进入临界区.

当然,还有try_lock()这样的方法,用于设定拿不到锁后该干嘛.

  1. 对于各种解决方法对于共享内存的访问问题,即-怎么在进程之间访问共享区域呢?有两种方法:一是把某些共享数据放在内核里,通过系统调用来访问,另一种是,也是Unix和Win使用的,是让进程之间共享部分地址空间,比如内存,甚至文件.
  2. 快速用户区互斥量futex.这是Linux的一个特性,它包含两个核心组件:内核服务和用户库.内核中维护着一个阻塞队列,实际线程操作尽可能在用户空间操作,当某个线程试图获取锁,如果它没拿到,这个线程不会自旋,而是使用一个系统调用把这个线程放到内核的阻塞队列上(既然跑不了,那相比于忙等待,转移到内核态似乎也没花很多代价);如果拿到了锁,那就运行,并在运行结束通知内核对阻塞队列里的一个或多个线程解除阻塞.
  3. pthread的使用请看下图,在这里提一下条件变量,啥意思呢?它允许线程在未满足某些条件时阻塞(wait),直到条件满足被唤醒(signal).当条件变量激活时,记得释放互斥量的锁,不然其他的线程还是没法运行去给当前线程创造条件.

条件变量与互斥量经常一起使用,这种模式用于让一个线程锁住互斥量,然后当它不能获得它所期待的结果时,它会等待条件变量,直到另一个线程向它发送信号,告诉它条件已具备,可以继续执行.pthread_cond_wait原子性地调用并释放它所持有的锁,由于这个原因,互斥量也是参数之一.

调度

  1. 许多适用于进程的调度算法也可以适用于线程,当内核管理线程时,调度经常是按线程级别的,与线程所属的进程无关.
  2. 进程有两个主要的行为:计算密集型/IO密集型,在一个进程里,计算占大多是计算密集型操作,而IO密集型则是IO操作之间较少进行计算而不是IO操作时间很长.如果是IO密集型,记得多运行这种类型的进程.

这张图看得出来,CPU挺闲的

  1. 根据如何处理时钟中断,可以把调度算法分为两类:非抢占式调度和抢占式调度.后者需要时钟中断来把CPU交给调度程序,前者希望程序主动让出CPU.
  2. 调度算法主要用于3种环境:
1.批处理
    主要用于商业领域
2.交互式
    常见于PC,移动终端,服务器
3.实时
    在多媒体播放设备比较常见
  1. 一个优秀的调度算法需要考虑很多问题

对于交互式系统,最小响应时间是一个很重要的指标.实时系统最主要的要求是满足几乎所有的截止时间要求.

  1. 批处理系统中的调度
1.先来先服务
2.最短作业优先(前提是均可运行,处于就绪状态)
3.最短剩余时间优先

7.交互式系统中的调度

1.轮转调度(最古老,最简单,最公平,使用最广的算法)
    使用时间片制度,某个进程时间片用完会强制剥夺CPU使用权并移到等待队列尾
    良好的设计可以在保证公平的前提下减少上下文切换(代价很高),所以认为时间片设置为20-50ms通常是一个不错的选择
2.优先级调度(每个进程被赋予了优先级,高优先级的先运行)
    为了防止高优先级的进程无休止的运行,调度程序可能在每个时钟中断时降低当前进程的优先级,或者给当前进程一个时间片,用完了后次优先级进程便获得了机会.
    对于IO密集型,可以考虑使用某个进程上次的时间片总长/结束时已使用时长=f来获得它的优先级
    当然,也可以通过分级制度,在各个不同优先级的进程组之间使用优先级调度,组内使用轮转调度.
3.多级队列
    属于最高优先级类的进程获得一个时间片,次级类的进程获得两个时间片.当某个进程使用完了他的时间片,便被切换到下一级中.这种做法的好处是,减少了上下文切换.
4.最短进程优先
    通过老化策略得到当前进程的估计执行时间,进行作业时间最短排序
5.保证调度
    尽可能使各个进程公平
6.彩票调度
    在保证公平的基础上,略微可以偏袒于优先级高的进程
7.公平分享调度
    保证用户公平,而不是进程公平,因为每个用户持有的进程数量不一定相同.
  1. 实时系统中的调度,实时系统是一种时间起主导作用的系统.实时系统通常可以分为硬实时和软实时,前者要求进程必须满足绝对的截止时间,后者可以稍微有点误差.在这里,调度程序的就是调度那些满足所有截止时间的进程.实时系统的调度算法可以是静态的或动态的.
  2. 实时系统中的事件按照响应方式可以分为周期性和非周期性事件,对于一个系统,可调度的条件是:每个周期事件需要使用的CPU时间/周期,之和<=1,如果大于1就说明无法保证在每个事件都执行一遍的情况下把它们都执行一遍.
  3. 对于如何更好地实现调度,这里把调度机制和调度策略分开来.用户可以提供自己的调度策略供操作系统使用,完成特定的调度方法.
  4. 关于线程调度,要求区分用户级和内核级,因为在用户级时,内核不知道线程的存在,所以分配的CPU是分给进程的,而进程又分给线程,假设进程为A,那么可能出现A1, A2, A3, B1, B2...的情况.对于内核级,因为是内核直接管理线程,所以可能会出现这样的执行顺序:A1, B1, A2, B2, A3...
  5. 内核知道上下文切换代价很高,所以在内核级时,内核在同样的线程选择上,会优先选择同一进程的.还有,用户级的线程阻塞会造成进程阻塞.

下一篇-内存管理