Linux进程
操作系统中,进程是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。在操作系统内,会同时运行很多个程序,而每个程序都会被加载到内存里,在这个过程中就一定会同时存在很多的进程。操作系统首先需要采用struct结构体来描述进程,结构体里保存着进程的相关属性和信息,接着通过这些结构体对多个进程进行管理(先描述,后组织),这是进程的内核数据结构部分;而加载到内存中的是与进程相关的代码和数据。综上
目录
3.D(Disk sleep):Linux特有的休眠状态,不可被中断的休眠状态(深度睡眠)
一.基本概念和基本操作
1.1 基本概念
操作系统中,进程是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位。
在操作系统内,会同时运行很多个程序,而每个程序都会被加载到内存里,在这个过程中就一定会同时存在很多的进程。操作系统首先需要采用struct结构体来描述进程,结构体里保存着进程的相关属性和信息,接着通过这些结构体对多个进程进行管理(先描述,后组织),这是进程的内核数据结构部分;而加载到内存中的是与进程相关的代码和数据。
综上所述,本质上,进程=内核数据结构(task_struct)+自己的代码和数据!
1.2 描述进程-PCB
1.2.1基本概念
PCB(Process Control block)
Linux操作系统下的PCB是task_struct。task_struct是操作系统用于管理多个进程的内核数据结构
1.2.2内容分类
标识符:描述本进程的唯一标识符,用来区别其他进程,pid.
状态:任务状态,退出代码,退出信号等。
优先级:相对于其他进程的优先级。
程序计数器:程序中即将被执行的下一条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据:进程执行时处理器的寄存器中的数据 [休学例子,要加固 CPU,寄存器]。
I/O 状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟总数和,时间限制,记账号等。
其他信息
1.2.3组织进程
task_struct可以在Linux的内核源代码中找到。所有在系统中运行的进程都会以task_struct双链表的形式存储在内核中。task_struct的结构如下图所示:

此处先做一个引入,具体结构会在下文做详细解释
1.3 查看进程
1.3.1ls /proc
进程的信息可以通过/proc系统文件夹查看。/proc是 Linux 系统中一个虚拟文件系统目录,它的核心作用是提供当前系统的运行状态、进程信息、硬件信息、内核参数等实时数据。
输入指令ls /proc列出/proc目录下的所有内容:

执行ls /proc后,你会看到两类主要内容:
1.数字命名的目录:
这些数字是当前系统中正在运行的进程 PID,每个目录对应一个进程,里面存储着该进程的详细信息(比如进程的内存使用、打开的文件、环境变量等)。例如,/proc/1对应系统中 PID 为 1 的进程(通常是systemd,系统的第一个进程)。
2.非数字的文件 / 目录:
这些是系统级的信息文件 / 目录,用于展示内核和系统的整体状态,
比如:
cpuinfo:CPU 的硬件信息(型号、核心数、频率等)。
meminfo:系统内存的使用情况(总内存、已用内存、缓存等)。
uptime:系统的运行时间。
sys:可配置的内核参数目录。
如果想查看 PID 为 1的进程信息,可以执行ls /proc/1 来查看进程的PID为1的进程信息

/proc显示的数字命名的目录是内核操作系统内部实时的每一个进程的PID命名的文件夹。进程存在则文件夹就存在,如果进程不存在那么这个文件夹就会自动被关闭。
首先使用一个死循环的代表一个正在运行的进程

正在运行中的进程输入ls /proc/3738120 -d 是可以查看到这个文件夹的。这也就说明了/proc显示的数字命名的目录是实时的每一个进程的PID命名的文件夹。
但当我使用ctrl^c将进程终止后输入ls无法查询

说明进程被终止后进程的系统文件夹也自动被关闭。
1.3.2查看和修改进程的工作路径
重新执行这段代码,输入ls /proc/PID -l
可以看到如下图的cwd

cwd(creat work director)
每个进程在启动时,都默认会有自己的cwd。当一个进程启动时,都会记录下这个进程启动时当前所在的路径。所以当前路径本质上就是当前进程的工作路径。
如果要修改一个进程的工作路径,则需要使用系统调用chdir。修改进程的工作路径就是修改进程的PCB中记录的当前进程的cwd路径,本质上就是修改内核数据结构。
使用man来查看chdir的详细信息

那如何证明使用系统调用chdir后当前进程的工作路径就被改变了呢?
使用下面的方法来说明:

当执行如上代码时,文件编译运行后会在当前目录下创建一个log.txt

但是当我们加入系统调用chdir后,如下图:

当文件编译运行后,输入ls -l会发现,log.txt并没有被创建在当前路径下。当我们返回到指定的路径下输入ls -l,会发现有一个新的log.txt被创建出来了。也就是说当前进程的工作路径被成功修改了。

1.3.3ps aux / ps axj 命令
a:显示一个终端所有的进程,包括其他用户的进程。
x:显示没有控制终端的进程,例如后台运行的守护进程。
j:显示进程归属的进程组 ID、会话 ID、父进程 ID,以及与作业控制相关的信息
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU 和内存使用情况等
ps ajx|head -1&& ps ajx|grep
ps ajx|head -1:将头部信息展示出来方便查看
ps ajx|grep :使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵尸、哪些进程占用了过多的资源等等

头部信息:
UID:代表执行者的身份。
PID:代表这个进程的代号。
PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号。
PGID:代表进程组ID。多个相关进程会被归为一个进程组(通常由同一个父进程创建),进程组的 ID 默认等于该组的第一个进程(组长进程)的 PID。
SID:代表会话 ID。会话是比进程组更高层级的管理单位,一个会话包含一个或多个进程组,通常对应一个终端(控制终端)。
TTY:代表进程关联的终端设备。表示进程是在哪个终端上运行的。
TPGID:代表终端前台进程组 ID,表示当前终端的前台进程组的 ID(前台进程组是指正在终端接收用户输入的进程组)。
STAT:代表进程状态码,用于反映进程当前的运行状态。
UID:代表进程所属用户的 ID,表示运行该进程的用户的数字标识(Linux 中每个用户有唯一的 UID,如 root 的 UID 是 0,普通用户从 1000 开始)。
TIME:代表进程累计占用的CPU 时间(单位:分钟:秒)。
COMMAND:代表启动进程的命令行(或进程的名称)。
1.4父进程和子进程
1.4.1进程PID的获取
一个进程如果要获取自己的标识符(PID)应该怎么做呢?
获取自己的标识符就是获取自己的task_struct结构内部的属性值,但是我们不能之间获取到进程task_struct结构内部的属性值,所以操作系统会给我们提供一个系统调用来获取。
使用man getpid来查看getpid()这个系统调用的详细信息

由上图可知,使用getpid()和getppid需要包含的以上两个头文件
pid_t:在绝大多数 Linux 系统中,pid_t被定义为有符号的 32 位整数(int或long),具体取决于系统架构(32 位 / 64 位)
getpid():获取当前进程的pid
getppid ():获取当前进程的父进程的pid

当前进程的PID是3706015,PPID是3694004。
1.4.2父进程创造子进程的过程
进程存在进程地文件夹就在,当进程结束时系统文件夹会随之消失
在Linux系统当中,新的进程往往是以父进程的方式创造出来的
创建进程,本质上是操作系统中多了一个进程。但是用户不能直接创建进程,需要系统提供系统调用来创建。而这个系统调用就是fork(),用户可以通过这个系统调用来创建进程。
输入man指令可以查看fork()的细节
使用fork()需要包含的头文件

fork()的返回值:
![]()
使用fork()之后进程将一分为二,fork()之后代码是共享执行的,而返回值可以用于父子分流
一份代码,两个进程。使用fork后,是子进程时id值返回0,是父进程时返回子进程的pid。一个父进程可以有多个的子进程,子进程和父进程的比例是n:1。因为父进程可以有多个子进程,给父进程返回子进程的pid本质上是为了标识指定的子进程,用来区分是哪一个子进程,未来控制指定的子进程。

运行结果:

fork()创建子进程时以父进程为模板。当成功创建子进程后,会使用父进程的属性来初始化子进程。也就是说,在默认情况下,fork之后后续代码和数据父子进程共享。
命令行解释器-bash

