操作系统——进程与线程

来源:互联网 发布:虐杀原形帧数优化补丁 编辑:程序博客网 时间:2024/05/22 14:13

进程

进程模型

计算机上的所有可运行的软件,通常包括操作系统,被组织成若干顺序进程(squential process),简称进程(process).一个进程就是一个正在运行的实例,包括程序计数器、寄存器和变量的当前值。从概念上说,每个程序拥有它自己的CPU.然而实际上是CPU在多个进程间切换.
在UNIX系统中,可以使用fork()系统调用创建系统调用.
进程的两个基本属性:

  • 进程是一个拥有资源的独立单元;
  • 进程同时又是一个可以被处理器独立调度和分配的单元。

进程状态

状态图:
这里写图片描述
进程状态切换的原因:

  • 就绪状态→执行状态:一个进程被进程调度程序选中。
  • 执行状态→阻塞状态:请求并等待某个事件发生。
  • 执行状态→就绪状态:时间片用完或在抢占式调度中有更高优先级的进程变为就绪状态。
  • 阻塞状态→就绪状态:进程因为等待的某个条件发生而被唤醒。

进程的实现

为了实现进程模型,操作系统维护着一张表格(一个结构数组),即进程表(process table),每个进程占用一个进程表项.进程表项也叫PCB(process control block),该表项包含了进程状态的重要信息,包括程序计数器,堆栈指针,内存分配状况,所打开的文件状态,账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程一会能够再次启动,就好像从来没有被切换过一样.

线程

线程的使用

线程是进程内一个相对独立的、可调度的执行单元。线程自己基本上不拥有资源,只拥有一点在运行时必不可少的资源(如程序计数器、一组寄存器和栈),但它可以与同属一个进程的其他线程共享进程拥有的全部资源。多线程是指一个进程中有多个线程,这些线程共享该进程资源。但是各线程自己堆栈数据不对其他线程共享。

经典的线程模型

  • 多对一模型:多对一模型将多个用户级线程映射到一个内核级线程上。只要一个用户级线程阻塞,就会导致整个进程阻塞
  • 一对一模型:一对一模型将内核级线程与用户级线程一一对应。这样做的好处是当一个线程阻塞时,不影响其他线程的运行。
  • 多对多模型:多对多模型将多个用户级线程映射到多个内核级线程,采用这样的模型可以打破前两种模型对用户级线程的限制,不仅可以使多个用户级线程真正意义上并行执行,而且不会限制用户级线程的数量。

线程的实现

  • 内核级线程:指依赖于内核,由操作系统内核完成创建和撤销工作的线程。
  • 用户级线程:指不依赖于操作系统核心,由应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制的线程
  • 混合实现 : 多对多模型实现,多个用户线程被分配到多个内核线程上.

用户级线程的优点:

  • 切换代价小.具体体现为 :切换都是在本地进行,不需要进入内核,只有一套栈,而内核有两套栈.所以速度会快很多;不需要trap,不需要上下文切换,也不需要对内存高速缓存进行刷新.
  • 允许每个进程都有自己的调度算法(进程主动使用yield()放弃)

用户级线程的缺点:
- 如何实现阻塞系统调用,因为这会停止所有的用户态指令.
- 如果发生缺页中断,由于操作系统不知道有其他线程存在,会阻塞到这个线程完成缺页中断,而不是去调度其他用户级线程

线程与进程的比较

  1. 调度:在传统的操作系统中,拥有资源和独立调度的基本单位都是进程。而在引入线程的操作系统中,线程是独立调度的基本单位,进程是拥有资源的基本单位。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程中,将会引起进程切换。
  2. 拥有资源:进程是拥有资源的基本单位,而线程不拥有系统资源(也有一点必不可少的资源,并非什么资源都没有),但线程可以访问其隶属进程的系统资源。
  3. 并发性:在引入线程的操作系统中,不仅进程之间可以并发执行,而且同一进程内的多个线程之间也可以并发执行。
  4. 系统开销:由于创建进程或撤销进程时,系统都要为之分配或回收资源,系统开销较大;而线程切换时,只需保存和设置少量寄存器内容,因此开销很小。

进程间通信

临界区

  • 临界资源:同时仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如打印机、绘图机等。
  • 临界区:每个进程中访问临界资源的一段代码。

互斥的概念与要求

