Bootstrap

Linux 进程手撕笔记——万字深剖详解

传统艺能😎

小编是双非本科大一菜鸟不赘述,欢迎大佬指点江山(QQ:1319365055)
此前博客点我!点我!请搜索博主 【知晓天空之蓝】

🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,打码路上一路向北,背后烟火,彼岸之前皆是疾苦
一个人的单打独斗不如一群人的砥砺前行
这是我和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
直达: 社区链接点我

🎉🎉🎉倾力打造转码社区微信公众号🎉🎉🎉
在这里插入图片描述


在这里插入图片描述
前文属于正文的铺垫,因为比较抽象所以写了一点铺垫,觉得啰嗦的可以直接跳到 “ 进程 ”。

冯.诺依曼体系🤔

冯诺依曼这个名字估计多少都听过一点,这位著名的数学家提出了历史上著名的冯诺依曼体系:

在这里插入图片描述
他把计算机硬件分为了五大单元:输入,输出,控制器,运算器,存储器

社会是由人为基础构建的社会,人拍的抖音会给人看,万物根源是从哪里来到哪里去,输入输出的本义就是在连接两个独立的对象——人和计算机。比如键盘,话筒,摄像头这些输入;显示器,磁盘,网卡这些输出运算器 + 控制器其实就是我们的 CPU ,存储器就是内存。

内存🤔

那么经典问题就来了,冯诺依曼体系下为什么要有内存?

首先,从技术角度 CPU 运行速度 > 寄存器速度 > 三级缓存(L1~L3 Cache )> 内存 > > 外设(磁盘) >> 光盘 。 这里每一个体之间差别是非常大的,甚至两者之间可以差一个单位量级。从图上看我们内存在数据角度上占据了体系的主导地位,而外设效率低速度慢,因此外设不和 CPU 直接交互,而是和内存交互,内存并不会读取数据,读取数据在外设完成然后将数据刷新回内存。

在我们看来, 内存就是体系下的一个大缓存,可以适配 CPU 和外设速度不均的问题;同时在也支持了数据加载到内存和数据的计算的并行,其实操作系统会提前将数据预加载到内存中,比如开机的那段等待时间就是在把操作系统预加载到内存中。

预加载的理论依据就是局部性原理,通俗一点就是我会将正在执行的代码附近的代码也提前加载进来,效率也就会嘎嘎提高了。

其次从成本角度虽然内存造价 > 外设磁盘,但是如今计算机已经走进全球寻常百姓家中,那就说明它是有效且便宜的,

控制器🤔

为什么我要掰开 CPU 来单独讲控制器呢?因为计算器大家都懂嘛,就算数运算+逻辑运算。

那控制器是搞毛的?我们说外设和 CPU 交互依靠内存,虽然外设和 CPU 在数据上没有交互,但是数据是否在输入设备上导入完成,预加载是否完成,这些问题都需要控制器进行交互,而且具体的指挥也是在 CPU 里面,控制器会直接和内存沟通,比如数据什么时候流向我 CPU?流多少过来?

但是记住几乎所有硬件都是被动配合的,一般需要配合软件才能完成某项功能(OS+CPU)

如何搞管理🤔

操作系统是一款软件,搞管理的软件,那么什么叫做管理呢?

计算机就像人一样,做事都有两步:决策和执行,在人身上这两步是一体的,但是对于计算机它是相对独立的,管理的本质不是对管理对象进行直接管理,而是拿到管理对象的所有相关数据,这种管理模式可以折射到人身上:

比如你和你学校,你学校也在管理你,但是并不是直接管理,而是通过辅导员提供的绩点,出勤率,学分等数据,来判断期末是给你发奖学金还是找你单独谈话。学校如同管理者,一个辅导员如同执行者,而学生就是被管理对象;这三者在计算机里其实就是操作系统,驱动和硬件三者的关系

数据是有多少的分别的,我们对于管理的第二层理解就是如何管理好大量的数据?

请记住六个字:先描述,再组织

想想我们人是如何认识世界的?是通过属性,告诉你你用什么来吃饭写字,你就知道是手,这就是属性。记得之前我写进博客的那句话:一切皆对象,啊不是那个对象(反正我是没有),说人话就是一切事物可以通过抽取对象的属性来达到描述对象的目的。面向对象的诞生也是必然的,因为他符合我们人认识事物的方式。

所以我们对数据的管理根据我们认识事物的方式,会变成对数据结构的管理。比如我们面对成千上万的同学,假如我就是校长,我肯定会去想有没有一种数据结构,能满足我来描述他们?于是乎,对学生信息的管理就变成了对链表的增删查改!

进程🤔

主角登场,在操作系统中有四大管理:内存管理,进程管理,文件管理和驱动管理,我们今天主要讲进程。

