进程详解(二)

进程

进程控制块PCB。回顾:命令行上启动的进程,一般它的父进程都是bash(特殊情况除外)。

fork()之后,会有父进程和子进程两个进程在执行后续代码,即fork之后的代码被父子进程共享。

函数会返回0给子进程,返回子进程ID给父进程,创建失败则返回-1,故使用fork通常会加上if判断。

进程具有独立性、竞争性、并行、并发。并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行。并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为
并发。

进程状态/任务状态

一些状态如运行、挂起、停止等是进程的内部属性,都在PCB里(task_struct里面),与进程队列里 进程的状态不同。

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。

各种进程状态,比如

运行、新建、就绪、挂起、阻塞、等待、停止、挂机、死亡...

普遍的操作系统层面如何理解上述状态?

运行状态

1、**所谓进程的不同状态,本质是 进程在不同的队列中,等待某种资源。**一般把在等待puc资源的进程队列称为运行队列,等待外设的称为阻塞队列。

2、因为cpu只有1个,但是有很多进程在等待执行,故有一个进程队列/运行队列,(1个cpu1个运行队列),

3、让线程进入队列,本质是将该进程的PCB结构体对象(task_struct)放入到运行队列,让PCB去进入队列去排队(不是让程序去排队)。

4、在运行队列runqueue里的进程,就是运行状态,R,表达的不是该进程是否正在运行(因为cpu执行速度很快,这个运行时间很短,对此衡量是无意义的)。运行状态与 有没有在cpu里跑 无关,不是说这个进程正在运行才是运行状态。

5、CPU执行速度很快,硬件执行速度比较慢,但是进程或多或少都要访问硬件。如果该硬件还没准备好,那么当有很多进程时,进程可能在等待cpu资源,也有可能在等待外设资源

阻塞状态

该进程不能直接被cpu调用,因为该进程还在等待其他资源(比如外设资源),在等待外设资源的队列称为阻塞队列。阻塞状态都是cpu把PCB进程块对象放到不同的队列中(阻塞队列,等待队列等),在阻塞状态的进程不能被立即调度;

挂起状态

与阻塞状态相区分,假如很多进程都在阻塞状态下,意味着不会被cpu执行,而这些程序的代码和数据短期内不会使用,假设此时内存被占满了,如果此时想运行其他程序,操作系统就会保留对应的PCB,但把那些程序的代码和数据暂时保存到磁盘上,变成挂起状态。进程从挂起状态-运行状态,将进程相关的数据加载或保存到磁盘,这个过程称为内存数据的换入换出

即阻塞不一定挂起,挂起一定阻塞。只要不是运行状态,其他状态均有可能被挂起。

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 * 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 */
};

sleep–S

sleep浅度睡眠 是阻塞状态的一种。当运行test程序while(1){printf("%d", 1);}时,使用ps axj查看进程状态。【IO密集型程序】

[yyq@VM-8-13-centos 2023_01_02_ProcessState]$ ps axj | head -1 && ps axj | grep testPPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND6106  6447  6447  6106 pts/0     6447 S+    1001   0:00 ./test6447  6448  6447  6106 pts/0     6447 S+    1001   0:00 ./test6357  6615  6614  6357 pts/1     6614 S+    1001   0:00 grep --color=auto test

这个程序一直在运行,为什么它的状态是S(sleep)+呢?+表示前台进程,可以用ctrl+c终止,或者用信号kill -9 进程ID来终止。

因为printf是显示在显示器上,等显示器就绪要的时间比较长(IO比cpu慢)【CPU编译仅占1%时间,99%是被IO在占用】,所以如果程序有访问外设的行为时,查看进程状态大概率看到的状态都是S。

当test.c的代码只有while(1);时,可以看到R+状态。【计算密集型程序】

[yyq@VM-8-13-centos 2023_01_02_ProcessState]$ ps ajx | head -1 && ps ajx | grep testPPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND6106 10956 10956  6106 pts/0    10956 R+    1001   0:22 ./test
10860 11023 11022 10860 pts/1    11022 R+    1001   0:00 grep --color=auto test

stop–T

stop暂停 也算阻塞状态的一种。

kill -19 进程ID此条命令是让程序暂停,状态会变成T

[yyq@VM-8-13-centos 2023_01_02_ProcessState]$ kill -19 10956
[yyq@VM-8-13-centos 2023_01_02_ProcessState]$ ./test[1]+  Stopped                 ./test
[yyq@VM-8-13-centos 2023_01_02_ProcessState]$ ps ajx | head -1 && ps ajx | grep testPPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND6106 10956 10956  6106 pts/0     6106 T     1001   2:45 ./test
10860 11518 11517 10860 pts/1    11517 S+    1001   0:00 grep --color=auto test

kill -18 进程ID此条命令是让进程继续执行,保存R,没有+,表示后台运行,即命令行此时也可以接受其他命令输入,该进程只能用信号kill -9 进程ID来终止。

[yyq@VM-8-13-centos 2023_01_02_ProcessState]$ kill -18 10956
[yyq@VM-8-13-centos 2023_01_02_ProcessState]$ ps ajx | head -1 && ps ajx | grep testPPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND6106 10956 10956  6106 pts/0     6106 R     1001   2:57 ./test
10860 14798 14797 10860 pts/1    14797 R+    1001   0:00 grep --color=auto test

disk sleep–D

