【Linux篇】数字世界的底层认识, 它是底层的地基——进程概念的认识

作者简介

Linux进程

|------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| 作者主页 | |
| Linux | 算法篇 |
| QT百日筑基 | 数据结构篇 |


目录/索引

目录

Linux进程

深度理解冯诺依曼体系

问题一:

木桶原理:

问题二:

问题三:

结论:

冯诺伊曼实际的应用:

场景一:

场景二:

[操作系统(Operator System)](#操作系统(Operator System))

操作系统的概念:

设计OS的目的:

这里输出有关基础硬件操作系统的简单认识:

OS的核心功能:

深度理解管理:

[校长1: 技术: 熟练的使用各种办公软件。](#校长1: 技术: 熟练的使用各种办公软件。)

[校长2: 在当校长之前,该校长是一个顶级的程序员, 他熟练掌握个中的C语言语法。](#校长2: 在当校长之前,该校长是一个顶级的程序员, 他熟练掌握个中的C语言语法。)

理解先描述再组织:

结论:

进程:

什么是进程:

场景的描述:

场景1:

描述进程---PCB

task_struct

组织进程

认识getpid和getppid

演示:

代码展示:

进程的查看:

方法一:

ps命令:

方法二:

通过文件来进行查询.

方法三:

top指令这里不做过多的介绍了一般前两者的使用频率更多一点。

杀死进程的方法:

方法一:

[快捷键:【ctrl】+ c](#快捷键:【ctrl】+ c)

方法二:

[kill指令:语法 kill -9 进程的pid / kill 信号 进程的pid](#kill指令:语法 kill -9 进程的pid / kill 信号 进程的pid)

[父进程的查询方法: getppid](#父进程的查询方法: getppid)

[查看ls /proc/ 进程](#查看ls /proc/ 进程)

cwd:

exe:

[修改cwd的方法: 函数chdir](#修改cwd的方法: 函数chdir)

进程的创建---fork

[返回值: (灰常重要)](#返回值: (灰常重要))

[问题一 : 为什么要把子进程的pid给父进程](#问题一 : 为什么要把子进程的pid给父进程)

[问题二: fork函数为什么会有两个返回值。](#问题二: fork函数为什么会有两个返回值。)

[问题三: 为什么他能同时执行if和else if](#问题三: 为什么他能同时执行if和else if)

进程的状态:

理解独立性:

深度理解运行&&阻塞&&挂起

进程的状态类型:

R(running):运行状态。

[S(sleep):阻塞。 (可杀)](#S(sleep):阻塞。 (可杀))

[D(disk sleep): 深度睡眠。(在高I/O的地方触发)(不可杀进程)](#D(disk sleep): 深度睡眠。(在高I/O的地方触发)(不可杀进程))

[T(stopped): 暂停 (【ctrl】+z)来暂停这个进程。](#T(stopped): 暂停 (【ctrl】+z)来暂停这个进程。)

[t:暂停 debug断点会触发。](#t:暂停 debug断点会触发。)

X:死亡状态

[Z: 表示僵尸进程。](#Z: 表示僵尸进程。)

僵尸进程的危害:

进程状态查看

ps指令

选项:

孤儿进程

进程的优先级

优先级ov权限:

修改NI(nice值):

[方法一: top命令:](#方法一: top命令:)

方法二:

使用nice和renice进行修改

方法三:

系统函数:

NI值的详解:

[补充: 竞争、独立、并行、并发](#补充: 竞争、独立、并行、并发)

进程切换:

概念:

时间片:

Linux的内核进程和调度队列

优先级的分类:

运行队列:

过期队列:

active和expired指针



在学习进程前我们要先了解一些关于硬件上的一些问题:

深度理解冯诺依曼体系

说到冯诺伊曼体系它是让我们的计算机变得更加有性价比的一个理论。 我们所用的笔记本

服务器...大多都遵循冯诺伊曼体系。

在我们计算机的日常的设备中

输入单元: 键盘、鼠标、扫描仪...

输出单元: 显示器、打印机...

中央处理器(CPU): 包括两大部分分别是运算器和控制器。

在这里我们的输入和输出设备(I/O)叫做外设。

我们的储存器和CPU叫做内设。


由这副图我来抛出三个问题来更好的了解我们的冯诺依曼体系。

问题一:

为什么我不能将我们输入设备中的直接加载CPU中, 而是先将他们放入内存中???

当我们这样的时候我们所遵循的效率会根据时间复杂度长的进行计算这样我们的计算机的效率会慢上很多的。 他们遵循木桶原理。


木桶原理:

所有的木板都是长的, 只要有一个木板是短的那么这个木桶所能承受的储水量是有短木板所决定的。

当然存储器(内存)的速度也是很快的所以加上内存能让我们的计算机的效率会更快的运行起来的。


问题二:

为什么数据的控制都要经过内存???

原因: CPU 只能在内存中读取数据,CPU这和内存进行打交道,不和其他的设备进行交流。

而外设只和内存打交道。 为什么这样设计?

问题三:

软件加载的过程中要从外设写入内存???

因为体系结构是这样规定的。

所以从键盘上进行输入设备本质上数据的"拷贝"。 我们在输入设备的数据拷贝到内存中在经或cpu处理后拷贝到输出设备上。 所以体系结构的效率是由计算机的拷贝效率所决定的。


有关内存的结构图:


结论:

综上所述:

  1. 这里的存储器是内存

  2. 不考虑缓存的情况下,CPU只能对内存进行读写, 不能访问外设(I/0数据层面)

  3. 外设(I/O)只能写入和读取内存的数据

4.一句话所有的设备只能和内存进行打交道


冯诺伊曼实际的应用:

我们对于冯诺伊曼的理解不能只是停留在概念上, 我们要更加的深刻的去理解冯诺伊曼体系的应用所以我们给出两个场景来更深刻的进行应用冯诺伊曼的体系结构。

场景一:

当我们给QQ好友发送信息是我们的冯诺伊曼体系的工作:

当你和你的QQ好友发送个你好,根据冯诺依曼体系来推导一下对应的硬件的工作流程:

场景二:

当我们给QQ好友发送文件时:

操作系统(Operator System)

操作系统的概念:

操作系统是一个进行软硬件管理的软件。简称OS

这个操作系统包含:

  1. 内核(又叫狭义上的操作系统):进程管理, 内存管理, 文件管理, 驱动管理。

  2. 其他程序:函数库, shell程序等。

大概原理图:

设计OS的目的:

1.对下, 与硬件交互, 管理所有硬件资源。(不是目的, 是手段)

  1. 对上, 为用户程序提供一个良好的执行环境。

设计操作系统的目的都是为了给用户提供服务的。

原理图:

我们能看到上面是软件的方面, 下面是对于硬件的管理。

操作系统对于硬件的管理, 不能直接的进行管理, 得通过驱动来进行管理。

这里输出有关基础硬件操作系统的简单认识:

1)软硬件是一个层状结构, 这里遵循高内聚,低耦合。

高内聚: 相同的代码放在同一个地方, 比如网卡驱动, 其他驱动都放在驱动程序中。

低耦合: 每一层的各个接口都只会暴露对应的接口来给同一层的其余接口提供对应的服务。

2) 访问操作系统必须使用系统调用, 也就是函数,只不过是系统来进行提供的。

3) 我们的程序判断出了对应的硬件那么他将会贯穿整个操作系统来进行对于该硬件的访问。

4) 我们在c/c++使用的函数库在底层可能封装了对应的系统调用。

OS的核心功能:

在整个计算机软硬件架构中, 操作系统的定位是:一款纯正: "搞管理"的软件。

那我们现在就要去深度的去理解管理的概念了。

深度理解管理:

我们举一个例子:

在学校有很多的角色,在这里我们主要以学生, 导员, 校长。 来进行直接的讲解。

我们的学生扮演的角色是被管理者, 我们的校长是管理者。

日长中我们身为学生时不要和校长直接的进行见面的。一般校长的决策是通过导员让后导员在告知学生的。 所以管理者和被管理者是不需要直接的进行见面的。 假如我们刚入学需要统计学生的信息。

校长1: 技术: 熟练的使用各种办公软件。

那么校长会告诉导员,经过班干部, 最后给各班的学生进行说学生填写各种的信息, 然后班干部交给了导员整理后交给了校长, 假如校长寻找数学成绩最高的去参加数学比赛, 那么直接对于这个文档进行排序找到最高数学成绩的学生。

校长对于表格的查找本质上就是对于该表格的增删查改。

校长2: 在当校长之前,该校长是一个顶级的程序员, 他熟练掌握个中的C语言语法。

校长得到这些信息后, 就创建了一个 struct_stu(学生链表)来管理学生的各种的信息。

然后校长对于表格的操作就转换为了对于链表的增删查改。

所以我们能够得到一个结论我们有"管理"的概念是都是"先描述在组织"。

理解先描述再组织:

所以我们校长得到学生的信息是属于导员将学生的信息描述给了校长(描述)。 然后在经过校长的一段操作(表格, 链表)这属于将学生的信息进行统一的管理起来(组织)。

系统的调用和库函数的概念:

理解这一块时我会给大家提供一个场景的描述来加深对于这里的理解

银行取钱: 这里可以简单的看作一个简易的操作系统:

它会给我们一些窗孔来进行操作, 我们不能直接的去访问金库因为银行系统不相信我们任何人。

所以操作系统不想信我们任何人和用户。 他们会给我们暴露一些接口给我们进行调用。 (这些接口统称为系统调用。 fork...一些列的系统函数)。

结论:

综上所述

在系统的层面上操作系统表现为一个整体, 但是会暴露一些接口供我们上层使用, 这种函数的调用我们统称为系统调用。

系统调用在使用上,功能比较基础, 对用户的要求也比较高, 所以有心的大佬会可以对内部的系统调用进行封装, 从而形成了库, 有了库就更有利于上层用户或开发者进行二次开发了。

库的调用需要查看是否进行了硬件的访问, 如果访问了那他一定时系统调用。


进程:

什么是进程:

课本上的概念:程序的一个执行案例, 正在运行的可执行程序。

在我们的内核层面: 担当分配系统资源(PCB时间,内存)的实体。

我认为的进程是:内核操作系统(task_struct, 简称PCB)+自己的可执行程序。

当然这里也是遵循"先描述在组织的", 描述: PCB ,组织: 数据结构。

对与进程的描述就是对于链表的增删查改。

现在我将要给出一个场景来更好的将这一块来进行更好的描述。


场景的描述:

场景1:

我们在在电脑桌面上想要打开我们的QQ, 我们的QQ当时没有直接加载到对应的内存中, 我们就会在磁盘中将QQ加载到内存中。 同时我们只是将QQ的exe文件加载进去了。 当我们笔记本开机的时候我们会等很长的一段时间那是因为电脑在加载对应的操作系统。 这是我们的电脑会将QQ对应的属性加载进去的(也就是对应的PCB)。

图解:


描述进程---PCB

基本概念:

进程信息被放在一个进程控制快的数据结构中,可以理解为进程属性的集合。

课本上称之为PCB(process control block), Linux 操作系统下的PCB是task_struct.

task_struct 是PCB的一种

在linux中描述进程的数据结构的结构体叫做task_struct。

task_struct 是linux内核的一种数据结构,他会装在到RAM(内存)里并包含进程的信息。

我们之前的命令和可执行程序全部都是进程, 运行起来。


task_struct

内容分类:

标识符: 描述进程的唯一标识符,用来区别其他的进程(pid)。

状态: 任务状态, 退出代码,退出信号等。

优先级: 相对于其他进程的优先级。

程序计数器: 程序中即将执行的下一条指令的地址。

内存指针: 包含进程代码和进程相关数据的指针,还有和其他进程共享的内存指针。

上下文数据:进程执行时处理器的寄存器中的数据。

I/O状态: 包括显示的I/O 请求, 分配给进程的I/O设备和被进程使用的文件列表。

记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制, 记账号等。

其他信息...。


组织进程

我们可以在内核源代码中找到它,他是一个双向链表

图解:

大概就是一个这样的模型

认识getpid和getppid

实现我们使用man指令进行查询:

复制代码
man getpid

查询的内容

他们的返回值是pid_t 我们在当前阶段可以将他们理解为int,他像我们之前的int,char...。 pid_t 他是系统给我们提供的数据类型。所以为我们采用int来进行打印。

所以现在我们写个程序开进行使用一下吧。

演示:

代码展示:

cpp 复制代码
  1 #include<stdio.h>
  2 #include<sys/types.h>  //getpid头文件
  3 #include<unistd.h>     //getpid和sleep的头文件
  4 
  5 int main()
  6 {
  7     while(1)
  8     {
  9         printf("当前的进程为:%d\n", getpid());//打印对应的进程
 10         sleep(5); //让程序休眠5s                                                                                                                                       
 11     }
 12 
 13     return 0;
 14 }
~         

我们编译运行一下:

就能查看当前的pid了。

进程的查看:

在系统中查看进程

方法一:

ps命令:

用法: ps 选项一般是ajx 文件名

a:代表所有。

ps ajx 查看系统的所有进程。

cpp 复制代码
ps axj

能查看系统的所有进程。

当然还是上面的那么死循环程序我们要查看他对应的进程信息。

这时候我们要打开两个窗口来进行操作了

我们使用指令为:

写法一:

cpp 复制代码
ps axj | head -1 ; ps axj | grep ./pid

写法二:

cpp 复制代码
ps axj | head -1 && ps axj | grep ./pid

指令解析: ps axj | head -1 打印所有的进程信息我们去第一行的介绍, ";", "&&"是将前后的指令进行合并起来。 后面的指令是我们打印所有的指令但我们过滤只要./pid 的信息。

现在会有个小小的问题, 为什么我会有两行进程。

我们因为使用了grep这样也会创建对应的进程, 所以他会有这一行,我们可以试着将他们过滤掉

cpp 复制代码
方法一:
ps axj | head -1 ; ps axj | grep ./pid | grep -v grep
方法二:
ps axj | head -1 && ps axj | grep ./pid | grep -v grep

方法二:

通过文件来进行查询.

对应系统的进程都放在/proc(内存文件系统)上面我们可查询下这个文件。

以文件的形式查看进程, 在文件系统中proc记录的是当前系统的进程的信息,该目录下的所有数据对应的都是一个进程。

cpp 复制代码
ls /proc

这样我们也能查到对应的进程

方法三:

top指令这里不做过多的介绍了一般前两者的使用频率更多一点。

杀死进程的方法:

方法一:

快捷键:【ctrl】+ c

当时就没有将后续的内容了

方法二:

kill指令:

语法 kill -9 进程的pid / kill 信号 进程的pid

-9是一个信号后续我们会讲对应的信号的,在Linxu中会有很多的信号给我们进行使用。

-8 : 暂停的进程继续运行。

当然这两中都可以杀死进程但有些特殊的情况只能使用kill, 快捷键有些情况是不能进行使用的。

两种方法都较为常用。 所以一些情况我们是指使用kill的。 我们后续会说明什么时候快捷键会失效。

父进程的查询方法: getppid

我们使用man看看getppid的基础信息:

那我们写一份代码来看看我们os会将这份代码的父进程会默认给谁。

cpp 复制代码
  1 #include<stdio.h>                                                                                                                                        
  2 #include <sys/types.h>
  3 #include <unistd.h>
  4 
  5 int main()
  6 {
  7     printf("pid对应的父进程为: %d ,子进程为:%d\n",getppid(),getpid());
  8     return 0;
  9 }
~           

这个子进程是我们当前程序的进程,那这个父进程是神么呢???

我们可以试着查询一下这个父进程是什么东西。

在这里我们能发现这个父进程是bash。那么是不是可以知道了Os把所有我们所创建的进程都交给bash来进行管理了。

我们在打开一个进行查看, 我们发现它有多了一个bash那就说明:OS会给每个人分配一个bash

bash是一个命令行解释器, 每次登录都会有一个命令行解释器。

我们这里补充一个小知识点

查看ls /proc/ 进程

我们知道ls /proc/ 进程 可以查看对应进程的详细信息。

我们找到的结果:

cwd:

这个cwd是什么呢???

cwd 本意为current work dir : 可可执行文件所在的路径。

这里我们可以发现=他就是当是我们所查询这个进程的可执行文件。

exe:

那这个exe就是可执行文件。

那么我们学过c语言的文件系统都知道我们fopen("文件名","r/w/a")

我们的文件就是在当前的目录下进行了创建那么OS就是通过这种方式来进行了文件的剪切, OS在我们创建文件的时候会将cwd这些信息自动的给加上。

我们可以写一份代码来进行验证一下:

修改cwd的方法: 函数chdir

我们要认识一个函数:chdir

函数的信息, 也是一个系统函数。 修改当前路径的cwd

我们可以发现果然改了就会到这个路径下了,不进行修改的话会在cwd这个路径(这里不演示了大家可以动手试试)。

cpp 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 {
  6     chdir("/home/zb");                                                                                                                                                                        
  7     fopen("aaa.txt","w");
  8     return 0;
  9 }

进程的创建---fork

我们在启动程序是会自动的自动的给与一个进程, 而这个进程的父进程会默认给bash来进行管理, 我们也想通过这种方式来给对应的进程创建他的子进程那么这时候就需要一个系统函数fork来进行对应的操作我们可以先进行查看fork对应的文档。

bash 复制代码
man fork

头文件:

返回值: (灰常重要)

如果失败就返回一个<0的值, 如果成功将子进程的pid返回给父进程, 将0返回给子进程。

那我们就写一段代码来验证这一快的为问题吧。

代码块:

cpp 复制代码
    1 #include<stdio.h>
    2 #include <sys/types.h>
    3 #include <unistd.h>
    4 
    5 int main()
    6 {
    7     pid_t pid=getpid();
    8     printf("pid对应的父进程为: %d ,子进程为:%d\n",getppid(),getpid());                                                                                                                     
    9     pid_t id = fork();
   10     while(1)
   11     {
   12      if(id < 0)
   13      {
   14             printf("无效进程\n");
   15         }
   16      else if(id==1)
   17      {
   18          printf("这是一个父进程:父进程为: %d ,子进程为:%d\n",getppid(),getpid());
   19          sleep(5);
   20      }
  21     else
   22     {
   23         printf("这是一个子进程:父进程为: %d ,子进程为:%d\n",getppid(),getpid());
   24         sleep(5);
   25     }
   26     }
   27     return 0;
   28 }

我们在这运行一下:

那么到了这里我们会有些疑问

我们在C/C++阶段没有一个函数是有两个返回值的, 并且也没有一个程序能同时走if和else if的。

还有为什么要把子进程的pid给父进程。

那我们现在就来解答这些问题。

问题一 : 为什么要把子进程的pid给父进程

因为父进程: 子进程=1:n

一个父进程要管理多个子进程,而一个子进程只能有一个父进程。 所以父进程要管理多个子进程。

问题二: fork函数为什么会有两个返回值。

那么就得了解fork内部的一些操作了。

子进程没有自己独立数据,只能暂时的共享父进程的数据。然后子进程被进行了调度, 那么这两个执行流就一直的被运行到了最后return是函数会执行两次。 父进程返回一个值, 子进程返回一个值, 所以它会返回两个值。

问题三: 为什么他能同时执行if和else if

因为它有两个输出流,两个输出流执行的结果是不一样的, 并且这两个执行流是相互独立的

在父子任何一方进行修改数据OS会把修改的数据在底层拷贝一份让目标进程修改这个拷贝。 ---写时拷贝。父子间也是相互独立的。

进程的状态:

在linux中描述进程进程状态的就是一个整形变量。 进程之间具有独立性。

理解独立性:

我们同时使用QQ和微信是突然我的QQ挂了他只会影响QQ不会影响到微信, 我的微信还是能正常的运行的。

进程的大致状态图:

深度理解运行&&阻塞&&挂起

运行: 我们在运行程序的时候我们将它按照优先级(PCB 中的一个整形变量)进行排序, 并不是一定得正在运行, 而是将这个PCB放入了运行队列中。

阻塞: 先说一个简单的了解我们C语言的scanf和c++的cin这两个都是将我们在运行是光标会卡在那个地方, 等在我们键盘鼠标进行写入。 我们一直不写入光标会一直卡在哪里。 这就叫做阻塞。

挂起: 我们内存中如果程序占用内存太大了我们就会将这个程序在运行队列中pop掉, 然后放入磁盘中等待合适的机会再次将进程进行调度(阻塞挂起)操作系统会有一个swap分区它能将我们的代码进行调换, 等到运行的时候在给我们调度到运行队列。

我们在系统中会存在一些队列, 有running队列是我们正在进程处于运行状态时候所在的队列, 当然我们运行完这一次的结果, 可能会进入等待队列中, 在等待队列的代码和数据还有可能会被重新的进行调用, 那么对应的pcb会在等待队列中再次的放入运行队列中。当然这其中的调度符合FIFO(先进先出)。

进程的状态类型:

R(running):运行状态。
S(sleep):阻塞。 (可杀)

在代码中的体现, 我们在下面可以看见我们运行时它的状态是S+

我们要将这些进程进行循环打印。

对应的指令:

bash 复制代码
while :; do ps axj | head -1 && ps axj | grep test; sleep 1;done
cpp 复制代码
  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     printf("这是一个进程。进程的pid:是%d, 父进程的pid:%d\n",getpid(), getppid());
  8     pid_t pid = fork();
  9     while(1)
 10     {
 11         if(pid==0)
 12         {
 13              printf("这是一个子进程。子进程的pid:是%d, 父进程的pid:%d\n",getpid(), getppid());
 14 
 15              sleep(1);
 16         }
 17         else
 18         {
 19              printf("这是一个父进程。进程的pid:是%d, 父进程的pid:%d\n",getpid(), getppid());
 20         }
 21     }                                                                                                                                                                                         
 22     return 0;
 23 }

为什么都是S呢?

那就印证了我们上面说的我们对应的进程不可能都是在运行队列中,当运行队列运行了一段的时间对应的进程就会被CPU 放入一个等待队列中,即使我们是1s打印一次但是我们打印的都是在等待队列中。

为什么我们有个+呢? +是什么意思呢?

这个命令是把可执行程序放在后台运行。

bash 复制代码
./test & 

我们发现没有+ 了。

所以我们带有+ 的程序时程序在前台的运行。

D(disk sleep): 深度睡眠。(在高I/O的地方触发)(不可杀进程)
T(stopped): 暂停 (【ctrl】+z)来暂停这个进程。

能看到对应的的状态为T。(特定的显示器来进行止损的)

t:暂停 debug断点会触发。

将rease改为debug

bash 复制代码
gcc -0 test test.c -g

进入debug

bash 复制代码
gdb test

b打断点。运行到断点出就能看到了

X:死亡状态

表示进程已经死亡

Z: 表示僵尸进程。

我们将进程杀死他们就会进入僵尸。 僵尸状态主要是为了获取错误信息, 当我们子进程退出时, 我们子进程会给父进程一个退出信息表示我们子进程已经退出。若是没有获取子进程的信息那么就会进入僵尸状态, 子进程就会一直挂在哪里那么就会一直的进行悬空。 那么就会造成内存的泄漏。我们可以使用wait()来进行将退出信息给出去。子进程退出父进程还在跑就说明处于Z状态。

僵尸进程的危害:

1.父进程一致不读取会导致子进程一致处于Z状态。

2.僵尸状态一致不退出那么我们的task_struct会一直的维护他。

  1. 一致不回收会导致内存的泄漏。

进程状态查看

ps指令
选项:

a: 显示一个终端所有进程, 包括其他用户的进程。

x: 显示没有控制终端的进程。 后台运行的守护进程,

j: 显示进程归属的进程组id、会话id、父进程id, 以及与作业控制相关的信息。

u: 以用户为中心的格式显示进程信息, 提供进程详细的信息, 如用户、CPU和内存使用的情况。

孤儿进程

场景:

如果在一个程序中父进程退出了, 子进程还在跑,那么我们对应的进程会变成孤儿进程。

我们通过代码的方式验证一下:

cpp 复制代码
  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<unistd.h>
  4 
  5 int main()
  6 {
  7     printf("这是一个进程。进程的pid:是%d, 父进程的pid:%d\n",getpid(), getppid());
  8     pid_t pid = fork();
  9     if(pid==0)
 10     {
 11           while(1)
 12           {
 13              printf("这是一个子进程。子进程的pid:是%d, 父进程的pid:%d\n",getpid(), getppid());
 14 
 15              sleep(1);
 16 
 17              }
 18     }
 19     else
 20     {
 21         int cnt=5;
 22         while(cnt--)
 23         {
 24             printf("这是一个子进程。子进程的pid:是%d, 父进程的pid:%d\n",getpid(), getppid());
 25             sleep (1);                                                                                                                                                                                                           
 26         }
 27     }
 28     return 0;
 29 }

注意: 变成孤儿进程后就会在后台进行运行我们无法用【ctrl】+c进行杀掉, 所以我们可以得到结论【ctrl】+ c无法杀死后台进程。 得使用kill -9 来进行杀死。

我们发现父进程推出后我们的子进程就会被1号进程进行领养, 那么这个一号进程是什么呢, 我们可以查一下。

我们top一下

我们对应的孤儿进程会被system(也就是操作系统)进行领养。

如果不领养的话会出现什么问题?

那么子进程会变成对应的僵尸进程。父进程的父进程是bush因此不会进入孤儿进程。

进程的优先级

基本概念: 简单来说就是得到资源的先后顺序, 当然在OS中也有对应的先后顺序,有了对应的优先级系统运行的效率会快很多的。也就是我们得到CPU资源的先后顺序。

场景描述: 我们在排队的时候的先后顺序也是对应的优先级, 我们买到饭的先后顺序表。

优先级ov权限:

优先级是都能得到资源只是先后的顺序不同而已, 权限是是否能够的到资源。

查看对应的系统进程:

查看当前运行的所有进程详细:

bash 复制代码
ps -al

我们能看到很多的名词。

我们现在来分别的解释对应名词所代表的概念:

UID : 代表执行者的身份。

我们的ls -n能将用户以对应的UID进行显示。

我们能看到对应的1007 代表我现在的这个用户, 那是不是我们也可以说明我们的用户也是一个进程。 linux中访问任何资源都是进程的访问。进程代表用户。

bash 复制代码
ls -aln

PID: 代表父进程, 只是个进程的代号。

PPID: 代表当前这个正在运行的进程。

PRI: 代表当前的优先级, 默认为80, PRI的数字越小所代表的优先级越大,进程最先被执行。

NI: 表示进程优先级的修正数据(nice值)。

进程的优先级=PIR(默认) + NI

当然NI> 0时让我们的程序后一点被执行, NI<0让程序优先被执行。

所以在linux下我们修改对应的优先级值本质上就是修改nice值。

修改NI(nice值):

方法一: top命令:

  1. 输入top
bash 复制代码
top

2.点击"r"进入

3.输入对应的进程

4.设置nice值:

查看

bash 复制代码
ps -al
bash 复制代码
ps -l PID

发现NI的值果然已经改为了12且当前的进程也变为了看92

但是这样top有个缺点就是不能一直的进行修改。

方法二:

使用nice和renice进行修改

nice的使用:

语法:

nice -n NI值 还没有运行的程序

renice的使用:

语法:

renice 新NI值 -p PID

renice -n 新NI值 -p PID

总结:

一句话总结nice值运用于未运行的程序,renice是适用于正在运行的程序。

方法三:

系统函数:

由ai生成:

NI值的详解:

NI(nice值)的取值范围:【-20, 19】

我们可以初步的进行验证这个值

我将值设置为-100 它最多只能接受-20的值。所以NI值最小是-20

注意:要提升优先级需要进行提权(sudo)

我将权限的NI设置为100, 他只能到19所以NI的最大值为19。

所以优先级的范围是:[60,100) PRI=80.

总结: 优先级设置不合理会导致优先级低的进程得不到CPU的资源, 进而导致进程饥饿。

补充: 竞争、独立、并行、并发

竞争: 很好理解就是在很多的进程中CPU的资源是少量的,进程之间得抢占CPU的资源所以进程之间是具有竞争属性的。

独立:多个进程运行时, 需要独享资源,各个进程之间相互不影响。

并行: 多个进程之间如果存在多个CPU这两个CPU会同时进行, 称之为并行。

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

进程切换:

概念:

CPU上下文切换:其实际含义是任务切换,或者CPU寄存器切换。当多任务内核决定运行另外的任务时,它保存正在运行任务的当前状态,也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中,入栈工作完成后就把下⼀个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器, 并开始下⼀个任务的运行,这⼀过程就是contextswitch。

在我们的CPU中有许多的寄存器, 每个寄存器都有相对的作用,形如eax ebx epx(栈顶寄存器)

寄存器就是CPU内部的临时空间。

注意: 寄存器 != 寄存器里面的数据空间。

当我们在进行A进程的时候我们将A进程的数据进行加载到CPU内部, 然后等到切换B进程的时候我会把A进程中的数据进行拷贝, 放在一个空间中进行保存。 然后进程B将之前A在CPU的内容直接覆盖试的进行替换,等到下次调度A进程的时候在将B进行进行拷贝然后将A进行加载。

一致这样就能实现进程之间的替换。

当然它这样是有一个时间片的概念,经过这个时间片就进行进程的切换。

时间片:

当代计算器都是分时操作系统,每个进程都有它适合的时间片(实际上就是一个计数器),时间到达进程就在CPU中剥离下来。所以进程运行具有临时性。

所以死循环不会一直占有CPU的资源,因为时间片的原因等过了这段时间对应的进程会被剥离下来。

所以进程切换的本质就是恢复当前进程的硬件上下文的数据。

Linux的内核进程和调度队列

下图的runqueue就是一个调度对列:

首先我们要先了解一下:一个CPU只拥有一个runqueue。

在我们运行对应的进程是大部分都是在调度对应的调度队列。

实现在了解上面的结构中我们要先进行深度了解优先级的概念:

优先级的分类:

我们日常分为两种优先级:

1.实时优先级: 这个优先级很高,只有等我运行完了下面的程序才能进行对应的运行, 这个大概一般在工业领域使用的多(例如汽车)。

  1. 分时操作系统: 这个有自己的调度队列。他又内核抢占的特点。

运行队列:

这就是对应的运行队列, 我们这queue开了140个空间, 其中这个空间的前100个属于实时优先级不受我们的管控,当然后面的100-139属于普通的优先级。

映射的规则: x(当前优先级)-60 +(140-40)。

我们这个节点queue里存储的不是一个个的数据而是一个链表: 这个类型大概是一个task_struct*类型。后面链接的是相同优先级的进程。当然这个样子有点像哈希表的味道。

我们在找寻一个进程的时候我们会将后40进行遍历一边但是这样虽然也是O(1)的时间复杂度, 但是从算法的角度上来说这就是一个 O(n)的时间复杂度, 当然这里我们也有了对应的改善。我们上面的bitmap是一个位图,他能帮助我们快速的进行查找让时间复杂度变为O(1)。

我们bitmap的类型是unsigned int 32个比特位。一共5个空间 那就是32x5=160;正好140个调度队列一一对应。.形如......000100表示第3个位置有进行正在等待被进行调度。1:表示有进程 2: 表示无进程。

上面的nr_active的意思是表示这个队列中有多少个进程。

过期队列:

当然我们是分时优先级所以被调度完队列就会被连入一个一摸一样的队列叫做过期队列。在这个队列中我们在前面运行队列运行完了之后就会把对应的进程连入到过期队列中。

所以我还可能有arr2来指向两个队列。

active和expired指针

上面的active指向的是运行队列, 而expired指向的是过期队列。

在运行的过程中我们的活跃队列的进程是越来越少的, 而过期队列的进程是越来越多的。

等到活跃队列结束时我们的OS会进行swap(&active,&expired)的操作。然后继续的进行运行。

当我们在运行的时候再次开启一个进程是把这个进程放在活跃队列还是过期队列中呢???

如果是在过期队列中,那就符合我们的进程在等待, 就说明进程处在就绪状态。

我们之前说过分时时间片具有抢占资源的特性, 所以我们加载进来的新程序大概率会进入活跃队列中。

我们运行的时候他不会直接的加载进来,因为调度队列的第一个参数就是lock锁。

本篇完, 下篇预告: 环境变量

相关推荐
BizViewStudio1 小时前
2026 年 GEO 成为企业线上流量增长核心风口|2026 品牌 GEO 运营指南,6 家全链路优化服务商解析
运维·网络·人工智能·microsoft·ai
A_humble_scholar1 小时前
Linux(六)深入理解 Linux 进程管理:从硬件到调度
linux·网络
Gong-Yu1 小时前
MySQL数据库运维——性能优化进阶1️⃣
运维·数据库·mysql·性能优化
曦月合一2 小时前
在 Linux 服务器上执行这些命令来导入 SSL 证书
linux·服务器·ssl
sdm0704272 小时前
网络原理-5.NAT技术
服务器·网络·智能路由器
一拳一个娘娘腔2 小时前
CVE-2026-46300 — “Fragnesia“ 深度拆解:当修复补丁亲手唤醒了另一只恶魔
linux·安全
蜀道山老天师2 小时前
OpenClaw 从零部署 + 飞书机器人完整接入(实操篇)
运维·docker·容器·飞书
花伤情犹在2 小时前
Hermes 清理飞书会话操作指南
linux·sqlite·飞书·agent·hermes
小小测试开发2 小时前
Goose AI Agent 完全指南:Linux 基金会加持的开源 AI 编程助手
linux·人工智能·开源