【Linux】进程概念
慕雪年华

本篇博客是有关进程状态的,好久没有写Linux的博客了,一起来看看吧!

实验系统:CentOS 7.6

1.系统进程的运行状态

当我们想到进程的时候,一定要首先想到task_struct结构体。该结构体内部有一个state状态码,用于标识当前进程处于什么状态

image

1.1 运行态

CPU会有一个进程队列(双链表),队列的每一个成员都是一个task_struct结构体,用来维护即将运行的进程。当轮到某个进程运行的时候,CPU就会将这个进程的数据和代码放入内存和自己的寄存器,并开始运行

只要进入了运行队列的进程,就是运行态的进程

所以运行态并不是正在运行的进程

为什么我们对这件事的感知不大呢?那是因为现代的CPU的运行速度非常快,这些运行队列的轮转周期很短

1.2 终止态

终止态:进程还在,但是永远不会运行,在队列中等待被释放

为什么进程都终止了,不立马释放对应的资源,而需要维护一个终止态?

  • 这是因为当前CPU/操作系统正在忙着干其他的事情,没时间过来释放你。所以会将不运行的进程放入终止态的队列(将该进程task_struct结构体插入该队列)
  • 当操作系统空闲的时候,便会取终止态队列里面的进程进行释放

1.3 阻塞态

一个进程使用资源的时候,不仅仅会申请CPU的计算资源,还有可能申请其他更多的资源,比如网络/硬盘/网卡/显卡等等

如果申请这些资源的时候得不到满足,就需要排队

  • CPU资源:运行队列
  • 其他资源:也需要进行排队

下面用伪代码的方式来描述一下进程为何会阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct cpuinfo
{
//……
//运行队列
task_struct *queue;
}
struct disk_div
{
//磁盘的等待队列
task_struct *queue;
}
struct net_div
{
//网卡的等待队列
task_struct *queue;
}

当我们的进程访问某种资源,特别是外存(磁盘)这种慢设备资源的时候,如果磁盘暂时还没有准备好,操作系统就会把当前进程从运行队列剥离,插入到对应需要访问的设备下的等待队列中

DDR4内存的读写大约是40GB/S,即便是现在市面上较快的pcie4.0固态硬盘其读写速度也只有7GB/S左右,这个差距还是很大的。所以才说磁盘是“慢设备”资源

操作系统移动task_struct到对应的队列下,就是起了它管理进程的作用。同时将进程剥离运行队列,也能让等待慢设备资源的进程不至于把整个系统卡死。当我们电脑上运行的进程很多的时候,就有可能遇到当前的进程在等待过程中出现了阻塞,此时进程的代码不会运行。最直观的反应便是当前进程卡住不动了。


1.4 进程挂起

进程挂起和进程阻塞很类似,但也有不同。

image

进程挂起和阻塞不同的是,阻塞只是单纯地在等待慢资源。而挂起则是该进程的数据被放入回了磁盘,进程本身依旧在排队等待。操作系统会有一个专门的swap分区,用来存放挂起进程的代码和数据。

操作系统这么管理,是为了不让内存在多进程运行的时候不够用了。这也是为什么,当我们内存不够用的时候,往往伴随着磁盘的频繁读取。

当然,内存不够也有可能是某一个进程需要一次性加载的代码数据已经超过了内存的大小😂

下图中左下角的部分便演示了操作系统在处理阻塞和挂起态的操作循环

image


2.Linux下的进程状态描述

在linux下进程的状态是存放在一个数组里面的

image

2.1 S和R状态的说明

其中R对应的是运行态,S对应的就是阻塞态(linux下为休眠)

我们可以运行一个程序看看它处于什么状态

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1){
printf("%d\n",getppid());
sleep(1);
}
return 0;
}

运行发现,左侧打印出了当前进程PID,而右侧当我们使用ps命令查询该进程的时候,发现该进程的状态是S+也就是休眠状态

1
2
[muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test
32080 30455 30455 32080 pts/0 30455 S+ 1001 0:00 ./test

image

这是为什么?程序不是一直都在运行吗?

首先需要知道的是,printf需要将数据打印输出到屏幕上,屏幕作为外设,同样属于慢资源。所以我们的进程绝大部分时间都是处于sleep(1)以及等待屏幕刷新的过程中。而CPU只需要执行printf一个操作,这个操作几乎是瞬间就进行,当然也看不到该进程处于运行态了

即便我们把sleep(1)去掉,进程也是需要等待屏幕刷新,同样处于S+状态

那要怎样才能让进程处于运行态呢?很简单,我们写一个不和外设交互的死循环即可

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
while(1){
int a = 10;
}
return 0;
}

可以看到现在进程的状态是R+,处于运行态!

