09 Linux进程的概念

文章目录

  • 一、什么是进程
  • 二、进程管理
    • 2.1.描述进程--PCB
    • 2.2.查看进程
      • 2.2.1.使用ps命令查看
      • 2.2.2.通过系统目录查看
  • 三、通过系统调用函数获取进程标示符PID和PPID
  • 四、fork创建子进程
    • 4.1.补充内容
  • 五、进程状态
    • R 可执行状态
    • S 可中断睡眠状态
    • D 不可中断睡眠状态
    • T/t 暂停状态和跟踪状态
    • X 死亡状态
    • 三种进程的基本状态和切换
    • 补充内容:echo $?
  • 六、kill杀死(中断)进程
  • 七、Z 僵尸进程
    • 7.1.僵尸进程的危害
  • 八、孤儿进程
  • 九、进程优先级
    • 9.1. ps -l查看优先级
    • 9.2. PRI和NI
      • 使用top命令更改进程的nice值
      • 使用renice命令更改进程的nice值
  • 十、补充几个概念


一、什么是进程

程序本质上是一个包含可执行代码的文件,是一个放在磁盘上的静态文件。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中,此时这个程序就被称为进程。所以进程就是一个开始执行但是还没有结束的程序的实例,是可执行文件的具体实现。当程序被系统调用到内存以后,系统会给程序分配一定的资源(内存、设备等)然后进行一系列的复杂操作,使程序变成进程以供系统调用。

综上所述,进程是程序的一次执行过程和资源分配的基本单位

区分程序和进程:

程序:程序本质是一个放在磁盘上的静态文件。
进程:程序运行起来之后,就叫做进程,静态是动态的,由操作系统管理。


二、进程管理

操作系统允许多个进程同时运行,此时就需要操作系统对进程进行管理,管理的核心也是先描述,再组织:所以对进程的管理,会转化成对由进程控制块(PCB)组成的链表的管理(增删查改)。
09 Linux进程的概念_第1张图片

进程信息包括对应的文件+进程属性。进程属性是方便操作系统对进程进行管理,所以进程信息的大小要比原本的文件要大。

创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。

综上所述,进程就是可执行程序与管理进程需要的数据的集合

2.1.描述进程–PCB

PCB的全称为Process Ctrl Block(进程控制块)。
具体操作系统的PCB名字是不同的,由于Linux操作系统是用C语言进行编写的,因此Linux当中的PCB是由结构体实现的–struct task_struct()

task_struct当中主要包含以下信息:

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

2.2.查看进程

2.2.1.使用ps命令查看

通过 ps(process status)命令可以显示当前进程的状态,类似于 windows 的任务管理器。
它的参数比较多,常用的有:

  • -e 列出所有的进程
  • -f:全格式
  • -h:不显示标题
  • -l:长格式
  • -w: 宽输出,显示加宽可以显示较多的资讯
  • a:显示终端上的所有进程,包括其他用户的进程
  • r:只显示正在运行的进程
  • x:显示没有控制终端的进程
  • u:试用用户格式输出
  • j:采用工作控制的格式显示进程状况。
  • l:采用详细的格式来显示进程状况。
    比较常用的是a、u、x三个选项。
    ps -aux为例:
    09 Linux进程的概念_第2张图片

USER: 行程拥有者
PID: 进程的ID号,用来识别进程
%CPU: 占用的 CPU 使用率
%MEM: 占用的记忆体使用率
VSZ: 占用的虚拟记忆体大小
RSS: 占用的记忆体大小
TTY: 终端的次要装置号码
STAT: 该进程的状态
START: 进程开始时间
TIME: 执行的时间
COMMAND:所执行的指令

ps命令与grep命令搭配使用,即可只显示某一进程的信息。

以下面的mybin程序为例:
09 Linux进程的概念_第3张图片
在这里插入图片描述

ps aux | head -1 &&ps aux | grep mybin | grep -v grep
09 Linux进程的概念_第4张图片
其中ps aux | head -1是为了显示第一行属性,ps aux | grep mybin | grep -v grep是为了过滤出mybin程序并且屏蔽掉grep这条命令本身。

通过kill PID这条命令可以终止进程:
09 Linux进程的概念_第5张图片
09 Linux进程的概念_第6张图片

