Linux_进程概念详解

news2024/12/29 13:41:22

本篇文章为作者学习Linux时所作总结,可能会有总结不足或疏漏之处,如有发现请各路大神批评指正

什么是进程?

课本上说,进程是程序的一个执行实例,正在执行的程序。对,也不对,我称之为正确的废话。在这里先给出定义:进程 = 内核数据结构 + 代码和数据。为什么这么说呢?请接着往下看。

进程的描述

我们平时所编写的代码,在编译和链接之后都会形成一个xxx.exe的文件(可执行文件)。其实该文件就是磁盘中的一个文件,根据冯诺依曼体系结构,当我们双击该文件(Windows)或者 ./ (Linux)时,该文件在运行之前会被从磁盘加载到内存(相当于拷贝了一份)。就我们先前说的课本上进程的定义,该文件现在已经是一个进程了,在该进程的整个生命周期(创建、就绪、阻塞、运行、终止)内,操作系统会一直管理着它。

而操作系统又是怎么管理进程的呢?其实是通过进程的属性来管理进程的,在操作系统加载进程时,不止是加载可执行程序到内存,为了方便操作系统管理,在加载时,还为进程创建了一种内核级的数据结构,称之为PCB(process control block)。为什么需要有PCB(task_struct)呢?因为OS要管理进程,管理进程就需要先描述、在组织。Linux下的PCB是: task_struct(任务结构体)

每一个task_struct都包含对应进程的所有属性,而所有的task_struct通过struct task_struct* next指针连接在一起,形成了一个名为struct task_struct* tasks_list的单链表,所以对进程的管理工作就变成了对链表的增删查改!!!为什么说进程是一个运行起来的程序?因为进程会被根据task_struct属性,被OS调度器调度,运行的

注意:task_struct结构体跟磁盘没有任何关系,该结构是操作系统内自己定义的类型

task_ struct内容分类——标示符

标识符是描述进程的唯一标识,用于区别其他进程。我们把这种标识叫做PID,如果我们想要获取某个进程的PID可以通过系统调用getpid()来获取,下图是getpid()的介绍和使用方法

// 获取pid
pid_t id = getpid();

我们若想终止掉某个进程,可以直接ctrl + c,也可以使用系统命令kill,具体格式为kill -9 pid

在linux中用ps命令检索进程的相关信息,所有正在运行的进程都会在 /proc文件夹(内存级文件)里面形成一个子文件夹。如果将正在运行的进程终止,其相应的文件夹也会消失


其实在Linux中,除了提供了getpid()来获取进程的PID,还提供了getppid()来获取父进程的PID。其原型为:pid_t getppid(void);。

我们使用下面代码来测试getppid():

// 获取父进程id
int main()
{
    pid_t id = getpid();
    pid_t ppid = getppid();
    while(1)
        printf("hello world, I am a peocess,pid:%d, ppid:%d\n",id,ppid);
    return 0;
}

通过多次运行、终止进程,我们发现子进程的pid每次运行都不一样,而父进程的ppid怎么每次都是同一个值啊。其实在Linux系统命令行中,执行命令/程序,本质是bash(请参考bash的手册–在提示符下键入 man bash)进程,创建的子进程,由子进程来执行我们的代码。所以这个ppid为11677的父进程,就是bash!


可是bash进程是怎样创建子进程的呢?下面我们就来聊一聊系统调用fork()函数

fork函数

上图中的返回值说明不太好理解,我们来看一段代码

// 初见fork函数
int main()
{
    printf("I am a process, pid:%d, ppid:%d\n",getpid(), getppid());
    pid_t id = fork();

    printf("I am a 分支! pid:%d, ppid:%d\n",getpid(), getppid());
}

根据运行结果我们可以发现,fork函数调用之后,我们竟然输出了两条printf语句唉,这是为什么呢?结合我们上面提到过的返回值说明,我们可以大胆推测fork函数有两个返回值,这两个返回值分别执行一条printf语句!!也就是说fork之后产生了一个新的进程,而且这个进程跟父进程是连续的!我们再来看一段代码

