Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1072498
  • 博文数量: 104
  • 博客积分: 3715
  • 博客等级: 中校
  • 技术积分: 1868
  • 用 户 组: 普通用户
  • 注册时间: 2006-04-30 08:38
文章分类

全部博文(104)

文章存档

2013年(1)

2012年(9)

2011年(41)

2010年(3)

2009年(3)

2008年(47)

分类: 系统运维

2011-01-06 10:08:25

* Introduction
** 操作系统是什么
    事实上,可以从两个观点来看待一个操作系统:
    1.  自上而下:操作系统在硬件之上构建了一个更抽象的、更高级、更易编程的虚拟机,为其他系统程序和
        应用程序提供了运行的平台;
    2.  自下而上:操作系统管理系统中所有的资源,负责资源使用的分配、跟踪谁在使用什么资源、资源请求
        冲突的仲裁等。操作系统通常使用分时共享/分空间共享两类方式在程序/用户间调度资源。

** OS的几个概念
    操作系统给应用程序提供的接口是一组“扩展指令”,通常称为系统调用。MINIX3的系统调用大体上可以分
为两类:与进程相关的和与文件系统相关的。事实上,进程和文件系统是UNIX系统的核心概念。
    1.  进程:这个概念是对执行中的程序的一个抽象。每个进程包含自己的地址空间以及一些进程执行相关的
        信息,如PC寄存器、栈寄存器的值等。一般而言,进程的地址空间存储自己的代码和数据,进程执行的
        相关信息存放在OS的进程表中。进程及相关信息是为了实现多道程序设计,使得一个执行在挂起、并再
        次唤醒执行时,可以恢复到暂停时精确的状态。
    2.  文件系统:文件系统中的文件、目录等概念是对IO设备的抽象。文件系统组织成层次结构,目录可以包
        含其它目录或文件。通过文件(和目录)的抽象,使得程序可以使用一个更高级的模型来操作设备。除了
        普通文件外,文件系统包含另外两类特殊文件,分别是块设备文件和字符设备文件。块设备文件代表的
        设备通常包含一个可以被随机寻址的块的集合,如磁盘;而字符设备代表可以进行字节流输入/输出的
        设备,如键盘、打印机。文件系统中存在“管道”的概念,管道是一种伪文件,用于进程间通信。一个
        进程可以向管道中写数据,另一个进程就从中读数据,就好象读写文件一样。
    系统调用的实现通常是机器相关的,使用特定的汇编指令序列。一般而言,系统会提供一个库,称为“系统
库”,使得程序可以像调用C函数那样来调用系统调用。在系统调用的过程中,会通过特殊的指令陷入内核。
    POSIX标准制定的例程不一定和系统调用一一对应。

** OS体系结构
    1.  传统单体内核(几乎无任何结构的宏内核)。在这种结构中,其实也有分层,从上到下分别是:主控驱动
        代码、服务例程、公共的工具例程。但基本上,任何一个例程都可以调用另一个任意的例程。
    2.  分层单体内核。在这种结构中,每一层都使用下一层的服务,提供自己的功能,提高机器的抽象程度并
        向上一层提供服务。
    3.  虚拟机(VMS)。
    4.  C/S结构(微内核)。

