【Linux进程】再谈进程—进程概念

 目录

承上启下

一、进程

1.1 基本概念

1.2 描述进程——PCB

1.3 task_struct——PCB的一种

1.4 task_ struct内容分类

1.5 组织进程

1.6 查看进程

1.7 通过系统调用获取进程标示符 

​编辑

1.8 通过系统调用创建进程-fork初识

1.8.1 运行 man fork 认识fork

1.8.2 fork的两个返回值

1.8.3 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

 1.8.4 fork 之后通常要用 if 进行分流

1.8.5 总结一下关于进程和fork函数的几个问题:

二、进程状态

2.1一般的操作系统学科进程状态

2.2具体的Linux状态是如何维护的?

2.2.1 看看Linux内核源代码怎么说

2.2.2进程状态查看

 2.2.3 Z(zombie)-僵尸进程

2.2.4 孤儿进程

2.3 进程优先级

2.3.1 基本概念

2.3.2 查看系统进程和进程优先级的命令

2.3.3 优先级修改

2.3.4 操作系统是如何根据优先级,开展的调度的呢?

2.4 几个概念


承上启下

学习进程之前,就问大家,前面我们学习了操作系统,那么操作系统是怎么管理进行进程管理的呢?很简单,先把进程描述起来,再把进程组织起来!

一、进程

1.1 基本概念

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU时间,内存)的实体。

也就是一个已经加载到内存的程序,叫做进程。当我们写好的程序经过编译后生成可执行程序,但这是可执行程序只是磁盘中的一个文件,只有当我们运行这个可执行程序,它就会被加载到内存,然后经过CPU处理变成一个进程。所以说只有加载到内存的程序,才叫做进程。或者说正在运行的程序,叫做进程

1.2 描述进程——PCB

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
  • 课本上称之为PCB(process control block)——进程控制块,Linux操作系统下的PCB是: task_struct

1.3 task_struct——PCB的一种

  • 在Linux中描述进程的结构体叫做task_struct
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

1.4 task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据: 进程执行时处理器的寄存器中的数据
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

1.5 组织进程

操作系统将每一个进程用task_struct描述,形成一个个PCB,并将这些PCB以链表或者其他数据结构的方式组织起来。如此一来,操作系统对进程的管理就变成了对链表的增、删、查、改操作。

可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。

1.6 查看进程

  • 进程的信息可以通过 ls /proc 系统文件夹查看

  • 这些数字其实是某个进程的PID(进程id号),对应文件夹当中记录着对应进程的各种信息,如果要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。

  • 使用ps aux获取所有进程信息
[root@hecs-202562 ~]# ps aux

还可以将ps命令与grep命令结合起来使用,显示某一个进程的信息

[GTY@hecs-202562 proc]$ ps aux | head -1 && ps aux | grep myproc | grep -v grep

1.7 通过系统调用获取进程标示符 

  • 进程id(PID)
  • 父进程id(PPID)

在Linux中我们可以通过系统调用接口getpid()、getppid()来分别获取进程与父进程的id。

我们来看这段代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    printf("pid: %d\n", getpid());
    printf("ppid: %d\n", getppid());
    return 0;
}

1.8 通过系统调用创建进程-fork初识

1.8.1 运行 man fork 认识fork

功能: 创建一个子进程

返回值: fork函数有两个返回值,一个返回值是给父进程返回子进程的PID,还有一个返回值是给子进程返回0(如果子进程创建失败,就会返回-1)

1.8.2 fork的两个返回值

我们来看这段代码

#include<stdio.h>
#include<unistd.h>
int main()
{
   pid_t ret = fork();
   printf("hello process! pid: %d ppid: %d ret = %d\n",getpid(),getppid(),ret);
   
   sleep(1);
   return 0;
}

运行结果:

 从输出结果中,我们可以看到'printf'语句的内容被打印了两次,但每次打印的'PID'和'PPID'是不同的。这是因为'fork'函数创建了一个子进程,而这个子进程的'PPID'(父进程ID)就是原始进程的'PID'(进程ID)。这再次证实了原始进程和由'fork'函数创建的子进程之间存在父子关系。因此,这段代码验证了'fork'函数的主要功能:它是用来创建子进程的。

通过检查结果,我们可以确认'fork'函数确实返回了两个值。在父进程中,它返回新创建子进程的进程ID。如果子进程成功创建,在子进程中返回值为0,否则返回-1。这进一步证实了我们之前所述的'fork'函数的返回值具有两个