// fork的返回值
int main()
{
     pid_t id = fork();
     if(id > 0)    // 分别打印 此进程pid、父进程pid、返回值id
         printf("我是父进程,pid:%d, ppid:%d, ret id:%d\n",getpid(), getppid(),id);
     else if(id == 0)
         printf("我是子进程,pid:%d, ppid:%d, ret id:%d\n",getpid(), getppid(),id);
}

 咦?if条件判断语句竟然同时成立了,不但没有报错,而且运行的还挺好。这更加证明了这是两个执行流,所以上面我们猜测的fork函数有两个返回值得到了验证!!!请结合运行结果理解。

一般而言这两个进程的代码是共享的,但是数据是各自私有的一份(结合上图理解)。

总结:fork()执行之后会有两个进程,这两个进程是父子关系,在父进程中fork返回新创建子进程的进程ID;在子进程中fork返回0;如果出现错误fork将返回-1


谈谈创建子进程的过程

我们前面已经知道,每一个进程都有一个与自己相对应的task_struct,而新创建的子进程的task_struct是怎么来的呢?

其实是拷贝父进程的,只不过拷贝之后又调整了子进程的task_struct的部分属性,然后再将task_struct链入到进程列表中,此时,子进程就已经创建完毕了,可以随时被调度了。所以,当执行return语句时,父进程执行一次,子进程也执行一次。这也解释了fork()函数有两个返回值的原因

task_ struct内容分类——状态

进程状态

进程共有五种基本状态,其中新建和终止最好理解。新建状态,进程在新建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。终止状态,进程结束,或出现错误,或被系统终止,进入终止状态,无法再执行。今天我们重点谈谈其余的三种状态

在操作系统中,每一个CPU都对应着一个struct runqueue的结构体,其结构体包含着一些基本信息和指向一个进程PCB的指针,多个这样的进程就形成了一个FIFO的调度运行队列(结合上图理解)。通过该队列就可以进行时间片轮转(当进程1时间片到了之后,会被操作系统从运行队列中剥离下来,尾插到进程3的后面,重复此过程直至运行结束)。那么什么是运行状态呢?只要进程在运行队列中,该进程就叫做运行状态。其本质是,我已经准备好了,可以被CPU随时调度。所以也有资料说就绪和运行状态是合二为一的。

我们知道操作系统会管理底层的硬件,那么是如何管理的呢?先描述,在组织!在操作系统中,每一个硬件都对应着一个struct device的结构体,而每个结构体中都包含着一个指针,指向下一个struct device。从而形成一个链表,将对硬件的管理转变成对链表的增删查改!

假设我们的CPU正在执行的代码中有scanf语句,这时操作系统就需要从键盘获取数据,如果键盘数据没有准备好,那么我们的CPU不可能一直卡在scanf处,等待数据输入。此时,该进程就会被从调度队列中剥离下来,插入到等待队列中,让你在键盘的等待队列中等待,当获取到数据之后再把你尾插到调度队列中(结合上图理解)。只要进程在等待队列中,该进程就叫做阻塞状态。其实运行和阻塞的本质就是:让不同的进程,处在不同的队列中!

Linux进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有多种状态,下面我们来看看 状态在kernel源代码里的定义:

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 */            //僵尸状态
};

先来一段代码

int main()
{
    int cnt = 0;
    while(1)
        printf("hello world, cnt = %d\n",cnt++);
}

 上述代码非常简单,就是一个死循环,我们多次运行该程序,并通过命令(ps axj | head -1 && ps axj | grep 可执行文件名)在另一个终端查看该进程的状态,如下图

唉?怎么回事啊,程序不是一直在跑吗?为什么不是R状态而是S状态呢?其实是printf语句一直在做I/O,大家知道做I/O是非常慢的,而CPU的运行速度是非常非常快的,所以上面代码99%的时间一直在做I/O,而做I/O本质上就是等待,所以我们得到了上图中的结果就没有什么奇怪的了。

