操作系统之进程管理篇
参考链接:
https://github.com/CyC2018/CS-Notes
https://blog.csdn.net/kuangsonghan/article/details/80674777
https://blog.csdn.net/chenhfm/article/details/26587367
https://www.cnblogs.com/alantu2018/p/8526916.html
目录
进程和线程
- 区别
- 根本:进程是资源分配基本单位,线程是任务调度执行基本单位
- 开销:每个进程有独立的代码和数据空间(程序上下文),在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,开销大。同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),切换只需保存和设置少量寄存器内容,开销小。
- 运行:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)。从一个进程的线程切换到另一个进程的线程时,会引起进程切换。
- 内存分配:系统给每个进程分配不同的内存,但不会给线程分配,线程之间共享进程的资源
- 举例:不同进程:qq和浏览器;不同线程:浏览器中的HTTP请求线程、事件响应线程、渲染线程
- 通信:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC(InterProcess Communication)
- 多线程共享和独享的资源
共享:
a. 堆 由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的)
b. 全局变量 它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的
c. 静态变量 虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的
d. 文件等公用资源 这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。
独享:
a. 栈 栈是独享的
b. 寄存器 这个可能会误解,因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC - 进程控制块(Process Control Block, PCB)
描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对PCB的操作。 - 内核线程(kernel thread)
Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。
内核要多个执行流并行,为了防止可能的阻塞,支持多线程是必要的。
内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。
这与用户线程是不一样的。因为内核线程只运行在内核态
因此,它只能使用大于PAGE_OFFSET(传统的x86_32上是3G)的地址空间。
进程状态切换


- 三种状态:就绪、运行、等待
- 状态切换
就绪->运行:通过CPU调度获得时间片
运行->就绪:时间片用完
运行->等待:缺少资源(不包括CPU时间)或I/O等待
等待->就绪:有资源或等待完成
进程调度
针对不同系统有不同策略:
- 批处理系统:没有太多的用户操作,目标是保证吞吐量和周转时间(从提交到终止的时间)。
- 先来先服务,first come first served (FCFS)
按照请求顺序服务,不利于短作业 - 短作业优先, shortest job first (SJF)
不利于长作业 - 最短剩余时间优先, shortest remaining time next (SRTN)
是抢占式的算法,调度程序总是选择剩余运行时间最短的那个进程运行。当一个新的作业到达时,其整个时间同当前进程的剩余时间做比较。如果新的进程比当前运行进程需要更少的时间,当前进程就被挂起,而运行新的进程。
- 交互式系统:有大量的用户交互操作,目标是快速地进行响应
- 时间片轮转
就绪进程按FCFS排队,每次调度时,把一个时间片分配给首进程。该时间片用完后,由计时器发出时钟中断,将当前进程送到就绪队列的末尾,而后把时间片再分配给队首。
时间片大小关系:太小,进程切换过于频繁;太大,实时性无法保证。 - 优先级调度
给每个进程分配进程,按优先级高低调度。为了防止低优先级饿死,可在过程中不断调整优先级。 - 多级反馈队列(可看成时间片轮转+优先级调度)
提出的原因:在时间片轮转算法中,有些进程需要轮转很多轮才能执行完。
多级反馈队列设置了多个队列,并且每个队列分配的时间片大小不同,按照从小到大(比如1,2,4,8...)时间片的顺序设置。
在每个队列,采用FCFS模式,当在该队列的时间片分配用完后,即转到下一队列的队尾等待下一次调度。
注意:只有当上一个队列都没有进程等待时,才能进程下一个队列的时间片分配(即最上面的优先级最高)。
- 实时系统:要求在一定时间内响应
分为硬实时(严格限制完成时间)和软实时(可以有一定的延时)
具体算法以后再补
进程同步
- 临界区:对临界资源进行访问的那段代码
- 同步与互斥:同步是进程按一定顺序执行;互斥是同个时刻只能有一个进程进入临界区
- 信号量(Semaphore)
- P操作(down)。如果down之前信号量=0,进程睡眠
- V操作(up)。唤醒睡眠进程。
- P和V操作要设计成原语,常见做法是屏蔽中断
- 如果信号量只能是0或1,则变成互斥量(mutex)
typedef int semaphore;
semaphore mutex = 1;
void P1() {
down(&mutex);
// 临界区
up(&mutex);
}
void P2() {
down(&mutex);
// 临界区
up(&mutex);
}
- 管程
使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。提供了** insert()** 和** remove() 方法。管程有一个重要特性:在一个时刻只能有一个进程使用管程**。进程在无法继续执行的时候不能一直占用管程,否者其它进程永远不能使用管程。 - 经典同步问题
- 生产者和消费者问题
https://blog.csdn.net/liushall/article/details/81569609
https://www.cnblogs.com/wkfvawl/p/11529681.html
该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据
(1)单生产者单消费者:
semaphore mutex = 1; //互斥信号量
semaphore empty = n; //同步信号量。空闲缓冲区的数量
semaphore full = 0; //同步信号量。产品的数量,非空缓冲区的数量
producer(){
while(1){
生成一个产品;
P(empty); //消耗一个空闲缓冲区
P(mutex);
把产品放入缓冲区;
V(mutex);
V(full) //增加一个产品
}
}
consumer(){
while(1){
P(full); //消耗一个产品
P(mutex);
从缓冲区取出一个产品;
V(mutex);
V(empty); //增加一个空闲缓冲区
使用产品;
}
}
注意:P操作的(empty和mutex)顺序不可以颠倒,而V可以
(2)多生产者多消费者
https://www.cnblogs.com/wkfvawl/p/11531382.html
2. 读者和写者问题
https://www.cnblogs.com/wkfvawl/p/11538431.html
要求:1.可以多个读者同时阅读;2.只能由一个写者在写;3.写者写的时候不能由其他读者或写者在
互斥关系:写进程-写进程;写进程-读进程
最核心的问题:是如何处理多个读者可以同时对文件的读操作,解决方法是设置count值来记录读进程数
(1)读优先
semaphore rw = 1; //实现对文件的互斥访问
int count = 0; //当前读者数
semaphore mutex = 1;//实现对count变量的互斥访问
int i = 0;
writer(){
while(1){
P(rw); //写之前“加锁”
写文件
V(rw); //写之后“解锁”
}
}
reader (){
while(1){
P(mutex); //各读进程互斥访问count
if(count==0) //第一个读进程负责“加锁”
{
P(rw);
}
count++; //访问文件的进程数+1
V(mutex);
**读文件**
P(mutex); //各读进程互斥访问count
count--; //访问文件的进程数-1
if(count==0) //最后一个读进程负责“解锁”
{
V(rw);
}
V(mutex);
}
}
(2)写优先
上面的算法有个问题:如果读者源源不断进入,则写者就会一直饥饿!解决此问题的方法是再设置一个互斥量w=1。