为了禁止两个进程同时进入临界区,软件算法或同步机构都应遵循以下准则:

  • 空闲让进:当没有进程处于临界区时,可以允许一个请求进入临界区的进程立即进入自己的临界区。
  • 忙则等待:当已有进程进入其临界区时,其他试图进入临界区的进程必须等待。
  • 有限等待:对要求访问临界资源的进程,应保证能在有限的时间内进入自己的临界区。
  • 让权等待:当一个进程因为某些原因不能进入自己的临界区时,应释放处理器给其他进程。

忙等待的互斥

几种互斥的方案

  • 屏蔽中断 :在单处理器的系统中,最简单的方法是在每个线程刚刚进入临界区的时候将中断屏蔽,并在离开临界区的时候将中断重新开启. 这个方案并不好,把屏蔽中断的权利交给用户进程是不明智的.如果一个恶意的进程屏蔽中断之后不再打开中断,那就歇逼了. 而且,如果系统是多处理器系统,屏蔽中断只能对执行的那个cpu有效
  • 锁变量 :锁变量是一种软件方法.设想有一个共享变量,其初始值为0.当一个进程想进入其临界区时,它首先测试这把锁.如果锁的值为0,则该进程将其设置为1并进入临界区.如果是1,就等到变成0再进入. 这种方案还是有问题,原因在于对锁变量的访问不是原子的.
  • 严格轮换法
  • Peterson解法
  • 最常用的方法-TSL和XCHG指令:TSL指令的功能是这样的:将一个内存字lock读取到寄存器RX中,然后在该内存地址上存一个非零值.读字和写字操作保证是不可分割的,即该指令结束之前其他处理器均不允许访问改内存字.执行TSL将总线锁住,防止其他CPU在本指令结束前访问内存.
    这个方法和锁变量法的思维基本一致,但是借助了硬件实现了原子操作.
    XCHG的功能是原子性的交换两个位置的内容.

睡眠与唤醒

Peterson解法和TSL或XCHG解法都是正确的,但他们都有忙等待的缺点.
另外一种进程间通信原语,他们无法进入临界区时将被阻塞,而不是忙等待.最简单的是sleep和weakup.sleep是一个将引起调用进程阻塞的系统调用,即被挂起,直到另一个进程将其唤醒.weakup调用有一个参数是weakup调用将要唤醒的进程的地址.

生产者消费者问题

代码如下:

#define N 100 /* number of slots in the buffer */int count = 0; /* number of items in the buffer */void producer(void){    int item;    while (TRUE) { /* repeat forever */    item = produce item( ); /* generate next item */    if (count == N) sleep( ); /* if buffer is full, go to sleep */    inser t item(item); /* put item in buffer */    count = count + 1; /* increment count of items in buffer */    if (count == 1) wakeup(consumer); /* was buffer empty? */    }}void consumer(void){    int item;    while (TRUE) { /* repeat forever */    if (count == 0) sleep( ); /* if buffer is empty, got to sleep */    item = remove item( ); /* take item out of buffer */    count = count1; /* decrement count of items in buffer */    if (count == N − 1) wakeup(producer); /* was buffer full? */    consume item(item); /* print item */}

上面这段代码由于对count的操作不是原子操作,所以会导致多线程问题.这种方法并没有很好的解决这个问题.

信号量

信号量是一种新的变量类型,它使用一个整形变量来累计唤醒次数,供以后使用.
Dijkstra建议设立两种操作:down和up.对一个信号量进行down操作,则实际检查其值是否大于0,如果大于0就减一.如果为0,就睡眠.此时down操作暂未结束,只有另一个进程up时,操作系统才会选择一个进程进行up操作.
检查数值,修改变量值以及可能发生的睡眠操作都是用原子操作完成的.对信号量原子性的保护可以用之前提到的TSL和XCHG实现.
up操作对信号量的值增加1.如果一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则系统选择一个完成down操作.于是,这种情况下,执行了一个up操作,但是信号量的值仍然是0,但是在其上睡眠的进程却少了一个.不会有进程因为执行up操作而阻塞.

信号量实现生产者消费者的代码:

