Linux下的进程1——进程概念,进程切换,上下文切换,虚拟地址空间

Linux 专栏收录该内容
23 篇文章 1 订阅

进程概述

  当一个可执行程序在现代系统上运行时,操作系统会提供一种假象——好像系统上只有这个程序在运行,看上去只有这个程序在使用处理器,主存和IO设备。
  处理器看上去就像在不间断的一条接一条的执行程序中的指令,即改程序的代码和数据是系统存储器中唯一的对象。这些假象是通过进程的概念来实现的。
  进程是操作系统对一个正在运行的程序的一种抽象。在一个系统上可以同时运行多个进程,而每个进程都好像在独占的使用硬件。而并发运行则是说一个进程的指令和另一个进程的指令是交错执行的。
  在大多数系统中,需要运行的进程数是多于可以运行他们的CPU个数的。
  传统系统在一个时刻只能执行一个程序,而现在的多核处理器同时能够执行多个程序。
  无论是在多核还是单核系统中,一个CPU看上去都像是在并发的执行多个进程,这是通过处理器在进程间切换来实现的。
  操作系统实现这种交错执行的机制称为上下文切换。

  操作系统保持跟踪进程运行所需的所有状态信息,这种状态,也就是上下文,它包括许多信息,例如PC和寄存器文件的当前值,以及主存的内容。
  在任何一个时刻,单处理器系统都只能执行一个进程的代码。
  当操作系统决定要把控制权从当前进程转移到某个新进程时,就会进行上下文切换即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从上次停止的地方开始

  深入计算机系统一书中对上下文切换的表达如下图:
  这里写图片描述

  如果现在有两个并发的进程:外壳进程和hello进程。
  开始只有外壳进程在运行,即等待命令行上的输入,当我们让他运行hello程序时,外壳通过调用一个专门的函数,即系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。
  操作系统保存外壳进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传递给新的hello进程。
  hello进程终止后,操作系统恢复外壳进程的上下文,并将控制权传回给他,外壳进程将继续等待下一个命令行输入。

  这里很重要的一个思想是:一个进程是某种类型的一个活动,他有程序,输入,输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程, 并转而为另一个进程提供服务。
  

进程

   进程的经典定义就是一个执行中的程序的实例。
  系统中的每个程序都是运行在某个进程的上下文中的。
  上下文是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,他的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。

  每次用户通过向外壳输入一个可执行目标文件的名字,并运行一个程序时,外壳就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。
  应用程序也能够创建新进程,且在这个新进程的上下文中运行他们自己的代码或其他应用程序。
  进程提供给应用程序的关键抽象:
  

  • 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器
  • 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用存储器系统

      而实际上,进程是轮流使用处理器的。
      每个进程执行他的流的一部分,然后被抢占,即暂时挂起,然后轮到其他进程。
      对于一个运行在这些进程之一的上下文中的程序,他看上去就像是在独占的使用处理器。

用户模式和内核模式

  为了使操作系用内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
  处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。
  当设置了模式位时,进程就运行在内核模式中,即超级用户模式。
  一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器位置。
  没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,比如停止处理器,改变模式位,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。
  运行应用程序代码的进程初始时是在用户模式中的。
  进程从用户模式变为内核模式的唯一方法是通过诸如中断,故障或者陷入系统调用这样的异常,当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。
  处理程序运行在内核模式中,当他返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