如果在上述代码中,在printf语句前面加上代码scanf语句,运行代码,此时是什么状态呢?没错,就是S状态!

S状态和D状态都是休眠状态,只不过S状态是浅睡眠(等待键盘等设备),可以被操作系统直接杀死;D状态是深度睡眠(等待磁盘等设备),只有“睡眠结束”,该状态才退出。

T状态有两种情况:一是进程做了非法但不致命的操作,被OS暂停了;二是将正在运行的进程通过[kill -19 进程PID]命令进行暂停,通过[kill -18 进程PID]命令就可以继续运行进程,只不过继续运行之后,该进程由前台进程变为了后台进程。t 状态,当进程被gdb追踪时,断点停下,进程状态为 t

死亡状态与僵尸状态

现在我们谈谈进程死亡的话题。

在此之前,我们先来谈谈进程为什么会被创建?进程创建出来,是为了完成用户的任务的。所以,如果进程退出了,我怎么知道任务完成了没有?如何通过进程的执行结果,告知父进程/操作系统,我的任务完成的怎么样了呢?

举个例子,我们平时写代码时,总是要先写一个main函数,而函数的最后一句代码总是return 0;这句代码的意思是什么呢?其实这句代码就是告诉父进程/操作系统程序执行的结果如何了。返回0,代表程序正常结束;若返回非0,则说明出现错误。

再举一个例子,若你在马路上碰见一个倒地不起的老大爷,你拨打110之后,警察来到现场之后,第一时间应该是封锁现场,然后法医从老大爷身上提取有效信息来判断老大爷是自然死亡还是其他原因死亡,之后才会通知老大爷家属来处理后续的事情。

其实从倒下——通知家属这段时间里,老大爷已经死亡,为什么不能立马抬走呢?因为警察和法医需要给社会和家属一个交代,这里的老大爷就是进程,社会就是操作系统,家属就是父进程。我们把这段时间中进程的状态称为僵尸状态。僵尸之后的进程状态才能称为X状态。

为什么在Linux系统中要设置僵尸状态呢?是为了维护退出信息,方便操纵系统和父进程进行查询!进程退出时,做了三件事情:1.立即释放进程对应的程序信息数据,因为代码不会执行了;2.进程退出时,要有退出信息(进程退出码)保存在自己的task_struct内部;3.管理结构task_struct必须被OS维护起来,方便用户未来进行获取进程退出的信息。

僵尸进程的危害

进程的退出状态必须被维持下去,因为他要告诉父进程,你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就会一直处于Z状态

维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB就会一直维护

如果一个父进程创建了很多子进程,但就是不回收,就会造成内存资源的浪费(内存泄漏),因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

孤儿进程

如果说僵尸进程是父在,子退;那么孤儿进程就是父退,子在

上图表示通过kill命令将父进程21689杀死的前后对比,我们可以清楚的看到父进程死亡之后,其子进程21690的父进程变为了1。这个1是谁呢?我们目前认为是操作系统就可以了。我们把父退,子在,但子进程会被系统自动领养的进程 ,叫做孤儿进程。

task_ struct内容分类——优先级

什么是优先级?优先级是获得某种资源的先后顺序。为什么要有优先级?因为要获取的目标资源比较少!比如说,CPU就一个,而进程却可以有多个,所以就会出现抢夺资源的情况,优先级的作用就在这里得到了体现。在Linux中进程的优先级属性,就是几个特定的int变量,保存在task_struct中,优先级数字越小,代表优先级越高

上图是我们通过ps -al命令查看正在运行的进程的相关信息(图中的各项数据注释的很清楚)。

 我们再来看一张图,上图清楚的说明了nice的取值范围为[-20, 19],而且每一次的优先级改变都是从PRI为80开始计算的,意思就是每次计算都会对PRI进行重置。