分析:这里我们来分析一下读者1->写者1->读者2这种情况。第一个读者1在进行到读文件操作的时候,有一个写者1操作,由于第一个读者1执行了V(w),所以写者1不会阻塞在P(w),但由于第一个读者1执行了P(rw)但没有执行V(rw),写者1将会被阻塞在P(rw)上,这时候再有一个读者2,由于前面的写者1进程执行了P(w)但没有执行V(w),所以读者2将会被阻塞在P(w)上,这样写者1和读者2都将阻塞,只有当读者1结束时执行V(rw),此时写者1才能够继续执行直到执行V(w),读者2也将能够执行下去。即先到先服务,对读写操作公平。如果目前没有写者,那读者可以源源不断进去;若出现写者的需求,则后面的读者就被堵塞,等到当前在里面的读者都读完后,轮到写者开始写。
3. 哲学家用餐问题
https://blog.csdn.net/qq_28602957/article/details/53538329
问题描述:

注意:若五位哲学家同时饥饿而各自拿起了左边的筷子,这使五个信号量 chopstick 均为 0,当他们试图去拿起右边的筷子时,都将因无筷子而无限期地等待下去,即可能会引起死锁。
方法:
(1)至多只允许四位哲学家同时去拿左筷子,最终能保证至少有一位哲学家能进餐,并在用完后释放两只筷子供他人使用。
(2)仅当哲学家的左右手筷子都拿起时才允许进餐。
(3)规定奇数号哲学家先拿左筷子再拿右筷子,而偶数号哲学家相反。
方法1:

方法2:

或是

方法3:

进程通信Inter Process Communication(IPC)
目的:进程之间传输信息。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。
- 管道
int pipe(int fd[2]);,fd[0] 用于读,fd[1] 用于写
特点:1.半双工通信(单向交替传输);2.只能在父子进程之间 - FIFO(命名管道)
int mkfifo(const char *path, mode_t mode);int mkfifoat(int fd, const char *path, mode_t mode);
特点:1.去除父子进程之间通信的限制,常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据 - 消息队列
https://blog.csdn.net/qq_22863733/article/details/80385765
![]()
整个消息队列有两种类型的数据结构:
(1)msqid_ds消息队列数据结构:描述整个消息队列的属性,主要包括整个消息队列的权限,拥有者、两个重要的指针分别指向消息队列的第一个消息和最后一个消息。
(2)msg消息数据结构:整个消息队列的主体,一个消息队列有若干个消息,每个消息数据结构的基本成员包括消息类型、消息大小、消息内容指针和下一个消息数据结构位置。
相比于 FIFO,消息队列具有以下优点:
(1)消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
(2)避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
(3)读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。 - 信息量
一个计数器,用于为多个进程提供对共享数据对象的访问 - 共享存储
允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。需要使用信号量用来同步对共享存储的访问。
多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用使用内存的匿名段。 - 套接字
用于不同机器间的进程通信

浙公网安备 33010602011771号