Linux进程理解

Posted SuchABigBug

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程理解相关的知识,希望对你有一定的参考价值。

目录

一、冯诺依曼体系

我们先来看一下日常生活中,我们所用的电脑或者公司用的服务器都遵守冯诺依曼体系

由五种组成,分别是输入设备,存储器,运算器,控制器和输出设备

输入单元:鼠标,键盘,扫描仪,写板,网卡,磁盘,话筒等
存储器:也就是物理内存,后续讲进程地址会详细讲
运算器和控制器:都属于CPU,运算器在CPU中会做两件事一是执行算术,二是执行逻辑
输出单元:显示器,网卡,磁盘,音响

输入和输出设备统称为外设,其次CPU并不和外设打交道,同样的外设只和内存打交到

过程:输入单元输入的是数据,数据写入存储器进行处理(运算器和控制器)然后再传出输出设备,因此冯诺依曼规定了硬件层面上的数据流向

那么从冯诺依曼体系的角度来解释一道题:
小明在广东给远在安徽的小红发了一句在吗?那么请问发送了这个“在吗?”信息经过了哪些步骤?
ans:输入在吗?通过输入设备,然后存储到存储器,qq软件运行的时候也跑在存储器上,通过加密运算等过程再写回到存储器然后qq再把数据刷新出去,到输出设备,此时的输出设备叫网卡。通过网络朋友家接收数据的输入设备此时是网卡,不是键盘然后网卡的数据首先也会贯道内存里,qq在内存中获得消息,经过CPU运算解包再写到存储器里,然后存储器再定时的刷到显示器上,朋友最终收到了消息

二、操作系统概念

操作系统是进行软硬件资源管理的软件,管理硬件用struct结构体进行描述,用链表或其他高效数据结构进行组织,操作系统包括以下:

  1. 内核(进程管理,内存管理,文件管理,驱动管理)
  2. 其他程序(例如函数库,shell程序等)

为什么会有有操作系统?
3. 可以减少用户使用计算机的成本
4. 对下管理好所有的软硬件,对上对用户提供一个稳定高效的运行环境

三、进程

3.1 进程概念

那么什么是进程?
进程 = 你的程序 + 内核申请的数据结构(PCB),程序加载到内存里,操作系统为此创建PCB结构体变量然后里面填上该进程的属性信息最终完成进程创建

举个例子,当我们启动一个google浏览器,或者进行下载等,程序一旦运行就是进程。
进程是资源分配的最小单位,且每个进程拥有独立的地址空间

那么问题来了!!
如果我们开启了几十个进程呢?对于操作系统来说就必须要进行管理
正如上面所提到的我们对于每个进程都遵守先描述再组织的原则,先描述就是为每个进程创建一个进程控制块PCB,进程信息被放在进程控制块的数据结构中,然后一个个的进程由链表或其他高效数据结构进行组织

3.2 描述进程-PCB

那么我们就详细了解一下PCB,可以理解为进程属性的集合,就好像在自然界中我们看到一个动物会通过它的特征进行描述组织从而精准的判断。
在Linux操作系统下的描述进程结构体叫做task_struct
task_struct是linux内核中的一种数据结构,被装在到内存里并且包含着对应进程的信息

  1. 标示符: 描述本进程的唯一标示符,用来区别其他进程。也就是我们在Linux下看到的PID
    我们可以grep一个进程看到
  2. 状态: 任务状态,退出代码,退出信号等。
  3. 优先级: 相对于其他进程的优先级。
  4. 程序计数器: 程序中即将被执行的下一条指令的地址。
  5. 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  6. 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  7. I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  8. 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等

注意:进程放在CPU上后,不是一直在运行知道进程结束的

一般进程让出CPU有两种情况

  1. 来了一个优先级更高的进程
  2. 时间片到了

并发和并行的概念:
单CPU:跑起来多个进程,通过进程快速切换的方式,有个时间片,在一段时间内给进程运行一段固定的时间片时间,然后给下一个,从而实现并发
多核CPU:任何时刻,允许多个进程同时执行,并行