1.8.3 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

 fork()之后父子进程的代码是共享的:

#include<stdio.h>
#include<unistd.h>
int main()
{

  printf("begin 我是一个进程, pid: %d ppid: %d\n",getpid(),getppid());
  pid_t ret = fork();

  printf("我是后续的代码\n");
  sleep(1);

  return 0;
}

运行结果:

 

从上面这段代码执行后,我们发现 fork()之后父子进程的代码是共享的,所以被执行了俩次。

 1.8.4 fork 之后通常要用 if 进行分流

我们上面知道了父子进程代码是共享的,但是我们如果让父子进程做相同的事情,那么创建子进程就没什么意义了。

实际上,通过在fork之后使用if/else语句来区分父子进程的执行路径,我们可以使它们执行不同的任务。由于fork给父子进程返回不同的值,我们可以通过判断返回值来编写条件语句,从而使父进程和子进程执行不同的代码,实现所需的差异化操作。这样做能够充分发挥fork函数创建子进程的能力,以便在进程间实现更复杂的操作和交互。

下面我们来看一段代码:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>

int main()
{
  printf("begin: 我是一个进程,pid:%d,ppid:%d\n",getpid(),getppid());

  pid_t id = fork();

  if(id == 0)
  {
    //子进程
    while(1)
    {
        printf("begin: 我是一个子进程,pid:%d,ppid:%d\n",getpid(),getppid()); 
        sleep(1);
    }  
  }
  else if(id > 0)
  {
    //父进程
    while(1)
    {
        printf("begin: 我是一个父进程,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
    }  
  }
  else{
    //error
  }

  return 0;
}

运行结果: 

我们观察到了一个令人惊讶的现象,即if和else语句中的代码都被执行了。在C/C++中,这是不可能发生的,但在系统中的多进程环境中,这是可以实现的。这是因为fork函数创建了两个独立的执行流,即父进程和子进程。通过使用if/else语句进行分流,我们可以让父进程和子进程分别执行不同的代码,从而实现所需的差异化操作。这种分流机制使得在多进程环境中,if和else语句中的代码都可以被执行,展示了fork函数在创建子进程时的强大能力。

下面我们来分析一下这段代码: 

  • 首先bash在命令行中执行了./proc,它和普通进程一样被创建出来,创建出来之后系统就给这个进程分配了一个pid:3478,然后这个进程被操作系统调度执行这一段代码,执行到fork之后,就由fork一分为二,变成了俩个执行分支,一个是父进程,一个是子进程,而父进程就是它自己,而fork之后创建的子进程就是他新的分支。
  • 执行流是从上往下执行的,当我们调用fork函数之后呢,fork函数会给父进程返回值子进程的pid,而pid > 0,给子进程返回值等于0。也就是fork之后,代码会变成俩个执行流,父子进程共享一份代码,一个执行流进入id等于0中,一个进入id大于0中执行代码。 

1.8.5 总结一下关于进程和fork函数的几个问题:

1. 我们为什么要创建子进程呢?

为了让父和子执行不同的事情! 需要想办法让父和子执行不同的代码块——让fork具有了不同的返回值!

2.为什么fork要给子进程返回0, 给父进程返回子进程pid?

返回不同的返回值,是为了区分让不同的执行流,执行不同的代码块!

父进程需要把子进程的pid拿到,用来标记子进程的唯一性因为如果fork用一个循环套上,就有多个子进程了未来要用父进程通过pid去明确我们要访问控制的是哪个子进程。而子进程不需要,只要用getppid就能找到父进程。只有返回值标记0,标识成功即可。

3.一个函数是如何做到返回两次的?如何理解?

fork也是一个函数,在fork函数里面:

  1. 创建了子进程PCB
  2. 填充PCB对应的内容
  3. 让子进程和父进程指向同样的代码
  4. 父子进程都是独立的task_struct,可以被CPU调度运行了

而return也是语句,也是父子共享的,所以return语句在父子进程都被返回了,所以有俩次返回值。

3.  一个变量怎么会有不同的内容?如何理解?

父进程要return就直接写入就行,子进程要return进行写时拷贝。发生写时拷贝时操作系统对同一个id变量拷了俩份,所以父子进程同样使用id时,父子进程看到的id内容就会不一样,所以最终id有俩个值。

4. fork函数,究竟在干什么? 干了什么?

  • 创建子进程,对子进程创建PCB,用父进程对应的字段来初始化子进程,并且让父子进程实现代码的共享。
  • 当我调用fork之后,父进程就把子进程创建出来了,创建出来之后,父子进程的代码就是共享的了。但是fork会由父进程和子进程各自执行return(return也是代码,也是共享的),而最终形成俩次返回,俩次返回,在id层面上是写时拷贝,让父子进程id变成不同的值,让后续我们可以对id进行if else判断,来对父子进程进行分流,执行不用的代码块,所以父子进程就可以执行不同的代码了。分别在各自的死循环执行对应的代码了。

5. 如果父子进程被创建好,fork(),往后,谁先运行呢? ?

谁先运行,由调度器决定,不确定的    

6. 创建进程的俩种方式:

1、./运行我们的程序

2、fork()---代码层面创建子进程(运行期间创建进程)

二、进程状态

2.1一般的操作系统学科进程状态

运行,阻塞, 挂起

在操作系统中,进程是执行任务的基本单位。一个进程可以是一个程序的执行实例,包括代码、数据和系统资源的使用。操作系统通常需要对进程进行管理,包括调度、内存分配、输入/输出操作等。

在操作系统的进程管理中,进程的状态通常有以下几种:

  1. 运行状态(Running):进程正在执行其代码,占用处理器和系统资源(在运行队列)
  2. 阻塞状态(Blocked):进程在等待某些事件(如输入/输出操作完成、等待获取锁等)发生后才能继续执行。在这种情况下,进程会被挂起,不再占用处理器和系统资源。
  3. 挂起状态(Suspended):在某些情况下,操作系统可能会将进程挂起,使其暂时无法执行。这可能是因为进程需要等待某些资源,或者操作系统需要将处理器资源分配给其他进程。

        这些状态之间的转换通常由操作系统的调度程序控制例如,当一个阻塞状态的进程等待的事件发生时,它可能会被转移到就绪状态,等待处理器执行当运行状态的进程完成其任务或被操作系统调度程序暂停时,它可能会被转移到挂起状态或阻塞状态。

        这些进程状态的转换对于操作系统来说非常重要,因为它们决定了系统的响应性和资源利用率。通过管理进程的状态,操作系统可以有效地分配处理器和系统资源,并确保系统的稳定性和可靠性。

2.2具体的Linux状态是如何维护的?

2.2.1 看看Linux内核源代码怎么说

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

我们看到当前这个进程处于运行状态(R+)的,这个+号表示这个进程是在前台运行。

  • S睡眠状态(sleeping): 意味着进程在等待事件完成,处于睡眠状态的进程可以被唤醒。(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

下面我们用sleep函数让它进行休眠50秒,然后来看运行结果:

通过ps命令我们可以看到当前进程是处于S状态的。睡眠状态是可以通过kill命令将其杀掉的。

  • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。当进程处于D状态时,它不能被杀死,只有该进程拿到数据时才会恢复。

当一个进程想要对硬盘进行读写操作时,由于IO操作(输入/输出操作)相对较慢,硬盘需要同时执行两个任务查找需要的数据以及将这些数据拷贝给进程。然而,硬盘是一个机械设备,查找数据的过程相对较慢,数据并没有准备好所以不能直接拷给进程,因此进程可能会处于不可中断睡眠状态。

在操作系统可用空间不足时,操作系统会采取内存管理措施,通过终止某些进程来释放空间。在这种情况下,如果其他进程处于运行状态,然而,如果有一个进程处于等待状态(休眠状态),操作系统可能会错误地终止它,导致IO操作失败,甚至可能导致硬件无法正常工作。

需要注意的是,处于深度睡眠状态的进程是不可通过kill命令终止的,这意味着它们不会被操作系统终止。因此,在上述情况下,即使硬盘找到了需要的数据并尝试将其拷贝给被误杀的进程,但由于该进程已被终止,数据传输将失败。

不可中断睡眠状态就可以避免这种情况的发生,它不可以通过kill命令将其杀掉的,也就是说它不会被操作系统杀掉,因此也就不会出现上面这种误杀的情况

  • T停止状态(stopped): 可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行

下面来看这个例子:

  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。它是一个进程结束后的状态,表示进程已经彻底结束,可以回收进程资源了。在这个状态下,进程几乎瞬间就会被清理掉,因此很难捕捉到处于这个状态下的进程。

2.2.2进程状态查看

ps aux / ps axj 命令

上面这个图显示了各个进程状态间的转换。 

 2.2.3 Z(zombie)-僵尸进程

  • 僵尸状态(Zombies)是一个比较特殊的状态,一个进程如果正在等待其退出信息被父进程读取,那么我们就称该进程处在僵尸状态
  • 当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

一个僵死进程例子:

 子进程运行一段时间然后退出,父进程继续运行但不对子进程做任何处理。

1 #include <stdio.h>  
  2 #include <unistd.h>  
  3 #include <stdlib.h>  
  4   
  5 int main()  
  6 {  
  7    pid_t id = fork();  
  8    if(id == 0)  
  9    {  
 10        //child  
 11        int cnt = 5;  
 12        while(cnt)  
 13        {  
 14            printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);  
 15            cnt--;  
 16            sleep(1);  
 17        }  
 18        exit(0);  
 19    }  
 20    else  
 21    {  
 22        //father  
 23        while(1)  
 24        {  
 25            printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());  
 26            sleep(1);  
 27        }  
 28                                                                                                                                                                                        
 29        //父进程目前并没有针对子进程干任何事事情                                                   
 30    }                                                                                              
 31                                                                                                   
 32   return 0;                                                                                       
 33 }                                                                                                 
