笔记理解之后可以进行深入解释→【操作系统】第二章–进程的描述与控制–深入与解释(2)
文章目录
第二章–进程的描述与控制–笔记与理解(2)
经典进程的同步问题
生产者-消费者问题
- 单生产者-单消费者-单缓冲区:系统中有一个生产者进程、一个消费者进程和一个一次只能放1个产品的缓冲区。生产者进程重复的生产产品并放入到缓冲区中;每当缓冲区中有产品时,消费者进程从缓冲区中取产品进行消费。
-
信号量:space,存储位置,初值1;prod,产品,初值0
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
sem_t space,prod;
void *producer(void *p) {
while(1) {
sem_wait(&space);
printf("Put a product\n");
sem_post(&prod);
}
return NULL;
}
void *consumer(void *p) {
while(1) {
sem_wait(&prod);
printf("Get a product\n");
sem_post(&space);
}
return NULL;
}
int main(void) {
sem_init(&space,0,1);
sem_init(&prod,0,0);
pthread_t tid[2];
pthread_create(&tid[0],NULL,producer,NULL);
pthread_create(&tid[1],NULL,consumer,NULL);
sem_destroy(&space);
sem_destroy(&prod);
pthread_join(tid[0],NULL);
return 0;
}
- 运行结果:
- 单生产者-单消费者-多缓冲区
-
产品存储:循环队列;信号量:space,存储位置,初值N;prod,产品,初值0
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define N 10
sem_t space,prod;
int BUffer[N];
int in=0,out=0;
void *producer(void *p) {
while(1) {
sem_wait(&space);
printf("Put a product into Buffer[%d]\n",in);
in = (in+1)%N;
sem_post(&prod);
}
return NULL;
}
void *consumer(void *p) {
while(1) {
sem_wait(&prod);
printf("Get a product from Buffer[%d]\n",out);
out = (out+1)%N;
sem_post(&space);
}
return NULL;
}
int main(void) {
sem_init(&space,0,N);
sem_init(&prod,0,0);
pthread_t tid[2];
pthread_create(&tid[0],NULL,producer,NULL);
pthread_create(&tid[1],NULL,consumer,NULL);
sem_destroy(&space);
sem_destroy(&prod);
pthread_join(tid[0],NULL);
return 0;
}
- 运行结果:
- 多生产者-多消费者
-
信号量:space,存储位置,初值N;prod,产品,初值0;buf,缓冲区,初值1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define M 8
#define N 10
sem_t space,prod,buf;
int Buffer[N];
int in = 0, out = 0;
void *producer(void *p) {
while(1) {
sem_wait(&space);
sem_wait(&buf);
printf("Put a product into Buffer[%d]\n",in);
in = (in+1)%N;
sem_post(&prod);
sem_post(&buf);
}
return NULL;
}
void *consumer(void *p) {
while(1) {
sem_wait(&prod);
sem_wait(&buf);
printf("Get a product from Buffer[%d]\n",out);
out = (out+1)%N;
sem_post(&space);
sem_post(&buf);
}
return NULL;
}
int main(void) {
sem_init(&space,0,N);
sem_init(&prod,0,0);
sem_init(&buf,0,1);
pthread_t tid[M],tid2[M];
for(int i = 0; i < M; i ++) {
pthread_create(&tid[i],NULL,producer,NULL);
}
for(int i = 0; i < M; i ++) {
pthread_create(&tid2[i],NULL,consumer,NULL);
}
sem_destroy(&space);
sem_destroy(&prod);
sem_destroy(&buf);
pthread_join(tid[0],NULL);
return 0;
}
-
运行结果:
-
总结:
读者-写者问题
- 一个数量集(如文件)被多个并发进程(线程)共享,一些进程(线程)只读取数据集内容(读者),而另一些进程(线程)则只修改数据集内容(写者)。
-
访问规则:
- 允许多个读者同时读取数据集;当有读者读数据集时,不允许任何写者进程写
- 当一个写者进程写数据集期间,不允许任何其他写者进程写,也不允许任何读者进程读
- 即:“读-写互斥”,“写-写互斥”,“读-读允许”
-
变量:readcount,读者数;信号量:sdata,数据集,初值1;srcount,读者数变量,初值1
-
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define M 10
sem_t sdata,srcount;
int readcount = 0;
void *reader(void *p) {
sem_wait(&srcount);
readcount++;
if(readcount == 1) {
sem_wait(&sdata);
}
sem_post(&srcount);
printf("Reading..\n");
sem_wait(&srcount);
readcount--;
if(readcount == 0) {
sem_post(&sdata);
}
sem_post(&srcount);
return NULL;
}
void *writer(void *p) {
sem_wait(&sdata);
printf("Writing..\n");
sem_post(&sdata);
return NULL;
}
int main(void) {
sem_init(&sdata,0,1);
sem_init(&srcount,0,1);
pthread_t tid[M],tid2[M];
for(int i = 0; i < M; i ++) {
pthread_create(&tid[i],NULL,writer,NULL);
}
for(int i = 0; i < M; i ++) {
pthread_create(&tid2[i],NULL,reader,NULL);
}
sem_destroy(&sdata);
sem_destroy(&srcount);
pthread_join(tid[0],NULL);
return 0;
}
-
运行结果:
-
存在问题:写者进程在实际应用中一般拥有更高操作优先级,但有可能长期推迟
-
读者-写者-写者优先
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define M 10
sem_t sdata,srcount;
int readcount = 0;
void *reader(void *p) {
sem_wait(&srcount);
readcount++;
if(readcount == 1) {
sem_wait(&sdata);
}
sem_post(&srcount);
printf("Reading..\n");
sem_wait(&srcount);
readcount--;
if(readcount == 0) {
sem_post(&sdata);
}
sem_post(&srcount);
return NULL;
}
void *writer(void *p) {
sem_wait(&sdata);
printf("Writing..\n");
sem_post(&sdata);
return NULL;
}
int main(void) {
sem_init(&sdata,0,1);
sem_init(&srcount,0,1);
pthread_t tid[M],tid2[M];
for(int i = 0; i < M; i ++) {
pthread_create(&tid[i],NULL,writer,NULL);
}
for(int i = 0; i < M; i ++) {
pthread_create(&tid2[i],NULL,reader,NULL);
}
sem_destroy(&sdata);
sem_destroy(&srcount);
pthread_join(tid[0],NULL);
return 0;
}
- 运行结果:
哲学家就餐问题
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <pthread.h>
#include <errno.h>
#include <math.h>
#include <unistd.h>
pthread_mutex_t chop[6];
void *eat_think(void *arg) {
char phi = *(char *)arg;
int l,r;
switch(phi) {
case 'A':
l = 5;r = 1;
break;
case 'B':
l = 1;r = 2;
break;
case 'C':
l = 2;r = 3;
break;
case 'D':
l = 3;r = 4;
break;
case 'E':
l = 4;r = 5;
}
int i;
for(;;) {
sleep(3);
pthread_mutex_lock(&chop[l]);
printf("哲学家 %c 获得了筷子 %d\n",phi,l);
/*
if(pthread_mutex_trylock(&chop[r]) == EBUSY) {
pthread_mutex_unlock(&chop[l]);
continue;
}
*/
pthread_mutex_lock(&chop[r]);
printf("哲学界 %c 获得了筷子 %d\n",phi,r);
printf("哲学家 %c 正在就餐\n",phi);
sleep(3);
pthread_mutex_unlock(&chop[l]);
printf("哲学家 %c 放下了筷子 %d\n",phi,l);
pthread_mutex_unlock(&chop[r]);
printf("哲学家 %c 放下了筷子 %d\n",phi,r);
}
}
int main() {
pthread_t A,B,C,D,E;
for(int i = 0; i < 5 ; i ++) {
pthread_mutex_init(&chop[i],NULL);
}
pthread_create(&A,NULL,eat_think,"A");
pthread_create(&B,NULL,eat_think,"B");
pthread_create(&C,NULL,eat_think,"C");
pthread_create(&D,NULL,eat_think,"D");
pthread_create(&E,NULL,eat_think,"E");
pthread_join(A,NULL);
pthread_join(B,NULL);
pthread_join(C,NULL);
pthread_join(D,NULL);
pthread_join(E,NULL);
return 0;
}
--------------------------------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <pthread.h>
#include <errno.h>
#include <math.h>
#include <unistd.h>
#include <semaphore.h>
sem_t chop[5];
void *eat_think(void *arg) {
int phi = *(int *)arg;
while(1) {
sem_wait(&chop[phi]);
printf("哲学家 %d 获得了右边的筷子 %d\n",phi,phi);
sleep(3);
sem_wait(&chop[(phi+1)%5]);
printf("哲学家 %d 获得了左边的筷子 %d\n",phi,(phi+1)%5);
sem_wait(&chop[(phi+1)%5]);
printf("哲学家 %d 放下了左边的筷子 %d\n",phi,(phi+1)%5);
sem_wait(&chop[phi]);
printf("哲学家 %d 放下了右边的筷子 %d\n",phi,phi);
/*
if(pthread_mutex_trylock(&chop[r]) == EBUSY) {
pthread_mutex_unlock(&chop[l]);
continue;
}
pthread_mutex_lock(&chop[r]);
printf("哲学界 %c 获得了筷子 %d\n",phi,r);
printf("哲学家 %c 正在就餐\n",phi);
sleep(3);
pthread_mutex_unlock(&chop[l]);
printf("哲学家 %c 放下了筷子 %d\n",phi,l);
pthread_mutex_unlock(&chop[r]);
printf("哲学家 %c 放下了筷子 %d\n",phi,r);
*/
}
}
int main() {
for(int i = 0; i < 5 ; i ++) {
sem_init(&chop[i],0,1);
}
pthread_t tid[5];
for(int i = 0; i < 5 ; i ++) {
pthread_create(&tid[i],NULL,eat_think,&i);
}
for(int i = 0; i < 5 ; i ++) {
sem_destroy(&chop[i]);
}
pthread_join(tid[0],NULL);
return 0;
}
进程通信
- 使用方便,高效地传送大量数据
进程通信类型
- 共享存储器系统:
- 基于共享数据结构的通信方式:适用于传递相对少量的数据,效率低下,低级通信
- 共享存储区的通信方式:高级通信,需要通信的进程在通信前,前向系统申请获得功能共享存储区的一个分区,并附加到自己的地址空间,便可进行操作,不再需要时将其归还给共享存储区
- 管道通信系统(pipe):
- 互斥:一个进程执行操作,其余进程等待
- 同步:读写进程的等待与唤醒
- 只有确定对方存在才能进行通信
- 消息传递系统:高级通信方式
- 直接通信方式:发送进程利用OS所提供的发送原语,直接把消息发送给目标进程
- 间接通信方式:发送和接收进程,都通过共享中间实体的方式完成进程间的通信
- 客户机-服务器系统:
- 套接字(socket):一个通信标识类型的数据结构,是进程通信/网络通信的基本构件
- 基于文件型:通信双方通过对这个文件的读写实现通信,原理类似于管道
- 基于网络型:非对称方式通信,发送者需要提供接收者命名,任何进程都可以发送请求,方便进程间通信的建立
- 优势:适用于网络环境中不同计算机间的通信,,每个套接字拥有唯一的套接字标识符,确保了通信双方间逻辑链路的唯一性,便于实现数据传送得到并发服务,,隐藏实现细节,采用统一接口进行处理
- 远程过程调用和远程方法调用:调用RPC(一个通信协议),用于通过网络连接的系统
- 套接字(socket):一个通信标识类型的数据结构,是进程通信/网络通信的基本构件
消息传递的实现方式
- 直接消息传递系统:
- 直接通信原语:
- 对称寻址方式:要求发送进程和接收进程都以显示方式提供对方的标识符,不是用于实现进程定义的模块化
- 非对称寻址方式:接收进程可能需要与多个发送进程通信,不需要命名发送进程
- 消息的格式:采用变长的消息格式,方便了用户
- 进程的同步方式:发送进程阻塞,接收进程阻塞,用于进程之间紧密同步,发送接受无缓冲,发送进程不阻塞,接收进程阻塞,发送进程和接收进程均不阻塞
- 通信链路:
- 单向通路:只允许发送进程或接收进程其中一方发送信息
- 双向通路:允许进程A与B互相发送信息
- 直接通信原语:
- 信箱通信:属于间接通信方式
- 信箱的结构:信箱头+信箱体
- 信箱通信原语:用于邮箱的创建和撤销,消息的发送和接收
- 信箱的类型:
- 私用信箱:可采用单向通信来实现,拥有此的进程结束,邮箱也消失
- 公用邮箱:双向通信链路实现,一半在系统运行期间始终存在
- 共享邮箱:拥有者和共享者都有权取走或发送自己的消息
- 关系:一对一,一对多,多对一,多对多
线程的基本概念
线程的引入
- 为了减少程序在并发执行时所付出的时空开销,更好的并发性
- 基本属性:
- 一个可拥有资源的独立空间
- 一个可独立调度和分派的基本单位
- 程序并发执行时所需付出的时空开销:
- 创建进程
- 撤销进程
- 进程切换
- 作为调度和分派的基本单位:拥有资源基本单位,,又不实施频繁的切换
线程与进程的比较
\ | 进程 | 线程 |
调度的基本单位 | 传统OS中,作为调度与分派的基本单位,开销较大 | 引入后为调度与分派的基本单位,线程切换不引起进程切换(同一进程中) |
并发性 | 允许进程间并发执行,并发性一般 | 允许进程间并发执行,允许一个进程多个线程并发,并发性更好 |
拥有资源 | 可以拥有资源,并作为系统中拥有资源的一个基本单位源 | 拥有自己少量资源外,允许多个线程共享该进程所拥有资源 |
独立性 | 拥有一个独立的地址空间和其他资源,独立性更好 | 共享进程的内存地址空间和资源,独立性一般 |
系统开销 | 创建/撤销进程,OS付出的开销大于线程创建/撤销所付开销 | |
多处理机 | 只能运行在一个处理机上 | 一个进程多线程分配到多个处理机上,使并发执行加速完成 |
线程的状态和线程控制块
- 三个状态:
- 执行状态
- 就绪状态
- 阻塞状态
- 线程控制块TCB:
- 线程标识符
- 一组寄存器
- 线程运行状态
- 线程专有存储区
- 优先级
- 信号屏蔽
- 堆栈指针
- 多线程OS中进程属性:
- 进程是一个可拥有资源的基本单位
- 多个线程可并发执行
- 进程已不是可执行的实体
线程的实现
线程的实现方式
- 内核支持线程KST:
- 优势:
- 多处理系统中,内核能同时调度同一进程中的多个线程并发执行
- 进程中一个线程被阻塞,可运行其他进程中线程
- 切换快,开销小
- 提高系统执行速度和效率
- 劣势:
- 模式切换开销大
- 优势:
- 用户级线程ULT:
- 优势:
- 线程切换不需要转换到内核空间
- 调度算法可以是进程专用的
- 实现与OS平台无关,可以在不支持多线程机制的操作系统上实现
- 劣势:
- 系统调用的阻塞问题
- 进程中仅有一个线程能执行,其他线程只能等待
- 优势:
- 组合方式:
- 多对一:多个用户线程映射到一个内核控制线程
- 优势:开销小,效率高
- 劣势:一个线程阻塞,整个进程阻塞
- 一对一
- 优势:更好的并发性,一个线程阻塞,允许调度另一个线程执行
- 劣势:开销大
- 多对多:结合上述两种方式的优势
- 多对一:多个用户线程映射到一个内核控制线程
线程的实现
- 内核支持线程的实现与进程的调度与切换相似
- 用户级线程的实现:不能利用系统调用,分为运行时程序和内核控制线程
线程的创建与终止
- 由创建而产生,由调度而执行,由终止而消亡
- 线程的创建:“初始化线程”在创建时,利用一个线程创建函数,提供相应的参数,返回一个线程标识符供以后使用
- 线程的终止:完成任务或出现异常而强行结束,用终止线程通过调用相应函数(或系统调用)对他执行终止操作
本章练习
-
在单处理机系统中,处于运行状态的进程(A)。
A.最多只有一个
B.可以有多个
C.不能被挂起
D.必须在执行完后才能被撤下 -
原语是(B)
A.一条机器指令
B.若干条机器指令组成,不可被中断
C.一条特定指令
D.中途能打断的指令 -
支持多道程序设计的操作系统在运行过程中,不断地选择新进程运行来实现CPU的共享,但其中(D) 不是引起操作系统选择新进程的直接原因
A.运行进程的时间片用完
B.运行进程出错
C.运行进程要等待某—事件的发生
D.有新进程进入就绪状态 -
在支持多线程的系统中,进程P创建的若干个线程不能共享的是(D)
A.进程P的代码段
B.进程P中打开的文件
C.进程P的全局变量
D.进程P中某线程的栈指针 -
在创建进程时,(A))不是创建进程所必须的步骤
A.由调度程序为进程分配CPU
B.建立一个PCB
C.为进程分配内存
D.将进程插入就绪队列 -
线程控制块TCB中不应拥有的内容是(A)
A.内存地址空间
B.指令计数器PC
C.用户栈指针
D.线程状态 -
我们把在一段时间内,只允许一个进程访问的资源,称为临界资源,因此,我们可以得出下列论述,正确的论述为(D)
A.对临界资源是不能实现资源共享的。
B.只要能使程序并发执行,这些并发执行的程序便可对临界资源实现共享。
C.为临界资源配上相应的设备控制块后,便能被共享。
D.对临界资源,应采取互斥访问方式,来实现共享。 -
有两个并发执行的进程 P1 和 P2,共享初值为 1 的变量 x。P1 对 x 加 1,P2 对 x 减 1,两个操作完成后,x 的值A.可能为©
A.可能为-1 或 3
B.只能为 1
C.可能为 0、1 或 2
D.可能为-1、0、1 或 2 -
关于wait()和signal()操作,下面哪个说法是对的(A)
A.wait()申请一个资源,资源不够,则阻塞,signal()操作释放一个资源,若有进程等待在唤醒
B.wait()申请一个资源,signal()操作释放一个资源,若有进程等待在唤醒
C.wait()申请一个资源,资源不够,则阻塞,signal()操作释放一个资源
D.wait()申请一个资源,signal()操作释放一个资源 -
关于读者和写者问题,下列说法错误的是(D)A.如果有一个读者在读,其他读者也可以读,因此读者与读者之间不需要互斥。
B.如果有一个读者读,其他写者就不能写,因此,读者和写者之间需要互斥。
C.只要有一个写者写,其他写者就不能写,因此,写者和写者之间需要互斥。
D.如果有多个读者,需要设一个共享变量来计数,这个共享变量是临界区 -
关于生产者消费者问题,下列叙述错误的是(E)
A.当只有一个生产者和一个消费者一个缓冲区时,他们之间只有同步关系,不需要互斥信号量。
B.当有多个生产者多个消费者多个缓冲区时,因为可能存在对某个缓冲区的竞争访问,既需要同步也需要互斥。
C.生产者消费者问题可以通过AND信号量解决
D.进程同步关键在于什么时候停(缓冲区满,生产者要停;缓存区空,消费者停),什么时候走(发信号让消费者走;发信号让生产者走)。
E.同步就是把临界区放在wait()和signal()之间,而互斥就是把wait()和signal()分别放在生产者和消费者两个进程/线程中。 -
关于哲学家就餐问题,下列叙述错误的是(D)
A.5个叉子表示5个临界资源,因此设5个信号量,每个的初值为1
B.每个哲学家必须获得两个资源才能吃通心粉,因此可以通过AND信号量解决同步问题。
C.当每个哲学家拿到一个叉子时,谁也吃不到通心粉,这就出现死锁了。
D.用AND信号量信号量解决哲学家就餐问题,依然会出现死锁。
E.当5个哲学家线程同时运行时,可能出现死锁,也可能不出现死锁 -
关于消息通信,以下说法错误的是©
A.当发送消息时,发送原语就陷入到内核态,然后申请消息缓冲区,消息被从用户态缓冲区拷贝到这个消息缓冲区中,接着要找到接收者进程的pcb,并将消息挂到该PCB消息队列队列的末尾。
B.因为消息队列是临界资源,因此插入操作要用wait-signal的操作,确保它们互斥的进行。
C.消息队列是临界资源,应该对其进行互斥的访问,不存在同步问题。D.当发送者进程把消息放到消息队列离开的时,要进行一个signal操作,就是要唤醒接收者进程,告诉接收者进程,队列中有消息了。 -
一般管道通信用于父子进程之间 ,当我们通过fork( )创建了父子进程,那么父子进程的管道都有两个文件描述符,必须关闭其中的一个读端和一个写端,建立一条“父进程写入子进程读取”的通道,或者“子进程写入父进程读取”的通道,这种说法(A)
A.对
B.错 -
管道通信有读端和写端,这个模型是(B)
A.读者-写者
B.生产者消费者
C.哲学家就餐
D.爸爸妈妈儿子女儿吃苹果和橘子