首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别再只说“进程”了!深入 Linux 内核看进程如何被调度与管理

别再只说“进程”了!深入 Linux 内核看进程如何被调度与管理

作者头像
夜雨声烦1413
发布2026-01-12 15:37:22
发布2026-01-12 15:37:22
1780
举报

1:进程的概念

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

当我们的代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程.

2:描述进程-PCB

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合.
  • 课本上称之为PCB(process control block),Linux下的操作系统的PCB为:task_struct.
  • 而当开机的时候启动的第一个程序就是操作系统(即操作系统是第一个加载到内存的),我们都知道操作系统是做管理工作的,而其中就包括了进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢?
  • 这时候就要讲到博主之前提到的先描述,再组织.操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB.
  • 操作系统将每一个进程都进行描述,形成了一个个的进程控制块,并将这些PCB以双向循环链表的形式组织起来.
  • 这样一来,操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作。

  • 操作系统中,可以同时存在多个进程,但是一个进程,必须要有一个对应的PCB.
  • 为什么要有PCB:因为操作系统要对进程进行管理,先描述,再组织.

3:task_struct

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

3.1:task_struct内容分类

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

3.2:查看进程

  • 进程的信息可以通过 /proc 系统文件夹查看
  • 通过ps命令查看
  • ps命令与grep命令搭配使用,即可只显示某一进程的信息。
代码语言:javascript
复制
ps aux | head -1 && ps aux | grep bash | grep -v grep

3.3:通过系统调用获取进程的PID与PPID

  • ./xxx 本质就是让系统创建进程并运行-----我们自己写的代码形成的可执行以及系统命令都是可执行文件.(在Linux中运行的大部分执行操作,其本质就是运行进程)
  • 每一个进程都要有自己的唯一标识符,叫做进程Pid.
  • 通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID.
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()

{
    while (1)
    {
        printf("I am a process And My Pid == %d,PPid == %d\n",getpid(),getppid());
        sleep(1);
    }
    
    return 0;
}

我们可以通过ps命令查看该进程的信息,即可发现通过ps命令得到的进程的PID和PPID与使用系统调用函数getpid和getppid所获取的值相同.

我们会发现,当我们每次启动进程的时候,所对应的进程的pid不一样是正常,但是所对应的父进程都是一样的,都是命令行解释器.

3.4:通过系统调用创建进程--fork

  • fork是一个系统调用级别的函数,其功能就是创建一个子进程.
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()

{
    fork();
    while (1)
    {
        printf("I am a process And My Pid == %d,PPid == %d\n",getpid(),getppid());
        sleep(1);
    }

    return 0;
}
  • 运行结果是循环打印两行数据,第一行数据是父进程的PID和PPID,第二行数据是代码中fork函数创建的子进程的PID和PPID。我们可以发现fork函数创建的进程的PPID就是test.exe进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系。
  • 每出现一个进程,操作系统就会为其创建PCB,fork函数创建的进程也不例外.

有的uu会有些疑问,加载到内存当中的代码和数据是属于父进程的,那么子进程的代码和数据又是从何而来的呢?我们来看下面这段代码

代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()

{
    printf("one process is running,pid == %d,ppid == %d\n",getpid(),getppid());
    sleep(3);
    fork();
    printf("hello vim,pid = %d,ppid = %d\n",getpid(),getppid());
    sleep(5)
    return 0;
}

使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,但是在fork之后父进程与子进程代码共享,创建一个进程本质就是系统多一个进程,多(1):task_struct(2)代码 + 数据.

  • 父进程的代码和数据是从磁盘加载进来的.
  • 子进程的代码与数据默认情况下继承父进程的代码和数据.
3.4.1:使用if进行分流

ork函数创建出来的子进程与其父进程共同使用一份代码,但我们如果真的让父子进程做相同的事情,那么创建子进程就没有什么意义了。实际上,在fork之后我们通常使用if语句进行分流,即让父进程和子进程做不同的事。

  • 如果子进程创建成功,在父进程返回子进程的pid,而在子进程中返回0.
  • 如果子进程创建失败,则在父进程返回-1.
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
    printf("one process is running,pid == %d\n",getpid());
    sleep(3);
    pid_t id = fork();
    //fork函数的返回值等于0则为子进程.
    if(0 == id)
    {
        while (1)
        {
            printf("I am child process and my pid == %d,ppid =%d\n",getpid(),getppid());
            sleep(1);
        }
    }
    //返回值为非0则为父进程.
    else
    {
        while (1)
        {
            printf("I am parent process and my pid == %d,ppid =%d\n",getpid(),getppid());
            sleep(1);
        }
        
    }
  return 0;
}

