
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [3 ~> 进程](#3 ~> 进程)
-
- [3.1 进程概念和基本操作](#3.1 进程概念和基本操作)
-
- [3.1.1 概念](#3.1.1 概念)
-
- [3.1.1.1 进程是什么?](#3.1.1.1 进程是什么?)
- [3.1.1.2 为什么要有进程?](#3.1.1.2 为什么要有进程?)
- [3.1.1.3 进程是怎么设计的?](#3.1.1.3 进程是怎么设计的?)
- [3.1.2 查看进程](#3.1.2 查看进程)
-
- [3.1.2.1 /proc](#3.1.2.1 /proc)
- [3.1.2.2 指令](#3.1.2.2 指令)
- [3.1.2.3 查看进程部分思维导图](#3.1.2.3 查看进程部分思维导图)
- [3.1.3 当前路径(含文件操作)](#3.1.3 当前路径(含文件操作))
-
- [3.1.3.1 文件操作:证明当前路径](#3.1.3.1 文件操作:证明当前路径)
- [3.1.3.2 更改工作路径](#3.1.3.2 更改工作路径)
- [3.1.4 PCB:进程控制块](#3.1.4 PCB:进程控制块)
-
- [3.1.4.1 PCB的概念](#3.1.4.1 PCB的概念)
- [3.1.4.2 task_struct:PCB的一种(Linux中的PCB)](#3.1.4.2 task_struct:PCB的一种(Linux中的PCB))
- [3.1.4.3 task_struct的分类](#3.1.4.3 task_struct的分类)
- [3.1.4.4 task_struct的组织进程](#3.1.4.4 task_struct的组织进程)
- [3.1.4.5 PCB的思维导图](#3.1.4.5 PCB的思维导图)
- [3.1.5 每个程序加载到内存时,OS都要创建描述的结构体](#3.1.5 每个程序加载到内存时,OS都要创建描述的结构体)
- [3.2 父进程和子进程](#3.2 父进程和子进程)
-
- [3.2.1 通过系统调用获取进程标示符](#3.2.1 通过系统调用获取进程标示符)
- [3.2.2 命令行直接启动时创建的对应的进程父进程都是bash](#3.2.2 命令行直接启动时创建的对应的进程父进程都是bash)
- [3.2.3 固定搭配:kill 9 [要杀掉的目标进程]](#3.2.3 固定搭配:kill 9 [要杀掉的目标进程])
- [3.2.4 类似bash,它是如何创建的子进程呢?](#3.2.4 类似bash,它是如何创建的子进程呢?)
- [3.2.5 fork初识:通过系统调用创建进程](#3.2.5 fork初识:通过系统调用创建进程)
-
- [3.2.5.1 运行man fork认识fork](#3.2.5.1 运行man fork认识fork)
- [3.2.5.2 关于fork的灵魂三问](#3.2.5.2 关于fork的灵魂三问)
- [3.2.5.3 fork流程演示](#3.2.5.3 fork流程演示)
- [3.2.5.4 fork函数可以做到一次创建多个子进程](#3.2.5.4 fork函数可以做到一次创建多个子进程)
- [3.3 进程状态](#3.3 进程状态)
-
- [3.3.1 task_struct不是属于双链表嘛?怎么能还属于调度队列呢?](#3.3.1 task_struct不是属于双链表嘛?怎么能还属于调度队列呢?)
- [3.3.2 已有的条件的情况下,计算出结构体对象的起始地址,进而访问其他进程属性](#3.3.2 已有的条件的情况下,计算出结构体对象的起始地址,进而访问其他进程属性)
- [3.3.3 task_struct既属于双链表又属于调度队列------内核为什么要这么做?](#3.3.3 task_struct既属于双链表又属于调度队列——内核为什么要这么做?)
- [3.3.4 进程运行和进程阻塞](#3.3.4 进程运行和进程阻塞)
- [3.3.5 进程挂起](#3.3.5 进程挂起)
- [3.3.6 结合Linux内核源代码,再看几种状态](#3.3.6 结合Linux内核源代码,再看几种状态)
- [3.3.7 进程状态查看命令](#3.3.7 进程状态查看命令)
- [3.3.8 前后台问题](#3.3.8 前后台问题)
-
- [3.3.8.1 什么叫做前后台?为什么要有前后台?](#3.3.8.1 什么叫做前后台?为什么要有前后台?)
- [3.3.8.2 为什么要有后台------提高效率是怎么体现的?](#3.3.8.2 为什么要有后台——提高效率是怎么体现的?)
- [3.3.8.3 前后台的理解](#3.3.8.3 前后台的理解)
- [3.3.9 T状态(stopped)和t(tracing stop)状态](#3.3.9 T状态(stopped)和t(tracing stop)状态)
- [3.3.10 D状态(disk sleep)](#3.3.10 D状态(disk sleep))
- [3.3.11 kill -l:查看Linux常见信号](#3.3.11 kill -l:查看Linux常见信号)
- [3.3.12 gdb创建子进程,让子进程完成调试](#3.3.12 gdb创建子进程,让子进程完成调试)
- [3.3.13 僵尸进程(Z)和死亡状态(X)](#3.3.13 僵尸进程(Z)和死亡状态(X))
-
- [3.3.13.1 僵尸进程小故事](#3.3.13.1 僵尸进程小故事)
- [3.3.13.2 理论](#3.3.13.2 理论)
- [3.3.13.3 实践:创建维持30秒的僵死进程例子](#3.3.13.3 实践:创建维持30秒的僵死进程例子)
- [3.3.13.4 僵尸进程的危害:内存泄漏问题](#3.3.13.4 僵尸进程的危害:内存泄漏问题)
- [3.3.13.5 僵尸进程总结](#3.3.13.5 僵尸进程总结)
- [3.3.14 孤儿进程](#3.3.14 孤儿进程)
- [3.3.15 进程状态总结](#3.3.15 进程状态总结)
- 结尾

3 ~> 进程
很多教材里面都是这样描述进程的概念的:运行起来的程序就是进程,或者说加载到内存的程序叫做进程。
这样讲一点也不好理解,很多人看见就是一脸懵,什么意思?因此,我们重新理解一下进程的概念。
3.1 进程概念和基本操作
3.1.1 概念

3.1.1.1 进程是什么?
一句话概括:当前:进程 = 内核数据结构(task_struct) + 自己的程序代码和数据 。

3.1.1.2 为什么要有进程?
一张思维导图图搞定------

3.1.1.3 进程是怎么设计的?
艾莉丝这里一张思维导图图搞定------

我们可以用一个指令来查看进程:ps axj | grep ./[可执行程序]------

3.1.2 查看进程
我们写这样的代码来测一测,看看进程。


3.1.2.1 /proc
进程的信息可以通过/proc系统文件夹查看 ,比如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
下面这些以数字命名的目录,这些数字本身是什么东西呢?这些数字就是进程对应的pid!即特定进程的pid。

我们来证明一下------比如下面这个父进程和子进程,我们可以获取一下pid分别为30535和30580的进程信息------
如下所示:ls /proc把进程内部的属性大部分也以文件的形式呈现出来了(Linux下一切皆文件),其实这些全都是内存的数据,包括像你把代码加载到内存,你的程序此时还在磁盘上,相当于在内存当中拷贝了一份,下图中上面反馈的属性大部分都是内存级的,简单理解一下------相当于查文件时,有些是磁盘上面的文件,有些是Linux操作系统对其做了封装(让我们以文件的形式看到了内核的数据)。蓝色字体是进程的目录。

如果按Ctrl + c,干掉了就没有了------



3.1.2.2 指令
大多数进程信息同样可以使用top和ps这些用户级工具来获取。
ps命令是查进程的,它自己也是一个命令,它自己也有一个进程------

ps的选项我们可以用大模型搜一下------


信息不太好看,我们可以再添加一下条件------



剩下的选项我们到时候再了解,所以 ps命令也能够支持查看进程属性。

我们现在已经认识了以上三个进程属性了。
c
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 while(1)
8 {
9 printf("hello,I am a process\n");
10 sleep(1);
11 }
12 return 0;
13 }


使用top命令效果如下所示------
bash
[Alice@VM-4-17-centos ~]$ top
top - 17:49:47 up 88 days, 18:56, 1 user, load average: 0.00, 0.01, 0.05
Tasks: 114 total, 1 running, 112 sleeping, 0 stopped, 1 zombie
%Cpu(s): 0.8 us, 1.0 sy, 0.0 ni, 98.0 id, 0.2 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 2046500 total, 294052 free, 349608 used, 1402840 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 1499332 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 191180 3852 2336 S 1.7 0.2 101:42.84 systemd
12038 root 20 0 1022656 104644 17192 S 1.0 5.1 523:36.86 YDService
7294 root 20 0 827612 22136 3400 S 0.3 1.1 179:37.55 barad_agent
28191 Alice 20 0 162108 2304 1612 R 0.3 0.1 0:00.43 top
2 root 20 0 0 0 0 S 0.0 0.0 0:01.70 kthreadd
4 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H
6 root 20 0 0 0 0 S 0.0 0.0 1:26.46 ksoftirqd/0
7 root rt 0 0 0 0 S 0.0 0.0 0:48.92 migration/0
8 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcu_bh
9 root 20 0 0 0 0 S 0.0 0.0 23:50.39 rcu_sched
10 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 lru-add-drain
11 root rt 0 0 0 0 S 0.0 0.0 0:22.13 watchdog/0
12 root rt 0 0 0 0 S 0.0 0.0 0:23.69 watchdog/1
13 root rt 0 0 0 0 S 0.0 0.0 0:49.03 migration/1
14 root 20 0 0 0 0 S 0.0 0.0 1:20.36 ksoftirqd/1
16 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/1:0H
18 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kdevtmpfs
19 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 netns
20 root 20 0 0 0 0 S 0.0 0.0 0:01.94 khungtaskd
21 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 writeback
22 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kintegrityd
23 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 bioset
24 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 bioset
25 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 bioset
26 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kblockd
27 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 md
28 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 edac-poller
29 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 watchdogd
35 root 20 0 0 0 0 S 0.0 0.0 0:01.18 kswapd0
36 root 25 5 0 0 0 S 0.0 0.0 0:00.00 ksmd
37 root 39 19 0 0 0 S 0.0 0.0 0:12.57 khugepaged
38 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 crypto
46 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kthrotld
48 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kmpath_rdacd
49 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kaluad
51 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kpsmoused
3.1.2.3 查看进程部分思维导图

cwd:属于进程的基本属性之一。
3.1.3 当前路径(含文件操作)
cwd:current work dir(当前工作路径)。
3.1.3.1 文件操作:证明当前路径
我们分别在Windows和Linux两个操作系统上面用log.txt做个实验------

如上图所示,我们在C语言部分已经介绍过文件操作的内容,这里"w"是重写,写入时候如果已经有文件了就会重写,如果没有文件就会新建文件。


为什么要有当前路径?为了支持进程访问文件时缺省路径的问题。

pwd这个命令我们都使用过,就叫做 查看当前所处的文件路径------

也就是说,当前路径就是进程启动时候所处的路径!


类似于这个工作:在你的当前路径下面创建了一个log.txt文件------

3.1.3.2 更改工作路径
我们可以用chdir(系统级函数,叫做 "更改当前系统的工作路径")来更改工作路径(作用)------

我们改掉文件的工作路径之后,再运行------

此时我们ll,发现没有log.txt这个新建文件,我们再查看一下------


log.txt果然在这个路径下。这不就证明了:文件建在哪个路径取决于当前进程的工作路径(cwd)。

未来我们可以在指定的路径下创建文件,不指定就会在当前路径下新建文件。
Windows也是一样的,修改文件路径之后,双击一下(F5,运行一下程序),新建文件会和源代码在一起------

我们再来看一个系统调用(库函数)getcwd:获取当前工作路径------

修改一下代码,观察修改前和修改后到底有没有变化------

运行一下------

换言之,既然能拿到ip地址,那就能够拿到工作路径。

这个路径下必然会有一个文件log.txt。
3.1.4 PCB:进程控制块
3.1.4.1 PCB的概念
进程信息被放在一个叫做 进程控制块 的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是:task_struct 。

3.1.4.2 task_struct:PCB的一种(Linux中的PCB)
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构类型,它会被装载到RAM(内存)里并且包含着进程的信息。
3.1.4.3 task_struct的分类
标示符:描述本进程的唯一标示符,用来区别其他进程,pid。
状态:任务状态,退出代码,退出信号等(就像投 简历也有状态)。
优先级:相对于其他进程的优先级。
程序计数器:程序中即将被执行的下一条指令的地址(和 进程切换 挂钩)。
内存指针(类比电话号码、邮箱):包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针(具有指向性,最难的)。
上下文数据:进程执行时处理器的寄存器中的数据【休学的例子,要加图CPU,寄存器】(取数据------方便下次恢复出来------面试官生成面试报告[ 临时数据 ],给"我"打分,一面二面三面,方便下一轮的面试官获得上一轮的面试官的数据)。
IO状态信息:包括显示的I/O请求,分配给进程的I / O设备和被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等(就如投简历面试,面试面了几轮?面了多久?)。
以及其他信息。我们后面会介绍,我们下面先介绍标识符pid。
3.1.4.4 task_struct的组织进程
我们可以在内核源代码里找到它(这里不演示了)。所有运行在系统里的进程都以task_struct双链表的形式存在内核里。

我们以前在数据结构学过的双链表如下所示,但是PCB这里的双链表是没有数据(data)的,这就是Linux内核中链表的设计,和我们以前实现的链表数据结构还是有区别的。

3.1.4.5 PCB的思维导图

3.1.5 每个程序加载到内存时,OS都要创建描述的结构体
程序在磁盘上,从磁盘上某个位置加载到内存中------

我们可以来做个实验检验一下------

3.2 父进程和子进程
两个死循环同时跑------两个进程(以前C语言时是单执行过程),意味着:未来同一个代码,两个进程做不同的事情。
如下图所示:子进程返回 0 执行 if,父进程返回 pid 执行 else if------

我们查看Linux的内核源代码,可以看到父进程也是会记录下pid的------

如果要访问父进程的pid(即ppid),我们还有这样一个系统调用:getppid------


效果演示------

这个父进程是谁呢?我们下面介绍。
3.2.1 通过系统调用获取进程标示符


c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}

3.2.2 命令行直接启动时创建的对应的进程父进程都是bash
此时我们发现,每次运行,pid都不同,但是ppid保持不变------

我们ps查看一下,此时一个叫28750的进程映入眼帘,这个进程叫做"bash"。

我们自己bash一下再用ps去查的时候会发现多了一个bash------

bash(死循环的一个软件):代码内部死循环的,不退出------要退出需要按Ctrl + D退出!

-bash:命令行直接启动时,创建的对应的进程,父进程都是bash(命令行解释器)。
-:
bash前面带"-"的代表它是远程登录进来的。

who(谁在登录?谁在连接?)命令
每一次进来时,系统都会自动分配一个bash。


3.2.3 固定搭配:kill 9 [要杀掉的目标进程]
怎么干掉bash呢?我们可以用到这样一个命令:kill 9 [要杀掉的目标进程]。


这也是bash所担心的风险------一旦"杀掉",机器就挂掉了------


正确的 kill 命令格式:kill -9 [PID] 或 kill -SIGKILL [PID]。
我们可以先找到PID再杀掉目标进程------
bash
# 1. 找到 myproc.exe 的进程ID
ps aux | grep myproc.exe
# 或使用 pgrep(如果可用)
pgrep myproc.exe
# 2. 杀死找到的进程
kill -9 [找到的PID]
# 或者
kill -SIGKILL [找到的PID]
3.2.4 类似bash,它是如何创建的子进程呢?

相当于整个的代码是由父进程提供的------

3.2.5 fork初识:通过系统调用创建进程

3.2.5.1 运行man fork认识fork

3.2.5.2 关于fork的灵魂三问

下面几个小结论一会儿艾莉丝会通过思维导图的方式进行解释。
c
(1)fork有两个返回值
(2)父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
问题1:为什么给子进程返回0,给父进程返回子进程的pid?

原因如下所示------

在数量上,一个父进程和它的子进程的比例是1 : n。父进程需要通过唯一值pid标识不同的子进程,并且根据pid来控制子进程,给父进程返回子进程的pid也是为了告诉父进程多了一个子进程,让它记住子进程的pid,未来用这个pid来控制子进程。
这里子进程返回0说明一件事:子进程被成功创建出来了。
问题2:为什么同一个函数,会返回两次?


走到return之前,子进程就已经被创建了(这是 return两次 核心要点)------父子进程各自return了一次,这样就是两次。
问题3:为什么同一个变量id,既满足==0,又满足大于0?
进程具有很强的独立性------任何一个进程崩溃都不会影响到另一个进程(就像微信程序奔溃了,你的QQ会不会崩溃?)

父子进程牵扯到浅拷贝(指针)的问题,有一些共享的情况很正常。
我们再用%p打印一下flag的地址------
父进程不修改flag,子进程修改:

运行结果如下图所示------

现象:上面这个父进程和子进程的全局变量flag地址竟然是一样的!可是内容明明不一样啊?
所以这个地址一定不是物理地址!如果是物理地址,就不可能一个是10(flag不做修改的父进程),一个是11、12......(flag修改的子进程),说明不是物理地址而是虚拟地址。

子进程有很多属性继承自父进程(如路径),但个别属性和父进程区别开来了(如pid)------总之,子进程以父进程为模版。
父进程也是从它的父进程(bash)那里继承来的。
代码和数据是从磁盘加载到内存的,子进程也会指向父进程的数据和代码。
从上图中我们可以得出关于父进程和子进程的两个重要结论------

父子同时读,内容竟然不同!同一个地址值!因此,绝不可能是物理内存的地址!那是什么?虚拟地址。

写时拷贝 ------父子数据分开所采用的机制。
这里是经过虚拟内存机制映射之后,实际到不同的内存空间去了。
这样初步解释了"问题3:为什么同一个变量id,既满足==0,又满足大于0?"这个问题。

这里我们先不深究了,等到后续学到虚拟地址空间的时候会再介绍的。

这里 fork 的返回值写入到id,说明 返回的本质就是写入。
3.2.5.3 fork流程演示
c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello proc : %d!, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}
fork之后通常要用if进行分流------
c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if(ret < 0){
perror("fork");
return 1;
}
else if(ret == 0){ //child
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}else{ //father
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
我们现在已经明确了下面几个问题------
(1)fork为什么会有两个返回值?
(2)两个返回值各种给父子如何返回?
(3)一个变量怎么能让
if和else if同时成立这个问题,需要在后面才能解释清楚,这里我们简单提了一嘴,具体还是放在后面的虚拟地址空间部分介绍。
3.2.5.4 fork函数可以做到一次创建多个子进程
我们封装一个Loop函数,里面执行打印子进程和父进程的id的功能------

在for循环里面执行10次,运行结果如下所示------
子进程一进去就陷入循环:

实验结果证明:fork函数确实可以做到一次创建多个子进程。
3.3 进程状态

3.3.1 task_struct不是属于双链表嘛?怎么能还属于调度队列呢?
内存地址是从低地址到高地址线性排列的------几乎所有连续存储的数据结构在内存中都是从低地址向高地址排列的。不同类型的存储区域在这个线性空间中的分布方式不同。大部分数据在内存中的存储顺序是"低地址指向高地址"。
进程内存空间布局(以典型32位Linux为例),如下所示------
bash
高地址 0xFFFFFFFF
┌─────────────────┐
│ 内核空间 │ ← 用户进程不可访问
├─────────────────┤
│ 栈(stack) │ ← 向下增长(向低地址)
│ ↓ │
├─────────────────┤
│ ↑ │
│ 堆(heap) │ ← 向上增长(向高地址)
├─────────────────┤
│ 未初始化数据 │
│ (BSS段) │
├─────────────────┤
│ 已初始化数据 │
│ (数据段) │
├─────────────────┤
│ 代码段 │
│ (text段) │
└─────────────────┘
低地址 0x00000000
以整型值为例------

以数组为例------

那么,结构体呢------

3.3.2 已有的条件的情况下,计算出结构体对象的起始地址,进而访问其他进程属性

已知struct内部元素的地址,反求结构体变量的起始地址------

3.3.3 task_struct既属于双链表又属于调度队列------内核为什么要这么做?

3.3.4 进程运行和进程阻塞

3.3.5 进程挂起

3.3.6 结合Linux内核源代码,再看几种状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态:一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。下面的状态在kernel源代码里定义:
c
/*
*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 */
};
R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 [interruptible sleep])。
D磁盘休眠状态(Disksleep)有时候也叫不可中断睡眠状态(uninterruptiblesleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

3.3.7 进程状态查看命令
进程状态查看命令的选项如下所示------

为了方便演示查看进程状态的命令,我们简单写一下这样一个代码------
bash
1 #include<stdio.h>
2
3 int main()
4 {
5 int a = 0;
6 scanf("%d",&a);
7 printf("a=%d\n",a);
8
9 return 0;
10 }

我们写一下对应的Makefile------
bash
7 myproc.exe:myproc.c
8 gcc -o $@ $^ -g
9 .PHONY:clean
10 clean:
11 rm -f myproc.exe
如下图所示,此时的这个状态就是我们前面提到的阻塞状态------

我们再来写一段代码,打印"我是一个进程以及pid",------
bash
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 while(1)
6 {
7 printf("I am a process,pid: %d\n",getpid());
8 printf("I am a process,pid: %d\n",getpid());
9 printf("I am a process,pid: %d\n",getpid());
10 printf("I am a process,pid: %d\n",getpid());
11 printf("I am a process,pid: %d\n",getpid());
12 printf("I am a process,pid: %d\n",getpid());
13 sleep(1);
14 }
15 return 0;
16 }

3.3.8 前后台问题
3.3.8.1 什么叫做前后台?为什么要有前后台?

3.3.8.2 为什么要有后台------提高效率是怎么体现的?

3.3.8.3 前后台的理解


3.3.9 T状态(stopped)和t(tracing stop)状态
T状态:stopped(非法、越权时,OS不想杀掉这个进程,就暂停了)。



T、t状态在进程上没有本质区别。
我们可以通过在gdb里面 打断点 的方式来观察暂停状态。

3.3.10 D状态(disk sleep)
我们用一个小故事来理解D状态的概念。
如下图所示------

3.3.11 kill -l:查看Linux常见信号
bash
[Alice@VM-4-17-centos lesson16]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

3.3.12 gdb创建子进程,让子进程完成调试
cgdb底层也是gdb,这里建议用gdb是因为cgdb有半图形化界面,挡着看着不舒服。

gdb(cgdb本质也是gdb)------
bash
[Alice@VM-4-17-centos lesson16]$ vim Makefile
[Alice@VM-4-17-centos lesson16]$ make clean
rm -f myproc.exe
[Alice@VM-4-17-centos lesson16]$ make
gcc -o myproc.exe myproc.c -g
[Alice@VM-4-17-centos lesson16]$ ./myproc.exe
我是一个进程: 9786
我是一个进程: 9786
^C
[Alice@VM-4-17-centos lesson16]$ gdb myproc.exe
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/Alice/118/linux-git/lesson16/myproc.exe...done.
(gdb) l 0
1 #include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 while(1)
7 {
8 printf("我是一个进程: %d\n",getpid());
9 sleep(1);
10 }
(gdb) r
Starting program: /home/Alice/118/linux-git/lesson16/myproc.exe
我是一个进程: 9873
我是一个进程: 9873
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ad29e0 in __nanosleep_nocancel () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.3.x86_64
(gdb) b 2
Breakpoint 1 at 0x4005c1: file myproc.c, line 2.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004005c1 in main at myproc.c:2
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/Alice/118/linux-git/lesson16/myproc.exe
Breakpoint 1, main () at myproc.c:8
8 printf("我是一个进程: %d\n",getpid());
(gdb) quit
A debugging session is active.
Inferior 1 [process 9994] will be killed.
Quit anyway? (y or n) y
3.3.13 僵尸进程(Z)和死亡状态(X)
3.3.13.1 僵尸进程小故事
X(dead):死亡状态会释放代码和数据,但是实际上没有直接------进程会进入到僵尸状态(Zombie,僵尸模式)。
这里有个小故事:假设有个头发稀疏的三十多岁的程序员正在街上跑步,忽然人身一歪影一斜,就死在街上了,路人看到后赶紧拨打了110,但是警察不会直接通知家属把人拉走,在排除他杀之后(给社会层面一个交待),才会再叫家属把人拉走。
3.3.13.2 理论
(1)僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死 / 尸进程。

(2)僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
(3)所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
3.3.13.3 实践:创建维持30秒的僵死进程例子
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id > 0){ //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}
else{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
编译并在另一个终端下启动监控 ------

开始测试------

观察到运行结果------

3.3.13.4 僵尸进程的危害:内存泄漏问题
进程的退出状态必须被维持下去,因为它要告诉关心它的进程(父进程)------你交给我的任务,我办的怎么样了(父进程需要知道------读取进程task_struct内部的退出信息)。父进程如果一直不读取,那子进程是不是就一直处于Z状态?是的,这会存在很大的风险问题。
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,是不是Z状态一直不退出,PCB就得一直维护?没错,Z状态一直不退出,PCB就必须一直维护。
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
会内存泄漏嘛?是,数据没释放,PCB一直维持,占据了内存空间资源。
这里有个小插曲:
3.3.13.5 僵尸进程总结
这里给uu们推荐一篇文章:Ptrace 详解
如下图所示,艾莉丝整理了一张简单的思维导图------

3.3.14 孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为"孤儿进程"。
孤儿进程被1号init / systemd进程领养,当然要有init / systemd进程回收喽。
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else{
//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}

我们也可以写注意一段代码------

父进程先退出,子进程还在,这就是 "孤儿进程"------

3.3.15 进程状态总结
整个的进程诞生、管理与消亡的过程如下图所示------

怎么不见进程挂起呢?

结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux进程(一)】深入理解计算机系统核心:从冯·诺依曼体系结构到操作系统(OS)
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
