0


【Linux】进程

文章目录

1.进程

在没有正式学习进程之前,通过理解操作系统的管理,我们就可以知道操作系统是怎么进行进程管理的了。

  • 很简单,先把进程描述起来,再把进程组织起来

在这里插入图片描述

那什么叫进程呢?

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

但是看完上面两句话我还是不懂进程是什么意思。

操作系统为了管理进程,必须得描述进程,描述进程就要有结构体

(课本上称之为进程控制块,PCB process control block,Linux操作系统下的PCB是: task_struct )

,有结构体就可以连接起来,连接起来就可以将管理进程转化为数据结构的增删查改操作。
我们先对进程简单建一下模:
在这里插入图片描述

所以,进程 = 内核的数据结构(task_struct)+程序的代码和数据,调度运行进程,本质就是让进程控制块task_struct进行排队!

那么task_struct中存放的都是什么呢?

2.task_stuct的属性

task_struct是Linux内核的一种数据结构,它会被操作系统在RAM(内存)里创建,并且里面包含着进程的信息。

task_ struct内容分类:

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

为了查看相关属性,我们先写一个简单的程序
在这里插入图片描述
当程序编译好以后,我们 ./myproc运行它,根据冯诺依曼体系结构的设计,它会被加载到内存中。此时它就不叫程序了,而应该叫做进程。

那么我们应该如何查看该进程的属性呢?

命令:ps axj ,用于显示系统中当前运行的进程的信息,包括进程ID(PID)、父进程ID(PPID)、CPU使用率、内存使用率等 ps(process status)

在这里插入图片描述
此时我们我们发现有很多进程在运行,所以我们可以使用grep命令过滤一下,并且使用head命令显示文件的第一行。
在这里插入图片描述
可使用反向过滤,去除grep myproc 进程

ps ajx | head -1 && ps axj | grep myproc | grep -v grep

在这里插入图片描述

因此,将程序运行起来,本质上就是在系统中启动了一个进程,此时进程可分为两种

  1. 执行完就退出的进程,例如指令 ls mkdir等
  2. 一直不退出,除非用户主动关闭 - - 常驻进程

2.1 标示符

2.1.1 PID

PID 标示符: 描述本进程的唯一标示符,用来区别其他进程。

每次运行我们的程序,它的PID都是不同的。
在这里插入图片描述

在程序中,可以通过调用

getpid()函数

获取当前进程的PID

在这里插入图片描述
在这里插入图片描述

当前我已经知道了这个进程的PID,如果我想终止该进程怎么办呢?

  1. 在程序运行处,直接Ctrl + C
  2. 使用kill命令,该命令可向进程发送信号。该命令的编号是9,所以我们直接可以:kill -9 + PID

在这里插入图片描述
此时,该进程就被杀掉了
在这里插入图片描述

查看进程信息时,除了使用ps命令外,如果我想看到进程更多的信息,那我应该怎么查呢?

linux下,一切皆文件,因此进程的信息也是以文件的方式保存的。因此在linux系统中,存在一个特殊的目录proc

在这里插入图片描述

proc目录中以数字命名的目录,这个数字就是指定进程的PID,每一个目录就代表一个进程,里面包含进程的所有信息。

在这里插入图片描述
这也就意味着,当我们运行一个程序后,操作系统就会立即在proc目录下,创建一个以该进程PID命名的目录,目录中存放进程的所有信息,方便上层去查看。进程结束后,该目录就被删除了,不存在了,是实时更新的。

不知道大家有没有一个疑问,频繁的创建、删除、修改文件会不会影响系统的效率?

注意:proc文件不是磁盘级别的文件,它是内存级别的文件,因此不会影响

我们进程的PID为2947,可以发现它确实存在于proc目录中。

在这里插入图片描述

因此,我们

如果要查看进程的详细信息,可以使用ls /proc/PID -l

,此时就显示出了该进程文件下的所有信息,即进程的信息。这时显示的比我们使用ps显示的就详细的多。
在这里插入图片描述
在这些信息中,我们重点关注exe 和cwd。

  1. exe :形成这个进程的源可执行程序文件是谁
  2. cwd:current work directory当前工作目录

在下面的代码中,我们打开了一个文件,此时文件不存在,它会自动创建一个文件,但它怎么知道在哪里创建文件呢?

在这里插入图片描述

运行程序我们会发现,test.txt文件和我们的进程 myproc创建在同一个位置了

在这里插入图片描述

