什么是进程?
在回答这个问题之前想问另一个问题:什么是程序?
- 我们写的代码(源文件)在经过编译器编译之后生成的是文件,那么就是存放在磁盘中的。所以程序的本质就是文件,在磁盘存放
所谓的进程,就是运行着的程序,我们知道程序的运行肯定是需要CPU做运算的,CPU只能直接与内存做交互,所以程序一定要加载到内存中才能运行(这是体系结构决定的!!)。
同一时间,内存中会有很多进程,所以我们就要这些进程**管理**起来。
那么,怎么管理呢?
在上一篇文章中我们讲到,所谓的管理就是:先描述,再组织
我们一般会采用一个结构体来描述这些进程的相关属性
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block), Linux操作系统下的PCB是: task_struct
上面是部分内核的代码截图,实际上内核中有关task_struct的代码有很多,主要可以分为以下几类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程;
- 状态: 任务状态,退出代码,退出信号等;
- 优先级: 相对于其他进程的优先级;
- 程序计数器: 程序中即将被执行的下一条指令的地址;
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针;
- 上下文数据: 进程执行时处理器的寄存器中的数据;
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表;
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等;
- 其他信息
抽象出来大概可以这样表示:(假设系统中进程使用链表的结构组织)
struct task_struct
{
//进程的所有属性
//...
//进程对应的代码和数据的地址
//...
//下一个进程的地址
struct task_struct* next;
};
关于task_strcut更详细的讲解,可以看一看这一篇博客:Linux中进程控制块PCB-------task_struct结构体结构(童嫣
有了上述的观念之后,我们从内核的角度来看,进程就是**内核数据结构(task_struct) + 进程对应的磁盘代码**
ps axj
可以查看当前所有的进程将ps axj
结合管道
和grep指令
可以显示我们想要看到的进程,同时想要看到所有信息的含义,就要加上head -1
表示将查询到的第一行数据也显示出来
/proc
目录下查看当前所有进程对于一般进程,我们可以使用**[Ctrl + c]结束,也可以使用kill**命令结束
每个进程都有一个唯一标识符:PID我们可以通过系统调getpid()
用来获取这个唯一标识符
注意:
这里的getpid和getppid获取到的就是进程的标识符,其中getpid
是获取进程的PID,getppid
是用于获取父进程PID
函数的返回值pid_t实际上就是int,只是在系统层面进行了封装
可以看到,我们通过 getpid() 和 getppid() 函数得到的值的确是我们进程对应的id;同时,我们发现myproc进程的父进程是 bash,即 shell 外壳,这也侧面证实了我们之前提到的结论 – shell 为了防止自身崩溃,并不会自己去执行指令,而是会派生子进程去执行。
同一个程序重新被运行时它的进程id可能与之前不一样,因为它的代码和数据需要重新从磁盘中加载;但是它的父进程id一定是一样的,因为它们都是通过 bash 来执行
我们可以通过系统调fork
来创建子进程
可以看到,所有子进程的id都是0,父进程的id都是子进程的pid,这是因为fork的返回值规定:当fork执行之后将会产生两个进程,其中父进程的返回值是子进程的pid,子进程的返回值是0,fork之后的所有代码都被父子进程共有
在真正进入的进程状态的学习之前,大家或多或少应该听说过一些进程的状态,比如:挂起,僵尸等等,在进入的进程状态的学习之前,我们需要统一一些观点:
状态是进程内部的属性,所有的属性都在PCB里
进程不只会占用CPU资源,也有可能随时要外设资源
运行状态
进程PCB在运行队列里面的进程状态就是运行状态(不是说进程在运行时才是运行状态)
运行队列的概念:
CPU需要对进程做执行,同一时间操作系统内会有很多的进程,那么这些进程谁先谁后呢?此时就需要一个队列将这些进程管理起来,确定谁先执行谁后执行。这个队列里面保存的实际上并不是每个进程对应的二进制代码,而是PCB(task_struct),这个队列就是运行队列(run queue)
每个CPU拥有一个运行队列
阻塞状态
我们在上面说到,进程不止会占用CPU资源,也可能随时需要外设资源,当一个进程在运行的过程中需要访问外设资源的时候,由于外设很慢(相对于CPU来说),所以进程需要等待外设,此时CPU就想去处理其他进程,那么当前进程就会进入到阻塞状态
拓展一下:
每个CPU执行一个进程,当前进程在处于阻塞状态,等待其他硬件资源的时候,实际上也就是当前进程的PCB进入到了其他硬件的run queue中,也就是说每一个硬件都拥有一个runqueue
挂起状态
如果系统中存在很多进程,当前进程短时间内不会被调度,代码和数据短时间内不会被执行,此时如果操作系统中的内存不够,就会将这些进程的相关信息保存在磁盘中,节省一部分内存。此时这些被保存在磁盘上的进程就是处于挂起状态
挂起一定是阻塞的,阻塞不一定挂起
在上一小节中我们讲到了操作系统内进程的概念性状态,那么在Linux下的表现形式是怎样的呢?
在Linux源码里面能够找到一个数组:task_state_array
罗列了Linux下所有的进程状态
/*
* 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 *task_state_array[] = {
"R (running)", /* 0 */ // 运行状态
"S (sleeping)", /* 1 */ // 阻塞状态
"D (disk sleep)", /* 2 */ // 磁盘休眠状态/深度睡眠状态(了解)
"T (stopped)", /* 4 */ // 暂停状态
"T (tracing stop)", /* 8 */ // 该进程正在被追踪(看到的是t状态)
"Z (zombie)", /* 16 */ // 僵尸状态
"X (dead)" /* 32 */ // 死亡状态
};
接下来我们来看一些进程状态的例子:
这里出现休眠状态的原因是:调用printf的时候需要访问外设——显示器,由于外设的速度很慢(相对于CPU而言),所以大部分时间都是在等在显示器资源,所以我们能看到的状态大多都是休眠。当然如果非常巧合的话,也是能看到R状态的
这里扩展一下:S是浅度睡眠,可以被终止;D是深度睡眠,无法被OS杀掉,只能通过断电、自己醒来进行解决。出现的原因一般是由于高IO导致的。
tracing stop表示该进程正在被追踪
进程是用来完成一些任务的,所有的进程在完成后都要保留资源,等待父进程来获取相关信息
1.僵尸进程
**僵死状态(Zombies)**是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
接下来我们看一个僵尸进程的例子
接下来我们写一个简单的脚本来监视这段代码跑起来的进程
while :; do ps axj | head -1 && ps axj | grep 'demo' | grep -v grep ; sleep 1; done
# ps axj | head -1 是为了输出查询结果的第一行
# ps axj | grep 'demo' | grep -v grep 是为了输出我们要查找的进程项,同时忽略grep进程
# sleep表述每1秒执行一次指令
上述脚本的运行结果:
由于当前没有名为demo的进程,所以查询结果就只有第一行的内容
运行起来demo进程之后:
defunct的意思是失效的,也就是进程是已经死亡的,但是没有被回收。把左侧终止,在执行上面监视的命令,就不存在上面的进程了,这是因为把父子进程都终止的时候,操作系统自动回收了
但是,如果父进程不做处理的话,子进程将会一直占用资源,就会引发内存泄漏等问题
僵尸进程的危害分析
- 进程在结束之后的退出状态需要被一直保存下去,直到父进程来读取结果(子进程完成任务,一定要跟父进程汇报这个任务完成的怎么样了)所以如果父进程一直不读取,那么子进程将会一直维持Z状态
- 维护进程的退出状态本身也是在维护进程数据,也就是要维护这个进程的PCB信息,换句话说,如果进程一直维持着Z状态,那么进程的PCB将会一直存在
- 如果一个父进程创建了很多子进程,就是不回收,就会造成内存泄漏问题
2.孤儿进程
我们刚刚讲了僵尸进程是由子进程结束之后父进程没有读取退出信息造成的,那么如果有一种进程,他的父进程先结束,然后子进程结束的时候的信息由谁来读取呢?
我们看一段代码:
运行后的监控脚本运行结果如下:
那么1号进程是啥啊?
如图可知,pid为1的进程就是操作系统,所以结论:
当父进程在子进程结束之前就已经结束,那么该子进程就被称为孤儿进程,孤儿进程将被操作系统领养。
这里还有两个细节:
S+
变成了S
即前台进程变成了后台进程,因此没有办法使用^C来终止,只能通过kill指令发送-9信号来终止进程本节完