进程间切换:
最重要的就是保存上下文,把临时数据保存在PCB中,让出给其他的进程
如果有4个进程被切,是出于运行状态的,所以我们的操作系统会给每个CPU形成对应的 运行队列,通过运行队列把所有的PCB链接起来(也是用全局的链表连接起来,因为PCB里面包含了大量的指针,和文件等其他事务关联),这些队列是CPU所绑定的运行队列,CPU拿任务时就从这个队列中去拿任务,状态都为 R 状态

操作系统进行进程调度,CPU执行
总结:

  1. 我们的进程进行切换时,因为某些原因如时间片到了,或者遇到了一个优先级更高的进程,当前正在运行的进程需要保存上下文信息,然后让出CPU,另一个新到来的进程需要将自己的数据放入寄存器中,当被切走的进程回来时,也需要首先做恢复工作,这就叫上下文的保存与恢复
  2. 每一个进程都隶属于某一个运行队列,CPU直接从队列中调度

代码中用fork进行理解

  1. 程序员角度:父子共享用户代码,而用户数据各自私有一份。其中父子共享用户代码是只读的,不可修改和写入。 为什么要私有?操作系统中,所有进程具有独立性,如一个app挂掉而不影响另外一个进程的运行。 所以私有一份,不让进程相互干扰所以是只读的不能修改
  2. 内核角度:fork后,系统多了一个进程。创建子进程,通常以父进程为模版其中子进程默认使用的是父进程的代码和数据(写时拷贝)

为什么给子进程返回的是0,父进程返回子进程的pid?
儿子 -> 唯一父亲 , 因此儿子找父亲是特别简单的,是0
父亲 -> 多个子女 ,而一个父亲找特定的儿子是特别难的,所以要用不同ID区分

3.3 进程状态


通过task state array定义可以看出一共有七种状态

  1. R状态可以时正在运行,但是R状态有时候也并不代表是在CPU上面运行,进程在队列中等待被调度时,或者说进程准备就绪时也是R状态
  2. S状态,也叫浅度休眠,对外部事件可以做出反应,如ctrl+c 可以停止休眠
  3. D状态,也叫深度休眠,不可以被kill掉,即便是操作系统通常在访问磁盘进行数据拷贝的关键步骤上是需要将进程设置为Deep sleep的,只能等待D状态进程自动醒来,或是关机重启
  4. 小T状态,是一种等待状态



-18让进程又开始跑起来

  1. 大T状态,tracing stop用于打断点,进程就不跑了

  2. Z状态,僵尸状态
    进程退出时会将自己退出的相关信息写入进程的PCB中,供OS或者父进程来进行读取,其中我们把一个进程退出了但还没有被读取的时间点,我们称该进程来僵尸状态。
    注意:这里Z状态一直不退出,PCB要一直维护。如果父进程创建了很多子进程就是不会收,就造成了内存资源浪费,因为数据结构对象本身就要占用内存,换言之造成内存泄漏

  3. X状态,进程结束
    读取成功后,该进程才算真正死亡!为X状态

举个僵尸状态的例子:


当子进程执行完自己的事情后退出了,父进程还在S状态,但父进程不回收子进程,这时候子进程都会处于僵尸状态

僵尸进程本质上是为了维护一种临时状态,让我们父进程对子进程进行读取,进而回收资源,以及释放对应进程所占的资源

⚠️注意:Z状态已经挂掉,是不能被Kill掉的,plus 深度休眠状态也是不能kill掉的

  1. 孤儿进程
    先来看一组代码
    下面的代码父进程首先fork后,父进程先退出了,那么就不会去读取子进程的返回代码了
    那么这个老爹退出了之后,子进程还在S状态,需要被领养,也就是被PPID 为1操作系统给领养
    那么这个被领养的进程就叫孤儿进程,非常的形象生动吧!

    我们给下命令> top
    查看 一下1号进程

四、进程地址空间

前面的都是开胃菜,这里才是重头戏!
首先看一幅图
进程地址空间从下到上为低地址到高地址