fork创建出子进程后,子进程会进入到 if 语句的循环打印当中,而父进程会进入到 else 语句的循环打印当中

虽然fork之后,后面的代码是被父子进程共享的,但是进程一定要做到:进程具有独立性. 进程 = 内核数据结构task_struct + 代码 + 数据(数据层面,父子进程各自独立,原则上数据要分开).

3.4.2:多个进程的创建
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

void Runchild()
{
    while(1)
    {
      printf("I am child Process and My pid = %d,ppid = %d\n",getpid(),getppid());
      sleep(1);
    }
}

int main()
{
  const int num = 5;

  for(size_t i = 0; i < num; i++)
  {
      size_t id = fork();
      //fork的返回值若为0则创建的是子进程,若返回值大于0则是父进程
      if(0 == id)
      {
         Runchild();
      }


      sleep(1);
  }
  
  //当循环结束了,则说明创建的子进程结束了
  while(1)
  {
    sleep(1);
    printf("I am parent process:pid = %d,ppid = %d\n",getpid(),getppid());
  }

  return 0;
}

4:进程的工作路径

代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while (1)
    {
        printf("I am process and my pid == %d\n",getpid());
        sleep(1);
    }
    return 0;
}

我们可以看到,当将进程所对应的可执行程序删除了,为什么该进程还能够运行呢,因为在原则上,一个程序要能够被调度,要在内存中里面存有一份可执行程序,磁盘上的可执行程序被删除了,但是内存空间中还有.

4.1:代码创建文件,该文件在当前路径下创建的原因

在C语言阶段的时候,我们学习过文件,并且使用过fopen来创建文件,但是有的uu会比较好奇,为什么每次使用代码创建文件的时候,总是在当前路径下创建呢?我们来看两个例子.

4.1.1:代码1
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    FILE * pf = fopen("test.txt","w");
    while (1)
    {
        printf("I am process and my pid == %d\n",getpid());
        sleep(1);
    }
    return 0;
}

我们可以看到,当运行程序时,在当前路径下创建了test.txt的文件,这是为什么呢?原因在于

  • 每个进程在启动的时候,会记录自己当前是在哪个路径下启动的.
  • 在执行代码的时候,实际上是进程的代码在执行,因此在代码里面使用fopen("test.txt","w")新建文件的时候,实际是建立在cwd/这个路径下的,而cwd则为进程当前的工作路径.
4.1.2:代码2

在举例之前,我们得先学习一下chdir函数,此函数的功能是:更改进程的工作路径.

代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    chdir("/home/ly/111");
    FILE * pf = fopen("test.txt","w");
    fclose(pf);
    pf = NULL;
    while (1)
    {
        printf("I am process and my pid == %d\n",getpid());
        sleep(1);
    }
    return 0;
}

  • 每个进程在启动的时候,会记录自己当前是在哪个路径下启动的.
  • 在执行代码的时候,实际上是进程的代码在执行,因此在代码里面使用fopen("test.txt","w")新建文件的时候,实际是建立在cwd/这个路径下的,而cwd则为进程当前的工作路径.
  • 在代码中创建文件的时候,会默认拼接上cwd/(即进程的当前工作路径).

5:进程状态

一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,进程是活动的且有状态变化的,于是便有了进程状态这一概念.

这里博主要带uu们谈一谈Linux操作系统中的进程状态,Linux操作系统的源代码当中对于进程状态有如下定义:

代码语言:javascript
复制
/*
* 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 */
    };

进程的当前状态是保存到自己的PCB中的,在Linux操作系统中也就是保存在task_struct中的.

5.1:R状态与S状态

  • R运行状态:并不意味着进程一定在运行中,它表明进程要么在运行中,要么在运行队列中.
  • S睡眠状态:意味着进程在等待事件完成(这里的睡眠有时候叫做可中断睡眠).

S状态的本质是:

  1. 进程在等待资源就绪.
  2. S状态是可中断的睡眠状态.
5.1.1:代码1
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while (1)
    {
        printf("I am process and my pid == %d\n",getpid());
        sleep(1);
    }
    return 0;
}

