Linux 系统编程 进程篇 (二)

文章目录

  • [Linux 系统编程 进程篇(二)](#Linux 系统编程 进程篇(二))
    • [1 进程状态](#1 进程状态)
      • [1.1 课本上定定义](#1.1 课本上定定义)
      • [1.2 运行,阻塞,挂起](#1.2 运行,阻塞,挂起)
    • [2 理解内存链表的问题](#2 理解内存链表的问题)
    • [3 进程状态 + 实验](#3 进程状态 + 实验)
    • [4 孤儿进程](#4 孤儿进程)

Linux 系统编程 进程篇(二)

1 进程状态

在上一篇里面,我们简单了解了进程是什么,怎么查看创建进程,本篇我们来聊聊进程状态。

进程状态其实就是一个整数,定义成了一个宏。

1.1 课本上定定义

通过这一个小标题呢,我们把这个进程主要的一下名词提炼一下。如上图,这个图里面有很多种进程状态,可以看到有创建状态,就绪挂起状态,阻塞状态,运行状态等等。

不同的操作系统的进程状态都大同小异,比方说linux系统,就没有这个创建和就绪状态,直接就归到这个运行状态里面来了。

也就是说,要搞懂进程状态,我们首先就要搞懂三个状态, 运行,阻塞,挂起。

1.2 运行,阻塞,挂起

我们知道,进程是要送到CPU里面去调度的,之前的篇章我们也曾经提到过调度队列,那么什么是调度队列呢?就是等着CPU去一个一个处理的进程的PCB的队列。

哎?这里大家或许会有疑惑,之前不是说PCB在一个双链表里面吗,的确,Linux内核早期源码里面也确实是这样的,换句话说,这个PCB,也就是task_struct既在一个队列里面,也在一个双链表里面。

这样是可以吗?可以的。怎么做到的?我们后面详细再说,这里先这样记住。

这个调度队列,linux 源码里面叫 runqueue, 我们这里沿用一下。 还有一件事,这个runqueue虽然叫调度队列,但其实不是完全意义上的队列,先进显出 FIFO 的,因为进程还有优先级这个概念。但是,确实有一个调度算法就是FIFO的,先进先出的。

一个CPU,一个调度队列。所以,话草图的话就可以画成这样:

所以,只要在这个调度队列里面的进程,都叫做运行状态。

那么什么叫阻塞状态, 这里直接说:阻塞状态就是等待某种设备和资源准备就绪。设备就是一些硬件,比如说,键盘,显示器,网卡,摄像头,话筒............

光这样说,还是有些不清晰,我们进一步解释一下。我们之前也提到过,操作系统是对软硬件资源管理的软件,这个管理的办法是先描述,再组织。这里我们之前都提到过。

所以,操作系统里面不可避免地就有设备相关的结构,比如说这个struct device吧,里面有id,厂商,状态,数据,等等,但最重要的是:

c 复制代码
struct device
{
    int id; 
    int vender;
    int status;
    void* data;
    struct device* next;
    int type;
    
    struct task_struct* wait_queue; 
}

最重要的是最后面这一个,task_struct* 的这个 wait_queue , 什么叫 wait_queue,比方说,这个

这是操作系统里面管理设备的链表,蓝色的就是 struct device。 当一个进程在CPU里面运行,CPU发现,比方说里面有scanf , 那么这样时候,就需要键盘的响应,操作系统就会把这个进程从CPU里面拿出来,放到这个 wait_queue里面,等待这个键盘的响应,然后下一个进程顶上,去CPU里面。

等键盘响应成功之后,操作系统再把这个进程重新挂会调度队列的尾部。当然,这个等待队列里面可以有很多个进程。

在这个等待队列里面的进程,就是在等待某种设备或者资源准备就绪,也就是我们说的阻塞状态。

根据上面两个状态,我们不难看出,进程状态的变化,表现之一,就是要在不同的队列里面进行流动。 本质都还是对数据结构的增删查改。对了,还要注意一点,这个流动指的时PCB的流动。

看完这两个状态以后呢,我们再来看这个挂起状态,挂起状态其实一个比较极端的状态。我们知道这个进程都是加载到内存里面的,如果内存满了怎么办?操作系统肯定不能不管。

其实我们在设计这个操作系统的时候,肯定是要考虑这个内存严重不足的情况的。怎么处理呢?我们的这个磁盘里面,有一个区域叫 swap分区,交换分区, 这个分区就是专门用来处理在这个情况的。这个分区差不多就是内存的1.5倍。

内存严重不足的情况下,有些不会被马上调用的进程,比如说这个等待队列队列里面这个,阻塞状态的进程,他在这里占着资源,但是啥事不干,那肯定不行啊,操作系统就会把这个阻塞状态的这个进程的代码和数据放到这个磁盘里面的swap'分区里面,注意,是代码和数据。这个过程我们叫唤出。那么这个只剩下PCB的进程的状态就是阻塞挂起状态。

那怎么回来呢?当这个键盘有反应了以后,一定是这个操作系统知道,操作系统是键盘的管理者嘛,这个时候,操作系统就会把这个进程的代码和数据再拿回来,然后这个进程就可以正常地进调度队列,然后被调度了。这个拿回来的过程叫唤入。

如果这个内存严严严重不足的时候,操作系统甚至会把这个调度队列里面的进程也进这个唤出,这个时候就叫做运行挂起状态。

像开头的这个图片里面的创建状态,linux里面就没有分的那么细,linux里面直接就是创建了以后进这个调度队列了,同样这个就绪和运行也算到一起了,这个后面会讲原因。不排除有别的操作系统会更加地细分。

2 理解内存链表的问题

刚才不是提出了一个问题就是这个task_struct结构体,为什么在双链表里面,又在这个调度队列,还可以去这个等待队列等等。这里,我们来解释一下。我们以前理解的双链表里面的每一个节点定义前后指针的办法是不是一个prev指向前一个节点的地址,然后一个next指向下一个节点的地址,注意,我说的是指向节点的地址,也就是指向结构体的开头,这样的:

c 复制代码
struct Node
{
    //.....
    struct Node* prev;
    struct Node* next;
}

但是Linux源码里面的这个是不一样的。

linux源码在这个task_struct里面放上了这样的一个结构体的成员变量:

c 复制代码
struct list_head
{
    struct list_head* next , *prev;
}

看出区别了吗 ?这里其实是把这个两个指向节点的指针包装成了一个结构体,然后这个结构体作为task_struct的成员变量。

然后让这个成员链接起来,画图解释就是这样:

可以看到,我们之前的做法是直接链接节点,但是这里是在链接这个节点的成员。

那么问题就来了,我们如果通过这个成员来访问到这个结构体呢?其实我们可以用偏移量来解释。

这个方法其实是非常巧妙的,虽然C标准里面没有说明这个办法是合法的,但是编译器和开发者算是达成了一个共识,这样是可以的,这也是以前实现这个泛型编程的一个办法,

首先 我们假设 0 地址处有一个task_struct结构体, 如何假设,强转一下就好了

c 复制代码
(struct task_struct*)0

我们假设这个 list_head 在task_struct里面的结构体的变量名叫links,其实真的叫links

那么我们通过这个0 地址处的这个变量访问这个 links, 然后取一个地址,就可以取到这里links,相对这个0开头的这个task_struct 的结构体的地址。

代码表示就是这样的:

c 复制代码
&((struct task_struct*)0 -> links )

第一个反常的点就是为什么 0 地址处可以看出是一个结构体,而且访问 空指针 还不会报错。

这里最关键的就是这样 & 地址符号, 其实,每一个结构体在编译的时候,这个结构体内部的地址情况就以及确定了,当我们把 0 这个地址强壮成task_struct* 以后,这里就可以看成是一个task_struct结构体,而我们虽然 有 -> 找links,但是因为 & 地址存在,告诉编译器的就是我不是要访问这个links,而是要访问这个地址,所以编译就不会报错

有了这个相对于起始地址的便宜量,我们就可以使用这个链表的头指针,或者前一个指向这个节点的指针的地址,减去这个便宜量,就可以使指针指向这个task_struct的开头了。还没完,再强转一下就好了。

也就是说,最后的就是这样的:

c 复制代码
(struct task_struct*) (next -  &((struct task_struct*)0 -> links ) );

C语言标准库里面有一个宏就叫做 offset_of 原理和代码实现和这个求偏移量的代码几乎一模一样。

能够通过这个结构体的成员来访问到这个结构体以后,那么这个再结构体里面放一个前驱和后继指针的结构体的策略就跑通了。那么,如果我在多放几个这样的结构体作为task_struct的成员变量呢?

如果把这个links以不同的方式链接起来,不久可以把这个task_struct放到不同的数据结构里面了吗?比如说之前提到的,既放在双链表里面,又放在这个调度队列里面。

3 进程状态 + 实验

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

可以看到这个进程状态,一共使有 7 个, 我们怎么研究呢?第一个单独一组。第二和第三个单独一组,第四个和第五个单独一组,第六和第七个一组来看。 这个后面的注释,就是这个状态的编号。

首先来看第一个,也是最好理解的,就是 R 状态, 这个就是 runing, 运行状态。

这个的例子非常简单,但是又有点意思,来看:

可以看到,我左侧写了一个打印hello的 Myprocess的程序,为什么右边我每隔1秒查一下,发现全是 S 状态而不是R'状态呢?

我们先来看一下这个S状态, sleep状态,就是休眠状态,说成之前我们提到的三个状态就是阻塞状态。

其实,因为这里的hello每隔一秒取执行打印屏幕这个操作,在这一秒的等待时间里面,CPU肯定值不能只守着他什么都不干吧,其实这个程序CPU判读出他要打印,要显示器资源,就会放到这个等待队列里面等待打印,打印完再放到调度队列后面,所以,大部分时间,这个程序都在等待队列里面,而不是调度。但是其实是有概率看到这个是R状态的,我就不等了。

如果不想让他S的话,不要让它IO就行。

那么又有同学问了,这个 + 是什么意思?

bash 复制代码
./Myprocess &

+的意思就是这是个前台进程,上述运行状态,就是加一个 & ,这个就是让这个进程取后台。进程就取去后台去了,我们可以正常输入命令。怎么杀掉? kill -9 PID ; crtl + C 杀不到后台进程。

刚才S状态提过了,就是阻塞。DS状态一会儿说。先来看这个T 和 t 状态。这个状态一看是暂停, 是不是有点不太对,刚才说的进程状态里面没有暂停啊,那这个暂停状态是什么样的呢?

这个时候,我们需要用到gdb,我用gdb调试一下这个可执行,然后在第九行打个端点,然后run 起来。

所以,t 叫做追踪状态,debug的时候进程因为暂停停下来。

那么 T 是什么情况呢?我们继续运行起来Myprocess,然后ctrl + Z停下来, 这里的状态就是 T 。用户暂停状态。

进程暂停往往是因为进程不具备某种条件,或者做了一些非法操作,这个时候操作系统就会把这个进程暂停,这是个linux特有的状态,别的可能没有。 比如说守护进程,这个后面讲。 所以这个 T 暂停主要就是为了止损。那为什么不杀掉这个进程呢?主要还是想让用户来决定。

然后我们再来看这个D状态, 对应的整形值是 2 。 他和S状态的区别就是 他是不可被中断睡眠的,也就是深睡眠的。而S是浅睡眠,可以被中断的。

我们用一个例子来距离,比如说内存里面现在有一个进程A, 这个小A有一个任务是要往磁盘里面写 100MB 数据,然后再继续执行,那小A就唤醒磁盘,把数据给磁盘数据,磁盘不管写没写成功,肯定都要给进程反馈的,让用户来判断,所以,这个磁盘就让进程先别走,先等等他写数据。 那小A就等着, 给自己挂上 S 的状态。

过了一会儿,这个内存空间严重不足,OS从小A身边路过,感觉不对劲,我这个内存都严重不足了,你还在这里S等着,那肯定不行啊,操作系统就把这个小A给挂了。 磁盘写完但是写失败了,磁盘空间也满了,还有15MB没写进去,磁盘回来一看,小A人没了,直接麻了。那这个错误信息也就传不回来了。

万一这个没写进去的是今天银行里面客户存钱的账目,经理肯定不愿意。 小A ,OS, 磁盘就相互踢皮球。经理也知道不怪他们,所以,后来就相处个办法,下次,再有类似情况,和磁盘相干的,就不要挂S状态了,挂D状态,操作系统不可杀掉他,不可中断睡眠,不就可以解决问题了吗? 这个进程要杀掉重启都杀不掉,要断电。

所以,这就是这个不可中断睡眠的意义。其实,如果这个进程出现D状态,说明这个计算机离挂掉不远了,那D状态操作系统又挂不了它,他慢慢累积迟早挂掉计算机,或许是因为磁盘老化等问题。 所以这里就先不演示了。

如果大家想试试的话可以试一试,用dd命令,创建内存块,往磁盘猛猛灌,

bash 复制代码
dd if=/dev/zero of=~/test.txt bs=4096 count=100000

什么意思呢就是把/dev/zero 这个设备里面拷数据考到这个test文件, bs块大小是4096 一共 1000000 个。

好了这几个状态就结束了,进程为什么可以暂停,杀掉,因为这个进程收到信号了。

bash 复制代码
kill -18 # 继续   kill -19 暂停  

这里只是简单提一下,后期讲信号的时候会说,包括之前提到的 -9 是杀进程。

下面终于到了最后两个状态 X 和 Z 状态。其实看这个括号里面的东西就差不多能看出来,X 是死进程,就是这样进程以及思了。那什么叫僵尸进程呢?

打个比方,比如有个人今天不小心就是晕过去了,在地上躺着等救护车(其实已经来不及了), 从这个人晕倒开始,到救护车把这个人拉走处理掉,中间是不是还要一段时间,在这断时间里面这个"进程"就叫做僵尸进程。

那么为什么会有僵尸进程这个状态?首先,父进程把子进程创建出来,是不是一定是要用他来完成一个任务的?这个子进程结束以后,那父进程肯定要知道这个任务完成的怎么样,要知道相应的信息吧,比如说成不成功,结果是什么。

那么这个信息放在那里呢?放在子进程的task_struct里面,所以,在你死彻底,彻底清理之前,一定要把信息传回去给父进程。

所以,僵尸进程存在,是为了获取退出信息。

那么如何验证僵尸进程呢?

我们还是先来一个父进程,返回fork创建子进程,最后单独把这个子进程杀掉就好,来看演示

可以看到,这个子进程 322988 被杀掉以后就变成了 Z 。

还有一件特别重要的事情,僵尸进程只能由父进程来回收,如果父进程一直不回收,那么这个 Z 会一直存在,操作系统不会去管他。 那这是不是就引起一共严重的问题了,进程一直存在,是不是就内存泄漏了。

这里还有一个只是点,如果一个进程退出了,那么内存泄漏问题还在不在? 当然是不在了。

所以,什么样的进程内存泄漏以后是比较麻烦的,当然就是常驻内存程序,一直赖着不走。

那操作系统为什么不去解决呢?操作系统不会去解决的,因为子进程要给父进程返回信息,所以操作系统要一直维护。

再补充一个小知识点:关于内核结构申请。

思考一个问题,操作系统里面的是不是应该有很多进程,包括很多新进程,这些新进程来的时候都要申请一个PCB,也就是malloc一个这个空间,给这个task_struct。这个过程之前提到过,第一步先申请,然后初始化。如果这个进程非常多的话,那么这个是不是就会有一些鸡肋?

linux里面维护了一张表,假设叫unuse,把已经结束的进程的PCB链到这个表里面,这样就形成了一个数据结构对象的缓存,然后下一次申请新进程的时候,不去申请空间,而是直接拿着这个表里面的直接初始化,这样就起到了一个优化的作用。

linux里面这个操作呢叫 slab 。 知道有这么回事就好。

4 孤儿进程

刚才我们提到过,如果子进程退出后,父进程不去处理,那么就会导致内存泄漏。那么?如果子进程还在运行,但是父进程退出了以后,会怎么样?

我们会发现这个子进程父进程变成了 1 。换句话说,这个子进程就是被领养了,这个子进程也叫做孤儿进程。那么这个 1 进程是是谁?可以理解为这个 1 进程就是操作系统的一部分。 linux 早期版本里面这个进程是 init进程, 后面变成了systemd了。为什么没有 0 进程呢?因为 0 进程出生了以后就被这个 1 进程给替换了。

为什么要领养呢?还是这个僵尸进程的问题,如果不去管它的话就会内存泄漏,所以,直接交给init进程去管理就好。

有一个有趣的现象就是,我们ctrl + c 杀不掉这个孤儿进程了,因为这个进程被领养之后就变成一个后台进程,要杀的话使用kill -9就好。

g-ibRiWyIg-1776054518005)]

我们会发现这个子进程父进程变成了 1 。换句话说,这个子进程就是被领养了,这个子进程也叫做孤儿进程。那么这个 1 进程是是谁?可以理解为这个 1 进程就是操作系统的一部分。 linux 早期版本里面这个进程是 init进程, 后面变成了systemd了。为什么没有 0 进程呢?因为 0 进程出生了以后就被这个 1 进程给替换了。

为什么要领养呢?还是这个僵尸进程的问题,如果不去管它的话就会内存泄漏,所以,直接交给init进程去管理就好。

有一个有趣的现象就是,我们ctrl + c 杀不掉这个孤儿进程了,因为这个进程被领养之后就变成一个后台进程,要杀的话使用kill -9就好。

相关推荐
克里斯蒂亚诺·罗纳尔达2 小时前
智能体学习22——智能体间通信(A2A)
人工智能·学习·ai
爱莉希雅&&&2 小时前
MySQL 高可用实战:PXC + HAProxy + Keepalived 完整版笔记
运维·数据库·mysql·haproxy·数据库同步·pxc
油丶酸萝卜别吃2 小时前
高效处理数组差异:JS中新增、删除、交集的最优解(Set实现)
开发语言·前端·javascript
Once_day2 小时前
Linux之(31)Shell的set命令
linux·运维·bash
HoneyMoose2 小时前
Npmp 安装时候提示警告: error (ERR_INVALID_THIS)
开发语言
亚空间仓鼠2 小时前
Ansible之Playbook(四):循环与判断
java·服务器·ansible
gskyi2 小时前
时间格式化神器:智能显示相对时间
开发语言·javascript·ecmascript
wanhengidc2 小时前
云手机能够实现哪些功能?
大数据·运维·安全·智能手机
念恒123062 小时前
Linux基础开发工具(编写一个简易进度条)
linux·c语言