** A to Problems
    1.  资源管理;提供程序运行的抽象平台(虚拟机)。
    2.  Kernel-Mode允许执行所有的CPU指令,User-Mode不能执行与硬件直接相关的特权指令。这样,OS才能
        有效的进行资源管理,否则等于每个进程都有直接访问这些资源的后门。另外,也可以将OS和应用隔离
        开,提高OS的稳定性。
    3.  多道程序设计指同时有多个正在运行的进程。
    4.  Spooling是同时在线的输入输出,当有多道程序设计时,可以利用这个特性提高机器的利用率。即,某
        些进程在进行IO的时候,CPU运行另外一些进程。
    5.  在没有DMA的时候,对于单CPU的机器而言,无法利用Spooling技术。但是多道程序仍然有用,即用于分
        时系统,提高每个用户的响应时间。
    6.  缺乏有效的隔离和保护机制。
    7.  所有的。
    8.  mainframes注重吞吐率,PC注重响应时间。
    9.  这个比较寒...OS的质量其实和是否开源无关。
    10. 可以执行。
    11. 系统由人控制,当需要修改一些系统级的东西时,需要superuser。
    12. 是。但两种方式更方便。
    13. 进程的抽象,在切换进程时,一个进程的状态要被精确地保存下来,以便后续回复,就像从没停过一样。
    14. 字符设备支持读写字节流,而块设备支持随机寻址并读写某一个“块”。
    15. 正常读。
    16. IPC是必须的功能,pipe是实现IPC的一种方式。
    17. shell。
    18. 创建一个新的进程并执行指定的程序。
    19. 否则,任何一个用户可以构造一个根文件系统层次,使用chroot进去并拥有所有的特权。

* Processes

** 进程介绍
    进程是一个概念模型,指"顺序进程"。一个系统中的所有程序的执行被组织成一组顺序进程,每个进程就是
一个程序的执行,包括它自己的当前的变量值、程序计数器、寄存器值等状态。逻辑上,每个进程都有它自己的
虚拟CPU,完全在它的控制之下顺序执行。甚至OS本身也可以组织成若干个进程。

*** 进程的创建
    除了一些非常简单的系统或一些仅仅包含一个进程的系统外(它们的进程可以在系统启动后默认创建),一般
的通用操作系统需要某种方式按需地创建、销毁进程。
    事实上,创建一个进程只有两种方式:
    1.  在系统启动时,用某种方式创建第一个(第一批)进程;
    2.  由一个正在运行的进程执行一个创建新进程的系统调用(fork)来创建一个进程;

*** 进程的终止
    进程的终止有四种可能:
    1.  进程完成了自己的工作,正常退出,使用系统调用exit;
    2.  进程发现了某种异常或错误,主动异常退出,使用系统调用exit;
    3.  进程执行中,操作系统发现它发生了错误,如访问非法地址,执行非法指令等,系统终止进程;
    4.  一个进程被另外一个进程使用系统调用kill强制退出;

*** 进程的状态
    进程最为核心的状态有三个:
    1.  Running,正在执行,指正在占用CPU运行的进程;
    2.  Ready,就绪,指随时可以运行,等待调度器调度CPU的进程;
    3.  Blocked,阻塞,指逻辑上不能执行的进程,它们一般在等待某些外部事件的发生,如磁盘读写、中断;

*** 进程的实现
    进程实现的核心数据结构是进程表,其中,每个表项对应一个进程(也称进程控制块)。进程控制块记录了
一个进程相关的状态信息。

*** 线程
    线程是一个控制流,传统上,每个进程只有一个线程,在现代操作系统中,每个进程可以有多个控制线程
并行地执行。当引入线程的概念时,进程变成了“资源的组织单位”,而线程是“执行单位”或“CPU的调度
单位”。在之前提到的进程表中,有些项是关联在线程上的。系统中有一个相应的线程表/线程控制块来记录
这些信息。(对于用户态实现的多线程,系统中没有线程的概念)
    线程的引入,为Unix系统带来了很多新的问题,包括fork等系统调用的语义,errno的使用方式等等。

** 进程间通信
    进程间通信要解决的主要问题是:
    1.  一个进程怎样把数据传递给另一个进程;
    2.  如何防止不同进程之间相互的干扰(在操作共享资源时);
    3.  如果不同进程之间有需要维护的时序,如何保证正确的执行顺序;
    后两个问题同样适用于多个线程。对于第一个问题,同一个地址空间中的线程有另一个完成方式,因为它们
共享着地址空间。

*** 竞争情况与临界区
    Race Condition:如果有多个进程同时在执行,而执行的结果依赖于进程交替执行的情况,这种情况称为
竞争情况。通常是由不合适的共享资源引起的,而且会导致结果错误或者程序异常。
    解决这种问题的方式是:互斥访问共享资源,即,每个时间点最多只能有一个进程正在访问共享资源。
    一个程序中访问共享资源的代码片段称为临界区。
    一个好的竞争情况解决方案应该满足以下四个条件:
    1.  任何两个进程不能同时处于临界区内(保证正确性);
    2.  不能对计算的速度、CPU的数目做任何假设(保证正确性);
    3.  一个在临界区之外的进程不能阻塞其它进程(保证效率);
    4.  任何进程都能在有限的时间(等待)后进入它的临界区(保证公平性);

*** 互斥方式
    常见的一些互斥方式。
**** 关中断
    在进入临界区时关闭中断,在出来时开中断。因为CPU切换发生在时钟中断或其它中断发生时,所以,这样
可以使得在某个进程的临界区中不会发生CPU切换。
    该方式的问题:
    1.  对CPU的数目做了假设,多个CPU时无效;
    2.  使得用户进程有了关中断的能力,如果某个用户进程关闭了中断但再也没有打开,系统就挂了。
    3.  即便所有的用户进程都正确地使用了关、开中断,仍然产生不公平,一个进程可以占据很久的CPU时间。
    另一方面,关中断这种方式在内核中常常有合适的使用时机,是内核进程互斥的技术之一。
**** 严格轮换
    假设两个进程,它们严格地交替进入临界区(通过一个共享变量控制,例如,1时A进程进入,0时B进程进入)。
    该方法的问题:
    1.  一个临界区之外的进程可能阻塞其它进程;
    这里面有另外一个概念,自旋锁(spin lock),这种锁指的是,当一个进程等待这个锁时,并不会处于阻塞
状态,而是使用忙等待,不断的检查锁是否可用。
**** Peterson算法
    这是一个纯软件实现的、满足前文四个要求的互斥算法,忙等待。(实现得太聪明了,呵呵)
**** TestAndSet指令
    一种简单的实现互斥的方式是让硬件支持一个原子的指令,TestAndSet register memory_location。该
指令的语义是,原子地(无论是单CPU还是多CPU)读取memory_location中的值,放入register,并设置
memory_location为一个非0值。
    有了这个硬件帮助,进入临界区时:
    1.  TestAndSet reg1 lock;
    2.  如果 reg1 != 0,返回1循环执行(忙等待),否则可以进入临界区;
    退出临界区时:
    1.  使用一条普通的储存指令move lock #0即可。
    由此可见,当有硬件支持的时候,这种trick的算法可以得到极大的简化。

    以上的Peterson算法和TestAndSet算法都是忙等待,它们可能有两个问题:
    1.  浪费CPU时间;
    2.  由于优先级反转而导致死锁;
    在同优先级、临界区执行时间很短时,这种忙等待的spin lock效率可能更高。
    下文中介绍一些通过阻塞等待锁的算法和原语,这些其实都是基于Peterson、TestAndSet等类似算法的。
能够完成互斥,才是最基础、最关键的问题,互斥也是最基础的原语。

*** sleep and wakeup
    这是两个系统调用,sleep使得调用者变成阻塞状态,它不再能运行,直到其它进程通过wakeup唤醒它。
sleep和wakeup有一个额外的参数,一个内存地址,用于将sleep和wakeup配对,wakeup只能唤醒在同一个地址上
sleep的进程。(也可以这样实现,sleep没有参数,wakeup有一个参数,进程id,指定它要唤醒的进程)。
    通过书上的生产者-消费者的例子可以看出,仅仅有sleep和wakeup是不行的,仍然会导致死锁。还需要另一
个互斥方式的支持。这也是pthread库中的cond_wait和cond_signal的用法的设计的原因。

*** 信号量
    信号量的提出,较好的解决了互斥的问题。每个信号量都是一个整数,支持down和up两个操作。原来OS的那个
老师的解释更容易理解,信号量的值代表了可以并行的资源的数目。
    事实上,信号量有两种用途:
    1.  互斥,信号量原始值为1,用于保证不会有多个进程同时进入关键区;
    2.  同步,用于规定事件发生的时序/条件;例如,缓冲区空时消费者不能继续执行。

