进程是资源分配的基本单位。html
进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的建立进程和撤销进程,都是指对 PCB 的操做。git
下图显示了 4 个程序建立了 4 个进程,这 4 个进程能够并发地执行。github
线程是独立调度的基本单位。算法
一个进程中能够有多个线程,它们共享进程资源。数组
QQ 和浏览器是两个进程,浏览器进程里面有不少线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新连接从而发起 HTTP 请求时,浏览器还能够响应用户的其它事件。浏览器
Ⅰ 拥有资源服务器
进程是资源分配的基本单位,可是线程不拥有资源,线程能够访问隶属进程的资源。markdown
Ⅱ 调度多线程
线程是独立调度的基本单位,在同一进程中,线程的切换不会引发进程切换,从一个进程中的线程切换到另外一个进程中的线程时,会引发进程切换。并发
Ⅲ 系统开销
因为建立或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于建立或撤销线程时的开销。相似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少许寄存器内容,开销很小。
Ⅳ 通讯方面
线程间能够经过直接读写同一进程中的数据进行通讯,可是进程通讯须要借助 IPC。
应该注意如下内容:
不一样环境的调度算法目标不一样,所以须要针对不一样环境来讨论调度算法。
批处理系统没有太多的用户操做,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
1.1 先来先服务 first-come first-serverd(FCFS)
非抢占式的调度算法,按照请求的顺序进行调度。
有利于长做业,但不利于短做业,由于短做业必须一直等待前面的长做业执行完毕才能执行,而长做业又须要执行很长时间,形成了短做业等待时间过长。
1.2 短做业优先 shortest job first(SJF)
非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
长做业有可能会饿死,处于一直等待短做业执行完毕的状态。由于若是一直有短做业到来,那么长做业永远得不到调度。
1.3 最短剩余时间优先 shortest remaining time next(SRTN)
最短做业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的做业到达时,其整个运行时间与当前进程的剩余时间做比较。若是新的进程须要的时间更少,则挂起当前进程,运行新的进程。不然新的进程等待。
交互式系统有大量的用户交互操做,在该系统中调度算法的目标是快速地进行响应。
2.1 时间片轮转
将全部就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程能够执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便中止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系:
2.2 优先级调度
为每一个进程分配一个优先级,按优先级进行调度。
为了防止低优先级的进程永远等不到调度,能够随着时间的推移增长等待进程的优先级。
2.3 多级反馈队列
一个进程须要执行 100 个时间片,若是采用时间片轮转调度算法,那么须要交换 100 次。
多级队列是为这种须要连续执行多个时间片的进程考虑,它设置了多个队列,每一个队列时间片大小都不一样,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,以前的进程只须要交换 7 次。
每一个队列优先权也不一样,最上面的优先权最高。所以只有上一个队列没有进程在排队,才能调度当前队列上的进程。
能够将这种调度算法当作是时间片轮转调度算法和优先级调度算法的结合。
实时系统要求一个请求在一个肯定时间内获得响应。
分为硬实时和软实时,前者必须知足绝对的截止时间,后者能够容忍必定的超时。
对临界资源进行访问的那段代码称为临界区。
为了互斥访问临界资源,每一个进程在进入临界区以前,须要先进行检查。
// entry section
// critical section;
// exit section
复制代码
信号量(Semaphore)是一个整型变量,能够对其执行 down 和 up 操做,也就是常见的 P 和 V 操做。
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);
}
复制代码
<font size=3> 使用信号量实现生产者-消费者问题 </font> </br>
问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才能够放入物品;只有缓冲区不为空,消费者才能够拿走物品。
由于缓冲区属于临界资源,所以须要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
为了同步生产者和消费者的行为,须要记录缓冲区中物品的数量。数量可使用信号量来进行统计,这里须要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才能够放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才能够取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。若是这么作了,那么可能会出现这种状况:生产者对缓冲区加锁后,执行 down(empty) 操做,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,由于生产者对缓冲区加锁了,消费者就没法执行 up(empty) 操做,empty 永远都为 0,致使生产者永远等待下,不会释放锁,消费者所以也会永远等待下去。
#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;
复制代码
生产者和消费者问题前面已经讨论过了。
五个哲学家围着一张圆桌,每一个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,须要先拿起本身左右两边的两根筷子,而且一次只能拿起一根筷子。
下面是一种错误的解法,若是全部哲学家同时拿起左手边的筷子,那么全部哲学家都在等待其它哲学家吃完并释放本身手中的筷子,致使死锁。
#define N 5
void philosopher(int i) {
while(TRUE) {
think();
take(i); // 拿起左边的筷子
take((i+1)%N); // 拿起右边的筷子
eat();
put(i);
put((i+1)%N);
}
}
复制代码
为了防止死锁的发生,能够设置两个条件:
#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]);
}
}
复制代码
容许多个进程同时对数据进行读操做,可是不容许读和写以及写和写操做同时发生。
一个整型变量 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);
}
}
复制代码
如下内容由 @Bandi Yugandhar 提供。
The first case may result Writer to starve. This case favous Writers i.e no writer, once added to the queue, shall be kept waiting longer than absolutely necessary(only when there are readers that entered the queue before the writer).
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
}
复制代码
We can observe that every reader is forced to acquire ReadLock. On the otherhand, writers doesn’t need to lock individually. Once the first writer locks the ReadLock, it will be released only when there is no writer left in the queue.
From the both cases we observed that either reader or writer has to starve. Below solutionadds the constraint that no thread shall be allowed to starve; that is, the operation of obtaining a lock on the shared data will always terminate in a bounded amount of time.
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
}
复制代码
进程同步与进程通讯很容易混淆,它们的区别在于:
进程通讯是一种手段,而进程同步是一种目的。也能够说,为了可以达到进程同步的目的,须要让进程进行通讯,传输一些进程同步所须要的信息。
管道是经过调用 pipe 函数建立的,fd[0] 用于读,fd[1] 用于写。
#include <unistd.h>
int pipe(int fd[2]);
复制代码
它具备如下限制:
也称为命名管道,去除了管道只能在父子进程中使用的限制。
#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,消息队列具备如下优势:
它是一个计数器,用于为多个进程提供对共享数据对象的访问。
容许多个进程共享一个给定的存储区。由于数据不须要在进程之间复制,因此这是最快的一种 IPC。
须要使用信号量用来同步对共享存储的访问。
多个进程能够将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用内存的匿名段。
与其它通讯机制不一样的是,它可用于不一样机器间的进程通讯。