1
2
[muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test
32080 32489 32489 32080 pts/0 32489 R+ 1001 0:06 ./test

image

2.2 D状态disk sleep

除了基本的S状态,linux下还有一个专门的disk sleep状态。如同它的名字一样,这个状态是专门为访问硬盘的进程设计的

假设有下面这样一个场景

1
2
3
4
5
1.进程A需要访问磁盘资源,写入1GB的数据
2.磁盘很忙,进程A进入S状态等待读写
3.操作系统发现这里有个进程没干活,内存又不够了,于是把进程A干掉了
4.轮到进程A独写的时候,磁盘发现进程A已经被干掉了,于是没管它的1GB数据
5.结果:这个1GB数据丢失了

出现这种数据丢失,谁都不想的嘛。所以Linux就设置了一个D状态,

  • S 浅度睡眠
  • D 深度睡眠

处于D状态的进程不能被操作系统kill掉。要想杀掉一个D状态的进程,只有下面三种办法

  • 等硬盘读写完毕,给进程返回结果之后,进程从D状态变成其他状态,操作系统进行处理
  • 关机重启
  • 拔掉电脑的电源

image

linux下可以用DD命令直接对硬盘进操作


3.僵尸进程Z

僵尸进程对应的状态码是Z,而X1.2提到的终止态

3.1 为什么会存在僵尸进程?

当Linux中的一个进程退出的时候,一般不会进入X状态(终止态,可以回收资源),而是进入Z状态

  • 进程运行了一定的程序,可以理解为这个进程的任务
  • 当该进程退出的时候,需要知道这个进程是如何结束的(可以理解为终止的原因)
  • 一般是将执行结果交还给操作系统或者父进程

维护一个状态Z,就是为了维护进程的退出信息,可以让父进程/操作系统读取

父进程/操作系统是通过进程等待来读取进程的退出信息的

task_struct里面就有一个专门的成员来维护退出信息

image

3.2 如何复现僵尸状态?

我们创建子进程的时候,只要父进程不搭理子进程,一直运行父进程,提前终止子进程,就可以观察到子进程进入僵尸状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
int ret = fork();
if(ret == 0)
{
int i = 6;
while(i)
{
printf("我是子进程,我的ppid是%d,pid是%d\n我还有%i秒终止",getppid(),getpid(),i--);
sleep(1);
}
printf("子进程进入僵尸状态\n");
exit(0);
}
else{
printf("我是父进程,我的ppid是%d,pid是%d\n",getppid(),getpid());
while(1)
{
int a = 10;
}
}
return 0;
}

image