所以,它应该是在“当前工作目录下”创建了文件test.txt,此时的cwd就是当前进程的cwd,将要创建的文件直接拼接在cwd路径后,即可创建对应文件。如果指定了绝对路径,则会在指定路径下创建。
在这里插入图片描述

在linux系统中,提供了一个系统级的接口

chdir()

,可供我们改变进程的cwd。
在这里插入图片描述

2.1.2 PPID

在Linux系统中,启动之后,新创建任何进程的时候,都是由父进程创建的!(父进程让操作系统建的)

getppid()函数可以获得父进程的id。
在这里插入图片描述
我们多次运行程序可以发现,子进程的id会变化,但是父进程的id没有改变。

在这里插入图片描述

那么父进程是谁呢?- - bash(命令行解释器,linux中是bash)

在这里插入图片描述

在命令行中,执行命令/程序的本质是:一个叫做bash的进程,创建了子进程,由子进程执行我们的代码!

那么子进程是如何创建的呢?

2.1.3 创建子进程

  1. 如何创建子进程?
fork函数

:用来创建子进程。
在这里插入图片描述

在这里插入图片描述
该函数的返回值非常特殊,

如果进程创建成功,它会返回子进程的PID给父进程(给父进程返回子进程的pid是为了方便父进程管理孩子),返回0给子进程;如果失败,返回-1给父进程。

创建成功它为什么会返回两个值呢?我们写个代码来看一下
在这里插入图片描述

经过fork后,该程序就会有两个执行分支,因此第二条printf会执行两次。
而且我们可以发现,第三个printf的父进程是执行第二条printf的进程,因此二三条printf是父子进程。
第二条就是执行该程序的进程,第三条就是程序中又创建的子进程。

而且我们可以发现,父进程可以有多个子进程,而子进程只能有一个父进程。所以,linux系统中,所有的进程都是树形结构!

对于fork的返回值,我们使用下面的代码来验证一下:
在这里插入图片描述
在调用fork函数创建子进程后,我们的if 和else两个条件同时满足了,而且两个死循环同时在跑,这是为什么呢?

这里就证明了,在fork以后,有两个循环同时在跑,那么它就一定是有两个执行流,分别叫做父进程和子进程。

那fork为什么会有两个返回值呢?

因为fork后,会有两个进程,两进程为父子关系。一般而言,代码是会共享的,但是数据只有一份。

通过下面的代码我们可以发现,父进程与子进程二者不是同一个gval,也就是说它俩各自有一个gval,

在这里插入图片描述

为什么父子进程代码共享,但是数据各自私有一份呢?- -

进程是相互独立的,对各进程之间运行时互不影响,即便是父子


对于代码,二者都是只读的;对于数据,各自私有一份(使用写时拷贝的思想)
对于接收fork返回值的id,它也是数据,因此父子进程中会各有一份,所以我们的if和else这两个条件都会走,两个进程各自运行。

fork是一个系统调用调用函数,创建子进程的时候,需要先拷贝父进程的task_struct,然后调整部分属性,最后在链入到进程列表中。在这个函数执行return语句之前,父子进程绝对都已经存在了,那两个进程执行各自执行依次return语句,返回两个值也就是理所当然的了。

如何创建多进程呢?一个程序中多次调用fork函数即可。并且在父进程中可以收集到子进程的pid,方便管理。

在这里插入图片描述
在这里插入图片描述

2.2 状态

2.2.1 进程的状态

在这里插入图片描述
相信学习过操作系统的伙伴,对进程的理解就是上图这些东西,但是你真的理解了吗?上图是宏观的描述了操作系统的状态,对于不同的操作系统,它们都满足上面的状态,但是不同的操作系统的处理方式又有区别。

在真正理解进程的状态之前,我们补充几点知识:

  1. 并发和并行
并发

:在单CPU的计算机中,并不是把当前进程执行完毕以后再执行下一个,而是给每个进程分配一个时间片,基于时间片,进行轮换调度,轮换调度的过程就叫做并发。

并行

:多个进程在多个CPU下分别、同时运行,叫做并行。

  1. 时间片

Linux/Windows等民用操作系统,都是

分时操作系统

(给每个进程分配一个时间片,当时间片耗尽时,就必须从CPU上剥离下来,然后把另一个进程放上去)。分时操作系统的特点:调度任务最求公平。

实时操作系统

:任务一旦执行,从开始到结束尽量、优先的执行完毕。

  1. 等待的本质
  • 阻塞

对于每一个CPU,操作系统都要给其提供一个叫做运行队列的东西。每次执行了一个进程,就是将进程的task_struct链入到运行队列中,CPU在处理进程时,只需要到运行队列中去取队头的task_struct。
在这里插入图片描述
当一个进程处于运行队列当中时,我们就称该进程为运行状态