printf("Lowest area: code Address : %p 		\\n",  main); //main是函数,一定是属于代码的一部分,所以可以充当代码段
const char* p = "I'm in const or you can call me static area"; //栈区定义一个p变量,p变量保存的是常量区的字符串
printf("Read Only 		  : %p 		\\n", p); //p就是保存该常量的起始地址, 如果是static int,也在这里(生命周期全局)
printf("global initAlready 	  : %p		\\n", &g_InitAlready);
printf("global Uninitalized	  : %p		\\n", &g_unInit);
char *q = (char*)malloc(10);
printf("Heap Addr		  : %p		\\n", q); //注意是q,而不是&q,如果是&q则是保持在栈区的变量名地址,而不是q内容
printf("Stack Addr p		  : %p		\\n", &p); //栈区向下增长,p先入栈,因此地址会比q的要大	
printf("Stack Addr q		  : %p		\\n", &q);
	
printf("args Addr		  : %p		\\n", argv[0]);
printf("argv Addr		  : %p		\\n", argv[argc-1]); //带一些选项就不一样了
printf("Env Addr		  : %p		\\n", env[0]);


上图论证了各个区域所存放的地址,从代码段到环境变量区域由低向高走

进程地址空间,不是内存!是虚拟出来的,后面会讲到到每一个进程都会认为自己有一块这么大的空间。
进程地址空间,会在进程的整个生命周期一直存在,直到进程退出! 这就是为什么定义的全局变量在为初始化,初始化,静态区一直存在了

我们可以论证这样一个观念:
假设内存是4GB,创建出来的每个进程都会认为自己有一块这么大的空间


val = 0的情况
如果一个进程创建了一个child后sleep 3秒钟,child先跑,对val进行修改后为10,父亲再读取这个val,child打印出来的val=10,而父val=0,但是发现他们的地址尽然一样!,这也从侧面论证了我们看到的不是实实在在的物理地址,而进程地址空间本质上就是一种虚拟地址。他们的值可以不一样是因为进程间是相互独立的,代码虽然是共享,数据是各自私有的,当发生写的动作时就是写时拷贝

进程地址空间的数据结构:

地址空间是在进程和物理空间之间的一个软件层,通过mm_struct来模拟 让操作系统给每个进程画大饼,让每个进程都认为自己有整个地址,或者说物理内存,这样我们的每个进程就可以根据自己的地址空间来划分自己的代码

地址空间和物理内存之间的关系:

  1. 把进程的代码和数据加载到内存中,并给进程创建进程地址空间
  2. 给每个进程创建一个页表结构,页表构建的就是从进程地址空间出来的虚拟地址到物理地址当中的映射

因此我们就可以说父子进程打印出来的地址可以是完全一样,而值不同

页表有权利去控制某段数据有没有权限写进物理内存,也就是说,地址空间中的字符常量区和代码区给到页表是只读权限的,我们创建的一个指针变量p,在栈中保存,指向的数据在常量区,不能进行修改,报segmentaltion fault

那么问题来了:

为什么不把输入设备的值直接放入物理地址不就完事了吗?还每个进程额外虚拟出来一块地址空间干嘛呢?
如果直接访问物理内存,进程一多就有可能发生指针越界,那么进程的独立性就无法得到保证,因为物理内存暴露,可能有恶意程序直接对内存数据进程篡改或者读取了

有了地址空间的好处:

  1. 保护物理内存,不收到任何进程内的地址的直接访问,方便进行合法性检测
  2. 将内存管理和进程管理进行解构( 比如创建进程,只需在页表中向系统申请内存,当进程释放只需通过页表释放内存(内存管理只需知道哪些内存区域(Page页表)是无效的,哪些是有效的),但如果没有页表进程和物理内存之间是强耦合的
  3. 让每个进程,以同样的方式,来看待代码和数据。想象一下当每个可执行程序都已经在执行前被划分好了一块块区域,通过页表,物理地址可以进行高效访问

创作不易,如果文章对你帮助的话,点赞三连哦:)

以上是关于Linux进程理解的主要内容,如果未能解决你的问题,请参考以下文章

Linux进程理解与学习(Ⅰ)

linux 性能自我学习 ———— 理解平均负载 [一]

Linux---进程管理

Linux之进程管理

Linux进程优先级&前后台理解

Linux优化如何理解“平均负载”