Linux进程概念(上):进程基本概念和进程状态
文章目录
- 一、进程的基本概念
- 描述进程(PCB)
- Linux的PCB——task_struct
- task_ struct内容分类
- 查看进程
- 系统目录查看
- ps命令查看
- 通过系统调用获取进程标识符
- 通过系统调用创建子进程(fork的初识)
- fork函数创建子进程
- 用if分流父子进程
- 二、进程状态
- R运行状态(running)
- S浅度睡眠随眠状态(sleeping)
- D磁盘休眠(深度随眠)状态(Disk sleep)
- T暂停状态
- X死亡状态
- Z僵尸状态
- 僵尸进程
- 僵尸进程的危害
- 孤儿进程
一、进程的基本概念
课本概念:一个已经加载到内存中的程序或者是正在运行的程序,叫做进程。
在现实生活中,我们通常称作进程为任务。比如Windows的任务管理器,实际上就是进程的管理器。
然而,我们以课本概念理解进程是肤浅的,我们需要站在操作系统的角度,才能深入理解进程。
我们知道,操作系统是做管理的,那操作系统是如何管理进程的呢???
先描述,再组织!
描述进程(PCB)
常识告诉我们:一个操作系统不仅仅只能运行一个进程,而是可以同时运行多个进程。
因此,操作系统必须将所有进程管理起来。
任何一个进程,在加载到内存的时候(形成进程时),操作系统会先创建该进程的一个结构体对象(描述进程),这个结构体对象包含进程的所有重要属性。
这个结构体对象叫做进程控制块(PCB)(process control block)
PCB属性中含有指向下一个PCB的指针成员变量(组织进程),这样一来,多个进程就组成了一个以PCB为节点的单链表的数据结构对象。此时在操作系统中,对进程的管理就变成了对单链表的增删查改。
一个程序,是包含代码和数据的,所以一个进程也应该包含代码和数据。那么,如果代码量和数据量非常庞大呢?而内存的资源又是十分有限的!
事实上,代码和数据根本不会加载到内存操作系统中,而是在磁盘上!在内存的PCB中,存在指向代码和数据的指针,在需要时,寻址获取即可。
总结:
进程 = 内核PCB数据结构对象(描述该进程的所有属性值) + 代码和数据
进程内核观点:担当分配系统资源(CPU时间,内存)的实体。
Linux的PCB——task_struct
那么在Linux系统中,具体是怎么做的呢?
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构(采用双向链表组织的),它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
task_ struct具体包含哪些属性呢?
其实非常多,翻阅Linux内核源码即可窥见全貌,此处列举部分属性:
- 标识符:描述本进程的唯一标识符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级
- 程序计数器:程序中即将被执行的下一条指令地址
- 内存指针:包括程序代码和进程相关数据的指针,还有其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息:包括处理时间总和,使用的时钟数总和,时间限制记账号等。
- 其他……
查看进程
系统目录查看
在我们的根目录下,有一个名为proc的系统目录:
目录中包含系统所有的进程信息,其中有很多的名为数字的目录:
这些数字其实就是进程的标识符PID,每个PID是唯一的,一个PID对应一个进程,打开这个进程目录,就可以查看该进程的所有信息。
例如,查看PID为1的进程,打开这个名为1的目录即可:
ps命令查看
查看所有进程:
ps axj
配合grep使用可查看我们需要查看的进程信息:
ps axj |grep myproc|grep -v grep
使用grep过滤时,也会产生进程(会被筛出来),我们可以用grep -v grep将其过滤掉:
还可以将所有信息属性显示出来,便于我们查看:
ps axj | head -1 && ps axj | grep myproc | grep -v grep
通过系统调用获取进程标识符
- PID:进程id
- PPID:父进程id
通过系统调用接口getpid()
和getppid()
,可以分别获取进程的PID和PPID。
测试:
我们运行这个程序,并查看该进程信息,观察到运行打印提示和我们预期结果一致,如图:
通过系统调用创建子进程(fork的初识)
fork函数创建子进程
fork是一个系统调用级别的接口,其功能就是创建一个子进程。
空口无凭,我们来测试一下:
运行结果:
运行结果是循环打印两行数据,第一行数据是该进程的PID和PPID,第二行数据是代码中fork函数创建的子进程的PID和PPID。我们可以发现fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系。
可以看到,父进程也有一个父进程2052,这个进程是bash,这里的父进程2455也是bash通过创建子进程(fork)完成的。
bash其实就是执行解释命名的程序。
我们知道,每创建一个进程,操作系统会为其创建一个PCB,fork创建的进程也亦如此。
那么,fork函数创建的子进程的代码和数据从何而来呢?与父进程共用吗?
事实上:
- 父子进程的代码是共享的,因为代码是不可修改的,所以共享是合理的。但是一般而言,fork之后的代码才是父子共享的(因为fork之前子进程还未被创建)(顺序编译)。
- 父子进程的数据是各自开辟空间的,私有一份(采用写时拷贝),数据是可能被修改的,所以不能共享数据。在默认情况下,父子进程的数据是相同的,但并非同一块数据
用if分流父子进程
如果父子进程执行的代码是一样的,也就是让父子进程做相同的事情,那么这个子进程就没有意义了。
因此,fork之后通常需要用if进行分流,让父子进程做不同的事情。
那么如何分流呢?
fork函数的返回值:
- 如果子进程创建成功,则在父进程中返回子进程的PID,在子进程中返回0
- 如果子进程创建失败,则在父进程中返回-1
既然fork函数在父子进程中的返回值不同,那么我们就可以利用它们的返回值进行分流:
运行结果:
在这里,相信大家还是存疑的,这里解答几个问题:
-
为什么fork要给子进程返回0?给父进程返回子进程的PID呢?
返回不同的返回值,是为了区分,让不同的执行流执行不同的代码块! -
一个函数是如何做到返回两次的?如何理解呢?
以fork函数底层逻辑理解,fork也只是一个函数,那么fork究竟干了什么呢?
-
一个变量怎么会有两个不同的内容呢?如何理解?
该问题与进程地址空间相关,后文我会交代清楚。 -
父子进程创建好,也就是fork之后,谁先运行呢?
由调度器的调度算法决定的,不确定!
二、进程状态
在操作系统中,一个进程从创建至消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间而无法执行。因此,进程是活动且有状态的。
我们来看看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 */
};
注意:进程的当前状态是保存到自己的进程控制块(PCB)当中的,在Linux操作系统当中也就是保存在task_struct当中的。
在Linux操作系统当中我们可以通过 ps aux 或 ps axj 命令查看进程的状态:
R运行状态(running)
一个进程处于运行状态,并不意味着进程一定在运行中,它表明进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程。
那为何要运行队列呢?
在现实中,几乎所有的计算机都只有一个CPU,但我们的计算机又是如何同时运行多个进程的呢?
靠的就是并发执行!
并行:多个进程在多个CPU下分别同时运行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进
因此,每一个进程都有一个叫做时间片的概念!
每个进程只会在CPU中运行一次时间片,然后就被切换,这就是进程切换。
在一个时间段,大量的进程会从CPU上放上去又拿下来,因为时间片是非常小的(ms级别),所以我们是感知不到进程切换的,最终给用户呈现的效果就是多个进程在同时运行。
所有处于运行状态可被调度的进程,被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行即可。
S浅度睡眠随眠状态(sleeping)
一个进程处于浅度睡眠状态,意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。
- 在平时我们用的sleep()函数,等待的过程中进程其实就是S状态,它等待的事情就是我们设置的时间倒计时
- 我们程序等待我们键盘输入时(I流函数),等待的过程中也是S状态,它等待的事情就是我们从键盘输入数据。
D磁盘休眠(深度随眠)状态(Disk sleep)
一个进程处于深度睡眠状态,表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。
例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。
T暂停状态
我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行。
使用kill -l
命令可以列出当前系统所支持的信号集。
kill -l
X死亡状态
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)。
Z僵尸状态
当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态(zombie)。
僵尸状态的意义:因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态。
我们在写C/C++代码时,都会在main函数的最后return 0。
int main()
{//...return 0;
}
其实这个0就是返回给操作系统的退出码,告诉操作系统代码已经执行结束了,可以回收释放这个进程了。
在Linux操作系统当中,我们可以通过使用echo $?
命令获取最近一次进程退出时的退出码。
echo $?
注意:进程退出的信息(例如退出码),是暂时被保存在其进程控制块(PCB)当中的
僵尸进程
处于僵尸状态的进程,我们就称之为僵尸进程
制造一个僵尸进程:
我们让子进程退出,让父进程继续运行,父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{printf("I am running...\n");pid_t id = fork();if(id == 0){ //childint count = 5;while(count){printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);sleep(1);count--;}printf("child quit...\n");exit(1);}else if(id > 0){ //fatherwhile(1){printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}}else{ //fork error}return 0;
}
僵尸进程的危害
- 僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护
- 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
- 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。
孤儿进程
父子进程,如果父进程先退出,子进程的父进程会被改为1号init进程(操作系统的进程)。此时,子进程就是孤儿进程。
定义:父进程是1号init的进程称为孤儿进程
其实,子进程是被系统领养了,这里系统好比现实中的孤儿院。
那么系统为什么要领养呢?
因为进程生命周期结束会退出,也需要被释放,而释放是交给父进程来做的。
那为什么要将父进程修改为1号呢?
因为除了1号进程和其原父进程,其他进程不具备释放该进程的能力。
制造一个孤儿进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{printf("I am running...\n");pid_t id = fork();if(id == 0){ //childint count = 5;while(1){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);sleep(1);}}else if(id > 0){ //fatherint count = 5;while(count){printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);sleep(1);count--;}printf("father quit...\n");exit(0);}else{ //fork error}return 0;
}
下篇预告:进程优先级和环境变量
有错误欢迎指出,万分感谢
创作不易,三连支持一下吧~
不见不散!