操作系统总结(二)——进程

2.进程和线程、协程

2.1 进程、线程和协程的区别与联系

  进程 线程 协程
定义 资源分配和拥有的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序 程序执行的基本单位,是轻量级的进程。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束 用户态的轻量级线程,线程内部调度的基本单位
切换情况 进程CPU环境(栈、寄存器、页表和文件句柄等)的保存以及新调度的进程CPU环境的设置 保存和设置程序计数器、少量寄存器和栈的内容 先将寄存器上下文和栈保存,等切换回来的时候再进行恢复
切换者 操作系统 操作系统 用户
切换过程 用户态->内核态->用户态 用户态->内核态->用户态 用户态
调用栈 内核栈 内核栈 用户栈
拥有资源 CPU资源、内存资源、文件资源和句柄等 程序计数器、寄存器、栈 拥有自己的寄存器上下文和栈
并发性 不同进程之间切换实现并发,各自占有CPU实现并行 一个进程内部的多个线程并发执行 同一时间只能执行一个协程,而其他协程处于休眠状态,适合对任务进行分时处理
系统开销 切换虚拟地址空间,切换内核栈和硬件上下文,CPU高速缓存失效、页表切换,开销很大 切换时只需保存和设置少量寄存器内容,因此开销很小 直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快
通信方式 进程间通信需要借助操作系统 线程间可以直接读写进程数据段(如全局变量)来进行通信 共享内存、消息队列

进程与线程的区别和联系:

1)线程是程序调度的基本单位,进程是资源拥有的基本单位。

2)当只有一个线程时,可以认为线程就等于进程;当进程拥有多个线程时,线程共享本进程中相同的虚拟内存和全局变量,并且在切换时不需要修改,但是多个进程之间的资源是互相独立的。

3)一个进程崩溃后,在保护模式下不会对其它进程产生影响。而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮。

4)线程不能脱离进程单独存在,只能依赖于进程运行。

5)相比于进程,线程能减少并发执行的开销。因为进程创建、切换、终止消耗的资源比线程多,同一进程内的线程具有相同的地址空间,且共享资源,所以切换和传递数据效率都比较高。

 

2.2 进程

1.进程的概念、状态及切换

进程资源分配和拥有的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序 。

进程具有的特征:

  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;

  • 并发性:任何进程都可以同其他进程一起并发执行;

  • 独立性:进程是系统进行资源分配和调度的一个独立单位;

  • 结构性:进程由程序、数据和进程控制块三部分组成。

一个进程具有以下几种基本状态:

  • 新的:进程正在创建。

  • 运行:指令正在执行。

  • 等待:进程等待发生某个事件(如 I/O 完成或收到信号)。

  • 就绪:进程等待分配处理器。

  • 终止:进程已经完成执行

如图所示:

只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。

2.进程的控制

(1)创建进程

操作系统允许一个进程创建另一个进程,进程在执行过程中可能创建多个新的进程。创建进程称为父进程,而新的进程称为子进程,每个新进程可以再创建其他进程。

一般来说,当一个进程创建子进程时,该子进程需要一定的资源(CPU 时间、内存、文件、I/O 设备等)来完成任务。子进程可以从操作系统那里直接获得资源,也可以只从父进程那里获得资源子集。父进程可能要在子进程之间分配资源或共享资源(如内存或文件)。限制子进程只能使用父进程的资源,可以防止创建过多进程,导致系统超载

当进程创建新进程时,可有两种执行可能:

1)父进程与子进程并发执行。

2)父进程等待,直到某个或全部子进程执行完。

新进程的地址空间也有两种可能:

1)子进程是父进程的复制品(它具有与父进程同样的程序和数据)。

2)子进程加载另一个新程序。

(2)终止进程

进程的终止主要有3种方式:正常结束、异常结束以及外界信号(kill)干扰。

当进程完成执行最后语句并且通过系统调用 exit() 请求操作系统删除自身时,进程终止。这时,进程可以返回状态值(通常为整数)到父进程(通过系统调用 wait())。

父进程终止子进程的原因有很多,如:

1)子进程使用了超过它所分配的资源。(为判定是否发生这种情况,父进程应有一个机制,以检查子进程的状态)。

2)分配给子进程的任务,不再需要。

3)父进程正在退出,而且操作系统不允许无父进程的子进程继续执行。

有些系统不允许子进程在父进程已终止的情况下存在。对于这类系统,如果一个进程终止(正常或不正常),那么它的所有子进程也应终止。这种现象,称为级联终止,通常由操作系统来启动

(3)阻塞和唤醒进程

当进程需要等待某一事件完成时,会调用阻塞语句让自己阻塞等待,此时需要由另一个进程唤醒。

(4)常用C库函数

pid_t fork();
//函数功能:创建进程
//返回值:错误返回-1;父进程中返回pid > 0;子进程中pid = 0
​
pid_t getpid();
//函数功能:获取本程序运行时进程的编号
​
pid_t getppid();
//函数功能:获取父进程编号
​
void exit(int status);
//函数功能:结束进程
//status是退出状态,为0表示正常退出

(5)进程图绘制

1)使用fork创建一个新进程和对应的进程图

/*使用fork创建一个新进程*/
int main()
{
    pid_t pid;
    int x = 1;
    
    pid = fork();
    
    if(pid == 0)
    {
        /*子进程*/
        printf("child : x=%d\n", ++x);
        exit(0);
    }
    
    /*父进程*/
    printf("parent : x=%d\n", --x);
    exit(0);
}

2)嵌套fork及其进程图

/*嵌套fork*/
int main()
{
    fork();
    fork();
    printf("hello\n");
    exit(0);
}

 

3.守护进程、僵尸进程和孤儿进程

(1)守护进程

指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等。

创建守护进程:

1)让程序在后台执行。方法是调用 fork() 产生一个子进程,然后使父进程退出。

2)调用 setsid() 创建一个新对话期。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,方法是调用 setsid() 使进程成为一个会话组长。setsid() 调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。

3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过 fork() 创建新的子进程,使调用fork的进程退出。

4)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。

5)将当前目录更改为根目录。

6)子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。

7)处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。

(2)僵尸进程

一个子进程在调用return或exit(0)结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个僵尸进程,在Linux中有<defunct>标志,如图所示:

如果按Ctrl+C终止父进程后,父进程退出,僵尸进程随之消失。如果要销毁僵尸进程,应该向创建子进程的父进程传递子进程的exit参数值或者return语句的返回值

僵尸进程的危害:

僵尸进程是子进程结束时,父进程又没有回收子进程占用的资源,僵尸进程在消失之前会继续占用系统资源。

如果父进程先退出,子进程被系统接管,子进程退出后系统会回收其占用的相关资源,不会成为僵尸进程。 由于子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束,子进程退出时会保留一定的资源,只有父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免

僵尸进程的解决办法:

1)子进程退出之前,会向父进程发送一个信号,父进程调用wait / waitpid函数等待这个信号,等到并把它彻底销毁后返回,这样就不会产生僵尸进程

wait函数声明如下:

pid_t wait(int* statloc);
//参数status用来保存被收集进程退出时的一些状态,如果只想销毁僵尸进程,可以直接设为NULL
//返回值:成功时返回终止的子进程ID,失败时返回-1

如果statloc不是NULL,wait就会把子进程退出时的状态取出并存入其中,该参数指针所指向的单元中还包含其他信息,因此需要通过下列宏进行分离:

//WIFEXITED 子进程正常终止时返回“true”
//WEXITSTATUS 返回子进程的返回值
​
//也就是说,调用wait函数后应该编写如下代码:
if(WIFEXITED(status))    //是正常终止的吗?
{
    puts("Normal termination!");
    printf("Child pass num:%d", WEXITSTATUS(status))    //返回值
}

调用wait函数,如果没有已终止的子进程,那么程序将阻塞直到有子进程终止。 而waitpid函数可以防止阻塞,函数声明如下:

pid_t wait(pid_t pid, int* statloc, int options);
//pid:目标子进程的id,若传递-1,则可以等待任意子进程终止,
//options:传递常量WNOHANG,即使没有终止的子进程也不会阻塞程序,而是返回0并退出函数
//返回值:成功时返回终止的子进程ID,失败时返回-1

这个方法说得容易,在并发的服务程序中这是不可能的,因为父进程要做其它的事,例如等待客户端的新连接,不可能去等待子进程的退出信号。

2)另一种方法就是告诉父进程子进程终止了,具体做法很简单,在主程序中启用以下代码:

signal(SIGCHLD,SIG_IGN); // 忽略子进程退出的信号,避免产生僵尸进程

如果父进程很忙,可以用signal注册信号处理函数,这样子进程结束后, 父进程会收到该信号,这样父进程可以暂时放下工作,处理子进程结束相关事宜,如调用wait回收

如果父进程不关心子进程什么时候结束,那么可以用signal通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号。

(3)孤儿进程

如果一个父进程终止了而子进程没有终止,则此时子进程变为孤儿进程。内核会安排init进程称为孤儿进程的养父,并由init进程完成对它们的回收工作。init进程的PID为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。

4.进程间通信

进程的数据空间是独立的,私有的,不能相互访问,但是在某些情况下进程之间需要通信来实现某功能或交换数据,包括:

1)数据传输:一个进程需要将它的数据发送给另一个进程。

2)共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如通知进程退出)。

4)进程控制:一个进程希望控制另一个进程的运行。

进程通信的方式大概分为六种。

  • 管道(pipe) 通过操作系统提供创建管道,让进程通信。

  • 消息队列(message) 进程可以向队列中添加消息,其它的进程则可以读取队列中的消息。

  • 信号(signal) 信号是由用户、系统或进程发送给目标进程的信息,以通知目标有某种事件发生。

  • 共享内存(shared memory) 多个进程可以访问同一块内存空间。

  • 信号量(semaphore) 也叫信号灯,用于进程之间对共享资源进行加锁。

  • 套接字(socket):可用于不同计算机之间的进程间通信。

(1)管道

所谓管道,就是内核里面的一段缓存,从管道的一端写入的数据,实际上是缓存在内核中,另一端读取,也是从内核中读取。

②无名管道:

一种半双工的通信方式,只能在具有亲缘关系的进程间使用(父子进程)

假设使用fork创建一个管道,会复制父进程的文件描述符,两个进程都有fds[0]和fds[1](读端和写端)。子进程通过 fds[1] 把数据写入管道,父进程从 fds[0] 再把数据读出来,如图所示:

相关函数声明如下:

#include<unistd.h>
​
int pipe(int fd[2]);
/*
fd[0]:通过管道接收数据时使用的文件描述符,即管道出口
fd[1]:通过管道传输数据时使用的文件描述符,即管道入口
返回值:成功时返回0,失败时返回-1
*/

优点:

  • 简单方便

缺点:

  • 局限于单向通信

  • 只能创建在它的进程以及其有亲缘关系的进程之间

  • 缓冲区有限

②有名管道

一种半双工的通信方式,它允许不相关进程间的通信。有名管道是FIFO文件,存在于文件系统中,可以通过文件路径名来指出。

相关函数声明如下:

#include<sys/types.h>
#include<sys/stat.h>
​
int mkfifo(const char *pathname, mode_t mode);
/*
pathname:即将创建的FIFO文件路径,如果文件存在需要先删除。
mode:用来规定FIFO的读写权限
返回值:成功时返回0,失败时返回-1
*/

优点:

  • 可以实现任意关系的进程间的通信

缺点:

  • 长期存于系统中,使用不当容易出错,

  • 缓冲区有限

 

(2)信号

信号一种比较复杂的通信方式,是由用户、系统或进程发送给目标进程的信息,以通知目标有某种事件发生。

Linux信号可由如下条件产生:

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号,例如Ctrl+C是中断信号

  • 系统异常,比如浮点异常和非法内存访问

  • 系统状态变化,比如alarm定时器到期将引起SIGALRM信号

  • 运行kill命令或调用kill函数