*** Mutex
    可以认为是一个简化的信号量,只能取1或者0,用于互斥。

*** Monitor、条件变量、wait、signal
    事实上,信号量编程是非常复杂和易于出错的。现在的编程语言支持的最为低级的同步/互斥机制一般就是
mutex、条件变量。mutex用于互斥,这样可以形成一个Monitor(管程),而条件变量用于同步,保证几个相互配合
的进程/线程按照规定的时序进行。条件变量的使用必须有mutex的配合才行。

    以上介绍的同步/互斥方式,常见于单机程序/内核,并不适用于分布式系统。另外,上面的机制保证了互斥
和同步,而不能在进程之间传递较多的数据。所以,进程间通信必然还要有传递数据的机制。下文会讨论一个更
为高级的机制,可以同时完成同步/互斥以及数据的传递。这就是消息。

*** 消息(Message Passing)
    消息机制的两个核心的原语是:
    1.  send(destination, &message):给指定的目标发送一个消息,如果对方的信箱(mailbox)已满,阻塞直到
        对方信箱接收该消息。
    2.  receive(source, &message):从指定的源(也可以指定ANY表示任意源)接收一个消息,如果当前有指定的
        源的消息,立刻返回,否则阻塞直到有消息。
    如果指定信箱的大小是0,那么就要求send和receive同步进行。send会被阻塞直到对方调用receive,同步
传输;如果receive先调用,也是类似的。

    消息机制的常见问题:
    1.  消息的可靠传输;
    2.  认证与安全;
    3.  性能;
    4.  寻址;

    一个广泛使用的消息机制标准是MPI(Message Passing Interface)。在Minix3中,为了简单、高效,实现的
消息机制是同步的(即信箱大小为0),并且每个消息的大小都是固定的。系统进程之间、用户进程与系统进程
之间,通常就是用这种消息机制通信的。在用户进程和用户进程之间,还有另一种通信方式:Pipe。

** 经典的IPC问题
*** 哲学家就餐问题
    启示:不要用信号量既表示状态,又用于同步。当有互斥锁的时候,完全可以用一般的变量、数据结构表示
并跟踪状态。信号量和mutex仅仅用于同步/互斥。
*** 读者写者问题

** 进程调度
*** 介绍
    进程从行为上讲,都是“计算——等IO——计算——等IO......"构成的序列。从相对时间上讲,进程分为
计算密集型的和IO密集型的。注意,IO密集型指的是在IO之间,计算量很小,而计算密集型指的是在IO之间,计算
量较大。
    理论上讲,调度器在某个IO密集型的进程需要CPU时,应该尽快调度它执行,使得它完成简单处理后发出下
一个IO请求并阻塞,这样,可以保持IO设备利用率和CPU的利用率都较高。
    调度要解决的第一个问题是,什么时机进行调度?
    1.  当正在运行的进程退出时(必须触发调度);
    2.  当正在运行的进程阻塞时(必须触发调度);
    3.  创建了一个新进程(通常触发,非必须);
    4.  IO中断(通常触发,非必须);
    5.  时钟中断(通常触发,非必须);

    调度器可以分为抢占式、非抢占式两类,抢占式的调度器要求系统必须支持时钟中断。在不同的环境下,对
调度器的需求也是不同的。常见的环境包括:
    1.  批处理系统;
    2.  交互式系统;
    3.  实时系统(奇怪的是,在实时系统中却不一定需要抢占式的调度器,因为每个进程都知道它们会运行多
        长时间,实时系统中的每个进程及系统需要紧密配合,才能真正做到实时);

    调度器目标:
    1.  通用目标
        a.  公平地分配CPU时间;
        b.  忠实地实现目标调度策略;
        c.  尽量提高系统各个部分的利用率;
    2.  批处理系统:
        a.  最大化吞吐率;
        b.  降低平均的“提交——获取结果”的时间;
    3.  交互式系统:
        a.  降低用户的响应时间;
    4.  实时系统:
        a.  严格遵守时序要求;
        b.  高度可预测、规则;

