进程
- 一、进程的基本知识
- 1、基本概念
- 2、进程的描述 —— PCB
- 3、task_ struct内容分类
- 二、进程的相关操作
- 1、在Linux下查看进程
- 2、通过系统调用在代码中获取进程标示符
- 3、如何创建子进程
- 4、关于fork()的一些深度理解
- 三、进程的状态
- Linux中的进程的状态
- 四、僵尸进程与孤儿进程
- 僵尸进程
- 孤儿进程
一、进程的基本知识
1、基本概念
在通常的课本中进程的概念:程序的一个执行实例,正在执行的程序等。
如果以内核的观点来看应该是:内核关于进程的数据结构(PCB) + 当前进程的代码与数据。
2、进程的描述 —— PCB
PCB(process control block)进程的相关信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,Linux操作系统下的PCB是: task_struct
一个结构体类型。
task_struct
是PCB的一种,也是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
进程的组织:所有运行在Linux系统里的进程都以task_struct
链表的形式存在内核里,可以在内核源代码里找到它。
3、task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息。
二、进程的相关操作
1、在Linux下查看进程
在Linux下查看当前进程的命令是ps
命令。
ps命令来自于英文词组“process status”的缩写,其功能是用于显示当前系统的进程状态。
使用ps命令可以查看到进程的所有信息,例如进程的号码、发起者、系统资源使用占比(处理器与内存)、运行状态等等。帮助我们及时的发现哪些进程出现”僵死“或”不可中断“等异常情况。
常用参数:
- a 显示现行终端机下的所有程序,包括其他用户的程序
- x 显示所有程序,不以终端机来区分
- j 采用工作控制的格式显示程序状况
ps -axj
查看当前正在运行的进程
可以看到每个进程都有自己的pid,每个进程的pid都是不一样的,通过pid操作系统和我们可以区别不同的进程。
当然在Linux中还有其他方式查看当前运行的进程,进程的信息可以通过/proc 系统文件夹查看,这是一个特殊的目录,里面有许多以pid来命名的目录,以pid命名的目录存放的是当前pid进程的相关信息,当当前进程结束后pid会消失,同时以pid命名的目录也会被删除。所以这个目录是一个动态的目录。
2、通过系统调用在代码中获取进程标示符
在Linux中进程id是(PID),进程的父进程id是(PPID)那么我们怎么在代码中获取一个进程的pid呢?
答案是:Linux系统为我们提供了两个系统调用接口getpid()
getppid()
通过这两个函数我们可以拿到当前进程的pid与当前进程的父进程的pid
注意:getpid() 与 getppid()的返回值是 pid_t ,此类型是int 的一个 typedef
那么我们来看一下下面程序运行起来后的结果:
运行结果
可以看到getpid()与getppid()为我们返回了相应进程的pid与其父进程的pid,我们再用查看进程的相关指令查看一下当前进程。
ps -axj |head -1 && ps -axj |grep processC|grep -v grep
我们查看到的进程pid与我们程序返回的pid相同,再次证明结果没有错。
那么我们还有一个疑问,我们当前进程的父进程又是谁呢?我们再次查看一下
ps 22927
我们可以看到我们程序的父进程是bash,也就是shell外壳,为什么会是这样呢?原来在Linux中我们让bash去执行命令./processC
让bash帮我们运行我们所写的程序,但是我们所写的程序可能有问题,如果bash自己帮我们运行了有问题程序,那么bash就无法进行正常的命令解释了,那么我们整个Linux接下来就无法进行任何操作了,所以bash为了保护自己同时为了能让Linux的其他程序能正常运行不受干扰,bash采用了创建子进程的方式来帮助我们运行程序,换句话说,在shell下直接运行的程序其父进程都是shell,shell是通过创建子进程的方式完成我们所交给shell的命令。
通过这个现象我们可以知道:
- bash命令行解释器,本质上它也是一个进程!
- 命令行启动的所有的程序,最终都会变成进程,而该进程对应的父进程都是bash
3、如何创建子进程
shell可以通过创建子进程的方式来完成我们交给shell的命令,那我们可不可以在我们所运行的进程里面再创建一个子进程来帮我们完成相应的任务呢?答案是可以的,Linux系统给我们提供了一个系统调用接口fork()
函数,通过这个函数我们能够将创建一个新的子进程,我们先来看看fork()
的man
手册吧。
简单的了解完了fork(),我们先动手用一下fork()吧。我们看以下代码,如果创建成功了那么下面两条printf
函数都应该会被执行!
实验结果:
这说明我们的fork()函数,确实能帮我们创建一个新的子进程,fork()通过返回给父进程子进程不同的值让父进程与子进程运行了不同的代码。
fork()的执行过程:
- fork之后,执行流会变成2个执行流.
- fork之后,两个执行流谁先运行由调度器决定。
- fork之后的代码共享,通常我们通过 if 和 elseif 来进行执行流分流完成不同的任务!
4、关于fork()的一些深度理解
- fork()做了什么
fork()函数创建子进程并不是在内存中重新拷贝一份代码与数据,而是在内存中以父进程为模板创建一个新的PCB结构,子进程的PCB与父进程大部分属性都是一致的但是也有一部分不一样,如:pid,ppid。
这个子进程的PCB与父进程PCB一样指向同一份代码与数据!
- fork创建的子进程与父进程是怎么保证相互独立性的
我们都知道,进程在运行时是具有独立性的,关闭A进程不会影响B进程,同样我们杀死父进程也不会影响子进程,但是我们知道fork()
创建子进程时并没有复制父进程的代码,那么父子进程之间又是怎么保持进程之间的相互独立性的呢?
首先,进程 = 内核数据结构(PCB) + 当前进程的代码与数据。
子进程与父进程有不同的PCB ,可以保证内核数据结构的独立性,然后就是讨论代码与数据是怎么保持独立性的呢?
我们知道对于代码,当我们编译完成以后代码便变成了是只读的常量,常量无法修改,于是父子进程执行同一份代码就不会影响父子进程彼此之间的代码逻辑了。
那么对于数据呢?我们看上面的代码中有一个变量 pid 明明是同一个pid,为什么一个变量里面存放了两个不同的值呢?答案是写时拷贝,当我们父子进程尝试去修改数据的值时,便会触发写时拷贝,写时拷贝会给我们复制一份当前数据的值,让我们的父子进程去其他位置修改数据,而不是真正在原数据的基础是修改数据。
通过上面的解释我们知道,父子进程拥有自己独立的PCB,父子进程的代码是只读的,所以父子进程都无法修改代码,也就无法相互影响,父子进程的数据是以写时拷贝的方式各自私有一份,通过这些便让我们的父子进程保持了相互的独立性。
- fork为什么有两个返回值
我们知道,当一个函数准备执行 return
语句的时候,该函数的主体功能就已经完成了,return 语句不影响函数的功能,仅仅起到返回结果的作用。因此, fork 系统调用函数在执行 return 语句之前,子进程就已经创建完成甚至已经被操作系统调度了,所以当执行 return 语句返回结果的时候,由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。就要给父进程与子进程各自返回一份结果,即执行了两次。最终返回结果被赋值给变量 pid 的时候,OS自动触发了写时拷贝,分别把结果存入两者的备份空间中。
三、进程的状态
在了解进程的状态之前我们先来了解两个概念:
阻塞:进程因为等待某种条件就绪,而处于一种不推进的状态。
为什么会有阻塞状态呢?因为计算机是资源是有限的,当某种资源被其他进程占据时,一些也要使用此资源的进程便要进入阻塞状态,等待资源处于就绪状态后才能继续运行。
挂起:进程在处于阻塞状态时,内存里面既有task_struct结构体又有代码和数据,为了缓解内存的压力,暂时将代码和数据存放进磁盘中,将内存中的代码和数据进行删除,此时内存里面只有该进程的task_struct结构体而没有此进程的代码与数据的状态被称为挂起状态。
Linux中的进程的状态
- R运行状态 (running):运行状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠( interruptible sleep)
我们来看看下面的代码运行起来后进程的状态:
要查看进程的状态我们可以用:
ps -axj | head -1 && ps -axj | grep test1c | grep -v grep
我们明明可以看到右边的程序一直在运行,但是我们的Linux却告诉我们,我们的进程处于睡眠状态,(+号表示正在台前运行而不是后台运行)为什么会这样呢?
这是因为我们的代码要使用显示器设备,在我们执行printf
函数时由于我们的进程需要使用显示器资源,但是显示器资源没有处于就绪状态,于是我们的CPU就无法继续执行此进程,此时进程便不在CPU队列里排队了,而跑去显示器队列里排队,所以此进程便被设置为了睡眠状态,当显示器资源就绪后我们的进程才会重新进入CPU队列里变为运行状态,而外设的运行速度是远远低于CPU的,所以运行此代码我们几乎99%时间看到的都是处于睡眠状态。
之所以S状态又被称为可中断睡眠是因为我们可以使用ctrl+C
或kill命令去杀掉此进程。
kill -9 对应进程的pid # 杀死一个进程
当我们对代码进行修改,不去访问外设时我们再看看此时进程的运行状态:
通过观察我们发现我们的进程一直处于R状态,这是因为我们的代码中没有去访问外设降低代码的执行速度。
-
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
当我们的处于R状态进程需要向磁盘内部写入一些数据时,我们的进程便会从R状态切换为D状态,这是因为CPU的执行速度很快,但是向磁盘写入数据很慢,同时呢,为了防止进程在向磁盘中写数据时被杀掉(Linux操作系统会在内存不足时进行杀后台)造成数据丢失,处于此状态的进程不能被中断,那怕是操作系统,kill
命令,CTRL+C
,都无法使此状态的进程中止。 -
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程,这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
通过kill -l
命令我们能列出kill
命令下的所有信号。
通过观察我们可以知道当我们想要暂停一个进程时可以发送
kill -19 对应进程的pid
当我们想要让一个暂停的进程继续运行时可以发送
kill -18 对应进程的pid
我们来看下面一段代码运行后,我们发送 kill -19
号信号
我们发现进程确实暂停了,但现在我们又想要让进程运行起来,我们需要发送 kill -18
号信号
-
t (tracing stop)追踪状态,此状态也是暂停状态的一种,当我们用gdb调试我们C语言代码时我们运行至断点时便处于此种状态。
-
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
-
Z(zombie)-僵尸状态
僵尸状态(Zombies)是一个比较特殊的状态。在子进程退出过程中,进程占有的所有资源将被回收,但是如果子进程立即退出,那么父进程就会拿不到子进程中的一些关键信息,于是,系统为了方便父进程拿到子进程的相关信息,在子进程退出时,子进程并不会立即退出,而是维持一段时间的僵尸状态。等到父进程拿到子进程中的关键信息后,子进程才能退出进程,变为X死亡状态。
在僵尸状态时操作系统会去除除了task_struct结构(以及少数资源)以外的所有资源。于是进程就只剩下task_struct这么个空壳,故称为僵尸。之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息。比如在shell中,$?
变量就保存了最后一个退出的前台进程的退出码,而这个退出码往往被作为if语句的判断条件。
四、僵尸进程与孤儿进程
僵尸进程
处于僵尸状态的进程被称为僵尸进程,僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就会一直处于Z状态。
我们先来看下面一段代码
我们先运行起来
我们可以利用kill
命令去杀死子进程,然后观察子进程的状态是直接死亡状态还是僵尸状态。
通过观察我们发现子进程处于僵尸状态,因为父进程一直没有去读取子进程的退出状态代码,于是子进程要一直维持僵尸状态。此时子进程就是一个僵尸进程。
僵尸进程危害
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,Z状态一直不退出,PCB一直都要维护那个子进程的僵尸状态,如果父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费!
孤儿进程
在僵尸进程中,如果子进程先退出,父进程后退出,子进程要进入僵尸状态一段时间直到父进程读取到子进程的退出状态代码。那如果父进程先退出,子进程后退出呢?
父进程先退出,子进程就被称之为“孤儿进程”,孤儿进程会被1号进程(操作系统)领养,当然要有操作系统进程回收子进程喽。如果操作系统不进行领养会造成子进程一直处于僵尸状态,从而造成内存泄漏。
注意:操作系统领养后,子进程会由前台进程转换为后台进程。
我们还是运行上面的代码,然后先用kill
命令杀死父进程(父进程死亡后会由bash回收)然后观察子进程。
kill
命令前
kill
命令后