上下文切换

  操作系统内核使用一种称为上下文切换的较高层形式的控制流来实现多任务。
  内核为每一个进程维持一个上下文。
  上下文就是内核重新启动一个被抢占的进程所需的状态。他有一些对象的值组成,这些对象包括:  

  • 通用目的寄存器
  • 浮点寄存器
  • 程序计数器
  • 用户栈
  • 状态寄存器
  • 内核栈
  • 各种内核数据结构

      内核数据结构,比如描绘地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
      在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。
      这种决定就叫做调度,是由内核中称为调度器的代码处理的。
      当内核选择一个新的进程运行时,我们就说内核调度了这个进程。在内核调度了一个新的进程运行后,他就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。
    上下文切换 

  • 保存当前进程的上下文
  • 恢复某个先前被抢占的进程被保存的上下文
  • 将控制传递给这个新恢复的进程

      当内核代表用户执行系统调用时,可能会发生上下文切换,如果系统调用因为等待某个时间发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。
      比如:如果一个read系统调用请求一个磁盘访问,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。
      另一个例子是sleep系统调用,它显式的请求让调用进程休眠,一般而言,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而不是将控制返回给调用进程。
      中断也可能引发上下文切换。

      比如,所有的系统都有某种产生周期性定时器中断的机制,典型的为每一毫秒或每十毫秒,每次发生定时器中断时,内核就能判定当前进程已经运行了足够长时间了,并切换到一个新的进程。
      上下文简单说来就是一个环境,相对于进程而言,就是进程执行时的环境

      具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。
      一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。

用户级上下文: 正文、数据、用户堆栈以及共享存储区;
寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈

  当发生进程调度时,进行进程切换就是上下文切换(context switch).操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。

  而系统调用进行的是模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。
  进程上下文主要是异常处理程序和内核线程。
  内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。所以在进程上下文中引用current是有意义的。

  内核进入中断上下文是因为中断信号而导致的中断处理或软中断。而中断信号的发生是随机的,中断处理程序及软中断并不能事先预测发生中断时当前运行的是哪个进程,所以在中断上下文中引用current是可以的,但没有意义。
  事实上,对于A进程希望等待的中断信号,可能在B进程执行期间发生。例如,A进程启动写磁盘操作,A进程睡眠后B进程在运行,当磁盘写完后磁盘中断信号打断的是B进程,在中断处理时会唤醒A进程。

  内核可以处于两种上下文:进程上下文和中断上下文。

  在系统调用之后,用户应用程序进入内核空间,此后内核空间针对用户空间相应进程的代表就运行于进程上下文。异步发生的中断会引发中断处理程序被调用,中断处理程序就运行于中断上下文
  中断上下文和进程上下文不可能同时发生。

  运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。
  因此,内核会限制中断上下文的工作,不允许其执行如下操作:

  • (1)进入睡眠状态或主动放弃CPU;

      由于中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程),所以中断上下文一旦睡眠或者放弃CPU,将无法被唤醒。所以也叫原子上下文(atomic context)。

  • (2) 占用互斥体

      为了保护中断句柄临界区资源,不能使用mutexes。如果获得不到信号量,代码就会睡眠,会产生和上面相同的情况,如果必须使用锁,则使用spinlock。

  • (3) 执行耗时的任务;

      中断处理应该尽可能快,因为内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。在中断处理例程中执行耗时任务时,应该交由中断处理例程底半部来处理。

  • (4) 访问用户空间虚拟内存。

  因为中断上下文是和特定进程无关的,它是内核代表硬件运行在内核空间,所以在中断上下文无法访问用户空间的虚拟地址

  • (5) 中断处理例程不应该设置成reentrant(可被并行或递归调用的例程)。

    因为中断发生时,preempt和irq都被disable,直到中断返回。所以中断上下文和进程上下文不一样,中断处理例程的不同实例,是不允许在SMP上并发运行的。

  • (6)中断处理例程可以被更高级别的IRQ中断。

      如果想禁止这种中断,可以将中断处理例程定义成快速处理例程,相当于告诉CPU,该例程运行时,禁止本地CPU上所有中断请求。这直接导致的结果是,由于其他中断被延迟响应,系统性能下降。