计算机大多数情况下都是在做IO的操作(外设的访问),比如获取键盘上的数据,执行scanf。

但是有时候键盘迟迟没有按下,进程也就获取不数据,但是CPU不会一直等该进程,此时该进程就会被设置为

阻塞状态

,等待对应的底层硬件准备好,该进程才会被重新放到运行队列中。

那阻塞状态具体是怎么做的呢?

由于操作系统也需要管理底层的硬件(先描述,再组织),所以它清楚的知道你硬件此时是什么状态(到底有没有数据),如果没有就该进程就会阻塞,那该进程去哪里“阻塞”呢?- - 去外部设备上,这里的外部设备指的是OS所管理的外设的PCB。
在这里插入图片描述
因此,等CPU的就叫做运行状态,等外设的叫做阻塞状态。

  • 阻塞挂起

阻塞期间,进程不会被调度,但进程对应的PCB与代码和数据也是会占用内存的,此时该部分内存是被白白浪费的。

当内存资源严重不足时,操作系统为了保证整个系统的安全,会将进程的代码和数据换出到磁盘中;当进程不阻塞时,OS会再将进程对应的代码和数据换入内存中。

磁盘中会有一块分区,专门进行换入和换出工作,该分区叫做交换分区(swap分区),是一种用时间换空间的策略。

当进程处于阻塞状态,并且操作系统将其代码和数据换出到磁盘中,此时被换出的进程就处于

阻塞挂起状态

2.2.2 Linux下进程的状态

上面我们已经知道了操作系统宏观上的状态解释,那在具体的Linux系统下又是什么样的呢?

我们看看Linux内核源代码怎么说

staticconstchar*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 */};
  1. R 和 S 状态
  • R:运行状态
  • S:休眠状态(阻塞等待状态),可中断睡眠(浅睡眠,即可直接被kill掉)

在这里插入图片描述

  1. D 状态

D:disk sleep,也是阻塞等待的一种状态(不可中断睡眠,深度睡眠),专门为磁盘设计的状态。

为了保护访问磁盘的进程(保护数据),禁止操作系统“杀掉”该进程,该进程就处于D状态

  1. T 状态

T:暂停进程。

在认识T状态前,我们先掌握两个kill 18和19号。
在这里插入图片描述

使用kill -19,就可以让指定进程暂停掉,此进程的状态就变成了T。

在这里插入图片描述
使用kill -18,就可以让指定进程继续执行,此进程的状态就变成了S。为什么不是S+了呢?而且我发现无法使用Ctrl+C杀掉该进程了。只能使用kill -9 命令了。

在这里插入图片描述

为什么不是S+了呢?因为当一个进程被暂停又恢复后,它就变到后台去运行。

  1. t 状态

t:tracing stop,遇到断点,进程就被暂停掉了。此时该进程就是被追踪状态,断点处停下来,此时进程的状态就是t.

gdb后,会有一个gdb进程,遇到断点,gdb调试进程的状态就是t了。

在这里插入图片描述

  1. X 与 Z状态

Z:僵尸状态
X:死亡状态,即进程结束。

先介绍linux下的一条命令:echo

$ ?

,显示最近一条进程的退出信息(执行状态,0为正常退出,非0为异常退出)

在这里插入图片描述

为什么一个进程要返回执行信息呢?- - 通过进程的执行结果,告诉父进程/操作系统,我把任务执行的怎么样。

那什么是僵尸状态呢?

  • 当一个进程退出时,它的代码和数据会被释放掉;但是它的task_struct还存在,task_struct中存着该进程的退出信息(执行信息)。
  • 当一个进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。
  • 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程则进入僵尸状态,僵尸状态一直不退出,PCB就要一直维护,直到父进程回收。
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,此时就会造成内存泄漏(系统级的)

为了验证僵尸状态,我们写一个测试代码
在这里插入图片描述

//循环查询命令
while:;dops axj |head -1;ps axj |greptest;sleep1;done

当我们的程序运行10秒后,子进程就结束了,但是依然可以看到子进程的信息,而且它的状态变成了Z,处于了僵尸状态。

在这里插入图片描述

  1. 孤儿进程

当子进程退出,父进程存在时,叫做僵尸状态。那如果反过来,父进程退出,子进程存在呢?

验证一下
在这里插入图片描述

当父进程被杀掉,子进程存在时,可以发现子进程的ppid变成了1。

我们top一下,发现1是系统。
在这里插入图片描述