在书里面你可能看到:进程是一个运行起来的程序。这句话就是个废话文学,进程和程序又有什么区别?问个问题,程序是文件吗?是的,我在 Linux 开篇也说过,这个文件他就放在磁盘。每一个 exe 执行文件都放在内存了,当你打开你的任务管理器你会发现有一堆的进程:

在这里插入图片描述
我们知道了操作系统里可能存在大量进程,那么操作系统也是需要将所以的进程进行管理的,根据我们上面的理念,对进程的管理也是先描述再组织的数据管理,类比一下学生信息,进程会被放进一个 task_struct 结构体进行封装,里面包含了他所有属性,但现在我们无从得知。然后再对每一个 struct 进行连接形成一个双链表,最终又变成了内核数据结构的管理!c所以进程的概念应该是:程序加载到操作系统的代码和它所对应的内核数据结构的总称就是进程

其中这个包含代码属性的结构体 task_struct 叫做 PCB ,即进程管理块,它是 Linux 内核的一种数据结构,转载在 RAM 里且携带进程信息:

在这里插入图片描述

软硬件和操作系统交互后就会以进程的方式对外提供服务,他如同一个银行,你能看到工作人员在透明的工作区,但是确实封闭的只暴露一些窗口,这就足以说明银行的中立性他不相信任何人!操作系统也是如此,他要防止少数不法分子又要给多数人提供服务,他提供的窗口就是接口形式。这个接口其实就是 C 语言,内核也是 C 语言写的,所以C语言接口给我们提供各种函数调用。

操作系统立大功🤔

OS 为什么要给我们提供服务?很简单,这种人道主义奉献是必须的他才会有意义,我们自己写的程序,是没有资格向硬件写入的,所以他涉及出来就是服务于人。

操作系统虽然提供那么多接口,但也如同银行一样,窗口有工作人员,但他们受到过专业训练,我作为使用者我可能是小白也可能是初级工程师,我又不懂该怎么办呢?银行有接待,操作系统也有,那就是图形化界面和命令行解释器,他可以具象出一个个窗口和选项给小白进行选择即可,让解释器去帮忙调用这些接口。而面对工程师就提供了== lib 这种封装好的库在操作系统上==才方便搞事情。

操作系统如何帮我们?操作系统是通过系统调用的方式对外提供接口服务,但是 Windows 和 Linux 的系统接口一样吗?答案很明显是不一样的,在 Windows 下他会选择 Windows 接口,在 Linux 下他会选择 Linux 接口,但是上层调用的函数确实一样的函数名,哎呀,这不就是多态嘛!底层差异交给库解决,我们只管写就行了,所以经常说的可移植性和跨平台性都是在库里面动的手脚。

进程查看🤔

当我在 mytest.c 中写了一个死循环:

while(1)
{
    printf("This is a process\n");
    sleep(1);
}

(我还是使用 Makefile 自动化构建了一下,但过程是一样的)我们编译运行,此时这个一直打印的程序就是一个进程。我们如何查看进程呢?

ps ajx

ps 命令是查看进程情况的命令(未截完):
在这里插入图片描述

图上的就是当前系统中所有启动的进程,我们再 grep 针对当前可执行文件进行查看:

ps axj | grep ‘exe-name’(exename 为可执行程序名)

在这里插入图片描述

我们自己写的代码,编译为可执行程序,启动之后就是一个进程,那么别人写的呢?启动之后他也是一个进程

进程的 PID 🤔

我们还有一种查看进程的方法就是 proc 目录下查看,在根目录下是有很多路径,很多我们没见过也不知道是干啥的,但是没有关系:

在这里插入图片描述
其中 proc 是内存文件系统,里面就记录了当前系统的实时进程信息,那我们进入 proc 发现里面又是一堆奇奇怪怪的东西:

在这里插入图片描述
这些蓝色数字是啥?我们引入一个新的东西:进程的 PID

每一个进程在系统中都有唯一的标识符,就像一个身份证号一样, PID 全称就是进程 id ,重新启动同一个程序会变成一个新的进程,我们依然可以针对 pid 进行 ls 查看,里面会有他的各种属性,其中我们只需要知道两个重要信息:

在这里插入图片描述
cwd 表示当前进程的工作路径,exe 表示对应可执行程序的磁盘文件。pid 和当前路径这些都是进程的内部属性,一般就会放在进程控制块 PCB 中!

获取进程 PID🤔

想知道自己程序的 pid 直接调用 getpid() 函数即可,头文件: #include<unistd.h>,定义函数为 pid_t getpid(void)

在这里插入图片描述

