1.进程概念:
进程概念上是程序执行的实例,从内核的观点来看,它是系统资源(CPU时间、内存等)分配的实体。创建进程时,它几乎和父进程相同,它接受父进程地址空间的一个拷贝,并以进程创建系统调用的下一条指令开始执行与父进程相同的代码。尽管父子进程可共享含有程序代码的页,但它们各自有独立的数据拷贝(堆和栈),所以子进程对一个内存单元的修改对父进程是不可见的。
2.轻量级进程:
Linux使用轻量级进程(LWP)对多线程应用程序提供更好地支持,轻量级进程和普通进程区别在于:
①前者没有独立的用户空间(内核态线程无用户空间,用户态线程共享用户空间),普通的进程有独立的内存空间
②前者线程的mm=NULL或与其他线程共享,而进程有独立的mm_struct
关于Linux为何引入轻量级进程的概念可以参考文章:用户线程和内核线程
3.进程描述符:
进程描述符都是task_struct类型结构,它包含一个进程相关的所有信息,其中pid字段用于描述进程标识符。
每个进程都有自己的内核栈,当进程从用户态进入到内核态时,CPU会自动设置该进程的内核栈,内核必须能同时处理很多进程,并把进程描述存放在动态内存中,而非放在永久分配给内核的内存区。内核态的进程访问处于内核数据段的栈,这个栈不同于用户态进程所用的栈,因为内核控制路径使用很少的栈,所以只需要几千字节的内核态堆栈。由于内核堆栈较小,所以不能有太深的嵌套,或使用太多太大的局部变量(用指针)。
内核态的进程堆栈和线程描述符thread_info被紧凑地放在两个连续的页框中,目的是在内核态运行时,能够方便地得到线程描述符的地址。如下图:
4.进程链表:
进程链表用于记录进程之间关系的数据结构,它通过双向链表把所有进程的描述符链接起来。每个task_struct结构包含一个list_head类型的tasks字段,这个类型的prev和next字段分别执行前面和后面的task_struct元素。进程链表的头是init_task描述符,它是所谓的0进程或swapper进程的进程描述符。
为了提高进程调度程序运行速度,Linux内核通过建立多个进程链表的方式,每种进程优先权对应一个不同的链表。顺序扫描进程链表检查pid字段可行但低效,为了加速查找引入了四个(因为进程描述符包含了表示不同类型PID的字段)散列表。如下图:
5.进程切换:
为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换。挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;进程上下文包含了进程执行需要的所有信息如用户地址空间(包括程序代码,数据,用户堆栈等)、控制信息(进程描述符,内核堆栈等)、硬件上下文(注意中断也要保存硬件上下文只保存的方法不同)。本质上,进程切换分两步:
①切换全局目录以安装一个新的地址空间
②切换内核态堆栈(也包括ss和esp这对寄存器的内容—存储用户态堆栈指针的地址)和硬件上下文,由switch_to宏执行
6.进程的创建:
传统的Unix操作系统以统一的方式对待所有进程:子进程复制父进程所拥有的资源。这种方法使得进程创建效率低下,因为子进程需拷贝父进程的整个地址空间。实际上,子进程几乎不必读或修改父进程拥有的所有资源,很多情况下,子进程立即调用execve(),并清除从父进程费力拷贝过来的地址空间。
现代Unix内核通过引入三种不同的机制解决了这个问题:
①写时复制(COW)
写时复制技术允许父子进程读相同的物理页,只要他们有一个想写物理页,内核就把这个页的内容拷贝到一个新的物理页,并把这个物理页分配给正在写的进程。
具体来说,fork创建进程的时候,并没有真正的copy内存,对于fork来讲,如果创建子进程就要内存拷贝的的话,一执行exec,辛辛苦苦拷贝的内存又被完全放弃了。写时拷贝即:fork子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为“只读”,父子进程一直是同一个页面,直到其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。
②轻量级进程
轻量级进程允许父子进程共享每进程在内核的很多数据结构,如页表(也就是整个用户态地址空间)、打开文件表及信号处理。Linux中,轻量级进程是由clone()的函数创建。
③vfork()系统调用创建的进程能共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,直到子进程退出或执行一个新的程序为止。