服务器程序必须处理(或至少忽略)一些常见的信号,以免异常终止。Linux的可用信号都定义在bits/signum.h头文件中。如果程序在执行处于阻塞状态的系统调用时接收到信号,并且设置了信号处理函数,则默认情况下系统调用将被中断。对于默认行为是暂停进程的信号,如果没有为它们设置信号处理函数,则它们可以中断某些系统调用。

相关函数:

#include<signal.h>
​
_sighandler_t  signal(int sig,  _sighandler_t  _handler);
/*
sig:指出要捕获的信号类型
_handler:是_sighandler_t 类型的函数指针,用于指定信号sig的处理函数
*/
    
int sigaction( int sig,  const struct sigaction* act, struct sigaction* oact );
/*
sig:指出要捕获的信号类型
act:指定新的信号处理方式
oact:输出信号先前的处理方式
*/

Linux相关信号

1)SIGHUP

当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重新读取配置文件。

2)SIGPIPE

默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号默认行为是结束进程。

3)SIGURG

在Linux环境下,内核通知应用程序带外数据(重要数据,不与普通数据使用相同的通道)到达主要有两种方法,一种是I/O复用技术,select等系统调用在接收到带外数据时将返回并向应用程序报告。另一种方法就是使用SIGURG信号。

 

(3)信号量

信号量本质上是一个计数器,用于协调多个进程对共享数据对象的读/写。它不以传送数据为目的,主要是用来保护共享资源(共享内存、消息队列、socket连接池、数据库连接池等),保证共享资源在一个时刻只有一个进程独享。

信号量是一个特殊的变量,只允许进程对它进行等待信号和发送信号操作。最简单的信号量是取值0和1的二元信号量,这是信号量最常见的形式。

相关函数:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
​
//创建信号量
int semget(key_t key, int nsems, int semflg);
/*
key:是信号量在系统中的编号
nsems:创建信号量的个数。
semflg:调用函数的操作类型,也可用于设置信号量集的访问权限
返回值:创建成功返回信号量标识符,失败返回-1。
*/
​
//控制信号量
int semctl(int semid, int semnum, int cmd, union semun arg);
/*
semid:semget函数返回的信号量集标识符。
semnum:信号量集数组上的下标,表示某一个信号量,一般填0。
cmd:对信号量操作的命令种类,
     IPC_RMID:销毁信号量,不需要第四个参数;
     SETVAL:初始化信号量的值,(信号量成功创建后,需要设置初始值),这个值由第四个参数决定。
arg:union semun类型。如下:
    union semun
    {
        int val;
        struct semid_ds *buf;
        unsigned short *arry;
    };
*/
​
//改变信号量的值
int semop(int semid, struct sembuf *sops, unsigned nsops);
/*
semid:semget函数返回的信号量集标识符
sops:指向struct sembuf结构的指针,如下:
    struct sembuf
    {
        short sem_num;     / / 信号量集的个数,单个信号量设置为0。
        short sem_op;      // 信号量在本次操作中需要改变的数据:-1-等待操作;1-发送操作。
        short sem_flg;     // 把此标志设置为SEM_UNDO,操作系统将跟踪这个信号量。
    };
nsops:操作信号量的个数,即sops结构变量的个数,设置它的为1表示只对一个信号量的操作
*/

优点:

  • 可以同步进程

缺点:

  • 信号量有限

 

(4)消息队列

消息队列是有消息的链表,存放在内核中并由消息队列标识符标识。

相关函数

#include<sys/msg.h>
​
//创建消息队列
int msgget(key_t key, int msgflg);
/*
key:是消息队列在系统中的编号
msgflg:调用函数的操作类型,也可用于设置消息队列集的访问权限
返回值:成功时返回一个正整数值,它是消息队列的标识符,失败时返回-1
*/
​
//将一个新的消息写入队列
int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg);
//从消息队列中读取消息
int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
/*
msqid:由msgget调用返回的消息队列标识符
msg_ptr:指向一个准备发送的消息,消息必须被定义为如下类型:
    struct msgbuf
    {
        long mtype;         // 消息类型 
        char mtext[512];    // 消息数据 
    };
msg_sz:消息的大小
msgtype:消息类型
返回值:成功执行时,msgsnd()返回0,msgrcv()返回拷贝到mtext数组的实际字节数。失败两者都返回-1
*/
​
​
//控制消息队列中的某些属性
int msgctl(int msqid, int cmd, struct msqid_ds* buf);
/*
cmd:指定要执行的命令
msqid_ds:队列结构体
返回值:成功返回0,失败返回-1
*/