~     

 运行该代码后,我们可以通过运行下面的监控脚本,每隔一秒对该进程的信息进行检测

while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep | grep -v nvim;echo "######################";sleep 1;done

我们看到当子进程退出后,由于父进程没有读取到子进程的退出信息,子进程的状态就变成了僵尸状态。

程序终止掉,子进程和父进程都停了

进程corl+c终止之后,父进程为什么没有处于僵尸状态?因为父进程的父进程是bash,进程停止之后bash立马就把父进程回收了

为什么子进程也没了呢?子进程在终止的瞬间就被1号进程(系统)回收并释放了

僵尸进程危害:

  • 1. 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态!
  • 2. 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护!
  • 3. 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
    • 4. 内存泄漏?是的!僵尸进程申请的资源无法被回收,当我们操作系统中僵尸进程越多,系统可用的资源就会变得越来越少,最终系统宕机卡死。也就是说,僵尸进程会导致内存泄漏。
  • 5. 如何避免?
  • 父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。
  • 如果父进程很忙,可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。
  • 如果父进程不关心子进程什么时候结束,可以用signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。

2.2.4 孤儿进程

如果父进程已经结束,那么子进程的退出信号将无法传递给父进程,这时子进程就会变成一个"僵尸进程"。这种情况下,我们称这样的子进程为"孤儿进程"。