#define N 100 /* number of slots in the buffer */typedef int semaphore; /* semaphores are a special kind of intsemaphore mutex = 1; /* controls access to critical region */semaphore empty = N; /* counts empty buffer slots */semaphore full = 0; /* counts full buffer slots */void producer(void){        int item;        while (TRUE) { /* TRUE is the constant 1 */        item = produce item( ); /* generate something to put in buffer *        down(&empty); /* decrement empty count */        down(&mutex); /* enter critical region */        inser t item(item); /* put new item in buffer */        up(&mutex); /* leave critical region */        up(&full); /* increment count of full slots */    }}void consumer(void){    int item;    while (TRUE) { /* infinite loop */        down(&full); /* decrement full count */        down(&mutex); /* enter critical region */        item = remove item( ); /* take item from buffer */        up(&mutex); /* leave critical region */        up(&empty); /* increment count of empty slots */        consume item(item); /* do something with the item */    }}

互斥量

信号量的一个简化版本称为互斥量
互斥量只有两个状态:解锁和加锁.常常使用一个整形量,0表示解锁,其他所有值表示加锁.当进程需要访问临界区时,它调用mutex_lock().当他出来时,它调用mutex_unlock().互斥量TSL实现代码如下:

mutex lock:    TSL REGISTER,MUTEX | copy mutex to register and set mutex to    CMP REGISTER,#0 | was mutex zero?    JZE ok | if it was zero, mutex was unlocked, so r    CALL thread yield | mutex is busy; schedule another thread    JMP mutex lock | tr y again    ok: RET | return to caller; critical region enteredmutex unlock:    MOVE MUTEX,#0 | store a 0 in mutex    RET | return to caller

mutex和enter_region的区别很明显:
enter_region()当测试不成功时就一直循环测试.而mutex会直接放弃时间片,让另一个进程得到调度,这样就避免了忙等待浪费资源.

Pthread中的互斥

Pthread提供许多可以用来同步线程的函数.其基本机制是使用一个可以被锁定和解锁的互斥量俩保护每个临界区.一个线程想进入临界区,它会先测试临界区有没有加锁,如果没有,就立即进入.如果加锁了,就阻塞直到解锁.如果多个互斥量等待同一个互斥量,就只允许一个线程复活.

线程调用 描述 pthread_mutex_init 创建一个互斥量 pthread_mutex_destroy 撤销一个已存在的互斥量 pthread_mutex_lock 获得一个锁或阻塞 pthread_mutex_trylock 获得一个锁或失败 pthread_mutex_unlock 释放一个锁

管程

使用mutex和信号量可能会引发死锁问题.为了更易于编写正确的程序,Brinch Hansen 和 Hoare 提出了管程.管程中是一个由过程,变量及数据结构等组成的一个集合,它们组成一个特殊模块.
管程的一个很重要的特性就是任意时刻管程内只有一个活跃进程.
进入管程时的互斥由编译器进行负责,但通常的做法是用一个互斥量或二元信号量.因为编译器安排互斥是的出错的可能小的多.
java中使用管程解决生产者消费者的代码:

public class ProducerConsumer {    static final int N = 100; // constant giving the buffer size    static producer p = new producer( ); // instantiate a new producer thread    static consumer c = new consumer( ); // instantiate a new consumer thread    static our monitor mon = new our monitor( ); // instantiate a new monitor    public static void main(String args[ ]) {        p.star t( ); // star t the producer thread        c.star t( ); // star t the consumer thread    }    static class producer extends Thread {    public void run( ) { // run method contains the thread code        int item;        while (true) { // producer loop        item = produce item( );        mon.inser t(item);        }    }    private int produce item( ) { ... } // actually produce    }    static class consumer extends Thread {    public void run( ) { run method contains the thread code    int item;    while (true) { // consumer loop        item = mon.remove( );        consume item (item);    }    }    private void consume item(int item) { ... }// actually consume    }    static class our monitor { // this is a monitor    private int buffer[ ] = new int[N];    private int count = 0, lo = 0, hi = 0; // counters and indices    public synchronized void insert(int val) {    if (count == N) go to sleep( ); // if the buffer is full, go to sleep        buffer [hi] = val; // inser t an item into the buffer        hi = (hi + 1) % N; // slot to place next item in        count = count + 1; // one more item in the buffer now    if (count == 1) notify( ); // if consumer was sleeping, wake it up    }    public synchronized int remove( ) {        int val;        if (count == 0) go to sleep( ); // if the buffer is empty, go to sleep        val = buffer [lo]; // fetch an item from the buffer        lo = (lo + 1) % N; // slot to fetch next item from        count = count1; // one few items in the buffer        if (count == N − 1) notify( ); // if producer was sleeping, wake it up        return val;    }    private void go to sleep( ) { try{wait( );} catch(InterruptedException exc) {};}    }}

调度

调度介绍

进程行为

几乎所有的进程I/O请求或计算都是交替突发的.
进程有IO密集型和计算密集型两种.

何时调度

有关调度处理的一个关键问题是何时进行调度决策.

