进程是对正在运行中的程序的一个抽象,是具有一定独立功能的程序关于某个数据集合的一次运行活动,它是进行系统资源分配、调度的一个独立单位。每个进程占有虚拟地址和页表。
一、进程的管理
可以用链接方式或者索引方式对PCB(系统为描述和控制进程的运行而为每个进程定义的一个数据结构,记录了操作系统所需关于进程全部的描述和控制信息)进行组织 。链接方式就是把PCB组织成各种队列:就绪队列 阻塞队列等等,索引方式就是用索引表的方式对PCB进行组织:就绪索引表 阻塞索引表等等。在 Linux 中,PCB 是一个名为 task_struct 的结构体,称为任务结构体。记录了比如进程的状态和标志等等信息。
内核空间设置了一个指针数组 task[],该数组的每一个元素指向一个任务结构体,所以 task 数组又称 task 向量。Task 数组的大小决定了系统中容纳进程最大数量,默认值被定义为 512。 task[]数组的第一个指针指向名为 init_task 的结构体,它是系统初始化进程 init 的任务结构体。
为了记录系统中实际存在的进程数,系统定义了一个全局变量 nr_tasks,其值随系统中存在的进程数目而变化。Linux 中把所有进程的任务结构相互连成一个双向循环链表,其首结点就是 init 的任务结构体 init_task。这个双向链表是通过任务结构体中的两个成员指针相互链接而成:
Struct task_struct *next_task;/*指向后一个任务结构体的指针*/
Struct task_struct *prev_task;/*指向前一个任务结构体的指针*/
二、进程的创建
系统初始化(init):
启动操作系统时,通常会创建若干个进程。其中有些是前台进程(numerous processes)
,也就是同用户进行交互并替他们完成工作的进程。进程运行在后台用来处理 e-mail,web 网页,新闻,打印等等称为 守护进程(daemons)
。在 UNIX 中ps
可列出正在运行的进程, Windows 中可以使用任务管理器。
正在运行的程序执行了创建进程的系统调用(比如 fork);用户请求创建一个新进程;初始化一个批处理工作。
在 UNIX 中,用fork来创建一个新的进程。这个调用会创建一个与调用进程相关的副本。在 fork 后,一个父进程和子进程会有相同的内存映像,相同的环境字符串和相同的打开文件。通常子进程会执行 execve
或者一个简单的系统调用来改变内存映像并运行一个新的程序。
三、进程的运行过程
1.从源代码编译到可执行程序
源代码-->预处理-->编译-->优化-->汇编-->链接-->可执行文件
预处理
读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。
编译阶段
确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。.s文件
汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件
链接阶段
链接程序的主要工作就是将有关的目标文件彼此相连接,将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
2.程序从磁盘加载到内存
进程的内核态和用户态
进程在用户态只能访问0~3G,只有进入内核态才能访问3G~4G,进程通过系统调用进入内核态,每个进程虚拟空间的3G~4G部分是相同的。
运行过程中 cpu通过页表找到逻辑地址对应的物理内存地址,执行对应指令,涉及到IO读写、内存分配等硬件资源的操作时,往往不能直接操作,而是通过内核态内存中使用系统调用,然后内核态的CPU执行有关硬件资源操作指令,得到相关的硬件资源后在返回到用户态继续执行。也就是说进程即可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。内核态用户态的意义:
让系统的数据和用户的数据互不干扰,保证系统的稳定性,方便管理;将用户的数据和系统的数据隔离开,就可以对两部分的数据的访问进行控制。确保用户程序不能随便操作系统的数据,防止用户程序误操作或者是恶意破坏系统。
内核态用户态的触发手段
1、发生系统调用时
这是处于用户态的进程主动请求切换到内核态的一种方式。用户态的进程通过系统调用申请使用操作系统提供的系统调用服务例程来处理任务。而系统调用的机制,其核心仍是使用了操作系统为用户特别开发的一个中断机制来实现的,即软中断。
2、产生异常时
当CPU执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行的进程切换到处理此异常的内核相关的程序中,也就是转到了内核态,如缺页异常。
3、外设产生中断时
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作的完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
每一个系统调用函数在内核当中都存在对应的处理函数,一般以sys_开头,这些处理函数作为一个系统调用表形式存在:linux-3.9.4/arch/x86/syscalls/syscall_32.tbl。可以看到上述三种由用户态切换到内核态的情况中,只有系统调用是进程主动请求发生切换的,中断和异常都是被动的。每一个系统调用的函数对应着内核里的一个具体实现,每一个系统函数都有一个相应的数字对应,即系统调用号,这个数字事实上是系统调用函数指针的偏移。
运行一个系统调用时,运行时库通过查找这个表来决定对应的函数代码,即系统调用号,然后存入到寄存器中,通常为eax寄存器,然后当切换到到内核态后,内核根据系统调用号来查找到对应的系统调用处理例程的函数名,从而找到对应的代码入口地址。
用户内存可分为静态分配内存(bss段(未初始化的全局静态变量),数据段(data:已初始化的全局静态变量),代码段),动态分配内存(堆,栈(局部变量,函数参数,指针))。
虚拟内存
虚拟内存是指把磁盘的一部分作为假想内存来使用,是假想的内存(实际上是磁盘)。虚拟内存是计算机系统内存管理的一种技术,它使得应用程序认为它拥有连续可用的内存(完整的地址空间) 。实际上通常被分割成多个物理碎片,还有部分存储在外部磁盘管理器上,必要时进行数据交换。
通过借助虚拟内存,在内存不足时仍然可以运行程序。例如,在只剩5MB内存空间的情况下仍然可以运行10MB的程序。由于CPU只能执行加载到内存中的程序,因此,虚拟内存的空间就需要和内存中的空间进行置换(swep), 然后运行程序。
虚拟内存与内存的交换方式
虛拟内存的方法有分页式和分段式两种。Windows 采用的是分页式。该方式是指在不考虑程序构造的情况下,把运行的程序按照一定大小的页进行分割,井以页为单位进行置换。在分页式中,把磁盘的内容读到内存中称为PageIn ,把内存的内容写入磁盘称为Page Out。Windows 计算机的页大小为4KB,也就是说,需要把应用程序按照4KB的页来进行切分,以页(page)为单位放到磁盘中,然后进行置换。
在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,就是初始化进程控制表中内存相关的链表,即每个进程都有自己独立的4G(32位系统下)内存空间。每个进程的4G内存空间只是虚拟内存空间,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,比如malloc要动态分配内存时,也只是分配了虚拟内存,只是通过分页式或者分段式里面的页表或者段表建立好虚拟内存和磁盘文件之间的映射,是通过mmap来建立映射的。当进程访问某个虚拟地址,去看页表,如果发现对应的数据不在物理内存中,则发生缺页异常。
缺页异常的处理过程,就是把进程需要的数据从磁盘上拷贝到物理内存中,如果内存已经满了,就根据页面置换算法找一个页覆盖,当然如果被覆盖的页曾经被修改过,需要将此页写回磁盘。将数据拷贝到内存以后每次访问内存空间的某个地址,都会把地址翻译为实际物理内存地址 ,所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。
虚拟内存还有请求调入功能和置换功能,
请求调入功能使得一个大程序可以分页载入内存,防止要求的内存空间超过了内存容量不能全部被装入致使该作业无法运行;置换功能可以将内存中暂时没用到的页置换下来防止有大量作业要求运行,但只能将少数作业装入内存让它们先运行,而将其它大量的作业留在外存上等待。
在linux下面有个交换分区swap就是用来置换用的。在安装系统的时候已经建立了 swap 分区。swap 分区通常被称为交换分区,这是一块特殊的硬盘空间,即当实际内存不够用的时候,操作系统会从内存中取出一部分暂时不用的数据,放在交换分区中,从而为当前运行的程序腾出足够的内存空间。就是说当内存不够用时使用 swap 分区来临时顶替。这种方式应用于几乎所有的操作系统中。
使用 swap 交换分区的优点是,通过操作系统的调度,应用程序实际可以使用的内存空间将远远超过系统的物理内存。由于硬盘空间的价格远比 RAM 要低,因此这种方式无疑是经济实惠的。当然频繁地读写硬盘会显著降低操作系统的运行速率,这也是使用 swap 交换分区最大的限制。
3.进程调度到CPU执行
cpu开始调度到某个进程执行,程序装在内存,代码段存的都是机器码,操作系统告诉cpu第一条指令的地址,然后开始执行一系列指令。这些指令可以通过汇编语言解读出来,有这么几类:
1. 把数据从内存加载到寄存器里
2. 对寄存器的数据进行运算, 例如把两个寄存器的数加起来
3. 把寄存器的数据再写到内存里
CPU 是如何执行一条条的指令的呢?几乎所有冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:取指令、指令译码、执行指令、访存取数、结果写回。
CPU 的内部由寄存器、控制器、运算器和时钟四部分组成,各部分之间通过电信号连通。寄存器是中央处理器内的组成部分。它们可以用来暂存指令、数据和地址。可以将其看作是内存的一种。根据种类的不同,一个 CPU 内部会有 20 - 100个寄存器。控制器负责把内存上的指令、数据读入寄存器,并根据指令的结果控制计算机。运算器负责运算从内存中读入寄存器的数据。
程序计数器(Program Counter)是用来存储下一条指令所在单元的地址。程序执行时,PC的初值为程序第一条指令的地址,在顺序执行程序时,控制器首先按程序计数器所指出的指令地址从内存中取出一条指令,然后分析和执行该指令,同时将PC的值加1指向下一条要执行的指令。
函数的调用和返回很重要的两个指令是call 和return 指令,再将函数的入口地址设定到程序计数器之前,call 指令会把调用函数后要执行的指令地址存储在名为栈的主存内。函数处理完毕后,再通过函数的出口来执行return指令。return 指令的功能是把保存在栈中的地址设定到程序计数器。
四、进程的终止
进程在创建之后,可能由于某些原因终止:
正常退出
;错误退出;
严重错误;
被其他进程杀死
参考:《Linux内核设计与实现》3,5,7,8,15