父进程如果提前退出,那么子进程后退出,进入Z(僵尸进程)之后,没有人读取子进程的退出信息的话,是会导致子进程变成孤儿进程从而导致内存泄漏的。那该如何处理呢?

  • 当出现孤儿进程的时候,如果一直没有人读取它的退出信息的话,孤儿进程会被1号init进程领养,由1号进程来读取它的退出信息。
 int main()
{
   //  while(1){
   //      printf("I am a process, pid: %d\n", getpid());
   //      sleep(1);
   //  }
 
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 100;
        while(cnt)
        {
            printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);                                                               
        }
    }
    else
    {
        //father
        int cnt = 5;
        while(cnt--)
        {
            printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
        printf("父进程退出");
        exit(1);
   }
 
   return 0;
}

运行结果:父进程运行一段时间先退出,子进程变成孤儿进程

我们可以看到在父进程未退出时,子进程的PPID就是父进程的PID,当父进程退出后,子进程就变成了孤儿进程,子进程的父进程变成了1号进程,这样的子进程叫做孤儿进程,该进程被系统领养,因此子进程的PPID就会变成1。

为什么要领养?因为子进程未来也要退出,也要被释放

2.3 进程优先级

2.3.1 基本概念

优先级vs权限

优先级:已经可以访问了,对于资源的访问,谁先访问,谁后访问

权限:还不知道能不能访问

  • cpu资源分配的先后顺序,就是指进程的优先权(priority)。
  • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
  • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

为什么有优先级?

  • 因为资源是有限的,而进程是多个的,注定了,进程之间是竞争关系。
  • 操作系统必须保证大家进行良性竞争确认优先级(谁先访问,谁后访问)
  • 果进程长时间得不到CPU的资源,该进程的代码长时间得不到推进——进程饥饿问题

2.3.2 查看系统进程和进程优先级的命令