因此,当父进程退出时,系统会领养子进程,以便接收子进程的退出信息,回收进程。这个被领养的进程就叫做

孤儿进程

。孤儿进程运行在后台

2.3 优先级

  1. 优先级是什么?

获得某种资源的先后顺序。

  1. 为什么要有优先级

因为资源有限。

  1. 优先级是怎么做的呢?

task_struct中有一个优先级属性priority,里面包含特定的几个int类型的变量表示优先级。Linux中,优先级数字越小,优先级越高。

在linux中,一共有两种数字来维护一个进程的优先级

  • PRI:当前进程的优先级,默认是80。
  • NI:nice,优先级的修正数据。
  • 最终优先级 = PRI + NI

在这里插入图片描述
上图中的UID是用来:标识该进程是谁启动的。

那如何修改优先级呢?

修改优先级,只能通过nice来修改的。
方式:用top命令更改已存在进程的nice:

输入top,进入top后按“r”–>输入进程PID–>输入nice值

在这里插入图片描述
在这里插入图片描述

  • 系统会禁止频繁修改nice值,有时需要使用root修改。
  • nice值的取值范围是:[-20,19]
  • 最终优先级是:使用默认pri(80),然后加上nice值,并不是使用上一次的pri,所以上图中的pri是99和60。因此调整优先级习惯称作:优先级的重置。

2.4 进程切换(上下文数据、程序计数器 )

当一个进程的时间片到了,它就需要被切换。Linux是基于时间片,进行调度轮转的。

但是当一个进程的时间片到时,它并不一定能够执行完毕,可以在任何地方被重新调度切换。

那如何切换呢?

  • 当一个进程被切换走的时候,需要保存该进程执行到了哪里;
  • 当进程被切换回来时,需要从上次执行到的地方恢复执行。
  • 即进程切换时,它“从哪里来,回哪里去”,那如何记录它从哪来到哪去呢?

当一个进程运行的时候,会有很多的临时数据,这些临时数据都是保存到CPU的寄存器中的,例如eip、ir。
这些

寄存器中,是进程执行时瞬时状态的信息数据,即:上下文数据

  • eip 中存放当前执行到的指令的下一条指令的地址
  • ir 中存放正在执行的指令

虽然CPU有很多寄存器,但是它只有一套寄存器,是被多个进程共享使用的。如果有多个进程同时运行时,会频繁的更新寄存器中的数据。

如果不预先将寄存器中当前进程①的数据保存,则会被其它进程②覆盖掉,当进程①又回来时,不知道从哪里开始了,此时就无法完成进程的调度与切换。

因此,进程切换的核心:进程上下文数据的保存和恢复。

那将寄存器中进程的上下文信息预先保存到哪里呢?

当前进程的PCB中的一个任务状态段中,也就是在内存中
在这里插入图片描述

2.5 Linux的进程调度算法

下图是Linux2.6内核中进程队列的数据结构
在这里插入图片描述

  1. 一个CPU拥有一个runqueue,如果有多个CPU就要考虑进程个数的负载均衡问题。
  2. 优先级
  • 普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,[-20,19],刚好20个)
  • 实时优先级:0~99
  1. 活动队列
  • 时间片还没有结束的所有进程都按照优先级放在该队列
  • nr_active: 总共有多少个运行状态的进程 queue[140]:
  • 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级! 相同优先级的进程在同一个数组下标中,使用类似于hash桶的思想挂在同一个桶中

从该结构中,选择一个最合适的进程,过程是怎么的呢?

  1. 从0下表开始遍历queue[140]
  2. 找到第一个非空队列,该队列必定为优先级最高的队列
  3. 拿到选中队列的第一个进程,开始运行,调度完成!
  4. 遍历queue[140]时间复杂度是常数!但还是太低效了

  • 借助位图bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空(即一次检查一个int,可一次跳跃32个比特位)。这样,便可以大大提高查找效率。
  1. 过期队列

过期队列和活动队列结构一模一样

  • 过期队列上放置的进程,都是时间片耗尽的进程/新建进程
  • 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算
  1. active指针和expired指针
  • active指针永远指向活动队列 expired指针永远指向过期队列
  • 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
  • 在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!

为什么进程会是运行起来的程序呢?因为进程会被调度,被切换。

在这里插入图片描述

标签: linux 服务器

本文转载自: https://blog.csdn.net/weixin_69380220/article/details/142265266
版权归原作者 戴墨镜的恐龙 所有, 如有侵权,请联系我们删除。

“【Linux】进程”的评论:

还没有评论