2.2.2.通过系统目录查看

在根目录下有一个名为proc的系统文件夹,这是个动态文件系统。
文件夹当中包含大量进程信息,还有的是一些以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的PID号为目录名,它们是读取进程信息的接口。
09 Linux进程的概念_第7张图片直接查看18014目录就可以查看启动这个进程所有的属性信息了。
在这里插入图片描述


三、通过系统调用函数获取进程标示符PID和PPID

使用系统调用函数,getpid()getppid()即可分别获取进程的PID和PPID。

09 Linux进程的概念_第8张图片

其中,PID是当前进程的ID号,PPID是父进程的ID号。

#include 
#include 
int main()
{
     
  while(1)
  {
     
  	printf("pid: %d\n", getpid());
		printf("ppid: %d\n", getppid());
		sleep(1);
	}                             
	return 0;                       
}  

09 Linux进程的概念_第9张图片

使用ps axj | head -1 &&ps axj | grep mybin | grep -v grep通过ps查看它的父进程:
09 Linux进程的概念_第10张图片

通过ps查看它的父进程:

可以看到,bash创建了mybin这个子进程,由于进程是有独立性的,子进程挂掉了,父进程bash也不会挂掉。
bash运行原理:bash叫做命令行解释器(在命令行下的所有命令几乎都是bash的子进程),bash只需要接受任务识别任务,然后创建子进程。通过创建子进程,让子进程去完成对应的任务。

这样,每次运行mybin程序虽然PID会改变,但是父进程IDPPID是不会变的,因为它是由bash创建的,而bash是不会终止然后重新创建的:
09 Linux进程的概念_第11张图片


四、fork创建子进程

09 Linux进程的概念_第12张图片

fork是一个系统调用级别的函数,其功能就是创建一个子进程。

09 Linux进程的概念_第13张图片
09 Linux进程的概念_第14张图片

可以看到两次输出的PID不相同,说明fork创建了一个新的进程,fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系。

对于父进程(调用fork函数的进程),fork函数的返回值是子进程的PID,如果子进程创建失败,则在父进程中返回 -1;对于子进程创建成功则是返回0。

使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行。
使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现。

由于父子进程的fork返回值不同,此时就可以使用if将父子进程分流,这样就可以实现让父子进程执行不同的代码,实现不同的功能:
09 Linux进程的概念_第15张图片

09 Linux进程的概念_第16张图片
09 Linux进程的概念_第17张图片

父子进程都有while死循环,但是由于父子进程是独立的,所以它们可以同时各自执行自己的while死循环,并不存在先调度完父进程然后再调度子进程的情况。这也就进一步证明了子进程的运行和父进程是无关的,只是这两个进程被操作系统调度的顺序是不确定的,取决于操作系统调度算法的具体实现

4.1.补充内容

  1. fork创建子进程以后,系统就要多一组进程管理的数据结构(操作系统为了管理这个进程就得新创建一个数据结构)以及该进程对应的代码和数据(子进程要执行的代码)。其实就是创建一个新的PCB插入到任务队列(链表)中。
  2. 父子进程的代码是共享的,但是父子进程的数据各自开辟空间并且各自私有(采用写时拷贝)这样就表现出父子进程的独立性
  3. 至于为什么fork会有两个返回值(子进程的PID和0),其实并不是一次返回两个返回值,而是从一个进程变成两个进程,每个进程的返回值不一样。

五、进程状态

要对进程进行监测和控制,首先必须要了解当前进程运行状态的情况,下面是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 :可执行状态。
S:可中断的睡眠状态。
D:不可中断的睡眠状态。
T/t:暂停状态或跟踪状态。
X:死亡状态,进程即将被销毁。
Z :退出状态,进程成为僵尸进程。

09 Linux进程的概念_第18张图片
进程的退出状态是会被保存PCB也就是task_struct中的。

R 可执行状态

只有在该状态的进程才可能在CPU上运行。处于这个状态的进程的PCB被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行。所以处于这个状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里等待被调度

S 可中断睡眠状态

处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的PCB结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。这样就实现了处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉。

上面的循环打印PID的程序就是这样:
09 Linux进程的概念_第19张图片