在linux或者unix系统中,用ps –l或ps-al命令则会类似输出以下几个内容:

我们很容易注意到其中的几个重要信息,有下:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级其值越小越早被执行,默认是80
  • NI :代表这个进程的nice值,是进程优先级的修正数据

2.3.3 优先级修改

PRI and NI

  • PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
  • 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
  • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 所以,调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是-20至19,一共40个级别。

PRI vs NI

  • 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
  • 可以理解nice值是进程优先级的修正数据

PRI(new)=PRI(old)+niceLinux为了不过多的让用户参与优先级的调整,在对应的范围内进行优先级调整,nice:[-20,19],而 PRI 默认是80,所以优先级的调整范围是[60,99]。

用top命令更改已存在进程的nice

命令行输入top,top命令类似于windows下的任务管理器,它能够实时的显示系统中进程的资源占用情况。

然后输入r(renice)

接着输入要改进程的pid,回车

最后输入nice值后按q即可退出,这里将nice改为-30

修改成功,但这里nice最多只能是-20,不是-30,因为操作系统只能让我们在nice:[-20,19]的区间修改。所以最终PRI变成了60(80-20)

同样的,我们将nice改为100,对应的pri最多也只能改到99

注意:

  • 要在root下修改,否则会修改失败
  • 每次重新设置,PRI都是从80开始,这样也方便计算

2.3.4 操作系统是如何根据优先级,开展的调度的呢?

     操作系统每一个CPU都要维护一个运行队列,运行队列中可以包含双列表,指向头指向尾,就可以形成队列,就可以进行调度了。        

        操作系统中的运行队列中定义了俩个数组,一个运行数组和一个等待数组,俩个都是140 task_struct的大小,但0-99是给其他种类的进程用的,我们用不到。所以整个数组,我们只需要用到100-139,一共四十个,对应四十个优先级。所以我们上面说的[60,99]的优先级进程在数组中是存放在下标[100,139]中的。

        也就是说每个运行队列都有俩个指针数组,我们只考虑数组下标在[100,139]范围的。假如我们现在有一个优先级60的进程,对应的我们将他存放在数组下标100的位置上去,未来再来一个同等优先级的进程,我们将他排在这个的后面,直接用双链表连接。如果来了一个61的优先级进程,同样的,将他放在数组下标对应101的位置排队。所以操作系统在调度时,就只需要从100号下标开始,依次扫描遍历这里,想拿哪个进程就拿哪个进程,非常方便的就能挑选出某一个进程。

        所以为什么说优先级越小的越先被调度,因为他存放在数组的前面

        当我们调度一个队列时,可能还有其他进程需要插入进来,所以设计者又维护了一个镜像队列,叫做等待队列。使用俩个二级指针run和wait,其中run永远指向运行数组,而wait永远指向等待数组。实际使用时通过run找到run数组,然后就可以找到对应优先级的队列。当调度时,还有其他进程要插入,就将新的进程根据优先级在wait队列中插入,当run数组中队列完全调度完毕,我们只需要将run二级指针和wait二级指针进行交换,这样run又指向了需要调度的队列,如此往复。

        那我们如何确定当前的运行队列为空了呢?我们要判断队列是否为空,只能遍历数组,但这样太麻烦了。所以又维护了一个isempty结构(位图结构),我们可以通过比特位行确定,假设我们约定最低的比特位代表第一个队列是否为空,所以这里只需要四十个比特位,开辟一个char数组5个元素即可,所以为了我们要判断队列是否为空,只需要判断这个位图就可以。如果不为0,就就需要找到这个位图中离最低位最近的为1的值。所以这样,我们就可以通过位图确定整个数组中队列的情况,所以我们可以仅使用近乎O(1)的时间复杂度来在众多进程当中选取一个来调度,称为Linux内核中O(1)的调度算法

2.4 几个概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

进程切换是操作系统中一种非常重要的过程,它指的是在操作系统中,由于CPU时间片轮转或者由于当前进程需要等待I/O等事件发生而被挂起,从而需要切换到其他就绪状态的进程的过程。进程切换涉及到保存(保存的目的是为了未来恢复)当前进程的上下文信息包括CPU寄存器(进程相关的数据)、程序计数器(记录当前进程挣扎执行指令的下一行指令的地址)、栈指针等,以及恢复调度执行下一个进程所需的上下文信息进程切换是实现多任务并发执行的关键过程

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值