【Linux】认识进程以及进程的状态
目录
认识进程
基本概念
查看进程
父子进程
进程的状态
进程排队
运行状态
阻塞状态
挂起状态
僵尸进程
孤儿进程
认识进程
基本概念
有些教材上会说:正在运行的程序就是进程。这并没有错误,但是太过于笼统。现在我们深入到Linux底层来了解一下什么是进程。
我们想要执行一个程序,就必须要把代码和数据给操作系统。也就是说将其加载到内存中。
但是操作系统有很多正在运行的程序,它肯定要把他们都管理起来。怎么做呢?先描述,再组织。也就是说先用结构体把各个进程的基本属性描述出来,然后再用数据结构将这些进程组织起来。
这个结构体被称为进程控制块(PCB)。在Linux中PCB命名为task_struct。
有了PCB之后,操作系统中一切管理进程的行为,本质都是管理对应的PCB。比如操作系统想要进程排队,只需要让PCB排队就好了。所以我们可以得出一个结论:
进程=内核PCB数据对象+可执行程序
task_ struct常见内容
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
查看进程
查看进程状态:ps ajx 或者 ps aux
但是这样会显示所有的进程,所以我们一般配合着grep使用。比如你要查找一个叫做xxx.exe的进程,你就输入ps ajx | grep xxx.exe即可查找到。
父子进程
在Linux中,每一个进程都有父进程。一切在命令行调用的进程,都是bash的子进程。
上图中PID代表进程的唯一标识符,PPID代表父进程的唯一标识符。我们可以用函数getpid()和函数getppid()去分别获取他们。其包含在<unistd.h>头文件中,返回值类型是pid_t 。在Linux中,每一个进程都有父进程。
_____________________________________________________________________________
进程的状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。大家可能看见过这样的图片:
我们一般把进程分为运行、阻塞、挂起三种状态。但是它们具体是什么样的呢?下面我们来看一看。
下面的状态在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 */
};
在Linux中,PCB内部有一个整型变量来表示进程的状态。比如R状态的位图就是00000000,S状态的位图就是00000001等等。
所以进程状态的本质其实就是一个整型变量,进程状态就是用一个整型来描述的。Linux中会有多个进程都要通过它们的状态来执行后续动作。
_____________________________________________________________________________
进程排队
软硬件资源是有限的,但是我们有很多进程,所以进程需要排队。
进程排队的本质就是PCB在排队。一个PCB内部有许多链表节点,因此可以把一个PCB放入到多个数据结构中进行管理。linux内核的PCB——task_struct是以双向链表的形式链接起来的。想要通过节点找回PCB,可以使用相对地址找回PCB的指针。
获取起始地址的运算:(task_struct*)(&n - (task_struct*)0 ->n)。task_struct中有很多类似于struct listnode的节点,因此它可以链入多个数据结构中。
每个软硬件都有自己的等待队列。当一个进程需要运行,就把它链接到CPU的等待队列中,当一个进程需要网络请求,就把它链接到网卡的等待队列中。对于PCB的状态改变,以及把PCB放到哪一个队列中,都是由操作系统来执行的。
____________________________________________________________________________
运行状态
操作系统给CPU维护了一个等待队列叫做运行队列,运行队列是一个双向循环链表,它容纳了系统中所有可以运行的进程。当task_struct链入到运行队列中就处入运行状态(R状态)。
_____________________________________________________________________________
阻塞状态
堵塞状态就是将task_struct从运行队列中移除再链入到其他等待队列中。
举个例子:你的程序中有一个scanf函数,该程序会从运行队列中移除,将自己的状态改为堵塞,链接到键盘的等待队列中,如果等待成功,就会再将自己从等待队列中移除,链接到运行队列中,将自己的状态改为运行。
_____________________________________________________________________________
S状态
S状态(sleep)休眠状态也可以叫做可中断睡眠状态或浅度睡眠状态,属于阻塞状态,其一般处于等待资源的状态。可以用Ctrl+c中断程序。
____________________________________________________________________________
D状态
D状态(disk sleep)也叫做不可中断睡眠状态或者深度睡眠状态,属于阻塞状态。
当Linux过于繁忙的时候,内存中可能会有大量进程,此时操作系统就会选择直接杀掉某些进程,防止崩溃。
D状态,相当于有一个免死金牌,操作系统不会杀掉D状态的进程,而是继续让其等待资源。
_____________________________________________________________________________
T状态
T状态,进程处于暂停状态,属于阻塞状态。
我们可以通过给进程发送信号 kill -19,就是暂停的信号。
如果想要从T状态恢复,可以使用信号 kill -18,即继续进程。
T状态的进程是后台运行的进程,哪怕通过kill -18恢复过来依旧是一个后台进程。
_____________________________________________________________________________
t状态
t状态也是一个暂停状态,属于阻塞状态,但是其是一种被追踪的暂停状态。
比如说使用调试程序的时候,当进程到某个断点处停止了,此时进程就属于t状态。
_____________________________________________________________________________
挂起状态
当内存非常吃紧的时候,操作系统会将一些进程的代码和数据放入到磁盘的一个特定的区域,当缓和时,再将代码和数据放入内存。但注意task_struct不会放入磁盘。这种状态就称为挂起状态。挂起状态是一个特殊的阻塞状态。
____
_________________________________________________________________________
僵尸进程
进程退出后,其代码和数据会被立即释放,但是这个进程的PCB会被保留,因为我们可能需要这个进程的状态信息,此时这个状态就是僵尸状态(Z状态)。所以进程在死亡前一定会进入Z状态。父进程调用wait()
或者waitpid()
系统调用取得子进程的终止状态,此时进程真正死亡(X状态)。
由父进程创建一个子进程,子进程结束后,如果父进程一直不读取子进程的数据,子进程就会一直被保留,这种进程称之为僵尸进程。
僵尸进程有很大的危害:
- 僵尸进程的 PID 还占据着,意味着海量的子进程会占据满进程表项,导致后来的进程无法
fock()
- 僵尸进程的内核栈无法被释放掉(1K 或者 2K大小),导致内存泄漏。
_____________________________________________________________________________
孤儿进程
如果一对父子进程,父进程比子进程先退出,那么子进程的PCB
就无法被父进程回收了,对于这种父进程先退出的进程,叫孤儿进程。孤儿进程将被init
进程(进程号为1)所收养,并由init
进程对它们完成状态收集工作。