1
2
3
4
5
6
[muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test1 | grep -v grep
32080 30962 30962 32080 pts/0 30962 R+ 1001 0:05 ./test1
30962 30963 30962 32080 pts/0 30962 S+ 1001 0:00 ./test1
[muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test1 | grep -v grep
32080 30962 30962 32080 pts/0 30962 R+ 1001 0:06 ./test1
30962 30963 30962 32080 pts/0 30962 Z+ 1001 0:00 [test1] <defunct>

英语小课堂:defunct

image

一般我们都会要求父进程回收子进程,不过这个得后续才能学到了!

ps循环监控脚本

我们可以使用一个监控脚本来更方便的监控结果

1
while :; do ps jax | head -1 && ps jax | grep test | grep -v grep;sleep 1; echo "########################"; done

上面这个语句的作用是,每一秒执行一次ps jax | head -1 && ps jax | grep test | grep -v grep命令,直到我们使用ctrl+c终止进程

需要注意分隔符,while后面的是:;不要写成双冒号!

3.3 长时间僵尸状态的弊端

如果一个僵尸进程长时间不被处理,就容易出现内存泄漏

子进程的状态是用数据维护的,如果父进程一直不回收子进程,该子进程的task_struct就一直留存在内存中,这就是一定的内存泄漏。

3.4 孤儿进程

当一个进程的父进程先退出的时候,子进程就会变成孤儿进程

image

为什么这里我们没有看到父进程进入Z或者X状态呢?那是因为这里父进程的父进程是bash,命令行回收了我们的父进程。

可子进程为何还在这里呢?

  • 父进程退出之后,子进程并不会不见,而是会被1号进程(操作系统)领养。
  • 这时候我们可以把子进程称为孤儿进程

注:1号进程又称init进程

image

image

而操作系统领养之后的子进程,即便你使用ctrl+c也干不掉这个进程

image

仔细观察,可以看到子进程被操作系统领养后,运行状态上的+不见了

1
2
3
4
5
6
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
32080 2842 2842 32080 pts/0 2842 S+ 1001 0:00 ./test2
2842 2843 2842 32080 pts/0 2842 S+ 1001 0:00 ./test2
################################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 2843 2842 32080 pts/0 32080 S 1001 0:00 ./test2

前台和后台进程

状态码上带有+号,代表进程是一个前台进程

  • 能被CTRL+C终止的都是前台进程
  • 后台进程一直在运行,会影响我们的命令行输入

我们可以使用kill -9干掉该进程,干掉进程之后,就可以使用CTRL+C恢复正常的命令行了

image

3.5 守护进程和精灵进程

守护进程&精灵进程:这两种是同一种进程的不同翻译,是特殊的孤儿进程,不但运行在后台,最主要的是脱离了与终端和登录会话的所有联系,也就是默默的运行在后台不想受到任何影响


4.进程暂停T

暂停一共有两种状态,一种是stopped,第二种是tarcing stop

image

一般linux系统是用大T指代stopped,用小t指代tarcing stop

4.1 T-stopped

kill发信号

在之前的操作中,我们已经学过使用kill -9 pid来干掉一个进程,实际上kill命令能干的事情远不止这一个

1
kill -l

使用这个命令可以查看到kill命令支持什么操作

image

其中我们要用到的是第19和第18,分别用于暂停/恢复一个进程

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
printf("start!\n");
while(1){
;
}
return 0;
}

写一个啥事不干的死循环用于测试,可以看到该进程处于R+状态

image

1
[muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ kill -19 9503

执行kill -19命令之后,我们可以看到该进程被终止,状态码变为大T

image

进程暂停并不代表进程结束,这就好比我们看视频的时候暂停一样。你暂停了播放,但是播放器这个进程并不会直接终止!

要想让这个进程重新运行,执行kill -18即可,进程恢复为R状态。此时也没有了+,代表这是一个后台进程

image

1
2
3
4
5
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
32080 9503 9503 32080 pts/0 32080 T 1001 0:55 ./test3
########################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
32080 9503 9503 32080 pts/0 32080 R 1001 0:55 ./test3

使用kill -9干掉该后台进程即可

4.2 t-tarcing stop

tarcing一词意为追踪,最简单的情况便是我们使用的gdb调试打断点

image

1
2
3
[muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps jax | grep test
30493 12333 12333 30493 pts/1 12333 S+ 1001 0:00 gdb test_g
12333 12412 12412 30493 pts/1 12333 t 1001 0:00 /home/muxue/GIT/raspi/code/22-09-24_?程/test_g

这时候test_g进程就是一个处于小t状态的进程


5. 进程优先级

  • 权限:能还是不能的问题,决定进程能不能访问某种特定的资源
  • 优先级:进程可以访问该资源,但有先后顺序(运行队列)

进程在排队获取资源的本质就是在确认优先级。这是因为系统的某些慢资源不够多个进程同时使用,这时候就需要让进程进入排队来先后访问。

而优先级越高的进程,操作系统执行它的响应就会更快。其会把它插入到优先级低于它的进程之前,先运行这个“vip进程”,再运行那些“普通进程”

5.1 linux进程优先级

可以用下面的ps -la命令查看当前bash下的进程

1
2
3
4
5
6
7
8
[muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 1001 27595 30493 0 80 0 - 38587 - pts/1 00:00:00 ps
0 S 1001 30493 30492 0 80 0 - 29576 do_wai pts/1 00:00:00 bash
[muxue@bt-7274:~/GIT/raspi/code/22-09-24_进程]$ ps -la
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
1 S 1001 27546 1 0 80 0 - 1833 hrtime pts/0 00:00:00 test2
0 R 1001 27598 30493 0 80 0 - 38595 - pts/1 00:00:00 ps

其中的PRINI就是我们进程优先级的数据

  • linux的进程优先级=priority_old+nice
  • linux下进程的默认优先级是80,PRI值越低,优先级越高
  • NI值是进程优先级的修正数据,我们修改进程优先级,修改的是NI值而不是PRI

这两个值允许的范围如下,Linux系统并不支持用户无节制的修改优先级

1
2
-20 <= NI <= 19
60 <= PRI <= 99

5.2 使用top命令进行修改优先级

linux下修改优先级的操作如下,运行test1程序后,先查看它的优先级信息

1
2
3
4
5
[muxue@bt-7274:~]$ ps -la
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 1001 29632 30493 98 80 0 - 1833 - pts/1 00:00:42 test1
1 Z 1001 29633 29632 0 80 0 - 0 do_exi pts/1 00:00:00 test1 <defunct>
0 R 1001 30732 29713 0 80 0 - 38595 - pts/0 00:00:00 ps

在使用sudo top后,进入界面按r,输入需要设置的进程pid后,再输入需要调整的nice值

image

image

1
2
3
4
[muxue@bt-7274:~]$ ps -la
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 1001 29632 30493 99 70 -10 - 1833 - pts/1 00:02:41 test1
1 Z 1001 29633 29632 0 80 0 - 0 do_exi pts/1 00:00:00 test1 <defunct>

这里可以看到,test1进程的优先级已经被我们改成了70

再来尝试第二次,这次nice设置为20看看

image

1
2
3
4
[muxue@bt-7274:~]$ ps -la
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 R 1001 30697 30493 98 99 19 - 1833 - pts/1 00:00:16 test1
1 Z 1001 30698 30697 0 80 0 - 0 do_exi pts/1 00:00:00 test1 <defunct>

诶,pid设置成20之后,为啥NI值变成了19,而PRI变成了99呢?

依据我们以往的惯性思维,既然进程优先级=priority_old+nice,那么修改了之后不应该是原本的70+20=90吗?为什么是99呢?

这是因为每一次设置的时候,priority_old都会被重置成80。所以可以直接记住,Linux下进程的优先级=80+Ni值

The End

感谢你看到最后,如果本篇博客有啥问题,欢迎在评论区提出!

image