bash是使用c语言编写的的。在没有成为进程之前,它是磁盘中的一个二进制文件。
bash本质上也是一个进程。当我们登录后,系统就会给我们分配一个bash进程,bash会不断从键盘上获取用户输入的命令行字符串,将用户输入的命令转化为进程。当我们在命令行中输入命令时,它读取完命令之后,就会在自己内部使用fork()来创建子进程。
二.进程状态
2.1核心状态
1.运行
进程的task_struct在调度队列时,该进程的状态就是运行状态。
2.阻塞
当进程处于运行状态时,若执行到需要等待外部资源的操作(比如调用scanf等待用户输入),进程会从运行状态切换为另一个状态。这里的另一个状态就是阻塞状态。
运行状态与阻塞状态相互切换的具体过程为:操作系统会将该进程的task_struct从从运行单元调度队列中剥离出来,放入等待的硬件单元所描述的内核数据结构所对应的等待队列中,此时进程的状态由run->block。当硬件资源就绪,比如用户完成输入后,硬件状态发生改变,操作系统查看硬件等待队列发现有进程在等待,将该进程状态由block->run,将进程的PCB重新放回调度队列中,等待 CPU 调度执行。
由运行状态与阻塞状态相互切换的具体过程我们可以得到,运行和阻塞的本质就是看进程的task_struck在谁的队列里,在等待队列时就处于阻塞状态,在调度队列时就处于运行状态。
3.挂起
进程的task_struct保存在内存中。当系统内存不足的时,操作系统会将部分代码和数据放入(swap out)磁盘中的swap分区,称为挂起状态。当PCD链入调度队列,需要进程对应的代码和数据时,系统会将这部分代码和数据从swap分区中取出(swap in)。若本身的进程是阻塞的,又因为系统内存不足将阻塞的进程相对应的代码和数据放入swap分区中,称为阻塞挂起;如果阻塞的进程相对应的代码和数据已经全部放入swap分区后,内存依然资源不足,操作系统就会将处于运行调度队列中但是还未运行的代码和数据放入磁盘的swap分区中,称为运行挂起;如果还是不足,linuxOS就会选择性地杀掉一些特定的进程。
2.2 具体操作系统的状态 Linux

2.2.1Linux中的状态
1.R(Running):运行状态
进程要么正在 CPU 上执行,要么就是处于就绪队列中等待被调度执行。即使进程在等待 CPU 时间片,状态也会显示为 R,而非阻塞态。
2.S(Sleep):休眠状态,可被中断的休眠(浅度睡眠)
进程因为等待某个事件(如 I/O 完成、信号到达)而暂停。S状态可以被信号(如 SIGTERM)唤醒并转变为运行状态。
我们可以使用kill -9 (SIGKILL)+进程的PID将 指定的进程杀掉


再查看进程时该进程就消失了

3.D(Disk sleep):Linux特有的休眠状态,不可被中断的休眠状态(深度睡眠)
当进程涉及数据的保存,往磁盘写入数据时,此时进程状态为D。D状态的进程不能被磁盘杀掉。
Linux中的S和D状态都是阻塞状态。
4.T(stopped):暂停状态
下面是将进程的状态变为暂停状态的两个方法:
1.手动输入命令
输入命令kill -19 (SIGSTOP)后,进程的状态就变为暂停状态:

当需要将暂停状态转换为运行或休眠状态时,输入命令kill -18 (SIGCONT):

2.自动
【scanf 变后台】

在while循环前加一个scanf,让终端等待键盘输入数据。如果直接运行./code1,此时进程为前台进程,处于休眠状态,因为终端正在等待硬件输入数据。但是如果运行。/code1&,此时进程为后台进程,在查看进程状态时会发现进程处于暂停状态。

原因是:由于只有前台进程才能从键盘上读取输入,而后台进程不能从键盘上读取输入。如果直接执行./code1&将进程变为后台进程,而后台进程又想从键盘中读取输入,那么操作系统就会直接将这个后台进程暂停,不允许后台进程进行从键盘上读取输入的操作。
5.t(tracing stop):追踪暂停
追踪一个进程所处的状态
在 Linux 中,当你用 GDB 调试一个进程时,通过 ps 命令查看该进程状态,会发现进程状态显示为 t,这表示该进程被调试器(GDB)附加并暂停,处于追踪暂停(tracing stop) 状态。
此时进程会停止执行所有指令,等待调试器的命令,这是 GDB 调试的核心状态之一。
6.X(dead):死亡状态
操作系统立即释放死亡状态的进程的内存空间
7.Z(zombie):僵尸状态
一个进程死亡时不能立即处于X状态,需要先处于Z状态。
例如,子进程先退出,父进程不退出,此时子进程的状态就是Z状态。

当进程处于僵尸状态,此时只保留这个进程的task_struct,未来让父进程或操作系统获取子进程的退出数据。
当通过一定方式读取Z状态进程退出时的相关信息,读取完毕后将进程的Z状态变为X状态后,操作系统才能释放掉这个进程占有的所有资源。
进程的退出信息保存在自己的task_struct结构体中:

系统检测时会回收Z状态本质上是在检查和获取task_struct结构体的上面这部分信息,获取完成之后将状态变为X并释放。
如果父进程创建了很多的子进程,但是一直读取Z状态的子进程的信息,处于Z状态的子进程的进程基本信息就会一直保留在task_struct中,造成内存资源的浪费,导致内存泄漏的问题。这就是僵尸进程的危害。
8.孤儿进程
当父进程先于子进程退出时,这些子进程就会变成孤儿进程。当父进程退出时,系统会检查该进程的所有子进程,将这些子进程的父进程的PPID修改为 1(init进程),相当于 这些孤儿进程被系统“领养”。
系统领养这些孤儿进程的目的是为了使得init进程持续监控它的所有子进程(包括领养的孤儿进程),当孤儿进程执行完毕退出时,init 进程会立即回收孤儿进程的退出状态和资源,避免孤儿进程变成僵尸进程。这样也就避免了孤儿进程出现僵尸状态导致内存泄漏问题。
2.2.2前后台进程
1.前台进程
当直接在终端执行 ./code 时,这个进程就是前台进程。它会独占当前终端的输入输出,终的命令提示符会消失,直到进程执行完毕才会恢复。在查看进程状态时,前台进程的状态后会带有 + 号 。此时无法在该终端执行其他 Linux 操作,必须等待进程执行完毕,或主动干预终止进程。

干预的操作有:1.按下 Ctrl + C,发送 SIGINT 信号终止前台进程。2.按下 Ctrl + Z,发送 SIGTSTP 信号暂停前台进程,进程会进入停止状态(Stopped),并返回终端控制权。
2.后台进程

当你执行 ./code & 时,这个进程就是后台进程。后台进程会在后台运行,不会独占终端,你可以立即在终端继续执行其他 Linux 操作。执行后终端会返回一个进程 ID(PID)(其中[1]是任务号),用于后续管理该后台进程。后台进程的状态中不会带有 + 号。

后台进程是多任务在应用层的表现。例如在进行下载任务时,终端可以同时在做下载和其他操作
前台进程只有一个,而后台进程可以有多个。
三、进程优先级
3.1基本概念
进程优先级是cpu资源分配的先后顺序,就是指进程在已经能得到某种资源的前提下,得到某种资源的先后顺序,优先权高的进程有优先执行的权利
3.2优先级存在的必要性
cpu的资源有限,必须合理地分配好资源。为了合理分配资源就设置了进程的优先级,用于决定进程获得某种资源的先后顺序。
在 linux 或者 unix 系统中,用 ps -l 命令则会类似输出以下几个内容:

其中我们会注意到以下几个关键信息:
UID:代表执行者的身份
PID:代表这个进程的代号
PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI:代表这个进程可被执行的优先级,其值越小越早被执行
NI:代表这个进程的 nice 值
3.3优先级的修改
先在终端运行一个进程./code1

当进程运行起来后,输入top指令,如何输入r,系统就会跳出以下这一行提示:PID to renice
如何输入你要修改的进程的PID的值,例如:230059

接着点击回车会提示让你把value改为多少,例如:10

然后输入ps -al指令就会发现PRI和NI的值被成功修改了,如下图:

优先级(PRI)的计算公式为:PRI(新) = PRI(旧) + nice值
所以上面我将nice值调整为10之后,新的PRI的值就变为90
调整进程所对应的优先级,在Linux下,本质上就是调整nice值
由于priority越小优先级越高,优先级越高越先被调度,优先得到资源。所以当nice值为正数,PRI数值调大,优先级变低;当nice值为负数,PRI数值调小,优先级变高。
nice值为正数例如: 80 10 90+10=100 优先级降低
nice值为负数例如 70 -10 70+(-10)=60 优先级提高
nice 值,可被执行优先级的修正数值固定为 -20 ~ 19(共 40 个等级);
PRI 值范围:由 nice 值范围推导,由于nice 值的范围固定为 -20 ~ 19,所以PRI 的变化区间是 60 ~ 99(因为默认 PRI 通常为 80,80-20=60,80+19=99)。
如果短时间内高频修改进程的nice值,系统会拒绝短时间内频繁调整同一进程的 nice 值,这样做是为了避免资源调度混乱。普通用户仅能将 nice 值调大(降低自身进程优先级),只有 root 用户可调小 nice 值(提高优先级)
优先级一共40个梯度。优先级的变化范围有限,因为我们目前使用的主流操作系统都是分时操作系统,给进程分配时间片,是相对公平、公正的分配策略,较为均衡地使得不同的进程在同一段时间内,都能获得cpu分配的资源。这种分配策略决定了优先级的变化幅度不能太大,如果某个进程优先级非常高,会导致其他优先级低的进程长时间得不到cpu的资源,使得系统的调度很难以控制,所以分时操作系统将优先级的范围限制在系统可控的范围内(60-99)。这种分配策略更符合人和互联网的需求。
与分时操作系统相对应的是实时操作系统。实时操作系统的调度算法较为简单,优先级范围更大,一般用于工业生产领域。
3.4补充概念
竞争:系统进程数目众多,而 CPU 资源只有少量,甚至 1 个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立:多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行:多个进程在多个 CPU 下分别,同时进行运行,这称之为并行
并发:多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
四、进程切换
进程切换是操作系统调度器的核心工作,指 CPU 从一个进程的执行切换到另一个进程执行的过程。由于 CPU 的寄存器是所有进程共享的硬件资源,但是寄存器内部的数据本质上是私有的,叫做进程的上下文。每个进程的执行状态是进程的私有数据,当进程暂时被切下来的时候,需要进程顺便带走并保存自己的上下文数据,保存在cpu内的寄存器中;当进程被切换回来时,需要将自己的上下文数据进行恢复。这样才能保证进程再次执行时无缝衔接之前的逻辑。
五、Linux中的进程调度
Linux内核的内核结构——链表结构
c语言中,任何变量的地址数字,都是在开辟的众多数据中,地址最小的那个
结构体中,知道任意一个成员的类型和地址,可以访问任意一个成员。
任意一个成员的地址减去该成员在结构体中的偏移量
Linux中设计了双链表来管理task_struct:

由c语言中结构体的相关知识可得,当我们知道了指定进程的结构体对象的地址后,就可以知道进程的其他属性元素的地址,也就能访问进程的各种属性
使用这样的双链表的方式是为了增加链式管理的拓展性,让内核在对特定数据结构做链式管理时,代码只需要维护一份。而在操作系统层面,Linux内核会将所有进程的task_struct统一放在一个双链表中,而在进程的管理中既有运行队列,也会有阻塞队列等一系列队列来管理进程,此时就需要多个链表来分开管理不同的队列,当进程处于相应的状态时,就将进程的task_strcut中的节点插入到相应的链表中,从而使得在能够管理进程状态的同时,所有进程的task_struct仍然处于同一个双链表结构中。如下图所示:

甚至,不同的结构体对象、二叉树、哈希表等数据结构也能通过链表连接。因此,在Linux中,一个进程的task_struct可能属于很多个数据结构。
5.1Linux内核进程调度队列
每一个CPU都有一个调度队列struct runqueue{}
将其中的queue[140]拿出来分析,可以分为分时优先级和实时优先级两部分,如下图:

前文提到的优先级的变化范围正好是[60,99],有40个梯度。当需要查询某个进程时,就可以通过优先级队列里的数组下标查询。优先级数字,其本质是数组下标。

queue[99]到queue[139],每一个queue又被分为独立的子队列,优先级相同的进程放在同一个队列中。当需要选择一个优先级下的特定的进程时,先确定该进程的优先级是多少,找到相应的队列,然后再通过FIFO的方式,就能选择到特定的进程了。根据优先级选择进程的过程,本质上就是一个哈希的过程。

bitmap[5]表示位图,long类型,一次查询32比特位,查位图比直接查数组效率高,提升了遍历队列时的效率和速度;nr_active 用于记录总进程的数量。当CPU在查找进程时先看nr_active总进程个数,如果>0,再看bitmap,使用位图遍历数组,最后再按照上面的步骤选择具体进程。
5.2存在的问题
上面这样的设计有两个问题,一是进程饥饿问题,如果所有的进程的优先级都是61,不断有优先级为60的进程进入队列,这样会一直新增优先级高的进程(pri=60),而优先级低(pri=61)的一直得不到资源;二是在进行优先级的修改操作时成本较高。
5.3Linux系统如何设计调度队列来解决这种问题
Linux在设计中为了避免这些问题的出现,在调度队列中都有两个完整的哈希表,active指针指向活跃140队列,expired指针指向过期140指针。进程被cpu调度完后,被cpu放入过期队列中,还没调度的进程则一直放在活跃队列中。活跃队列里的进程越调度越少。当其中的进程被全部调度完,cpu会将active指针和expired指针指向的内容进行交换,此时active指针指向过期队列,expired指针指向活跃队列,然后循环往复地进行下去。
如果不断有优先高的新进程进入队列时,放入过期队列中,等到本轮的活跃队列里的进程全部调度完,指针指向的内容交换,下一轮调度开始,这些新插入的进程的优先级高优先分配资源。这样就可以很好地避免进程的饥饿问题。
解决修改进程优先级成本较高问题的逻辑是——修改进程的nice值后,进程本次的优先级还是保持不变。当调度完后,根据nice值重新计算进程的优先级,然后链入相应的优先级的过期队列当中。这样就可以减少一次断开链接的操作,减少了优先级修改操作的成本。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)