task_ struct内容分类——上下文数据

想要谈上下文数据,就离不开进程切换。

进程切换

再谈进程切换之前,先补充一下知识储备:

1.时间片到了,进程就要被切换。

2.Linux是基于时间片,进行调度轮转的

3.一个进程在时间片到了的时候,并不一定跑完了,可以在任何地方被重新调度切换

切换过程
  1. 程序在运行时,会有很多的临时数据,都被放在CPU的寄存器中保存
  2. CPU内部的寄存器数据,是进程执行的瞬时状态信息数据,我们把该数据称为上下文数据
  3. CPU内有很多个寄存器,这些寄存器构成一套寄存器。寄存器 != 寄存器里面的数据,这些数据为上下文数据。

进程切换的核心就是进程上下文数据的保存和恢复

当编译好的代码和数据被加载到内存,而此刻CPU调度的进程current指针刚好指向进程1,那么PC指针就会指向第一条指令的地址(图中的圈1),接着IR寄存器读取相应的汇编代码(图中的圈2),然后放到控制器中执行(图中的圈3),此时,PC指针不在指向第一条指令的地址,转而指向第二条指令的地址,接着把IR寄存器的数据放在eax寄存器中。循环往复,直至程序结束为止。而图中的10、20、0x10这些数据就叫做当前进程的硬件上下文

假设当上述进程执行到代码0X10地址处时时间片到了,内存中又来了一个进程2,此时的current指针就指向了进程2,把进程1放回了调度队列,如果不做任何的数据保护,直接就开始在CPU上执行进程2的代码,那么就会把CPU上关于进程1的代码数据覆盖掉!

而当进程2的时间片到了之后,current指针又会指向进程1,这时就会出现一个非常尴尬的事情——我进程1之前的临时数据被覆盖了,我应该从哪里开始执行呢?难道重新从main函数地址开始执行吗?此时,调度就已经出现问题了。

结论:不做保护,是无法完成多进程之间的调度与切换的!

我们再来看看做保护的情况,在进程1时间片到了之后,进程1没有着急走,而是在自己的task_struct内部找了块空间把CPU中临时数据保存起来,然后才回到调度队列。接着进程2开始在CPU执行代码,时间片到了之后,进程2也在自己的task_struct中找了块空间将自己的临时数据保存起来。现在又切回到进程1,进程1不是立即就开始执行自己的代码,而是先将自己之前保存的临时数据恢复出来,然后再继续执行代码,这样就可以调度轮转了。task_struct2同理。

所以上面所说的进程上下文数据的保存和恢复本质就是:切走,将相关寄存器的内容,保存起来;切回,将历史保存的寄存器数据,恢复到寄存器中。所有的进程都要做这个工作。

进程调度

我们之前在进程状态模块所说的FIFO调度队列,只是为了更容易理解,实际上FIFO调度队列根本很少见,只有在特别老的版本的内核才有可能遇见。原因很简单,当用户改变了进程的优先级,FIFO队列怎么处理?所以我们来见识一下Linux下真实的调度算法。ps. 这里我们只是浅浅了解一下,不做深入细致的探讨。

Linux2.6内核进程调度队列

Linux下真实的调度算法是怎样处理优先级的问题的呢?

上图是一个指针数组,共包含140个struct task_struct* 类型的指针,前100个我们不需要考虑,只需考虑后40个。为什么偏偏只考虑40个呢?我们在进程优先级模块曾说过,nice的取值范围是[-20, 19]正好也是40个数字,难道它们之间有什么关联?没错这40个指针就对应nice的取值范围。

我们多多少少都了解过哈希结构,其实这里就是利用哈希桶,将相同优先级的进程映射到同一个指针下面,如下图。