因为这个进程是在等待IO(往显示器输入数据)的。

如果状态中有+则表示这个进程是在前台运行的,此时可以直接Ctrl+c终止进程,如果在执行程序时加上&符:./proc &,则表示将程序放到后台运行,此时无法直接使用Ctrl+c终止进程,只能使用kill -9 PID的方式。

D 不可中断睡眠状态

处于这个状态的进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。

比如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于不可中断睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。

包括操作系统在内,任何人都无法杀掉D状态的进程,除非操作系统重启或者该进程拿到数据(得到磁盘的回复)。

T/t 暂停状态和跟踪状态

可以通过发送 SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT信号让进程继续运行。
09 Linux进程的概念_第20张图片
t(跟踪状态):当进程被gdb调试的时候,会产生t状态。

X 死亡状态

死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态。

三种进程的基本状态和切换

一些书上也会提到进程的三种基本状态:运行态、就绪态和阻塞态(或等待态)。
其中,运行态和就绪态就是R可执行状态,而阻塞态就是S可中断睡眠状态。
以下是对这三种状态的解释:

  • 运行态(running):运行状态是指当前进程已分配到CPU,并在CPU上运行。
  • 就绪态(ready):就绪态表明进程已经具备运行的条件,但是因为其他进程正占用CPU,所以暂时不能运行而等待分配CPU的状态。
  • 阻塞态(blocked):阻塞态表明进程因等待某种事件发生而暂时不能运行的状态。‘

三种状态的转换关系描述如下:

  • 就绪—>运行:处于就绪状态的进程被调度进程选中,分配到CPU后,该进程的状态就由就绪态变为运行态。
  • 运行—>阻塞:正在运行的进程因某个条件未满足而放弃对CPU的占用,这个进程的状态就由运行态变为阻塞态。
  • 阻塞—>就绪:处于阻塞状态的进程所等待事件发生了,系统就把该进程的状态由阻塞态转变为就就绪态。
  • 运行—>就绪:正在运行的进程如用完了本次分配给它的CPU时间片(进程运行的时间),就得从CPU上退下来,暂停运行。该进程状态就由运行态变为就绪态。

09 Linux进程的概念_第21张图片

补充内容:echo $?

使用echo $?命令获取最近一次进程退出时的退出码。
进程退出码可以告诉操作系统代码顺利执行结束(0表示正常退出,非0表示不正常)。


六、kill杀死(中断)进程

对于前台程序通常使用Ctrl+c来中断,但对于后台进程则必须使用kill命令完成。

kill命令是通过向进程发送指定的信号来结束进程的,如果没有指定发送的信号,则默认值为SIGTERM信号,也就是-15信号。-15信号将终止所有不能捕获该信号的进程,至于那些可以捕获该信号的进程则需要使用-9信号。

使用kill -l指令列出当前系统所支持的信号集。一共62条(没有32和33):
09 Linux进程的概念_第22张图片

-15信号被称为优雅的退出。该信号只是通知对应的进程要进行"安全、干净的退出",程序接到信号之后,退出前一般会进行一些"准备工作",如资源释放、临时文件清理等等,如果准备工作做完了,再进行程序的终止。但是,如果在"准备工作"进行过程中,遇到阻塞或者其他问题导致无法成功,那么应用程序可以选择忽略该终止信号。
-9信号要比-15信号强硬一点,系统会发出SIGKILL信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。

所以,相比于kill -15命令,kill -9在执行时,应用程序是没有时间进行"准备工作"的,所以这通常会带来一些副作用,可能会导致打开的文件出现错误或者数据丢失之类的错误,所以不到万不得已不要使用kill -9这种强制结束的办法。如果连kill -9都没法结束进程,那就只有重启计算机了。

综上,想要杀死进程就有了以下三种命令:kill PIDkill -15 PIDkill -9 PID。其中,kill PIDkill -15 PID是等价的,kill -9 PID是强制杀死进程,不到万不得已不要用。


七、Z 僵尸进程

当一个进程的状态为Z时就为僵尸状态,此时这个进程就被称为僵尸进程。
僵死状态是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码,如果退出信息一直未被读取,则相关数据是不会被释放掉的。
09 Linux进程的概念_第23张图片