*** 批处理系统调度算法
**** 先来先服务
    这是最简单的一个算法,系统维护一个就绪队列,每次取出队列头的进程运行,直到它结束或者进入阻塞
状态,然后取下一个进程。新创建的进程和从阻塞刚刚变成就绪的进程放入队尾。该算法简单、公平。
    该算法最大的问题就是对IO密集型进程“不公平”,当IO完成(发生中断时),IO密集型进程得不到及时的
调度。
**** 短任务优先
    这个算法在所有的任务几乎同时到达的情况下可以获得最短的轮转时间。但是,如果任务不是同时到达的,
则未必能保证获得最短的轮转时间。
    短任务优先是非抢占式的调度算法,它有一个抢占式的版本:最短剩余时间任务优先。此时,并不是比较
各个任务的总时间,而是比较当前各个任务还剩下多少时间完成。

    对于批处理系统而言,它的调度器是分层的。
    最前面一层的调度器维护一个任务队列,这个队列中的任务还没有启动,该调度器决定下面应该启动哪些
任务。该调度器尽量使得系统中正在运行的任务是IO密集型和CPU密集型的混合。这个调度器是作业调度。
    第二个是进程调度器,它负责调度所有正在运行中的任务。

*** 交互式系统调度算法
**** Round-Robin
    时间片轮转算法。该算法简单、公平。唯一需要权衡的地方就是时间片的长短。过短的时间片会导致进程
切换的开销过大,过长的时间片又会影响相应时间。
**** 基于优先级的调度
    RR算法有一个假设:所有的进程都是同等重要的。而这个通常不成立。一般而言,我们会为进程设置一个
优先级,优先级越高的进程,得到CPU的概率越大,越优先运行。一般而言,基于优先级的算法也不会让优先级
最高的那个进程一直运行下去,例如通过动态调整优先级或者设置最长运行时间片等方式。
**** CTSS多队列调度器
    想法很简单,故事比较有意思。
**** 最短任务优先
    通过老化算法估计下一次运行的时间。
**** 彩票算法
    这个算法值得说说。对于CPU时间资源(对系统的其它共享资源也可以使用类似的调度算法)准备一组彩票,
例如100张,每张的中奖概率相同。
    给各个进程分配不同数目的彩票,例如A:20,B:30,C:40,D:10。调度时,调度器随机的抽奖,哪个
进程拥有中奖的彩票,则会被调度一个时间片。从长久来看,A进程平均得到20%的CPU时间资源,B进程得到30%
的CPU时间资源...
    彩票算法还有另外一些有趣的特性。例如,对于一个服务器而言,如果没有客户端,它不需要任何彩票。
当有客户端请求它时,客户端进程可以在请求发出后把自己的彩票送给服务器,因为客户端会阻塞,它这样可以
提高服务器被调度的概率,进而降低自己请求的响应时间。服务器响应后,再把彩票还给客户端。
    还有,一个刚刚启动的进程可以授予更大比例的彩票以提高它的响应时间。
**** 用户公平调度
    前面提到的调度都是以进程为单位的,而不考虑进程的所有者。这样会导致,一个拥有更多进程数目的用户
占用的CPU时间更多。实际系统中,会避免这样的问题,让每个用户获得的CPU资源基本持平。如果一个用户的
进程多,那么每个进程得到的资源就相对少,反之亦然。
**** 机制和策略
    内核可以提供进程调度机制(例如基于优先级的调度,优先级越高,获得CPU的资源越多),而把策略交给
用户进程(一个进程可以根据业务逻辑决定如何调度其子进程,通过设置其子进程的优先级告诉调度器)。

    其实,在今天的分时系统中,调度器既要照顾到交互式应用的响应时间,又要照顾后台任务的吞吐率、轮
转时间。

**** 线程调度
    线程的调度和进程类似。但是,内核应该考虑,同一个进程中的线程切换的代价要远远小于不同进程中的
切换代价。

阅读(1498) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~