disk sleep深度睡眠是阻塞状态的一种,无法被OS杀掉,只能通过断电、重启或者进程自己醒来解决。sleep和stop是浅度睡眠,可以被终止。(在高IO的情况下,可能看到D状态的进程。)

tracing stop–t

表示该进程正在被追踪,比如用gdb调试的程序。也是阻塞状态的一种。

dead-X

这个状态只是一个返回状态,一个进程一旦死亡,在任务列表是看不到这个状态的。

zombie–Z

正常的进程退出时,不能立即释放该进程对应的资源!要保存一段时间,让父进程或者OS来进行读取。

僵尸状态就是一个进程从退出的那一刻开始,直到被父进程读取到返回代码(让上层知道是因为什么原因退出的)的这一段时间所处的状态。这是一个问题状态。

//平时的查进程代码
//grep -v grep就可以不显示grep这个进程
ps ajx | head -1 && ps ajx | grep zombie | grep -v grep
//监控进程脚本
while :; do ps ajx | head -1 && ps ajx | grep zombie | grep -v grep; sleep 1; done
//zombie.c
//进程退出了,但是没有被回收,所处的状态就是僵尸状态
//让OS或父进程创建子进程
//父进程不退出也不读取子进程返回代码也不回收资源
//让子进程正常退出
#include 
#include 
#include 
int main()
{pid_t id = fork();//创建子进程//不考虑创建失败的情况if(id == 0){//子进程printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());sleep(5);//睡5sexit(-1);//让子进程直接中止}else{//父进程while(1){printf("我是父进程,pid:%d, ppid:%d", getpid(), getppid());sleep(1);}}return 0;
}

运行上述两个代码,可以观察到如下结果

 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
19148  6470  6470 19148 pts/0     6470 S+    1001   0:00 ./zombie6470  6471  6470 19148 pts/0     6470 S+    1001   0:00 ./zombiePPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
19148  6470  6470 19148 pts/0     6470 S+    1001   0:00 ./zombie6470  6471  6470 19148 pts/0     6470 S+    1001   0:00 ./zombie
-------出现Z状态 进程对应的名字变了+defunct失效的-----PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
19148  6470  6470 19148 pts/0     6470 S+    1001   0:00 ./zombie6470  6471  6470 19148 pts/0     6470 Z+    1001   0:00 [zombie] PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
19148  6470  6470 19148 pts/0     6470 S+    1001   0:00 ./zombie6470  6471  6470 19148 pts/0     6470 Z+    1001   0:00 [zombie] 

僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵尸进程。僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

僵尸状态会导致内存泄漏!!

孤儿进程:如果父进程先退出,子进程就是孤儿进程。会被1号init(即操作系统)进程领养,由init进程来释放资源。PID为1的进程是systemd或者initd(不同操作系统底层实现不同),I号就是操作系统。如果是前台进程创建的子进程变成孤儿了,会自动变成后台进程。被领养以后,该进程就会变成后台进程(+不见了)。

 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND1615  3964  3964  1615 pts/0     3964 S+    1001   0:00 ./fatherless3964  3965  3964  1615 pts/0     3964 S+    1001   0:00 ./fatherless----- 执行 kill -9 3964 -----PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND1  3965  3964  1615 pts/0     1615 S     1001   0:00 ./fatherlessPPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND1  3965  3964  1615 pts/0     1615 S     1001   0:00 ./fatherless
----- 父进程变成1,+消失 -----

如果孤儿进程不由操作系统领养,那么该进程的资源就没法释放。

进程优先级

优先级priority,进程先或者后获得资源。是cpu资源分配的先后顺序。本质就是PCB里面定义的一个或几个整数。

linux支持进程在运行过程中修改优先级,通过修改NI值完成。在调整时,PRI(old)就是80为基准。

为什么存在优先级?因为资源太少了。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。

//执行ps -l命令,可以看到
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行。80为基准来调整
NI :代表这个进程的nice值,表示进程可被执行的优先级的修正数值

PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)(即80)+nice。这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。所以,调整进程优先级,在Linux下,就是调整进程nice值,nice其取值范围是-20至19,一共40个级别

注意:进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。

如何修改优先级?执行top命令后,输入r(r表示renice),输入进程的PID,再输入NI值即可。

进程切换

cpu里面有寄存器,当程序要被加载到内存里,cpu通过pc(eip)指令计数器去找PCB,PCB里面有代码地址,把代码加载到内存中分析并执行。

cpu一直在做三件事:取指令;分析指令;执行指令。

当程序在运行的时候,一定会产生非常多的临时数据,这份数据属于当前进程。CPU内部虽然只有一套寄存器硬件,但是寄存器里保存的数据是属于当前进程的。寄存器硬件 不等于 寄存器内的数据。

进程在运行的时候,占有CPU,但不是一直占用到进程结束。比如死循环时,CPU也能运行别的进程。因为进程在运行的时候,都有自己的时间片(假设时间片10ms,跑完了最好,变成Z状态就不再调度了,没跑完就等下一个时间片)。

**当一个进程没跑完就被替换了,它的临时数据(上下文数据)存在哪里?**进程离开cpu,保留临时数据,称为“上下文保护”,回到cpu,恢复临时数据,称为“上下文恢复”。这两个概念就是进程切换的必经动作。

注意:寄存器不是这个进程的上下文,寄存器内的数据才是上下文。上下文保护的是数据。

寄存器被所有进程共享,但是寄存器内的数据是每个进程私有的,称为上下文数据。