通过观察我们可以发现,在注释前后,进程的状态由最初的S+状态变成了R+状态,那么这是为什么呢?

  • printf函数的本质是往显示器上打印,printf在往显示器上打印,根据冯诺依曼体系结构,显示器是一个外设,CPU在跑程序时,要先将数据写入到内存中,然后再刷新到外设当中.

那么有的uu就会问,能否保证每次打印的时候,显示器都是处于就绪状态呢,答案:并不是. 因为程序在内存中是通过CPU跑起来的,CPU的运算速度相较于显示器这个外设来说,CPU的运算速度要比显示器快的多,进程在被调度时,要访问显示器资源,因为printf函数的本质是往显示器上打印,所以大部分时间,进程都在等待显示器资源是否就绪,那么所以查到的进程状态就是R状态,但实际上在向显示器资源打印的时候,进程一直在等待显示器资源就绪,如果显示器资源不就绪,那么进程就会处于S状态,只有在真正执行printf函数的时候,才能看到进程处于R状态,但是对于CPU来说,执行printf函数是十分的快速的,大概是几纳秒左右,但是打印的时候可能是几毫秒完成.

  • 所以在没有注释掉printf函数的时候,在查进程状态的时候,基本上是处于S+状态,而并非是R+状态,而当注释掉printf函数的时候,由于不再和外设打交道了,此时只有CPU资源,只要进程被调度,那么此时查到的进程状态都是R+状态.
5.1.2:代码2
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while (1)
    {
        sleep(20);
        printf("I am process and my pid == %d\n",getpid());
    }
    return 0;
}

在睡眠的20s期间,中间突然ctrl + c杀掉进程,那么此时睡眠状态就被中断了,因此S+状态是可中断的睡眠状态.

5.2:T状态与t状态

  • T停止状态:可以通过发送SIGSTOP信号给进程来停止进程.这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行.
  • t状态:进程因为被追踪而导致的暂停.

在Linux中,我们通过kill指令来向进程发送信号

  • kill -9 pid 杀死进程
  • kill -19 pid 暂停进程
  • kill -18 pid 启动进程
5.2.1:T状态
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while (1)
    {
        sleep(1);
        printf("I am process and my pid == %d\n",getpid());
    }
    return 0;
}
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while (1)
    {
        sleep(1);
        printf("I am process and my pid == %d\n",getpid());
    }
    return 0;
}
5.2.2:t状态
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while (1)
    {
        sleep(1);
        printf("I am process and my pid == %d\n",getpid());
    }
    return 0;
}

5.3:D状态

  • D状态是Linux系统中比较特有的一种进程状态,当进程处于D状态时,此时进程不可被操作系统杀掉,进入了深度睡眠模式,不可中断睡眠.
  • D状态存在的意义是为了保证进程IO,如果在等待外设资源就绪的时候,可能已经写入了一部分数据,此时进程在等待外设资源就绪,那么在等待的过程中,这个状态为D状态,不可以被操作系统杀掉.
  • 如果要消除D状态,要么让进程自己醒来,要么重启或者断电.

5.4:僵尸进程与Z状态

  • 僵尸状态是一个比较特殊的状态,当进程退出并且父进程没有读取到子进程退出的返回代码时会产生僵尸进程.
  • 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码.
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态.
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("I am child process,cnt:%d,pid:%d\n",cnt,getpid());
            sleep(1);
            cnt--;
        }
    }
    else
    {
        while(1)
        {
            printf("I am parent process,pid:%d\n",getpid());
            sleep(1);
        }
    }
    return 0;
}
  • 僵尸进程不可被kill掉

  • 进程 = 内核数据结构task_struct + 进程的代码和数据
  • 如果一个进程退出了,那么它所对应的代码和数据就不会再被访问了,此时操作系统会将进程的代码与数据释放掉,但是进程的task_struct需要一直维持,等待父进程来进程读取.
  • 如果父进程对子进程一直不回收的话,那么此时子进程会一直存在,那么会引起内存泄漏的问题.
5.4.1:僵尸进程的危害
  • 进程的退出状态必须被维持下去,因为他要告诉关心她的进程(父进程),你交给我的任务,我办的怎么样了.可父进程如果一直不读取的话,那子进程就会一直处于Z状态?是的.
  • 维护退出状态本身就是要用数据维护,也属于进程的基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态不退出,PCB就要一直被维护?是的.
  • 那么一个父进程创建了多个子进程,就是不回收的话,就会造成内存资源的危害即内存泄漏.

