目录
认识操作系统的各个状态
运行状态
- 有一个在叫做调度器的东西需要保证CPU的资源被合理的使用,所以他需要维护一个运行队列。这些队列里就是进程的task_struct,用来表示每个进程。而调度器把这些task_struct链接起来,这些链接起来的进程是随时可以调度的。所以,运行状态并不是进程正在运行,而是只要是在调度队列中,随时等待被CPU调度执行即可。
阻塞状态
- 操作系统管理硬件也是先描述再组织,所以这时候也是把硬件的task_struct放在一个叫做设备等待队列的里面。如果说我们的硬件还没准备好进行读写类似一样的工作,那么我们的task_struct就需要在设备等待队列中等待,等待硬件动作就绪后,再从设备等待队列中删除,链入到CPU的运行队列里面,准备调度。(这也对应了我们上节说的,对软件和硬件的管理实际上就是对数据结构的增删查改)
- 比如说我们调用scanf函数实际上就是在等待键盘读入数据,然后我们scanf函数再从键盘那里去取数据。这也对应我们Linux下一切皆文件的概念,我们就是从键盘这个文件去读取数据。
挂起状态
- 当操作系统发现内存资源严重不足的时候,就需要想办法腾出空间到磁盘分区(swap)分区中,来缓解内存资源紧张的情况。我们称被置换的进程的状态叫做挂起状态
- 注意,为了方便队列对进程的管理,我们交换到swap分区中一般只是把进程的代码和数据放在swap分区中,不会把task_struct这个描述结构体交换过去。然后等到内存资源不那么紧张了,再把进程的代码和数据通过某种方法恢复到对应的task_struct中(什么方法恢复的,后面虚拟地址空间讲)
- 一个进程是否被挂起并不需要让你知道,就跟你把钱存银行里一样,你并不知道自己的钱是被干什么用了,银行并不会告诉你,只是你想要的时候他能及时给到你就好!!
运行挂起状态和阻塞挂起状态
- 一天内存资源很紧张,4GB的内存只剩下100MB了,这时候操作系统忙得很,突然看到两个进程在等待队列里面躺着睡觉,这时候操作系统上去问,你们两个还不赶快准备去运行在这里干啥,进程回答道:我们两个的需要资源还没就绪呢,现在还不能去运行,操作系统说:现在内存空间不足了,你两不要在这里占地方去磁盘的swap分区里面等待。如果就绪了需要被运行了,你们再回来。
这就是阻塞挂起状态。 - 甚至在内存很严重不足的时候,操作系统会把调度队列中的准备执行(CPU一次只能执行一个进程),把这些准备执行的PCB的代码和数据也交换到swap分区中,这就是运行挂起状态
Linux内核管理进程状态的方法
如何查看进程状态
ps aux / ps axj 命令
R状态
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列中。和我们上面说的一样
S状态
-
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。 ——>其实就相当于是阻塞状态(因为需要跟硬件保持联系)
-
接下来我们通过一段代码来了解一下这个状态
- 问题:为什么是s状态呢?
- 因为printf函数需要常时间和显示器这个设备建立联系,但是由于CPU的处理太快了,显示器有点跟不上速度,所以printf输出这个进程就处于等待显示器的状态
- 还有一个问题就是,为什么叫做可中断休眠?因为处于这个状态是可以被直接杀死的,这也和我们后面的不可中断休眠的区别。
D状态
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
- 区分:S是浅度睡眠(可以被唤醒)、D是深度睡眠 (不响应如何需求)为了能够更好地理解他们的区别,以下会讲述一个故事
- 今天呢进程有一个任务把100MB的数据写入磁盘,这个时候进程对磁盘说,磁盘啊,你帮我把这100MB的数据写入到磁盘中,磁盘说:没问题,可是我不能保证我能成功写入到里面去哦,但是不管写入成功还是失败,我都会给你结果,进程你一定要等我给你结果哈。这个时候进程说好的,于是就躺在椅子上悠闲等着,这个时候操作系统因为资源不足忙里忙外的,看到进程在哪里躺着,问:你这个进程怎么回事,你没看到资源空间已经不足了吗?你还在这里啥事都不做,占着地方。于是操作系统直接把进程杀掉了。这时候磁盘写入失败了,跑出来说,进程我写入失败了,把这100MB数据拿回去处理一下,没有声音,就一直在找进程,找了半天没找到,这100MB数据也没人管理了。假设我们是银行的系统,这个时候行长发现了今天有100MB数据丢失了,导致1w块钱的损失,这时候就把应该存入数据的磁盘叫过来问怎么回事,磁盘说,冤枉啊,我写入失败了这100MB没进程管理啊,我叫他在哪里等我的,出来就没人影了。这时候问进程,进程说:你看我干嘛,我在哪里等得好好的,操作系统看我不顺眼直接把我杀了。这时候问操作系统:操作系统说:行长,那个时候资源太紧张了,你曾经说过,如果资源太紧张了我是有杀进程的权力的。这个时候行长觉得好像都有道理,所以行长这样规定,如果进程状态是D,操作系统不管资源是否紧张,都没有权力杀掉状态是D的进程,解决了数据丢失的问题。
T状态
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
指令:kill -l
- 其中9是杀进程,19是暂停进程,18是重启进程,这些东西我们在信号那个章节仔细讲解,这里了解即可
- T状态存在的意义:可能是需要等待某种资源,或者是我们单纯不想让该进程运行!!
- 应用场景就是gdb,当程序运行起来的时候遇到了我打的一个断点,然后就停下来了,这是这个过程就可以被应用于gdb这个进程在控制被调试的这个进程!!
T和t
- Ctrl+z暂停进程,就是大T
- 个程序debug到断点就是小t
X状态
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
Z状态(僵尸状态重点)
- 比如你正在公园跑步,突然看见一个老人走了两步就倒地上了,这时候你叫了120,120发现人已经没救了,于是走了。然后你又叫了110,但是110并不会立马清理现场,因为本质查明真相的原则,可能会需要先带着法医对尸体进行检测然后再确认结果,比如说异常死亡或者是正常死亡(因为家人需要了解情况),然后才会去清理现场。 其实这段已经死亡一直到清理现场之前的这段时间,就相当于是僵尸状态。
- 那么我们站在进程的角度考虑,一个子进程死亡后我们父进程就如同子进程的家人一样需要关心子进程的死亡信息,所以子进程死亡后并不能马上释放,而是处于僵尸状态,等待父进程来回收资源。
- 接下来我们用一段代码来看一下,进程退出,父进程还在的情况子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0){ //child
int 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){ //father
while(1){
printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else{ //fork error
}
return 0;
}
- 这里的exit函数其实就是结束该程序的执行,不再执行后面的代码。后面进程替换会细讲。
- 运行该代码后,我们可以通过以下监控脚本,每隔一秒对该进程的信息进行检测。
while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
- 所以只有子进程死亡了,但是父进程如果一直没有回收子进程的资源。子进程就一直处于僵尸状态。相关的资源尤其是task_struct不能被释放,因为父进程要回收子进程资源来知道子进程的信息。
问题1:为啥要有僵尸状态?
因为父进程很关心子进程,关心它交给子进程的任务,子进程完成的怎么样,所以需要子进程在父进程回收它的资源之前一直不能退出。
问题2:僵尸进程的危害是什么?
- 僵尸进程的退出信息一直保持在task_struct(PCB)中,如果僵尸进程一直不退出,那么我们就需要一直维护PCB。
- 如果一个父进程创建了很多子进程,但是这些子进程最后都处于僵尸状态,父进程一直不去回收资源,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
- 僵尸进程一直无法回收,如果这样的进程越多,那么内存可用的资源就越少,也就是说,僵尸进程会导致内存泄露(是程序不断地占用内存,但却不释放不再需要的内存,导致可用内存逐渐减少)。
孤儿进程
- 孤儿进程就是父进程先退出 子进程还在,导致子进程没有父进程回收
- 例如,对于以下代码,fork函数创建的子进程会一直打印信息,而父进程在打印5次信息后会退出,此时该子进程就变成了孤儿进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(1){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
sleep(1);
}
}
else if(id > 0){ //father
int 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;
}
- 观察代码运行结果,在父进程未退出时,子进程的PPID就是父进程的PID,而当父进程退出后,子进程的PPID就变成了1,即子进程被1号进程领养了。
问题1:为什么要领养?
- 因为孤儿进程未来也会消亡,也会被释放,不能让他一直占用内存资源
问题2:ctrl+c为什么无法中止异常进程,他的底层原理是什么??
- 本质上就是瞬间父进程会被bash进程回收掉。所以子进程也在父进程退出的瞬间回收了。所以由于子进程的PPID不是bash进程而是系统进程,所以无法中止
问题3:子进程是bash进程的孙子进程,为什么父进程消亡后不由bash进程回收而是交由系统进程回收???
- ——>因为bash做不到,因为孙子进程不是他去创建的!! 他没有这个权限,而系统进程可以做到,因为要将孤儿进程托孤给系统进程 当然不同的操作系统具体的实现方法可能也不同!!
Linux到底是如何维护进程的
- 我们前面说过,进程之间的task_struct是用双链表来链接起来的,所以进程一定使用双链表这个数据结构来维护的。但是,实际场景不仅如此。有可能他还嵌套放在队列,二叉树,红黑树管理。那到底是怎么实现维护task_struct之间的关系呢?
- 首先并不是整个task_struct结构体链接在一起,而是通过单独创建一个llist_head(list_head结构体两个成员next和prev)结构体来进行链接,所以其实节点都是指向该结构体中间的位置而不是头部
- 既然链表中链接的不是头部,那么我们通过节点的链接找到task_struct的其他成员呢?
- 将0强转成task_struct结构体的类型,其实就是假设在0位置都有一个task_struct结构体大小的内存,然后找到他的links节点并取他的地址,由于低地址是0,那么找到links节点的地址就相当于知道了links在task_struct中位置的偏移量——> &(task_struct*)0—>links
- 然后用当前指向的位置links地址减去links的偏移量(对于头的),就可以找到task_struct结构体的头部了。
- 最后将这个头部的地址强转成task_struct* 就可以拿到整个PCB结构体了 ,就能访问里面的其他数据
总结:( task_struct*)(links-&(task_struct*)0—>links)get到 other结构体成员
补充知识
进程退出了,内存泄露问题是否还存在?
答案是不存在,进程退出了,系统会自动回收那块内存。那种进程最怕内存泄露,常驻内存的进程,比如操作系统,,他一直不退出,为啥一直不退出,因为操作系统要管理软件和硬件这些啊,除非是系统关机了才退出,如果他发生内存泄露,那么就会问题很大。
关于内核结构重复申请优化
我们在C++中,写一个类,实例化出一个对象。是不是很方便啊,因为这个类在程序运行中一直存在,那么我们在Linux中描述信息的task_struct也可以在回收的时候缓存在一个数据结构对象的链表中,如果我再次需要这个task_struct信息的对象,我们就可以直接从链表中拿出来实例化,不需要重新写一个task_struct
end
感谢大家的阅读,希望对你们有帮助。