程序的父进程 ppid 也可以用 getppid 获取,我们多重启几次就会发现一个亮点:重启程序会重新分配进程 pid 可以理解,但是父进程为什么不会变?
在这里插入图片描述
其实父进程就是一个 bash ,几乎我们再命令行上所执行的所有指令(cmd),都是 bash 进程的子进程。

我们结束进程除了用 ctrl + c ,还可以用 kill 命令直接杀掉进程,kill 会牵扯到信号现在不做讲解。

fork🤔

代码创建子进程需要 fork() 函数,他的返回值很有意思就是给父进程返回子进程的 pid 然后给子进程返回 0,失败就返回 -1,就意味着他有两个返回值,怪诶。可为什么是这样的呢?

我们知道一个父亲可以有很多儿子,在这个背景下父进程必须要有标识子进程的方案,就像父亲面对几个儿子喊一声儿子,这时候不知道喊的谁就会全部跑过来,因此 fork 之后就需要给父进程返回子进程 pid。

而子进程最重要的就是知道自己是否被创建成功,因为子进程他只有一个父进程,直接 getppid 就能得到,所以他找父进程的成本非常低,所以就没必要返回喽给个 0 即可。

在这里插入图片描述

那 fork 为什么能返回两次?子进程的控制块的内部数据基本上是从父亲哪里继承下来的,也不是全部,起码 pid 是自己的,子进程的代码也是父进程的,在 fork之后 ,父进程和子进程代码共享但是数据私有!因为不同的返回值,让不同的进程可以执行不同的代码。

如果一个程序调用一个函数,但这个函数准备 return 了是不是意味着核心功能已经完成? 答案是是的,return 就代表着此时子进程已经被创建,而且已经将子进程放进了运行队列,这就又要说一说进程的运行。

那又该如何理解进程被运行?在 Linux 内核中每一个 CPU 都会存在一个叫作运行队列的东西,他将存有代码和数据的结构体 task_struct 以链表形式存进队列里面, head 指针指向运行队列,然后会有一个专门调用代码的调度器来调用其中合适的进程放到 CPU 上去运行,这样就能运行一个进程。

进程状态🤔

在 task_struct 中会包含进程的状态信息,进程的状态是用整数来表示的:

在这里插入图片描述
在这里插入图片描述

运行状态有四种:运行态,终止态,阻塞态,挂起态。

运行态:不是指正在运行的进程,进程只要在运行队列中就叫做运行态,代表他已经准备好随时被调度了。

终止态:不是指该进程已经被释放,而是该进程还在但是永远不会运行,随时等待被释放。

那么问题来了,进程不运行了为什么不立马释放却要维持一个所谓的终止态?那么思考一下,释放是否需要花时间?是的,那有没有可能当前系统他抽不身来释放呢?是可能的。

阻塞态:一个进程使用资源的时候,不仅仅是在 CPU 中申请资源,可能会需要磁盘,网卡,显示器等资源。如果我们申请 CPU 资源,如果暂时无法得到满足,需要进行排队也就是加入运行队列,或者说申请其他慢设备的资源也会需要排队。

以上状态都是 task_struct 在进程排队,以此我们再推导,我们 CPU 处理多个进程可能一秒十几个,但是对于许多速度慢的外设就很恼火了,根据冯诺依曼体系,外设速度慢 CPU 块因此内存在其体系核心地位进行协调。因此,当进程访问某些资源时,该资源暂时没有准备好,或者还在为其他进程提供服务,那么当前进程就需要先从运行队列移除并将进程放入对应设备的描述结构体中的等待队列进行排队。

struct _div //某设备的描述结构体
{
//该设备属性(频率,速度等)
task_struct *wait_queue (等待队列)
}

进程在等待外部资源的时候,该进程的代码就不会被执行,这个状态就被视为进程卡住了,也就是我们所谓的进程阻塞

挂起态挂起态其实和阻塞态有一点类似,但是更恶心,对于挂起我们首要面对的问题就是内存不足了该怎么办?要明白计算机里面 99.99% 的内存不足都是进程导致的,轻者其他程序无法运行,重者操作系统自己无法工作,所以操作系统对进程的管理又能体现了,他会帮我们进行辗转腾挪。

假设此时等待队列已经爆满,排到了二三十个进程之外了,后面的短时间内是无法被调度的,但是此时他的 PCB 和数据仍然在内存中占着茅坑不拉屎,内存告急时操作系统就会将该进程的代码和数据置换到磁盘上,这样的进程就被称为进程挂起

所以往往在内存不足时,我们的磁盘就会发生高频率访问。

Linux 进程状态🤔

操作系统它是计算机学科的哲学,单说操作系统的教材,书籍,文章,并没有指明是 Linux 还是 Windows 还是移动端,所以大部分情况他会让结论满足各种平台,所以我们需要站在 Linux 角度去具体理解。