  • 创建一个新进程之后,需要决定是运行父进程还是运行子进程
  • 在一个进程退出时必须要做出调度决策
  • 当一个进程阻塞在I/O和信号量上或由于其他原因阻塞时,必须选择另一个进程运行.
  • 当一个I/O中断发生时,必须做出调度决策.

如果硬件时钟提供50HZ,60HZ或其他频率的周期性中断,可以在每个时钟中断或者在每k个中断时做出调度决策.
根据如何处理时钟中断,可以把调度算法分为两类.

调度算法的目标

不同调度算法有不同的调度策略,这也决定了调度算法对不同类型的作业影响不同。在选择调度算法时,必须考虑不同算法的特性。为了衡量调度算法的性能,人们提出了一些评价标准。

  • CPU利用率:CPU是系统最重要、也是最昂贵的资源,其利用率是评价调度算法的重要指标。
  • 系统吞吐量 :系统吞吐量表示单位时间内CPU完成作业的数量。对长作业来说,由于它要占用较长的CPU处理时间,因此会导致系统吞吐量下降,而对短作业来说则相反。
  • 响应时间 :在交互系统中,尤其在多用户系统中,多个用户同时对系统进行操作,都要求在一定时间内得到响应,不能使某些用户的进程长期得不到调用。
  • 周转时间 :从每个作业的角度来看,完成该作业的时间是至关重要的,通常用周转时间或者带权周转时间来衡量。

批处理系统中的调度

  1. 先来先服务 :适用范围:可用于作业调度和进程调度。
    基本思想是按照进程进入就绪队列的先后次序来分配处理器。先来先服务调度算法采用非抢占的调度方式
  2. 短作业优先调度算法 :基本思想是把处理器分配给最快完成的作业。

交互式系统中的调度