虚拟存储器——私有地址空间

  虚拟存储器是一个抽象的概念,他为每一个进程提供了一个假象,即每个进程都在独占地使用主存。
  每个进程看到的是一致的存储器,成为虚拟地址空间。
  在一台有n位地址空间的机器上,地址空间是2的n次方个可能地址的集合,0,1,…,2^n - 1。
  一个进程为每个程序提供他自己的私有地址空间,一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或者写的,从这个意义上来说,这个地址空间是私有的。
  下图是进程的虚拟地址空间图示。
  这里写图片描述
  

  地址空间顶部是保留给内核的,这个部分包含内核在代表进程执行指令时使用的代码,数据和栈,这对所有进程来说都是一样的。地址空间的底部区域是保留给用户程序的,包括通常的文本,数据,堆和栈段。
  图中的地址是从下往上增大的

  每个进程看到的虚拟地址空间由大量准确定义的区构成,每个区都有专门的功能。
  1.程序代码和数据
  对于多有的进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置,代码和数据区是直接按照可执行目标文件的内容初始化的。
  2.堆
  代码和数据区后紧随着的是运行时堆。
  代码和数据区是在进程已开始运行时就被规定了大小,与此不同,当调用malloc和free这样的C标准库函数时,堆可以在运行时动态的扩展和收缩。
  3.共享库
  在地址空间的中间部分是一块用来存放像C标准库和数学库这样共享库的代码和数据的区域。
  4.栈
  位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态的扩展和收缩,特别是我们每次调用一个函数时,栈就会增长,从一个函数返回时,站就会收缩。
  5.内核虚拟存储器
  内核总是驻留在内存中,是操作系统的一部分。
  地址空间顶部的区域是为内核保留的,不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。

  一个系统中的进程是与其他进程共享CPU和主存资源的。
  然而,共享主存会导致内存不够用的情况。
  为了有效的管理存储器并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟存储器(VM)。
  虚拟存储器是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,他为每个进程提供了一个大的,一致的和私有的地址空间。
  虚拟存储器提供了三个重要的能力:  

  • 1.它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效的使用了主存
  • 2.他为每个进程提供了一致的地址空间,从而简化了存储器管理
  • 3.他保护了每个进程的地址空间不被其他进程破坏

物理地址

  物理地址(Physical Address) 是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。

  如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。
  计算机系统的主存被组织成一个有M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址。
  第一个字节的地址为0,接下来的字节地址为1,再下一个为2.以此类推。给定这种简单的结构,CPU访问存储器的最自然的方式就是使用物理地址。
  我们把这种方式叫做物理寻址。
  当CPU执行加载指令时,他会生成一个有效物理地址,通过存储器总线,把他传递给主存。主存取出从物理地址指定处开始的指定字节,并将它返回给CPU,CPU会将它存放在一个寄存器里。

虚拟地址

  使用虚拟寻址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。
  将一个虚拟地址转换为物理地址的任务叫做地址翻译。

  CPU芯片上叫做存储器管理单元的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理的。
  
  在一个带虚拟存储器的系统中,CPU从一个有N=2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。

  一个地址空间的大小是由表示最大地址所需要的位数来描述的。
  例如:一个包含N=2^n个地址的虚拟地址空间就叫做一个n为地址空间。

逻辑地址

  逻辑地址(Logical Address) 是指由程序产生的与段相关的偏移地址部分。

  例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。
  应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。

线性地址

  线性地址(Linear Address) 是逻辑地址到物理地址变换之间的中间层。

  程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。 
  如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。

进程的隔离

  地址空间分两种: 

  • 虚拟地址空间
  • 物理地址空间
      物理地址空间是实实在在存在的,存在于计算机中,而且对于每一台计算机来说只有唯一的一个,可以把物理空间想象成物理内存。
      虚拟地址空间是虚拟的,人们想象出来的地址空间,其实他就不存在,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间,这样就做到了进程的隔离。

分页

  分页的基本方法是把地址空间人为地等分成固定大小的页,每一页的大小有硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。
  对整个系统来说,页就是固定大小的。
  当我们把进程的虚拟地址空间按页分割,把常用的数据和代码页装载到内存中,把不常用的代码和数据保存在磁盘里,当需要用到的时候再把它从磁盘里取出来即可。
  这里写图片描述
  如上图所示:
  假设有两个进程Process1和Process2,他们进程中的部分虚拟页面被映射到了物理页面。
  比如VP0.VP1和VP7映射到PP0,PP2和PP3,而有部分页面却在磁盘中,比如VP2和VP3位于磁盘的DP0,DP1中,另外还有一些页面如VP4,VP5和VP6可能尚未被用到或访问到,他们暂时处于未使用状态。
  我们把虚拟空间的页就叫做虚拟页VP,把物理内存中的页叫做物理页PP,把磁盘中的页叫做磁盘页DP。
  我们可以看到虚拟空间的有些页被映射到同一物理页,这样就可以实现内存共享。
  虚拟存储的实现需要依靠硬件的支持,对于不同的CPU来说是不同的,但是几乎所有的硬件都采用一个叫做MMU的部件来进行页映射。
  在页映射模式下,CPU发出的是虚拟地址,即我们程序看到的是虚拟地址,经过MMU转换以后就变成了物理地址。

Linux虚拟存储器系统

  Linux为每个进程维持了一个单独的虚拟地址空间
  这里写图片描述
  

共享区域

  如果虚拟存储器系统可以集成到传统的文件系统中,那么就能提供一种简单而高效的把程序和数据加载到存储器中的方法。
  在前面已经提到,进程这一抽象能够为每个进程提供自己私有的虚拟地址空间,可以面授其他进程的错误读写。
  不过,许多进程有同样的制度文本区域。
  例如,每个运行Unix外壳程序tcsh的进程都有相同的文本区域,而且,许多程序需要访问只读运行时库代码相同的拷贝。
  例如,每个C程序都需要来自标准C库的,像printf这样的函数。
  那么如果每个进程都在物理存储器中保持这些常用代码的复制拷贝,那就是极端的浪费了。
  其实,存储器映射给我们提供了一种清晰的机制,用来控制多个进程如何共享对象。
  一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,要么作为私有对象。
  如果一个进程将一个共享对象映射到他的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于哪些也把这个共享对象映射到他们虚拟存储器的其他进程而言也是可见的。
  而且,这些变化也会反映在磁盘上的原始对象中。

  另一方面,对一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反映在磁盘上的对象中。
  一个映射到共享对象的虚拟存储器区域叫做共享区域,相反的叫做私有区域。

  假设进程1将一个共享对象映射到他的虚拟存储器的一个区域中,现在假设进程2将同一个共享对象映射到他的地址空间,并不一定要和进程1在相同的虚拟地址处。
  这里写图片描述
  因为每一个对象都有一个唯一的文件名,内核可以迅速的判定进程1已经映射了这个对象,而且可以使进程2中的页表条目指向相应的物理页面。
  关键点在于即使对象被映射到了多个共享区域,物理存储器中也只需要存放共享对象的一个拷贝。

写时拷贝技术

  私有对象是使用一种叫做写时拷贝的巧妙技术被映射到虚拟存储器中的。
  一个私有对象开始生命周期的方式基本上与共享对象的一样,在物理存储器中只保存有私有对象的一份拷贝。
  这里写图片描述
  上图中,两个进程将一个私有对象映射到他们虚拟存储器的不同区域,但是共享这个对象的同一份物理拷贝。
  那么怎样体现是私有的呢?
  对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时拷贝。
  只要没有进程试图写它自己的私有区域,他们就可以继续共享物理存储器中对象的一个单独拷贝。
  然而,只要有一个进程师试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障。
  当故障处理程序注意到保护异常是由于进程试图写私有的写时拷贝区域中的一个页面而引起的,他就会在物理存储器中创建这个页面的一份新拷贝,更新页表条目指向这个新拷贝,然后恢复这个页面的可写权限,当故障处理程序返回时,CPU重新执行这个写操作,现在在新创建的页面上这个写操作就可以执行了。
  这就是进程的写时拷贝技术。

评论 5 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页

打赏作者

长着胡萝卜须的栗子

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值