计算机操作系统--进程管理(二)
1.进程与线程
进程
进程是资源分配的基本单元。
进程控制块(Process Control Block, PCB)描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是针对PCB的操作。
下图显示了四个程序创建了四个进程,这4各进程可以并发地执行。

线程
线程是独立调度地基本单位。
一个进程中可以有多个线程,它们共享进程资源。
例如,QQ和浏览器是两个进程,浏览器进程里面有很多线程,例如HTTP请求线程、事件响应线程、渲染线程等,线程地并发执行使得在浏览器中点击一个新链接从而发起HTTP请求时,浏览器还可以响应用户地其他事件。

线程是在进程的基础上提出的概念,目的是提高处理器效率
线程有以下优点:
- 易于调度
- 提高并发性。进程可以创建多个线程来执行同一程序的不同部分
- 开销小。不拥有系统资源,只拥有一点在运行中必不可少的资源
- 利于充分利用多处理器的性能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现程序的并行性
区别
- 拥有资源:进程拥有资源,线程不拥有资源,线程可以访问隶属进程地资源。
- 调度:线程是独立调度地基本单位,在同一进程中,线程的切换不会引起进程切换。从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
- 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O设备等,所付出的开销远大于创建或撤销线程时的开销。类似的,在进行进程切换时,涉及当前执行进程CPU环境的保存及新调度进程CPU环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
- 通信:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC。
2.进程状态的切换

- 就绪状态(ready):等待被调度
- 运行状态(running)
- 阻塞状态(waiting):等待资源
应该注意以下内容:
- 只有就绪态和运行态可以相互转换,其他的都是单向转换。就绪状态的进程通过调度算法从而获得CPU时间,转为运行状态;而运行状态的进程,在分配给它的CPU时间片用完之后就会转为就绪状态,等待下一次调度。
- 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括CPU时间,缺少CPU时间会从运行态转换为就绪态。
3.进程调度算法
不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。
批处理系统
批处理系统没有太多的用户操作,在该系统中,调度算法的目标是保证吞吐量和周转时间(从提交到终止的时间)
- 先来先服务 first-come first-serverd (FCFS) : 非抢占式,不利于短作业。因为短作业必须等前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业执行时间过长。
- 短作业优先 shortest job first (SJF) : 非抢占式,长作业有可能饿死。如果一直有短作业来,长作业永远得不到调度。
- 最短剩余时间优先 shortest remaining time next (SRTN) : 短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。当一个新的作业到达时,其整个运行时间与当前进程的剩余时间做比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
交互式系统
交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。
- 时间片轮转:效率和时间片的大小有很大关系,如果时间片过小,会导致进程切换频繁;如果时间片过大,那么实时性就不能得到保证。
- 优先级调度:为每个进程分配一个优先级,按优先级进行调度。为防止低优先级地进程永远等不到调度,可以随着时间推移增加等待进程的优先级。
- 多级反馈队列:可以看成是时间片轮转和优先级调度地结合。多级队列设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8···。进程在一个队列调度完,会进入下一级队列。每个队列优先级不同,时间片越小的优先级越高。

