目录
前言:
1.Linux内核源代码怎么说
1.1 运行&&阻塞&&挂起
1.1 1 运行
1.1 2 阻塞
1.1 3 挂起
(1)阻塞挂起
(2)就绪挂起
1.2 理解内核链表的话题
1.3 Linux进程状态
前言:
在上一章进程章节中,我们详细讲解了操作系统是什么,操作系统的核心功能,进程是什么,进程是如何管理的以及通过fork创建一个进程并回答了关于fork的几个问题。今天的章节,我们仍然要讲解关于进程的知识。如果你是一个学生,你在学校努力学习,那么你就是一个好学生,如果不思进取,那么就不是一个好学生,学生有多种状态,同样的,进程也会有不同的状态,本章节的知识也是围绕进程状态来展开的。
1.Linux内核源代码怎么说
1.1 运行&&阻塞&&挂起
1.1 1 运行
在系统中,进程调度是通过一个叫“进程调度队列”的数据结构来调度的。一般来说,只要在这个队列中的进程的状态都是运行中(runing),这个队列是一个双指针队列,它遵循先进先出的规则:
但其实,这个队列中包含了新建,就绪和运行三种状态的进程。
运行:进程在调度队列中,进程状态都是runing!
1.1 2 阻塞
这种状态其实我们在c语言/c++中遇到过,那就是——cin、scanf,一个写好的程序运行,它本身是一个进程,当执行到scanf是,它就停下来了,等我们从键盘输入对应的数据它才会重新开始运行,这其实就是典型的阻塞状态。在遇到scanf时,我们需要输入数据到键盘,而不输入数据,就会一直等,这是系统在等键盘资源就绪,不输入数据就是键盘资源不就绪。那么阻塞状态的定义也就很明显了。
阻塞:等待某种设备或资源就绪。
那这种状态是如何识别的呢?以各种硬件来展开,我们知道,操作系统管理着计算机的软件和硬件,如何管理的呢? 先描述,再组织。
在底层,操作系统同样用一个struct来描述计算机的硬件设备:
在这个名为device的struct中,还有一个等待队列, 以scanf为例,当运行到scanf,需要用到键盘,操作系统会到键盘的device中查看键盘是否被按下,如果没有,这个进程会被操作系统从运行队列中取下,链接到键盘的等待队列中,而只有在运行队列中的进程会被调度,这个被链接到键盘等待队列的进程,就不会被调度了,那么这个进程就处于阻塞状态,这就是阻塞。所以从运行状态到阻塞状态的本质就是:“将进程的PCB从运行队列取下链接到不同的等待队列中”。
那么这个进程是如何从阻塞状态回到运行状态的呢?
我们在按下对应的数据之后,会被操作系统第一时间知道,当操作系统知道后将这个进程struct的某个属性变为活跃,然后检查键盘的等待队列,发现了这个进程的PCB,将它重新链接到运行队列中,我们输入的数据此时还没有给这个进程的PCB,虽然它回到了运行队列,但是有可能处于等待状态,只有当它再次被运行时,才会将数据给它。
那么我们会发现:“进程的状态变化,就是要在不同的队列中进行流动。本质都是数据结构的增删查改”!
1.1 3 挂起
(1)阻塞挂起
一个计算机会有许多的进程,但是cup的空间只有特定大小,当cup空间不足时,操作系统会将处于阻塞状态的进程的代码和数据唤出到磁盘空间上,只保留PCB,而这样的进程的状态就被称为阻塞挂起状态。而一旦某个处于阻塞挂起状态的进程等待的资源准备就绪,操作系统就会将它的代码重新唤入到内存。
(2)就绪挂起
当cpu空间严重不足时,操作系统可能还会将处于运行队列中末端的进程的代码和数据唤出到磁盘中,这就叫就绪挂起状态。
1.2 理解内核链表的话题
讲到这里,我们有一个问题一直没有提,那就是——进程在系统中不是链式结构吗,为什么又变成了队列呢?这个问题是很有必要提的。
在前期的讲解中,我们说进程的PCB组成了一个双链表,每个链表有两个指针,分别指向前一个节点和后一个节点:
而操作系统的内核并没有这么设计 。
在操作系统内核中,单独定义了一个LIst_head结构体:
这个节点包含了两个指针,一个是next,一个是prev。将来我们有一个进程的PCB,里面包含了各种各样的数据, 我们可以将这个结构体list_head单独作为进程的成员。这种设计与上述设计的区别是什么呢,这样设计,虽然也是两个指针指向前后,但是上述的两个指针指向它前面和后面的节点,也就是进程本身,而这种设计下的指针,只能找到前面和后面的list_head,无法找到前后的节点本身:
既然前后指针无法访问指向的节点的数据,那么前后的数据又是如何找到的呢?
我们知道,一个结构体的地址与它的第一个变量的地址在数字上是相等的:
我们假设在零号地址处有一个task_struct结构体,对它取地址,然后减去它内部那个包含指向前后的指针的成员变量links:
这一步是在干什么呢?这样做可以得到一个task_struct第一个变量相较于它的另一个成员变量links的偏移量。 links的地址对于task_struct的第一个成员变量来说是一个高地址,我们只需要得到每个task_struct中links的地址,再减去我们之前得到的偏移量,就可以得到第一个变量的地址,这样就可以开始访问数据了。
在将来,我们的进程的PCB中可以有很多个像links这样包含指针的成员变量:
我们不仅可以让它在一个链表中,也可以让它的下一组list_head在一个运行队列、等待队列,甚至二叉树中。
结论:一个PCB可以同时属于多个数据结构中。
1.3 Linux进程状态
为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状
态(在Linux内核⾥,进程有时候也叫做任务)。
下⾯的状态在kernel源代码⾥定义,Linux的进程状态被维护在一个叫task_state_array的数组里:
/*
*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 */
};
进程状态查看
ps aux / ps axj 命令
• a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
• x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
• j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
• u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等
R运⾏状态(running): 并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏
队列⾥。
使用一个命令。再打开一个状态,查看它们的进程状态:
右边红框为进程的名字,左边红框为进程的状态,可以看到我们启动的两个进程此时都显示为R状态,也就是运行状态。
• S睡眠状态(sleeping): 意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
S状态其实就是我们在前面介绍的阻塞状态,如何证明呢?
我们运行一个带有scanf这种需要等待硬件就绪的进程:
将它运行起来:
这时它在等待我们输入数据,也就是等待键盘资源就绪,再查看进程状态:
可以看到,此时我们运行的进程就变成了S状态。S状态也叫浅度睡眠(可中断睡眠),也就是当某个进程处于此状态时,我们可以手动将它杀掉。
• D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
当内存空间严重不足,采取各种挂起措施都会无法腾出空间时,操作系统有可能会采取杀进程的措施,但是有时杀掉一些进程会导致丢失数据,所以规定了一个状态——D,处于这个状态的进程无法被中断,其目的是为了防止丢失数据。
• T停⽌状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的
进程可以通过发送 SIGCONT 信号让进程继续运⾏。
T状态分大小写,T和t都叫暂停状态,但是也有区别,先讲t状态:
t状态为程序调试时遇到断点停下来时的状态。我们来调试一个程序,在它的第9行打上断点:
查看进程状态:
多了一个叫gdb的进程,也就是我们调试的这个进程正处于R状态,我们原本的进程此时处于t状态。也就是说,如果进程是被gdb暂停的,那么它的暂停就是t状态。那T状态呢?
写一个死循环c程序:
运行它,然后通过CTRL+Z暂停:
查看进程状态:
此时进程处于T状态。结论:“被手动暂停的进程,会处于T状态”。
• X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
在task_struct中,我们将不同的状态的进程用数字来表示,这个数字可以是#define定义的,一个特定的数字代表一种状态,每当某个进程切换到另一个进程,它的这个表示进程状态的数字就会被改变。
僵⼫进程
• 僵死状态(Zombies)是⼀个⽐较特殊的状态。当进程退出并且⽗进程(使⽤wait()系统调⽤,后
⾯讲)没有读取到⼦进程退出的返回代码时就会产⽣僵死(⼫)进程
• 僵死进程会以终⽌状态保持在进程表中,并且会⼀直在等待⽗进程读取退出状态代码。
• 所以,只要⼦进程退出,⽗进程还在运⾏,但⽗进程没有读取⼦进程状态,⼦进程进⼊Z状态
•Z僵尸状态(zombie):如果一个子进程终止,会变成此状态,将退出信息告知它的父进程。
子进程退出会保留退出信息返回给父进程,退出信息被保留在子进程的PCB中,它的PCB被保留了下来,代码和数据则被删除了。
写一个程序模拟Z状态:
先让子进程运行五秒,然后退出:
然后子进程会退出:
子进程就由S进程变为了Z进程:
这里我们补充一个结论:“如果父进程一直不回收,不获取Z进程的退出信息,那么Z进程就会一直存在”!如果一直存在,那么就会引起内存泄漏的问题。
僵⼫进程危害
• 进程的退出状态必须被维持下去,因为他要告诉关⼼它的进程(⽗进程),你交给我的任务,我
办的怎么样了。可⽗进程如果⼀直不读取,那⼦进程就⼀直处于Z状态?是的!
• 维护退出状态本⾝就是要⽤数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,
换句话说,Z状态⼀直不退出,PCB⼀直都要维护?是的!
• 那⼀个⽗进程创建了很多⼦进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数
据结构对象本⾝就要占⽤内存,想想C中定义⼀个结构体变量(对象),是要在内存的某个位置
进⾏开辟空间!
• 内存泄漏?是的!
• 如何避免?后⾯讲!
孤儿进程
• cpu资源分配的先后顺序,就是指进程的优先权(priority)。
• 优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性
能。
• 还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以⼤⼤改善
系统整体性能。
来段代码:
#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;
}
运行它看看:
在一开始,22703的父进程为22702,22702退出之后,22703的父进程又变为了1,事实上,这个1号进程就是操作系统。父子进程关系中,如果父进程先退出了,子进程就要被一号进程领养,这个子进程被称为孤儿进程。 一但一个进程变为孤儿进程,那么它就会变成后台进程,也就是说,一个进程变成孤儿进程之后,我们无法手动停止它:
只能通过杀进程的方式停止:
这样就能将它停止。
本章完。