在这里插入图片描述

也就是说一个程序不访问显示器等外设,只访问 CPU 且处于运行状态,那么这个进程就是 R 运行态,一访问外设了即便一个 printf 都会呈现 S 阻塞态,说明他肯定在等待某种资源。而我们这里的 S 状态也叫做浅度睡眠或者可中断睡眠,他随时可以被调度唤醒。

D 状态也是一种阻塞状态,也是去等待某种资源,D 全称是 disk sleep ,Linux 中等待的是资源如果是磁盘资源,那么我们所处的状态就是 D 状态!这个状态有些人一辈子见不到,系统开发倒是很常见,一般服务器压力过大时,大部分闪退不是电脑问题,而是操作系统自动杀掉了进程,但是有些重要进程数据要是丢了该找谁背锅?操作系统还是磁盘?由此就诞生出了连操作系统都无权生杀的 D 状态进程。

要想终止 D 进程,关机是没有用的,唯一的办法就是拔掉电源。

Z 状态,z 即 zombie,僵尸状态,当 Linux 中一个进程退出时,一般不会立刻进入 X 状况(死亡状态,资源可立即回收),而是先进入 Z 状态。为什么呢?

首先要知道为什么进程会被创建?我们进程的创建就是为了完成任务,为了得知完成情况,我们是需要将进程的执行结果告知操作系统或者父进程,所以维护 Z 状态就是为了让父进程和 OS 来读取执行结果。

我们可以自己模拟一个僵尸进程:

   int main()
  {
    pid_t id = fork();
    if(id==0)
    {
      int num = 5;
      while(num--)
      {
        printf("child has left %d\n",num);
        sleep(1);
      }
      printf("child has down");
      exit(0);
    }
    else
    {
       while(1)
       {
         sleep(1);
       }
    }
  }   

结果是这样的:
在这里插入图片描述
子进程已经终止了。但是父进程任然还在嘎嘎执行,这个状态下的子进程就是所谓的僵尸进程,Z 状态。想想一个父进程创建了很多子进程却不回收,僵尸进程就会造成内存泄漏和资源浪费!

但是反过来,如果子进程还在嘎嘎跑但是父进程已经歇菜了,这样的进程就是孤儿进程,但是孤儿进程中父进程并没有 Z 状态,为什么呢?因为此时父进程的父进程是 bash ,bash 会自己回收他的子进程也就是此时的父进程,其实也不是没有 Z 状态,只是父进程有人回收他从 Z 到 X 的过程就非常快。

如果父进程没有了,还在运行的子进程就是被 1 号进程领养,1 号进程就是操作系统。

T 状态 ,T 就是 stopped 暂停状态,我们可以在得到进程 pid 的情况下,用 kill 命令发送 19 号暂停信号来达到目的,想继续就使用 kill 发送 18 号继续信号即可。

在这里插入图片描述
t 状态 t 和 T 都是暂停但区别在于功能性, t 状态是进程被调试过程中,遇到断点时发生的暂停。

进程优先级🤔

优先级就是进程获取资源的先后顺序,注意区分优先级和权限两个概念,权限谈的是能和不能,优先级是已经有权限了谈的是先还是后。

为什么会有优先级?🎉

排队的本质就是确定优先级,很明显嘛为什么有优先级其实就是为什么要排队,作为一个人类这个问题很简单,排队就是为了解决资源不够,要是 5000 人大食堂有 5000 个窗口那还排个屁的队啊是吧,因为系统里面永远都是进程占大多数但是资源却是少数,那就铁铁的排队呗,这就直接决定了进程管理中会有队列这种结构。

Linux 优先级相关操作🎉

ps 指令即可看到对应的 优先级,如果需要更改进程优先级,需要更改的不是 pri 而是 NI,这个 NI 就是优先级的修正数据。
在这里插入图片描述
我们修改可以直接用 top 指令,进入界面后按 r 进入 return 跳转模式,输入 pid 就可以跳转到需要修改的进程,然后再输入你想设置的优先级,但是一个进程优先级不能轻易被修改会显示报错 ,因为他会破坏优先级平衡,要强制修改就需要超级用户权限

sudo top

假设我们给一个 process 进程设置了优先级为 -100 ,完成了我们 ps 查看一下:

在这里插入图片描述
发现 process 的 pri 确实变小了,那么优先级就变高了,但是,我不是 -100 吗? 为什么 NI 是 -20?其实 Linux 不允许用户对进程无节制的设置优先级,Linux 中权限修正范围为 【19,-20】。

这 40 个优先级并不是全部,Linux 其实一共有 140 个优先级,但很多并不是给普通进程用的。

aqa 芭蕾 eqe 亏内,代表着开心代表着快乐,ok 了家人们。

;