优点:

  • 消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  • 可以实现任意进程间的通信,并通过系统调用函数来实现消息发送和接收之间的同步,无需考虑同步问题,方便

缺点:

  • 信息的复制需要额外消耗 CPU 的时间,不适宜于信息量大或操作频繁的场合

 

(5)共享内存

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是在多个进程之间共享和传递数据最高效的方式,它是针对其他进程间通信方式运行效率低而专门设计的。如果某个进程修改了共享内存中的数据,其它的进程读到的数据也将会改变。 共享内存并未提供锁机制,也就是说,在某一个进程对共享内存的进行读写的时候,不会阻止其它的进程对它的读写。如果要对共享内存的读/写加锁,可以使用信号量

相关函数:

#include <sys/types.h>
#include <sys/shm.h>
​
//创建共享内存
int shmget(key_t key, size_t size, int shmflg);
/*
key:是共享内存段在系统中的编号
size:创建共享内存大小。
shmflg:调用函数的操作类型,也可用于设置共享内存的访问权限
返回值:创建成功返回信号量标识符,失败返回-1。
*/
​
//控制信号量
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
shmid:shmget函数返回的共享内存标识符。
cmd:对共享内存操作的命令种类,
    IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
    IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
    IPC_RMID:删除这片共享内存
buf:共享内存管理结构体
*/
​
//把共享内存区对象映射到调用进程的地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
shmid:共享内存标识符
shmaddr:指定共享内存出现在进程内存地址的什么位置,通常直接指定为NULL让内核自己决定一个合适的地址位置
shmflg:调用函数的操作类型,也可用于设置共享内存的访问权限
返回值:调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
*/
    
//断开共享内存连接
int shmdt(const void *shmaddr);
/*
shmid:shmat连接的共享内存的起始地址
返回值:成功返回0,失败返回-1。
*/

优点:

  • 无须复制,快捷,信息量大,不存在读取文件、消息传递等过程,只需要到相应映射到的内存地址直接读写数据即可。

缺点:

  • 通信是通过将共享空间缓冲区直接附加到进程的虚拟地址空间中来实现的,因此进程间的读写操作存在同步问题。

  • 利用内存缓冲区直接交换信息,内存的实体存在于计算机中,只能同一个计算机系统中的诸多进程共享,不方便网络通信。

补充:ipcs命令用于报告共享内存、信号量和消息队列信息。

  • ipcs -a:列出共享内存、信号量和消息队列信息。

  • ipcs -l:列出系统限额。

  • ipcs -u:列出当前使用情况。

 

(6)套接字

适用于不同机器间进程通信,在本地也可作为两个进程通信的方式。

优点:

  • 传输数据为字节级,传输数据可自定义,数据量小效率高

  • 传输数据时间短,性能高

  • 适合于客户端和服务器端之间信息实时交互

  • 可以加密,数据安全性强

缺点:

  • 需对传输的数据进行解析,转化成应用级的数据。

 

5.进程调度算法

调度的概念:

当 CPU 有一堆任务要处理时,由于其资源优先,这些事情就没有办法同时处理。这就需要确定某种规则来决定处理这些任务的顺序,这就是 “调度” 研究的问题。

所谓的进程调度,就是从进程的就绪队列(阻塞)中按照一定的算法选择一个进程并将 CPU 分配给它运行,以实现进程的并发执行,这是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。进程调度的频率很高,一般几十毫秒一次。

进程调度算法分如下:

(1)非抢占式的调度算法

1)先来先服务 first-come first-serverd(FCFS)

