前言
本文介绍了操作系统中的进程管理,文章中的内容来自B站王道考研操作系统课程,想要完整学习的可以到B站官方看完整版。
二:进程管理
2.1.1:进程的概念、组成、特征
程序:是静态的,就是存放在磁盘里的可执行文件,就是一系列的指令集合
进程:是动态的,是程序的一次执行过程。同一个程序多次执行会对应多个进程
进程控制块:一个进程中分配了哪些资源、运行情况等信息都会保存在一个数据结构PCB(进程控制块)中,是进程存在的唯一标志,供操作系统使用。
PCB中所包含的信息:
程序段、数据段、PCB三部分组成了进程实体
进程是进程实体的运行过程,是系统进行资源分配和调度的独立单位,一个进程被调度就是指操作系统决定让这个进程到CPU上运行
2.1.2:进程的状态(运行态、就绪态、阻塞态)与转换、进程的组织
阻塞态:在进程运行过程中,可能会请求等待某个事件的发生(如等待某种系统资源的分配,或者等待其他进程的响应)。在这件事发生之前,进程无法继续往下执行,此时操作系统会让这个进程下CPU,并让他进入“阻塞态”。
终止态:一个进程可以执行exit系统调用,请求操作系统终止该进程。此时该进程会进入“终止态”,操作系统会让该进程下CPU,并回收内存空间等资源,最后还要回收该进程的PCB。
五种状态的切换:
Linux中进程的状态分类:就绪态、运行态、僵尸态、停止态、可中断睡眠状态、不可中断睡眠状态
就绪态(Ready):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调度程序会从就绪态链表中调度一个进程
运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
Zombie(僵尸状态):进程已经结束,但是其父进程还没有调用wait()函数来获取其退出状态,此时进程成为僵尸进程。
Stopped(停止状态):进程被暂停,例如通过Ctrl+Z命令将进程挂起
Interruptible sleep(可中断睡眠状态):进程正在等待某个事件的发生,例如等待输入输出完成或者等待信号量,此时进程可以被中断。
Uninterruptible sleep(不可中断睡眠状态):进程正在等待某个事件的发生,例如等待磁盘I/O操作完成,此时进程不可被中断。
2.1.3:进程通信(共享存储、消息传递、管道通信)
共享存储
共享存储(在内存中开辟一段区域供进程间交换数据,实际是会生成一个文件,在/dev/shm目录下)shm_open、mmap两个函数来实现相关功能,下面通过两个读写文件来展示相关的用法。
write.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#define MMAP_DATA_SIZE 1024
int main(int argc,char* argv[])
{
char* data;
int fd = shm_open("shm1", O_CREAT|O_RDWR, 0777);
if (fd < 0) {
printf("shm_open failed!\n");
return -1;
}
ftruncate(fd, MMAP_DATA_SIZE);
data = (char*)mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (!data) {
printf("mmap failed\n");
close(fd);
}
sprintf(data, "This is a share memory! %d\n", fd);
munmap(data, MMAP_DATA_SIZE);
close(fd);
return 0;
}
read.c:
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <linux/input.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#define MAP_FILE_SIZE 1024
int main(int argc, char* argv[])
{
char *data;
int fd = shm_open("shm1", O_RDONLY, 0777);
if(fd < 0){
perror("shm01 open error");
}
data = mmap(NULL, MAP_FILE_SIZE, PROT_READ, MAP_SHARED, fd, 0);
if(data == MAP_FAILED){
perror("data error");
}
printf("open read.c\r\n");
printf("%s\r\n", data);
munmap(data, MAP_FILE_SIZE);
close(fd);
}
两程序的执行结果如下
open和shm_open的主要区别在于它们打开的对象不同,open打开的是普通文件,而shm_open打开的是共享内存对象。
消息传递
消息 = 消息头(发送进程ID、接收进程ID、消息长度)、消息体
消息队列
P通过发送原语send()发送,Q通过接收原语receive()将PCB消息队列的重大的msg拷贝到自己的的地址空间
管道通信
无名管道:匿名管道是一种无名的管道,只能用于父子进程之间或者兄弟进程之间的通信
代码示例
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
//自己写数据到管道,然后从管道中读数据,最后将数据写到标准输出上,终端打印hello
int fd[2];
int pp = pipe(fd);
char buf[100];
for(int i = 0; i < 10; i++){
write(fd[1], "hello", 5);
sleep(1);
int r = read(fd[0], buf, 128);
write(1, buf, r);
printf("\r\n");
}
close(fd[0]);
close(fd[1]);
//父进程写东西到管道,子进程从管道中读数据,然后打印到终端上
int fd[2];
int pp = pipe(fd);
char buf[100];
if(fork() == 0){
close(fd[1]);
for(int i = 0; i < 3; i++){
int r = read(fd[0], buf, 128);
//printf("lseek = %ld\r\n", lseek(fd[0], 0, SEEK_CUR));
write(1, buf, r);
printf("\r\n");
}
close(fd[0]);
_exit(0);
}
else{
close(fd[0]);
for(int i = 0; i < 3; i++){
write(fd[1], "hello", 5);
sleep(1);
}
wait(NULL);
close(fd[1]);
exit(0);
}
}
有名管道:命名管道是一种有名的管道,可以用于任意进程之间的通信
创建有名管道:
int ret = mkfifo("/tmp/lcdpipe", 0777);
if(ret < 0){
perror("!!mkfifo!!");
}
有名管道读端等待:
if(fork() == 0){
//通过管道来接收要执行程序命令,然后执行
char rec_buf[100];
int fd = open("/tmp/lcdpipe", O_RDWR);
read(fd, rec_buf, sizeof(rec_buf));
printf("receive message\r\n");
system(rec_buf);
}
有名管道写端:
//发送命令回到主界面
else if((ts_y > 0 && ts_y < 100) && (ts_x > 0 && ts_y <100)){
char temp2[30];
char command2[30];
int pid = getpid();
sprintf(temp2, "%d", pid);
sprintf(command2, "kill -9 %s", temp2);
int fd = open("/tmp/lcdpipe", O_RDWR);
char send_buf[100] = "cd /mnt/udisk && ./mainWindowApp";
write(fd, send_buf, sizeof(send_buf));
printf("send message\r\n");
system(command2);
}
Linux中的共享内存和管道通信都是进程间通信的方式,但它们之间有以下区别:
1. 共享内存是一种进程间通信的方式,它允许多个进程访问同一块内存区域,而管道通信是一种单向的进程间通信方式,只能在一个方向上传递数据。
2. 共享内存是通过将一段内存区域映射到多个进程的地址空间中来实现的,而管道通信是通过创建一个管道文件来实现的。
3. 共享内存的数据可以直接在内存中进行读写,速度较快,而管道通信需要将数据从一个进程的缓冲区复制到另一个进程的缓冲区中,速度较慢。
2.1.4:线程的概念
线程是一个基本的CPU执行单位,也是程序执行流的最小单位。进程只作为除CPU之外的系统资源的分配单位。
引入线程后的变化
1:系统资源的分配和调度
传统进程机制中,进程是资源分配和调度的的基本单位。引入线程之后进程是资源分配的基本单位,线程是调度的基本单位。
2:并发性
传统进程机制中只能进程间并发,引入线程后,各线程之间也能并发,提升了并发度。
3:系统开销
传统进程间并发,需要切换进程的运行环境,系统开销大。线程间并发,如果是同一进程内的线程切换,则不需要切换进程环境,系统开销小。
线程属性
2.1.5:线程的状态与转换(TCB)
线程控制块:
线程表:
2.2.1:调度的概念和层次(作业、调度的层次、挂起)
调度的核心理念就是当有一堆任务要处理,但由于资源有限,这些事情没法同时处理。这就需要确定某种规则来决定处理这些任务的顺序。
高级调度(作业调度)
按一定的原则从外存的作业后备队列中挑选一个作业调入内存,并创建进程。每个作业只调入一次,调出一次。作业调入时会建立PCB,调出时才撤销PCB。
中级调度(内存调度)
内存不够时,可将某些进程的数据调出外存。等内存空间或者进程需要运行时再重新调入内存。暂时调到外存等待的进程状态为挂起状态。被挂起的进程PCB会被组织成挂起队列
中级调度:按照某种策略决定将哪个处于挂起状态的进程重新的调入内存
低级调度(进程调度)
按照某种策略从就绪队列中选取一个进程,将处理机分配给他
挂起和阻塞的区别:两种状态都是暂时不能获得CPU服务,但挂起状态是将进程映像调到外存去了,而阻塞态下进程映像还在内存中。阻塞态是进程主动等待某个事件的发生,而挂起态是进程被动被中止执行。
三层调度的联系和对比
2.2.2:进程调度的时机和方式(内核临界区、普通临界区、调度时间、抢占式、进程切换)
需要进行进程调度和切换的情况(主动放弃和被迫放弃)
不能进行进程切换的情况(处理中断、临界区、原子操作)
内核临界区访问临界资源之前会上锁,如果不尽快释放的话,极有可能影响到操作系统内核的其他管理工作。因此在访问内核程序临界区期间不能进行调度和切换。普通临界区访问的临界资源不会直接影响操作系统内核的管理工作。因此在访问普通临界区的时候可以进行调度和切换。
非抢占式:只允许进程主动放弃处理机,在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止和主动要求进入阻塞态。
抢占式:当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用处理机,则立即暂停正在执行的过程,将处理机分配给更重要紧迫的那个进程。
进程切换:是指一个进程让出处理机,由另一个进程占用处理机的的过程。进程切换主要完成了对原来运行进程的各种数据保存,对新的进程各种数据的恢复。(如:程序计数器、程序状态字等,这些信息一般保存在进程控制块PCB中)
2.2.3:调度器和闲逛进程
当没有其他就绪进程时,调度程序就会运行闲逛进程(idle)
2.2.4:调度算法的评价指标(CPU利用率、系统吞吐量、周转时间、等待响应时间)
CPU利用率:CPU“忙碌”的时间占总时间的比例
系统吞吐量:单位时间内完成的作业数量
周转时间:是指从作业被提交给系统开始,到作业完成为止的这段时间间隔
周转时间包括四部分:作业在外存后备队列上等待作业调度(高级调度)的时间+进程在就绪队列上等待进程调度(低级调度)的时间+进程在CPU上执行的时间+进程等待I/O操作完成的时间
2.2.5:调度算法(FCFS、SJF、HRRN、时间片轮转、优先级调度)
1、先来先服务:
饥饿现象:某进程/作业长期得不到某种资源或服务
FCFS的优缺点:对长作业有利,对短作业不利(食堂打饭时前面有个人给全宿舍的人带饭)
2、短作业优先算法:
追求最少的平均等待时间,最少的平均周转时间
3、高响应比优先算法:
要综合考虑作业/进程的等待时间和要求服务的时间
响应比 = (等待时间 + 要求服务的时间)/ 要求服务的时间
4、时间片轮转:
公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应。如果时间片太大,使得每个进程都可以在一个时间片内就完成,则时间片轮转调度算法退化为先来先服务调度算法,并且会增大进程响应时间。另一方面如果时间片太小,进程调度和切换有时间代价的(保存、恢复运行环境),如果时间片太小,会导致进程切换过于频繁,系统会花大量的时间来处理进程切换,从而导致实际用于进程执行的时间比例减小。
5、优先级调度算法:
可能会导致低优先级进程饥饿
2.3.1:进程同步和互斥
进程互斥软件实现(单标志法、双标志先检查法)
单标志法:
缺点:自己访问完临界资源之后,一定要等另一个人用一次临界资源自己才可以继续使用。如果人家不用自己也不能再用。
双标志先检查法:(类似于上锁和解锁)
缺点:当两个进程并发运行时,若flag[0]未置为true,P1就执行到了flag[1] = true,那么两个进程都会访问临界区资源,违反了“忙则等待”的原则。
双标志后检查法:
缺点:会导致各个进程都无法访问临界资源而产生“饥饿”现象,谁都用不了
皮特森算法(Peterson)
核心思想:主动争取、主动谦让、检查对方是否也想用,且最后一次是不是自己说了“客气话”,谁最后说了“客气话”就会失去优先权。
2.3.3:进程互斥硬件实现(中断屏蔽、TSL、SWAP)
1:中断屏蔽方法
优点:简单、高效
缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果让用户随意使用就会很危险)
2:TestAndSet指令(硬件实现)
TSL指令把“上锁”、“检查”操作用硬件的方式变成了一气呵成的原子操作。
缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL命令,从而导致“忙等”。
3:SWAP命令(硬件实现)
缺点:不满足“让权等待”原则,暂时无法进入到临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。
2.3.4:互斥锁
特性:
1:需要忙等,进程时间片用完后才下处理机,违反“让权等待”
2:等待期间不用切换进程,多处理器系统中,若上锁的时间短,则等待代价低
3:常用于多处理器系统,一个核等,其他核照常工作,并快速释放临界区
4:不太适用于单处理机系统,忙等的过程中不可能解锁(在那个时间片内)。
2.3.5:信号量机制(整型信号量、记录型信号量)
进程实现互斥(软件、硬件)方式存在的问题
1:双标志先检查法中,进入“检查”、“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题。
2:所有的解决方案都无法实现“让权等待”。
整型信号量:(原子操作但是不满足“让权等待”原则)
用一个整数型的变量作为信号量,用来表示系统中某种资源的数量,与普通整数型变量的区别,对信号量的操作只有三种(初始化、P操作、V操作)
记录型信号量:
2.3.6:生产者-消费者问题(单生产者-单消费者)
实现互斥的P操作一定要在实现同步的P操作之后,V操作不会导致进程阻塞,因此两个V操作的顺序可以交换
P操作不要轻易换位置,当empty = 0或者full = 0时都会造成两个进程相互等待,导致死锁。
2.3.7:多生产者、多消费者问题(多种类型)
2.3.8:吸烟者问题(单生产者-多消费者)
2.3.9:读者-写者问题
要求
1:同一时间允许多个读者可以同时对文件执行读操作
2:同一时间只允许一个写者往文件中写信息
3:任意写者在完成写操作之前不允许其他读者或写者工作
4:写者执行写操作前应让已有的读者和写者全部退出
读进程优先,读进程之间不互斥,核心就在count这个变量
在最后一个读进程完成之前,写进程都不能上处理机执行,可能发生“饿死”现象的版本
写进程解决饥饿的版本,增加了一个互斥信号量
2.3.10:哲学家进餐问题(进程死锁)
2.4.1:死锁基本概念(死锁四个基本条件)
死锁:在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象。
饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象
死循环:某进程执行过程中一直跳不出某个循环的现象
死锁产生的四个必要条件
1:互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁(哲学家的筷子),内存、扬声器这样可以同时对多个进程使用的资源是不会死锁的。
2:不剥夺条件:进程在获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
3:请求和保持条件:进程现在已经持有了至少一个资源,但又提出了新资源的请求,而新资源又被其他进程占有;但又对自己已持有的资源不放手
4:循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
2.4.2:预防死锁(破坏死锁的四条件其中之一)、避免死锁、检测和解除、银行家算法
安全序列:指如果系统按照这种序列分配资源,则每个进程都能顺利完成,只要能找出一个安全序列,系统就是安全状态。如果系统进入不安全状态,就可能发生死锁。
银行家算法: