0%

计算机是如何执行任务的(5个发展阶段)

2020年5月2日 下午10:24

注:这篇文章题目很大,但我主要的目的是为了在学习linux系统的过程中串联一下知识,尤其是linux进程和内存相关的知识。从题目本身说更应该从计算机组成原理的角度去整理,如果从这个角度出发,那么推荐阅读:

  1. 性能之殇(一)— 天才冯·诺依曼与冯·诺依曼瓶颈 - 岁寒
  2. 极客时间的计算机组成原理课程

前提知识:

第一个阶段:只有cpu,没有内存

  1. 穿孔纸带

第二个阶段:cpu + 内存

  1. 有了内存有什么好处?

第三个阶段:cpu + 分段内存

  1. 下面分出这么多点,关键有两点:
    1. 函数调用的过程:栈帧是如何一步步的走的?函数调用过程的理解是理解线程调度的基础。
    2. linux的虚拟内存设计:要理解这么设计的原因,不要死背
  2. 为什么分段,有什么好处
    1. 进程运行过程中,代码指令根据流程依次执行,只需访问一次(当然跳转和递归可能使代码执行多次);而数据(数据段和BSS段)通常需要访问多次,因此单独开辟空间以方便访问和节约空间。具体解释如下:
    2. 防止程序指令被有意或无意地改写:
      1. 当程序被装载后,数据和指令分别映射到两个虚存区域。数据区对于进程而言可读写,而指令区对于进程只读。两区的权限可分别设置为可读写和只读。以防止程序指令被有意或无意地改写。
    3. 提高CPU缓存命中率:
      1. 现代CPU具有极为强大的缓存(Cache)体系,程序必须尽量提高缓存命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU一般数据缓存和指令缓存分离,故程序的指令和数据分开存放有利于提高CPU缓存命中率。
    4. 方便共享:
      1. 当系统中运行多个该程序的副本时,其指令相同,故内存中只须保存一份该程序的指令部分。若系统中运行数百进程,通过共享指令将节省大量空间(尤其对于有动态链接的系统)。其他只读数据如程序里的图标、图片、文本等资源也可共享。而每个副本进程的数据区域不同,它们是进程私有的。
    5. 可以区分变量的声明周期:
      1. 此外,临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。全局数据和静态数据可能在整个程序执行过程中都需要访问,因此单独存储管理。堆区由用户自由分配,以便管理。
  3. linux的虚拟地址空间布局:
    1. Linux虚拟地址空间布局 - clover_toeic - 博客园
  4. 从存储的角度去理解函数调用栈:
    1. C语言函数调用栈(一) - clover_toeic - 博客园
    2. 函数调用过程中最关键的三个寄存器指针是:
      1. IP(Instruction Point) :指向处理器下一条等待执行的指令地址
      2. BP(Base Point) :栈帧基地址指针寄存器,存放执行函数对应栈帧的栈底地址,用于c运行库访问栈中的局部变量和参数。
      3. SP(Stack Point):堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址,且始终指向栈顶
    3. 为什么说这三个寄存器指针是关键呢?
      1. cpu其实就是一个傻子,你给他下达一个指令他动一下,他没有积极主动向性,主动的去找活干。对待这样的人你可倒了霉了,你做好饭叫他吃饭他都不会,必须从张嘴开始给他下达命令,这个命令就是桐楠格IP给他下达的。
      2. 关于BP和SP可以分为两个场景:
        1. 当前函数运行的时候,我们需要将局部变量保存在栈中,这是linux内存模型规定的,这个过程中我们需要使用BP、SP来保存当前函数使用栈的哪段位置区间,分为头和尾。
        2. 在函数调用的过程中,我们需要将调用函数的BP、SP保存起来在被调用函数的栈中。为什么不保存在堆中?因为没有必要,我们知道函数调用的过程是短暂的,并且函数调用可能会非常多,每次调用完之后,如果不释放的话就全是垃圾,而是用栈就能很好的做到自动释放。
      3. 函数调用的过程关键是上下文信息的保存,这里的上下文包括当前函数执行到了那里IP,使用了哪些局部变量local variable,栈帧BP/SP。当我们进行函数调用,切换到另外一个函数的时候,由于cpu寄存器是只有唯一的一份,所有的函数都使用这一个,那么必然会引起冲突,我们就需要将冲突的寄存器数据保存起来
  5. 如何实际的函数调用过程中栈使用情况:
    1. 反汇编
    2. gdb单步调试
    3. 直接运行程序print出来
    4. 缓冲区溢出详解 - clover_toeic - 博客园
  6. 函数调用栈的人为干扰(造成溢出是其中一种)方法原理:
    1. 缓冲区的定义:
      1. 缓冲区是一块连续的计算机内存区域,可保存相同数据类型的多个实例。缓冲区可以是堆栈(自动变量)、堆(动态内存)和静态数据区(全局或静态)。在C_C++语言中,通常使用字符数组和malloc_new之类内存分配函数实现缓冲区。
    2. 溢出的定义:
      1. 溢出指数据被添加到分配给该缓冲区的内存块之外。缓冲区溢出是最常见的程序缺陷。
    3. 从栈帧的角度解释原理:
      1. 栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持,但由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此也给系统安全带来隐患。若将函数返回地址修改为指向一段精心安排的恶意代码,则可达到危害系统安全的目的。此外,堆栈的正确恢复依赖于压栈的EBP值的正确性,但EBP域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改EBP值,则程序的行为将变得非常危险。
    4. 语言层面的原因:
      1. 由于C/C++语言没有数组越界检查机制,当向局部数组缓冲区里写入的数据超过为其分配的大小时,就会发生缓冲区溢出。攻击者可利用缓冲区溢出来窜改进程运行时栈,从而改变程序正常流向,轻则导致程序崩溃,重则系统特权被窃取。
    5. 缓冲区溢出详解 - clover_toeic - 博客园

第四个阶段:cpu + 分段内存 + 进程调度

  1. 进程的调度与函数之间的调用有什么区别?
    1. 从执行阶段进行比较:
      1. 一个进程中包含很多的函数调用来完成一个进程的任务,也就是函数调用是进程的子集。从这个角度来说,进程和函数是不应该进行比较的,他们是两中不同体量的任务。
    2. 调用和调度的区别:
      1. 函数调用的过程中,比如A调用B,这个调用是在A函数中在代码中就指明确定的;而进程的调度,是在进程发生主动调度、被动调度之后,由linux内核中的调度类进行选择下一个需要执行的进程。注意进程中多的这个调度类,我认为这就是函数调用和进程调度之间的区别:一个是调用,一个是调度,从文字上也能很好的解释。
    3. 可以进行调用、调度的背后机制:
      1. 多进程任务的完成依靠的是调度类进行管理,而函数调用任务的完成依靠的是内存的栈帧结构的支持。栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持

第五个阶段:cpu + 分段内存 + 进程调度 + 线程

  1. 有了进程的调度,我们就已经可以完成进程的并发执行、中断、进程的状态的转换等等复杂的功能,为什么我们还需要设计一个线程呢?
    1. 这里就需要提到老生常谈一句话:进程是linux资源的分配单位,线程是cpu调度和执行的基本单位。
    2. 上面提到的这个概念其实就和范式编程中提到的logic、control、data的分离,思想上是一致的。专门的这样分离式的设计更加符合我们人类的抽象思维。通过这样的设计我们能够让上层程序员强调资源和执行的概念区别,也提示开发人员在进行开发的时候,可以从这两个角度进行思考
    3. 同时,也类似于分段内存的设计一样,这样的设计也不仅仅有利于人类思维,对于当我们多个任务需要相同的资源的时候,这种底层的支持就显得至关重要,自然的降低了重复资源的拷贝,让操作系统也容易的整理自己的资源。
  2. 解释了为什么我们需要线程,那么在linux内核中线程是如何在进程的基础上实现的呢?
    1. 先说一下我的结论:线程的创建过程是进程这个任务执行出来的!一般来说,进程对程序员来说是完成它规定的业务功能的,而我结论的意思是:进程这个task生成了另一个task,也可以说这是task生成task是一个特殊的业务功能。
      1. 这其中有一点需要强调一下,这里所谓的进程task其实也是线程task,因为创建一个进程之后,这个进行本身就是一个线程,或者说这个进程中本身就包含一个线程来执行任务
      2. 从linux内核的角度看来,线程和进程都会变成task
    2. 说完结论,我说为什么我会有这样的认识:
      1. 首先,我们需要知道在linux内核中给进程创建新的线程,这个过程中最最重要的就是线程栈,因为线程栈是线程执行过程底层实现核心机制。在进程的创建过程中,注意这里是进程,不是线程,我们根据linux虚拟内存模型的知识可以知道其中有栈内存段来控制进程(唯一的线程)的执行过程,那么新来的线程他的线程栈如何分配呢?栈这个东西应该是不可以多个线程公用的,所以一个合理的位置就是内存模型中的堆段,堆段在linux2.6中有将近2.9G的空间,而栈区默认只有8M。
      2. 另外,老师曾经说过:完成线程的创建是用户态和内核态共同完成的。我认为的结论正好可以解释这样的行为,进程的在执行的过程中就需要用户态和内核态共同完成功能,只不过完成的功能是创建另个task而已。
  3. 进程,线程与多核,多cpu之间的关系 - 南宫饱虎 - 博客园
  4. 内核栈和用户栈_运维_qq_41727218的博客-CSDN博客