实时系统
要求一个请求在确定时间内得到响应。分为硬实时和软实时,前者必须满足绝对地截至时间,后者可以容忍一定地超时。
4.进程同步
临界区 critical section
对临界资源进行访问的代码称为临界区。为了互斥访问临界资源,每个进程进入临界区之前,需要先进行检查。
// entry section // critical section; // exit section
同步和互斥
- 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
- 互斥:多个进程在同一时刻只有一个进程能进入临界区。
信号量
信号量(Semaphore)是一个整形变量,可以对其执行down和up操作,也就是常见的 P 和 V 操作。
- down:如果信号量大于0,执行-1操作;如果信号量等于0,进程睡眠,等待信号量大于0‘
- up:对信号量执行+1操作,唤醒睡眠的进程让其完成down操作
down和up操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
如果信号量的取值只能是0或者1,那么就成为了互斥量(Mutex),0表示临界区已经加锁,1表示临界区解锁。
typedef int semaphore; semaphore mutex = 1; void P1() { down(&mutex); // 临界区 up(&mutex); } void P2() { down(&mutex); // 临界区 up(&mutex); }
用信号量来实现生产者-消费者问题
问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
用互斥量mutex来控制对缓冲区的互斥访问。
用两个信号量:empty记录空缓冲区的数量,full记录满缓冲区的数量。
注意,不能先对缓存区加锁,再测试信号量。如先执行down(mutex),再执行down(empty)。
#define N 100 typedef int semaphore; semaphore mutex = 1; semaphore empty = N; semaphore full = 0; void producer() { while(TRUE) { int item = produce_item(); down(&empty); down(&mutex); insert_item(item); up(&mutex); up(&full); } } void consumer() { while(TRUE) { down(&full); down(&mutex); int item = remove_item(); consume_item(item); up(&mutex); up(&empty); } }
管程
使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
c语言不支持管程,下面使用了类Pascal语言来描述管程。示例代码提供了insert()和remove()方法,客户端代码通过调用这两个方法来解决生产者消费者问题
monitor ProducerConsumer integer i; condition c; procedure insert(); begin // ... end; procedure remove(); begin // ... end; end monitor;
管程的重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其他进程永远不能使用管程。
管程引入了条件变量 以及相关的操作:wait()和signal()来实现同步操作。对条件变量执行wait()操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal()操作用于唤醒被阻塞的进程。
使用管程实现生产者-消费者问题
// 管程 monitor ProducerConsumer condition full, empty; integer count := 0; condition c; procedure insert(item: integer); begin if count = N then wait(full); insert_item(item); count := count + 1; if count = 1 then signal(empty); end; function remove: integer; begin if count = 0 then wait(empty); remove = remove_item; count := count - 1; if count = N -1 then signal(full); end; end monitor; // 生产者客户端 procedure producer begin while true do begin item = produce_item; ProducerConsumer.insert(item); end end; // 消费者客户端 procedure consumer begin while true do begin item = ProducerConsumer.remove; consume_item(item); end end;
5.经典同步问题
生产者消费者问题已经讨论过了
1.哲学家进餐问题

问题描述:五个哲学家围着一张圆桌,每个哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子,
当五个哲学家同时拿起左手边的筷子,就形成了死锁。为了防止死锁的发生,可以设置两个条件。
- 必须同时拿起左右两根筷子;
- 只有在两个邻居都没有进餐的情况下才允许进餐。
#define N 5 #define LEFT (i + N - 1) % N // 左邻居 #define RIGHT (i + 1) % N // 右邻居 #define THINKING 0 #define HUNGRY 1 #define EATING 2 typedef int semaphore; int state[N]; // 跟踪每个哲学家的状态 semaphore mutex = 1; // 临界区的互斥,临界区是 state 数组,对其修改需要互斥 semaphore s[N]; // 每个哲学家一个信号量 void philosopher(int i) { while(TRUE) { think(i); take_two(i); eat(i); put_two(i); } } void take_two(int i) { down(&mutex); state[i] = HUNGRY; check(i); up(&mutex); down(&s[i]); // 只有收到通知之后才可以开始吃,否则会一直等下去 } void put_two(i) { down(&mutex); state[i] = THINKING; check(LEFT); // 尝试通知左右邻居,自己吃完了,你们可以开始吃了 check(RIGHT); up(&mutex); } void eat(int i) { down(&mutex); state[i] = EATING; up(&mutex); } // 检查两个邻居是否都没有用餐,如果是的话,就 up(&s[i]),使得 down(&s[i]) 能够得到通知并继续执行 void check(i) { if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] !=EATING) { state[i] = EATING; up(&s[i]); } }
2.读者-写者问题
问题描述:允许多个进程同时对数据进行读操作,但不允许读和写以及写和写操作同时发生。
整型变量count记录在对数据进行读操作的进程数量,一个互斥量count_mutex用于对count加锁,一个互斥量data_mutex用于对读写数据加锁。
typedef int semaphore; semaphore count_mutex = 1; semaphore data_mutex = 1; int count = 0; void reader() { while(TRUE) { down(&count_mutex); count++; if(count == 1) down(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问 up(&count_mutex); read(); down(&count_mutex); count--; if(count == 0) up(&data_mutex); up(&count_mutex); } } void writer() { while(TRUE) { down(&data_mutex); write(); up(&data_mutex); } }
第一个例子可能导致,写者的等待时间远远超过合适时间。当有读者先于写者访问,且持续有读者访问时,写者会一直挂起。
int readcount, writecount; //(initial value = 0) semaphore rmutex, wmutex, readLock, resource; //(initial value = 1) //READER void reader() { <ENTRY Section> down(&readLock); // reader is trying to enter down(&rmutex); // lock to increase readcount readcount++; if (readcount == 1) down(&resource); //if you are the first reader then lock the resource up(&rmutex); //release for other readers up(&readLock); //Done with trying to access the resource <CRITICAL Section> //reading is performed <EXIT Section> down(&rmutex); //reserve exit section - avoids race condition with readers readcount--; //indicate you're leaving if (readcount == 0) //checks if you are last reader leaving up(&resource); //if last, you must release the locked resource up(&rmutex); //release exit section for other readers } //WRITER void writer() { <ENTRY Section> down(&wmutex); //reserve entry section for writers - avoids race conditions writecount++; //report yourself as a writer entering if (writecount == 1) //checks if you're first writer down(&readLock); //if you're first, then you must lock the readers out. Prevent them from trying to enter CS up(&wmutex); //release entry section <CRITICAL Section> down(&resource); //reserve the resource for yourself - prevents other writers from simultaneously editing the shared resource //writing is performed up(&resource); //release file <EXIT Section> down(&wmutex); //reserve exit section writecount--; //indicate you're leaving if (writecount == 0) //checks if you're the last writer up(&readLock); //if you're last writer, you must unlock the readers. Allows them to try enter CS for reading up(&wmutex); //release exit section }
这个例子导致只要有写者在,读者就要一直等待。
下面的方案添加了不允许线程饿死的约束条件,也就是说,获取共享数据锁的操作总是会在有限的时间内终止。
int readCount; // init to 0; number of readers currently accessing resource // all semaphores initialised to 1 Semaphore resourceAccess; // controls access (read/write) to the resource Semaphore readCountAccess; // for syncing changes to shared variable readCount Semaphore serviceQueue; // FAIRNESS: preserves ordering of requests (signaling must be FIFO) void writer() { down(&serviceQueue); // wait in line to be servicexs // <ENTER> down(&resourceAccess); // request exclusive access to resource // </ENTER> up(&serviceQueue); // let next in line be serviced // <WRITE> writeResource(); // writing is performed // </WRITE> // <EXIT> up(&resourceAccess); // release resource access for next reader/writer // </EXIT> } void reader() { down(&serviceQueue); // wait in line to be serviced down(&readCountAccess); // request exclusive access to readCount // <ENTER> if (readCount == 0) // if there are no readers already reading: down(&resourceAccess); // request resource access for readers (writers blocked) readCount++; // update count of active readers // </ENTER> up(&serviceQueue); // let next in line be serviced up(&readCountAccess); // release access to readCount // <READ> readResource(); // reading is performed // </READ> down(&readCountAccess); // request exclusive access to readCount // <EXIT> readCount--; // update count of active readers if (readCount == 0) // if there are no readers left: up(&resourceAccess); // release resource access for all // </EXIT> up(&readCountAccess); // release access to readCount }
6.进程通信
- 进程同步:控制多个进程按一定顺序执行
- 进程通信:进程间传输信息
进程通信是一种手段,而进程同步是一种目的。可以说,为了达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。
管道
管道是通过pipe函数创建的,fd [ 0 ]用于读,fd[1]用于写。
#include <unistd.h> int pipe(int fd[2]);
它有以下限制:
- 只支持半双工通信(单向交替传输)
- 只能在父子进程或者兄弟进程中使用。

FIFO
也称为命名管道(named PIPE),去除了管道只能在父子进程中使用的限制。
#include <sys/stat.h> int mkfifo(const char *path, mode_t mode); int mkfifoat(int fd, const char *path, mode_t mode);
FIFO常用于客户-服务器应用程序中,FIFO用作汇聚点,在客户进程和服务器进程之间传递数据。

消息队列
相比于FIFO,消息队列具有以下优点:
- 消息队列可以独立于读写进程存在,从而避免了FIFO中同步管道的打开和关闭时可能产生的困难;
- 避免了FIFO的同步阻塞问题,不需要进程自己提供同步方法;
- 读进程可以根据消息类型有选择地接收消息,而不像FIFO那样只能默认地接收。
信号量
它是一个计数器,用于为多个进程提供对共享数据对象的访问。
共享内存
允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种IPC。
需要使用信号量来同步对共享存储的访问。
多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外XSI共享内存不是使用文件,而是使用内存的匿名段。
套接字
它可用于不同机器间的进程通信。
浙公网安备 33010602011771号