实际上,在内存里面有两个这样的数组,用指针*active(活跃)指向一个数组,用指针expired(过期)指向另外一个数组。而CPU调度只会从active所指的队列里选择进程进行调度,调度有三种情况:1.运行之后退出了(我们不用考虑)、2.不退出,但是时间片到了、3.有新的进程产生了

对于第2第3种情况的进程,不再链入到active队列,而是链接到expired队列中。所以active队列中的进程只会越来越少,不会增多。也就是说active队列一定会有为空的时候,等active队列为空时,操作系统只需做一件事,就是交换active指针和expired指针的所指向空间的地址。此时,之前的active队列变为了expired队列,expired队列变为了active队列。然后CPU继续进行调度,重复此过程。 

怎么样?现在了解了进程的切换与调度,再来看文章开头所说的“进程是程序的一个执行实例,正在执行的程序”有没有更深刻的理解呢?进程在运行的时候,你脑子里有没有一幅关于进程切换调度的动图呢?嘻嘻~~~

由于篇幅太长,进程的相关知识点又特别多,本篇文章就到此结束了,如果你感觉本篇文章对你有所帮助的话,不要忘记三连哟~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2203187.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式面试——FreeRTOS篇(七) 软件定时器

本篇为:FreeRTOS 软件定时器篇 一、软件定时器的简介 1、定时器介绍 答: 定时器:从指定的时刻开始,经过一个指定时间,然后触发一个超时事件,用户可以自定义定时器周期。 硬件定时器:芯片本…

python项目实战-xPath下载人生格言

事先声明一下,仅分享爬虫经验,不会对网站有影响的,也请想要实操的小伙伴不要对网站频繁访问,如有侵权请联系我删除文章 文章目录 代码展示步骤解析思路整理节点树xPath语法分析网页结构 实战思路获取源代码源代码转化成节点树提取…

新手必备!百度思维导图在内四款必备工具分享

作为一名资料员,每天都要处理大量的信息和数据,思维导图对我来说简直就是救星。它帮我理清思路,把复杂的信息条理化,让我能更高效地完成工作。今天,我就来跟大家分享一下我用过的几款思维导图工具:福昕思维…

java项目之基于保密信息学科平台系统源码(springboot+vue+mysql)

项目简介 基于保密信息学科平台系统实现了以下功能: 基于保密信息学科平台系统的主要使用者管理员有个人中心,用户管理,教师管理,学科概况管理,人才培养管理,学科动态管理,学科资源管理&#…

2本书让你轻松入门大模型!《大模型入门:技术原理与实战应用》+《自然语言处理:大模型理论与实践》

随着大模型技术的不断完善和普及,我们将进入一个由数据驱动、智能辅助的全新工作模式和生活模式。个人和企业将能够利用大模型来降本增效,并创造全新的用户体验。 人工智能是人类探索未来的重要领域之一,以GPT为代表的大模型应用一经推出在短…

信息系统与计算技术国际学术会议

第十二届信息系统与计算技术国际会议(ISCTech 2024)将于2024年11月8日-11月11日在中国西安盛大举行,由长沙理工大学主办,同济大学、西北工业大学联合协办。 会议聚焦信息系统与计算技术等相关研究领域,广泛邀请国内外…

JavaGuide(3)

一、项目背景与简介 JavaGuide由GitHub用户Snailclimb开发并维护,是一个全面而深入的Java学习资源库。它旨在为Java初学者和有经验的开发者提供一个系统的学习路径和丰富的资源,帮助他们系统地学习和巩固Java及相关技术知识。 二、项目内容与特点 Jav…

【HTML】defer 和 async 属性在 script 标签中分别有什么作用?

需要这两个属性的原因? 首先我们要知道的是,浏览器在解析 HTML 的过程中,遇到了 script 元素是不能继续构建 DOM 树的。 它会停止解析构建,首先去下载 js 代码,并且执行 js 的脚本;只有在等到 js 脚本执行…

try、catch、finally、return执行顺序超详解析与throw与throws区别