5.5:孤儿进程

  • 在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程.
  • 若一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收
代码语言:javascript
复制
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 5;
        while(cnt)
        {
            printf("I am child process,cnt:%d,pid:%d\n",cnt,getpid());
            sleep(2);
            cnt--;
        }
    }
    else
    {
        int cnt = 5;
        while(cnt--)
        {
            printf("I am parent process,cnt:%d pid:%d\n",cnt,getpid());
            sleep(1);
        }
    }
    return 0;
}

5.6:僵尸进程与孤儿进程相关总结

6:进程的阻塞和挂起,运行

  • 就绪状态:如果进程没有入运行队列,那么进程就处在就绪状态.
  • 阻塞状态:类似与Linux中的S状态与D状态.
  • 运行状态:进程在运行队列中,那么就处于运行状态即Linux中的R状态.
  • 终止状态:等价于Linux中的Z状态与X状态(死亡状态).

6.1:阻塞状态

  • 对于CPU来说,肯定是有很多的进程想在里面进行运行的,那么一个CPU就会配置一套运行队列.
  • 一个进程在运行队列中,那么该进程的状态就是R状态,也就是说,只要进程在运行队列,那么它所处的状态就是R状态即运行状态(进程已经准备好了,可以随时被调度).
  • 一个进程只要被放在CPU上,那么它的状态就是R状态.

那么有的uu会比较好奇,一个进程一旦有CPU,那么CPU会一直运行到这个进程结束吗?

  • 不会(可以想象一下一个进程的代码里头打了个死循环),除此之外,内核是基于时间片进行轮转调度的.
  • 让多个进程以切换的方式进程调度,在同一个时间段内同时得以推进代码,这种方式叫做并发.
  • 任何时刻,都同时有多个进程在真的同时运行,这种方式叫做并行.

6.2:挂起状态

6.3:进程切换

7:进程优先级

  • 优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序,就是指进程的优先权(priority),优先权高的进程有优先执行的权力。
  • 优先级存在的主要原因是由于资源是有限的,而存在进程优先级的主要原因就是CPU资源是有限的,一个CPU一次只能跑一个进程,而进程是可以有多个的,所以需要存在进程优先级,来确定进程获取CPU资源的先后顺序。
  • 还可以将进程运行到指定的CPU上,这样依一来,把不重要的进程安排某个CPU,可以大大改善系统整体性能.

7.1:查看系统进程

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

7.2:PRI与NI

  • PRI为进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小,进程的优先级越高.
  • NI为nice值,其表示进程可被执行的优先级的修正值.
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + nice值.
  • 这样子,当nice值为负值的时候,那么该程序将会把优先级值变小,即优先级会变高,则其越快被执行.
  • 所以,调整进程优先级,在Linux,就是调整进程nice值.
  • nice其取值范围是-20到19,一共40个级别.
  • 进程的nice值不是进程的优先级,他们不是一个概念,但是进程的nice值会影响到进程的优先级变化.
  • 可以理解nice值是进程优先级的修正数据.

7.3:其他概念

竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便有了优先级。

独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。

并行: 多个进程在多个CPU下分别同时进行运行,这称之为并行。

并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发.

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1:进程的概念
  • 2:描述进程-PCB
  • 3:task_struct
    • 3.1:task_struct内容分类
    • 3.2:查看进程
    • 3.3:通过系统调用获取进程的PID与PPID
    • 3.4:通过系统调用创建进程--fork
      • 3.4.1:使用if进行分流
      • 3.4.2:多个进程的创建
  • 4:进程的工作路径
    • 4.1:代码创建文件,该文件在当前路径下创建的原因
      • 4.1.1:代码1
      • 4.1.2:代码2
  • 5:进程状态
    • 5.1:R状态与S状态
      • 5.1.1:代码1
      • 5.1.2:代码2
    • 5.2:T状态与t状态
      • 5.2.1:T状态
      • 5.2.2:t状态
    • 5.3:D状态
    • 5.4:僵尸进程与Z状态
      • 5.4.1:僵尸进程的危害
    • 5.5:孤儿进程
    • 5.6:僵尸进程与孤儿进程相关总结
  • 6:进程的阻塞和挂起,运行
    • 6.1:阻塞状态
    • 6.2:挂起状态
    • 6.3:进程切换
  • 7:进程优先级
    • 7.1:查看系统进程
    • 7.2:PRI与NI
    • 7.3:其他概念
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档