所谓非抢占式的意思就是,当进程正在运行时,它就会一直运行,直到该进程完成或发生某个事件而被阻塞时,才会把 CPU 让给其它进程。

1)先来先服务 first-come first-serverd(FCFS)

该算法按照进程到达的先后顺序进行调度,先到的进程就先被调度,或者说,当前等待时间越久的越先得到服务。

该算法公平、实现简单,但是对短进程不利,排在长进程后面的短进程需要等待很长时间,短进程的响应时间太长了,用户交互体验会变差。

2) 最短作业优先 shortest job first(SJF)

该算法每次调度时选择当前已到达的、且运行时间最短的进程。

该算法和FCFS相反,对长进程不利长,长进程处于一直等待短进程执行完毕的状态。因为如果一直有短进程到来,那么长进程永远得不到调度。

(2)抢占式的调度算法

1)最短剩余时间优先 shortest remaining time next(SRTN)

该算法是最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。

该算法比比 SJF 稍微公平一点,如果当前进程即将完成,那么即使不断进来短进程,还是有很大概率优先级比短进程高,不会长时间得不到调度

2)轮转调度 Round-Robin(RR)

该算法也称时间片调度算法,调度程序每次把 CPU 分给就绪队列的第一个进程,并规定固定的使用时间,成为时间片,通常为 10ms ~ 200ms。就绪队列中的每个进程轮流地运行一个时间片,当时间片耗尽时就强迫当前运行进程让出 CPU 资源,转而排到就绪队列尾部,等待下一轮调度。

需要注意的是,时间片的长度是一个很关键的因素:

  • 如果时间片设置得太短,就会导致频繁的进程上下文切换,降低了 CPU 效率

  • 如果时间片设置得太长,那么随着就绪队列中进程数目的增加,轮转一次消耗的总时间加长,即每个进程的响应速度变慢。当时间片大到足以让进程完成其所有任务,RR 便退化为 FCFS

(3)最高优先级调度算法 Highest Priority First(HPF)

该算法就是从就绪队列中选择最高优先级的进程执行。进程优先级分为静态优先级和动态优先级。

  • 静态优先级:创建进程时,就预先规定优先级,并且整个运行过程中该进程的优先级都不会发生变化。一般来说,内核进程的优先级都是高于用户进程的。

  • 动态优先级:根据进程的动态变化调整优先级,比如随着进程的运行时间增加,适当地降低其优先级。随着就绪队列中进程的等待时间增加,其优先级也会适当地被提高。

另外需要注意的是,HPF 并非是固定的抢占式或非抢占式策略,系统可预先规定使用哪种策略:

  • 非抢占式:当就绪队列中出现优先级高的进程,则运行完当前进程后,再选择该优先级高的进程。

  • 抢占式:当就绪队列中出现优先级高的进程,则立即强制剥夺当前运行进程的 CPU 资源,分配给优先级更高的进程运行。

(4)多级反馈队列算法 Multilevel Feedback Queue (MFQ)

该算法是RR和HPF算法的结合。“多级”表示有多个队列,每个队列的优先级从高到低,同时优先级越高的时间片越短。“反馈”表示如果有新的进程加入优先级高的队列,立刻停止当前正在运行的进程,转去运行优先级高的队列。如图所示:

在上图中,新的进程会按照先来先服务的原则在第一级队列末尾等候,如果第一级队列的时间片内该进程还没有执行或没有处理完,则移至第二级队列的末尾,依次类推。对于短进程,往往在前几级队列就处理完了,对于长进程,可以在多个队列中完成,所以该算法对于短进程和长进程都有较好的执行效率

 

 

参考:

  1. 《深入理解计算机系统》

  2. 《图解系统》- 小林coding
  3. 《TCP-IP网络编程》 韩-尹圣雨

  4. 《Linux高性能服务器编程》

  5. https://github.com/huihut/interview

  6. https://blog.csdn.net/qq_32642107/article/details/102919692
  7. https://blog.csdn.net/qq_45593575/article/details/120779693
posted @ 2021-11-13 16:02  烟消00云散  阅读(250)  评论(0编辑  收藏  举报