进程被创建的目的就是完成某项任务,那么当任务完成的时候,父进程是应该知道任务的完成情况的,所以必须存在僵尸状态,使得父进程得知任务的完成情况,以便进行相应的后续操作。

综上,存在僵尸状态的原因是为了保持进程基本退出信息,方便父进程读取,获得退出原因
一般而言僵尸进程可以将不想关的资源比如代码数据等释放掉,但是PCB是会被保留的,进程的退出信息,会放在PCB中。

7.1.僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为他要告诉诉其父进程相应的退出信息。可父进程如果一直不读取,那子进程就一直处于僵尸状态。
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
  • 如果一个父进程创建了很多子进程但是就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存。由于子进程申请的资源没有被回收,就会内存泄漏。

八、孤儿进程

父进程先退出,那么将来子进程进入僵尸状态时就没有父进程没有读取到子进程退出的返回代码,此时该子进程就称之为孤儿进程孤儿进程会被1号systemd进程领养,最终由1号进程进行回收
09 Linux进程的概念_第24张图片

Linux中除了1号进程所有的进程都有父进程(1号进程相当于所有进程的父进程,类似于多叉树的根节点),如果1号进程也挂掉了,系统也就挂掉了。

09 Linux进程的概念_第25张图片


九、进程优先级

进程是有多个的,而CPU的资源是有限的,一个CPU一次只能跑一个进程,因此CPU先跑哪个进程后跑哪个进程就成了问题。类似于排队一样,进程优先级实际上就是进程获取CPU资源分配的先后顺序,优先级高的进程有优先执行的权力。

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

9.1. ps -l查看优先级

优先级是通过整数来表示的,一般数值越小,优先级就越高。
使用ps -l指令可以输出当前用户上下文中所创建的进程,并不会显示全部进程:
09 Linux进程的概念_第26张图片

  • UID:代表执行者的身份。
  • PRI:代表这个进程可被执行的优先级,其值越小越早被执行。
  • NI:代表这个进程的nice值。

9.2. PRI和NI

  • PRI即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
  • NI(nice)表示进程可被执行的优先级的修正数值
  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice,PRI(old)的值在Linux中默认为80,nice值默认为0
  • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
  • 所以,调整进程优先级,在Linux下,就是调整进程nice值
  • nice其取值范围是-20至19,一共40个级别,所以PRI(new)的范围是[60,99]

进程的优先级是可以通过修改nice值来修改的,但是除非特殊情况,否则尽量不要改进程的优先级!

只有root用户才能提高进程的优先权(将nice值改为负数),其他用户需要使用sudo进行权限提升。

使用top命令更改进程的nice值

top命令就相当于Windows操作系统中的任务管理器,它能够动态实时的显示系统当中进程的资源占用情况,即可以通过按键来不断刷新当前状态。如果在前台执行该命令,将独占前台直到终止该程序位置。比较准确地说,top命令提供了实时的对系统处理器的状态监视。

修改进程的nice值有以下三步:

  1. 使用top命令后按“r”键,输入待调整nice值的进程的PID。
  2. 输入进程PID并回车后,输入调整后的nice值。
  3. 输入nice值后按“q”即可退出

使用renice命令更改进程的nice值

renice命令允许修改一个正在运行进程的优先权,其命令语法格式如下:
renice -number PID其中参数number表示优先级别号
09 Linux进程的概念_第27张图片

使用该命令的时候要注意以下三点:

  • 只能对自己所有的进程使用renice命令,
  • root用户可以在任何进程上使用renice命令
  • 只有root用户才能提高进程的优先权(将nice值改为负数),其他用户需要使用sudo进行权限提升。

十、补充几个概念

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

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

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

  • 并发: 多个进程在一个CPU下采用进程切换的方式,基于时间片轮转的多个进程看起来同时推进,称之为并发。

  • 时间片:即CPU分配给各个进程的时间。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。由于时间片是一个小的时间单位,通常为10~100ms数量级,所以各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。

目前很多操作系统都是采用可抢占式调度:即高优先级的任务可以打断低优先级的任务,抢占CPU控制权。这种方式并不是让低优先级的任务时间片变短,而是将其时间片后移。这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。

你可能感兴趣的