✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】
目录
1、僵尸进程
上一弹我们讲解了僵尸状态的基本概念,此处我们通过书写代码查看一下僵尸状态。
当一个进程运行完毕、出现问题或者被杀掉以后,它所占用的内存资源和退出状态没有被它的父进程回收,此时这个进程的状态就称为僵尸状态。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
int cnt = 5;
while (cnt)
{
printf("I am a child,cnt: %d,pid:%d\n", cnt, getpid());
sleep(1);
cnt--;
}
}
else
{
//parent
while (1)
{
printf("I am parent,running always,pid: %d\n", getpid());
sleep(1);
}
}
return 0;
}
可以明显看到上图中出现了僵尸状态。
1.僵尸进程与僵尸状态的关系???
僵尸进程是指:一个子进程已经结束运行,但是父进程没有调用wait或waitpid来获取子进程的退出状态信息。
僵尸状态是指:当一个进程运行完毕、出现问题或者被杀掉以后,它所占用的内存资源和退出状态没有被它的父进程回收。
僵尸进程与僵尸状态的关系是,僵尸进程是指处于僵尸状态的进程。这种状态下的进程虽然已经结束执行,但是其进程描述符等资源仍然存在于系统中,直到父进程对其进行善后处理。
2.为什么出现僵尸进程???
僵尸进程是由于,子进程先于父进程退出,但是父进程没有对子进程的资源进行回收(这里的资源指的是内核资源PCB),从而导致子进程的用户资源虽然已被释放,但还占据着PCB资源。
子进程在退出时用户区的资源(子进程的代码和数据)可以自己释放,但是其所占据的内核资源(PCB)需要它的父进程来释放。
注意:如果没有父进程读取,僵尸进程会一直存在(内核数据结构task_struct会一直存在)。(kill -9 不能杀掉进程)
3.为什么父进程不回收子进程PCB??
1.父进程要知道子进程为什么要退出,父进程要获取子进程退出的信息。
2.独立性,父进程只读取子进程的信息,不对信息做修改,依旧保持独立性。
将来子进程被 waitpid(系统调用接口) 等待方式读取了,那么这个子进程会由Z(僵尸)状态变为X(死亡)状态,之后由OS来释放。
僵尸进程危害
进程 = task_struct内核数据结构 + 进程的代码和数据,僵尸进程时会释放进程的代码和数据,但是task_struct内核数据结构还占据一块内存,内存不释放会一直存在,内存不释放会引起内存泄漏问题。
2、孤儿进程
孤儿进程:父进程如果先退出,子进程就会变成孤儿进程。孤儿进程被1号init进程(OS本身)领养,当然要有init进程回收喽。
1. 孤儿进程为什么要被OS领养?
依旧要保证子进程正常被回收。(kill -9能回收)
2.我们已经启动的所有进程,我们怎么从来没有关心过僵尸进程呢?内存泄漏呢?
直接在命令行中启动的进程它的父进程是bash,bash会自动回收新进程的Z(僵尸)。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
int cnt = 5;
while (cnt--)
{
printf("I am a process,pid: %d\n", getpid());
sleep(1);
}
}
3、运行状态
★ 操作系统为了合理分配CPU以及各种硬件资源,保证各个进程的正常运行。操作系统会为CPU创建一个进程队列,也为每一个硬件都创建一个等待队列。
★ 而某一个进程处于运行状态本质上就是操作系统将该进程对应的PCB放入CPU的运行队列中,然后再将PCB中维护进程状态的变量修改为相应的值。
★ PCB里面有进程的各种属性值,以及对应的代码的地址。所以CPU从运行队列中找到PCB取出数据后,可以根据数据得到进程的各种属性值和指令,然后执行相应代码。进程处于运行状态并不意味着该进程此时一定正在被运行,只要该进程处于CPU的运行队列中即可。
注意:CPU是处理数据的速度在纳秒级,运算速度非常快,所以只要进程处于CPU的运行队列中,我们就可以认为该进程正在被运行。
一个进程一旦持有CPU,会一直运行到这个进程结束吗?
不会。CPU基于时间片进行轮转调度的。(Linux不是这样调度的,这只是OS教材调度方法之一)
让多个进程以切换的方式进行调度,在一个时间段内同时得以推进代码,就叫做并发。
每一个CPU都要有一个自己的运行队列,有两个CPU的话,会存在真正意义上的有两个调度队列。任何时刻,都同时有多个进程在真的同时运行,我们叫做并行。
4、阻塞状态
阻塞状态:相当于S状态,浅度睡眠,D也算。
用一个C语言代码理解阻塞:
#include<stdio.h>
int main()
{
int a=0;
scanf("%d",&a);
printf("a = %d\n",a);
return 0;
}
运行该代码,等待键盘资源是否就绪,键盘上面有没有被用户按下的按键,等待则进程没有被调度,也就不会在运行队列中。
阻塞:把进程从运行队列剥离到等待队列。
操作系统如何对硬件进行管理?
本质是对硬件的数据进行管理,因此在操作内核会有对应该硬件属性的结构体。
#define KEY_BOARD 1
#define SCREEN 2
struct device
{
int type;
int status;
//其他属性
struct device* next;
task_struct* wait_queue;
}
不是只有CPU有自己的运行队列,各种设备也有自己的wait_queue。
★ CPU处理数据的速度极快,是我们计算机中的各种硬件处理数据的万倍,比如:一个磁盘或者一个网卡同时只能为一个进程服务,但是在计算机中需要使用这些硬件资源(磁盘等)的进程会有很多。
★ 如果在硬件为一个进程服务时,有其他运行中的进程也需要使用该硬件资源,操作系统就会将该进程的PCB放入硬件的等待队列中,进程会等待硬件来为自己提供服务。
★由于多个进程需要访问某种硬件,进程PCB在硬件等待队列中等待硬件服务自己的状态就被称为阻塞状态。阻塞状态在本质上就是将进程的PCB从CPU的运行队列中,放入硬件的等待队列中,然后将PCB中维护进程状态的变量也会修改为相应的值。当该进程获得对应的硬件资源服务时,再将该进程放回CPU的运行队列中。
注意:并不是只有等待硬件资源的进程才处于阻塞状态,一个进程等待另一个进程就绪、一个进程等待软件资源就绪等也是阻塞状态。
5、挂起状态
上面我们学习了阻塞状态,处于阻塞状态的进程由于需要等待某种资源,所以它对应的代码和数据在短期内不会被处理(这里的短期指的是对于操作系统而言)。但它们的数据仍储存在内存中,占用存储空间但是不执行,相当于浪费了内存资源。而如果当前操作系统处于高IO(大量向内存输入和向外部设备输出数据)的情况下,可用的内存空间不足,操作系统就会选择性的将这些处于阻塞状态的进程对应的代码和数据转移到磁盘中(唤出),从而节省出内存空间,如果需要使用到这些代码和数据,则从硬盘中转移到内存(唤入)。
频繁的唤入唤出会导致效率问题。用效率换空间。
这种由于内存空间不足,操作系统将在等待资源的进程对应的代码和数据放到磁盘中以节省内存空间的状态就被称为(阻塞)挂起状态。挂起状态不会移动进程的PCB,只会移动进程对应的代码和数据。
补充:虚拟机挂起是将整个操作系统以及代码和数据移动到硬盘中。
挂起并不是释放进程,因为对应的PCB仍然在硬件的等待队列中,当该进程获得对应的资源服务以后,操作系统仍然可以将该进程对应的代码和数据从磁盘加载到内存中来继续运行,其本质是对内存数据的唤入唤出。
6、进程切换
A进程在CPU运行队列中运行的时间片到了之后,A进程会脱离CPU的运行进程;B进程会进入CPU的运行队列中运行,那么比如:A进程在CPU中已经运行到了第50行代码后,时间片到了,退出CPU,当A进程再次进入CPU的时候,是重新开始运行呢?还是接上上回第50行的代码继续运行呢?
CPU里面有一套寄存器,会保留A进程运行的数据,当A进程退出CPU的时候,A进程在CPU中已经运行的数据,将由 task_struct 这个结构体来保存,当A进程再次进入CPU时,数据还会回到寄存器原来的位置,所以A进程会接着第50行继续运行。
进程的切换,最重要的一件事情是:上下文数据的保护和恢复。
CPU内的寄存器:
寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套!!!CPU内部的数据,可以有多套,有几个进程,就有几套和该进程对应的上下文数据。
寄存器 != 寄存器的内容
总结
本篇博客就结束啦,谢谢大家的观看,如果公主少年们有好的建议可以留言喔,谢谢大家啦!