try、catch、finally、return执行顺序超详解析(针对面试题) 有关try、catch、finally和return执行顺序的题目在面试题中可谓是频频出现。总结一下此类问题几种情况。 写在前面 不管try中是否出现异常,finally块中的代码都会执行&#xff1b…

IPguard与Ping32敏感内容防护能力对比,两款知名防泄密软件对比

在信息安全的新时代,企业面临着日益严重的敏感内容泄露风险。为了保障数据安全,选择合适的防护工具至关重要。IPguard与Ping32作为市场上两款知名的终端管理解决方案,各自具备独特的敏感内容防护能力。本文将对这两者进行深入对比&#xff0c…

kubeadm 搭建k8s 1.28.2版本集群

kubeadm 搭建k8s 1.28.2版本集群 1、kubuadm介绍: kubeadm 是官方社区推出的一个用于快速部署kubernetes 集群的工具,这个工具能通过两条指令完成一个kubernetes 集群的部署: 创建一个Master 节点kubeadm init将Node 节点加入到当前集群中…

minio集群部署

最近接触到minio, 将本地集群部署,分别在ubuntu、centos stream9上进行了搭建,目前看里面的小坑不小,记录以下教程,以备忘、以供他人借鉴。 #### 准备 1、因新版本的minio要求,集群部署必须使用挂载非 roo…

Java基础:字符串详解

1 深入解读String类源码 1.1 String类的声明 public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence { }String类是final的&#xff0c;意味着它不能被子类继承&#xff1b;String 类实现了Serializable接口&#xff0c;意味着…

没人告诉你的职场人情世故

看到前同事在群里分享的新年开工遭遇&#xff0c;真是让人感同身受。 第一天就遇到挫折&#xff0c;因为工作做得太快、太早交付&#xff0c;结果反被领导批评&#xff0c;还得重做&#xff0c;头大如斗。这不就提醒我们得时时刻刻记着职场里的那些不成文的规矩吗&#xff1f;…

Hunuan-DiT代码阅读

一 整体架构 该模型是以SD为基础的文生图模型&#xff0c;具体扩散模型原理参考https://zhouyifan.net/2023/07/07/20230330-diffusion-model/&#xff0c;代码地址https://github.com/Tencent/HunyuanDiT&#xff0c;这里介绍 Full-parameter Training 二 输入数据处理 这里…

一、Spring入门

文章目录 1. 课程内容介绍2. Spring5 框架概述3. Spring5 入门案例 1. 课程内容介绍 2. Spring5 框架概述 3. Spring5 入门案例

Java反射专题

目录 一.反射机制 1.Java Reflection 2.反射相关的主要类 3.反射的优缺点 4.反射调用优化—关闭访问检查 二.Class类 1.基本介绍 2.常用方法 3.获取Class对象的方式 4.那些类型有Class对象 三.类加载 1.介绍 2.类加载时机 3.类加载各阶段 四.获取类结构的信息 1…

25.第二阶段x86游戏实战2-背包属性补充

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要…

机器学习项目——运用机器学习洞察青年消费趋势

1. 项目背景 在21世纪的第三个十年&#xff0c;全球经济和技术的飞速发展正深刻影响着各个领域&#xff0c;尤其是青年消费市场。随着数字化进程的加速&#xff0c;尤其是移动互联网的广泛普及&#xff0c;青年的消费行为和生活方式发生了前所未有的转变。 然而&#xff0c;面对…

开源版GPT-4o来了!互腾讯引领风潮,开源VITA多模态大模型,开启自然人机交互新纪元[文末领福利]

目录 总览 VITA 模型的详细介绍 2.1 LLM 指令微调 2.2 多模态对齐 2.2.1 视觉模态 2.3 音频模态 多模态指令微调 3.1 训练数据 3.1.1 训练过程 模型部署&#xff1a;双工策略 4.1 非唤醒交互 4.2 音频中断交互 评估 5.1 语言表现 5.2 音频表现 5.3 多模态表现 …