  1. 轮转调度:每个进程被分配一个时间片,即允许该进程在该时间片结束前阻塞或结束,即CPU立即进行切换
  2. 优先级调度算法:轮转调度假设所有的进程都一样重要,事实上进程分为前台和后台进程,前台和后台进程对于调度的要求都不一样.
    基本思想是把处理器分配给优先级最高的进程。进程优先级通常分为两种:a.静态优先级:是指优先级在创建进程时就已经确定了,在整个进程运行期间不再改变;b.动态优先级:是指在创建进程时,根据进程的特点及相关情况确定一个优先级,在进程运行过程中再根据情况的变化调整优先级。
  3. 多级队列调度算法: 适用范围:主要用于进程调度。
    基本思想是根据进程的性质或类型,将就绪队列划分为若干个独立的队列,每个进程固定地分属于一个队列。每个队列采用一种调度算法,不同的队列可以采用不同的调度算法。
  4. 彩票调度 :其基本思想是向进程提供各种系统资源的彩票.一旦需要作出一项调度决策时,就随机抽出一张彩票,拥有该彩票的进程获得该进程获得该资源.在应用时,系统可以掌握每秒钟的一种彩票,作为奖励每个进程可以获得20ms的CPU时间

线程调度

用户级线程

由于内核并不知道用户级线程的存在,所以内核会调度进程,在每个进程执行的时间片内,进程自由调度它的用户线程.

内核级线程

内核选择一个特定的线程运行.它不用考虑该线程属于哪个进程,不过如果有必要的话,它可以这么做.对被选择的线程赋予一个时间片,如果超过了时间片,就强制挂起该线程.

用户级线程和内核级线程的差距在于性能.用户级线程的线程切换需要少量的机器指令,而内核级线程需要完整的上下文切换,修改内存映像,是告诉缓存失效,这导致了若干数量级的延迟.

切换同一个进程的线程开销小于切换进程.切换进程之间的进程需要比切换同一个进程的线程多做一些工作.比如:修改内存映像,清除高速缓存

经典的IPC问题

哲学家就餐问题

错误的解法:

#define N 5 /* number of philosophers */void philosopher(int i) /* i: philosopher number, from 0 to 4 */{    while (TRUE) {        think( ); /* philosopher is thinking */        take fork(i); /* take left fork */        take fork((i+1) % N); /* take right fork; % is modulo operator */        eat( ); /* yum-yum, spaghetti */        put fork(i); /* put left fork back on the table */        put fork((i+1) % N); /* put right fork back on the table */    }}

当出现这样一种极端情况:每个哲学家都拿到了左边的叉子,尝试去拿右边的叉子.
这种情况下就出现了死锁
为了解决这个问题,可以让其中一位哲学家不先拿左边的,而是先拿右边的叉子.这样就不会出现死锁了.
下面这种解法,不仅没有死锁,而且获得了最大的并行度.

#define N 5 /* number of philosophers */#define LEFT (i+N−1)%N /* number of i’s left neighbor */#define RIGHT (i+1)%N /* number of i’s right neighbor */#define THINKING 0 /* philosopher is thinking */#define HUNGRY 1 /* philosopher is trying to get forks */#define EATING 2 /* philosopher is eating */typedef int semaphore; /* semaphores are a special kind of int */int state[N]; /* array to keep track of everyone’s state */semaphore mutex = 1; /* mutual exclusion for critical regions */semaphore s[N]; /* one semaphore per philosopher */void philosopher(int i) /* i: philosopher number, from 0 to N−1 */{    while (TRUE) { /* repeat forever */        think( ); /* philosopher is thinking */        take forks(i); /* acquire two forks or block */        eat( ); /* yum-yum, spaghetti */        put forks(i); /* put both forks back on table */    }}void take forks(int i) /* i: philosopher number, from 0 to N−1 */{    down(&mutex); /* enter critical region */    state[i] = HUNGRY; /* record fact that philosopher i is hungry */    test(i); /* tr y to acquire 2 forks */    up(&mutex); /* exit critical region */    down(&s[i]); /* block if forks were not acquired */}void put forks(i) /* i: philosopher number, from 0 to N−1 */{    down(&mutex); /* enter critical region */    state[i] = THINKING; /* philosopher has finished eating */    test(LEFT); /* see if left neighbor can now eat */    test(RIGHT); /* see if right neighbor can now eat */    up(&mutex); /* exit critical region */}void test(i) /* i: philosopher number, from 0 to N−1 */{    if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING) {        state[i] = EATING;        up(&s[i]);    }}

算法使用一个state数组跟踪每个哲学家是在进餐、思考还是饥饿(正在试图拿叉子).一个哲学家只有在两个邻居都没有进餐时才允许进入到进餐状态.第i个哲学家的邻居则由宏LEFT和RIGHT定义.
该程序使用了一个信号量数组,每个信号量对应一个哲学家,这样在所需的叉子被占用时,想进餐的哲学家就被阻塞.

读者写者问题

读者-写者问题为数据库建立了一个模型.考虑一个飞机订票系统,其中有许多竞争进程试图读写其中的数据.多个进程同时读数据库是可以接收的.但是只要有一个写者在写,那么其他进程都不能访问,即使读操作也不可以.
在该解法中,隐含着一个需要注解的条件.假设一个读者正在使用数据库,另一个读者来了.同时两个读者并不存在问题.第二个读者也允许进入.如果有第三个和更多的读者来了也同样允许.
现在假设一个写者到来.由于写者的访问时排他的,不能允许写者进入数据库,只能被挂起.只要还有一个读者在活动,就允许后续的读者进来.这种策略的结果是,如果有一个稳定的读者流,那么写者将永远得不到访问.
为了避免这种情况,可以稍微改变一下程序的写法:在一个读者到达,且一个写者在等待时,读者在写者之后被挂起,而不是立即允许进入.

typedef int semaphore; /* use your imagination */semaphore mutex = 1; /* controls access to rc */semaphore db = 1; /* controls access to the database */int rc = 0; /* # of processes reading or wanting to */void reader(void){    while (TRUE) { /* repeat forever */        down(&mutex); /* get exclusive access to rc */        rc = rc + 1; /* one reader more now */        if (rc == 1) down(&db); /* if this is the first reader ... */        up(&mutex); /* release exclusive access to rc */        read data base( ); /* access the data */        down(&mutex); /* get exclusive access to rc */        rc = rc − 1; /* one reader fewer now */        if (rc == 0) up(&db); /* if this is the last reader ... */        up(&mutex); /* release exclusive access to rc */        use data read( ); /* noncritical region */    }}void writer(void){    while (TRUE) { /* repeat forever */        think up data( ); /* noncritical region */        down(&db); /* get exclusive access */        write data base( ); /* update the data */        up(&db); /* release exclusive access */    }}
0 0
原创粉丝点击