第三章
(由于linux不区分进程和线程,所以它们在linux中被称为task,也叫任务)
总结:本章主要包括进程以及线程的概念和定义,Linux内核如何管理每个进程,他们在内核中如何被列举,如何创建,最终如何消亡。操作系统存在的意义在于运行用户程序,进程管理是所有操作系统的心脏所在。
3.1进程
进程是处于执行期的程序,是正在执行的程序代码的实时结果。但不仅局限于一段可执行的代码,还包括其他资源(打开的文件,挂起的信号,内核内部数据,处理器的状态,一个或者多个内存映射的内存地址空间,一个或者多个执行线程,存放全局变量的数据段)
线程
执行线程,即为线程,进程中活动的对象,是内核调度的对象。(内核并不是调度进程哟~)每一个线程都拥有一个独立的计数器,进程栈和一组进程寄存器。
进程提供的两种虚拟机制:
虚拟处理器:给进程一种自己在独享处理器的假象
虚拟内存:让进程在分配和管理内存空间的时候觉得自己拥有整个系统的所有内存资源
程序
进程处理执行期的程序以及相关资源的总称。
进程的整个周期
从它被创建的时刻开始存活(一般是fork()系统调用创建),接着调用exec()创建新的地址空间,并把新的程序载入,最终程序通过exit()系统调用退出执行。进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或者waitpid()为止。
3.2进程描述符及任务结构
进程描述符的结构:
task_struct,定义在<linux/sched.h>文件中。任务队列的双向循环链表(内核存放进程列表的地方),每一项都是task_sturct.它包含一个具体的进程的所有信息(打开的文件,进程的地址空间,挂起的信号,进程的状态等)
3.2.1分配进程描述符
Linux通过slab分配器分配task_struct结构。只需要在栈底(对于向下增长的栈)或者栈顶(向上增长的栈)创建新的struct thread_info来动态生成task_struct结构。
每个任务的thread_info在它的内核栈的尾端分配。结构中的task域存放指向该任务的实际task_struct指针。
3.2.2进程描述符的存放
内核通过唯一的pid进程标识符(其类型是pid_t,但实际上是int)来标识每一个进程。内核把pid放在她们各自的进程描述符中。它的默认值为32768(short int的 max),这个值表示系统中允许同时存在的进程的最大数目。(可以通过/proc/sys/kernel/pid_max来提高上限)
访问任务需要获得task_struct指针,通过current宏查找进程描述符的速度很重要。X86内核栈的尾端创建thread_info结构,通过偏移间接查找。栈大小(8KB)需要屏蔽掉后面13位(8192),4kb屏蔽12位(4096)
汇编代码:
图
返回:
图
X8的寄存器太少了才这样做,实际上访问进程描述符是一个很频繁的操作,所以寄存器多一些的操作系统,都会用一个专门的寄存器来保存task_struct。
3.2.3进程状态
进程描述符state描述进程当前的状态:
TASK_RUNNING运行
TASK_INTERRUPTIBLE可中断(阻塞)
TASK_UNINTERRUPTIBLE不可中断
_TASK_TRACED被其他进程跟踪
_TASK_STOPPPED停止
图3
3.2.4设置当前进程状态
使用set_current_stste(state)或者set_task_state(current,state)函数。
图4
等价于
图5
3.2.5进程上下文
可执行代码从一个可执行文件载入到进程地址空间执行。当它陷入内核时,内核“代表进程执行”并处于在进程上下文中,在此上下文中,current宏是有效的。
3.2.6进程家族树
进程之间有明显的继承关系。Linux系统中所有进程都是pid=1的进程的后代。(pid=1的进程是操作系统启动的最后阶段启动的init进程)
进程间的继承关系存放在进程描述符中。(task_struct结构)
获得父进程的进程描述符
图6
获得子进程的进程描述符
图7
因为有这样的继承关系,我们从任意一个进程去访问任务列表中其他所有进程。欣慰任务队列本身又是一个双向链表,所以其实是很容易实现的。
获取下一个进程:
图8
获取上一个进程
图9
遍历整个进程队列(没有必要不要遍历,因为代价很大)
图10
3.3进程创建
分成三步:1.在新的地址空间里创建进程2.读入可执行文件3.开始执行
3.3.1写时拷贝
写时拷贝是一种让子父进程共享同一个拷贝的技术。
相当于资源只有在需要写入的时候才会被复制,在此之前都是以只读的方式共享。
3.3.2fork()
do_fork()完成了创建进程的大部分工作,其中copy_process完成的工作:
1) 调用dup_task_struct()为新进程创建一个内核栈、thread_info、task_sturct这些值与当前进程同步
2) 检查用户所拥有的进程数目是否超出限制
3) 子进程开始把自己与父进程区分
4) 将子进程状态设置为不可中断
5) 调用copy_flags更新flag成员
6) 调用alloc_pid()为新进程分配一个有效的pid
7) 传递参数标识
8) 返回指向子进程的指针
3.3.3vfork()
除了不拷贝页表以外,和fork是一样的。
图11
3.4线程在linux中实现
线程被linux抽象成耗费比较少资源,运行迅速执行的单元。
3.4.1创建线程
和创建进程类似,但是在调用clone()需要传递一些标志性参数
图12
参数的含义:
图13
3.4.2内核线程
内核的后台任务一般通过内核线程执行,内核线程没有独立的地址空间只能运行在内核,内核线程启动以后就一直运行知道调用do_ex()退出,或者内核其他部分调用thread_stop()退出。
3.5进程终结
终结时要释放资源并告知父进程。大部分任务靠do_exit()来完成。
图14
3.5.1删除进程描述符
调用do_exit()以后,线程僵死但是保留了它的进程描述符。父进程获得已终结的子进程信息,或者它通知内核它不关注那些信息以后,子进程的进程描述符才被释放。
Wait()是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的pid。
Release_task是最终执行释放进程描述符放入函数:
图15
3.5.2孤儿进程造成的进退维谷
如果父进程在子进程之前退出,需为子进程找到一个新的父亲。如果找不到,就让init作为它们的父进程
3.6小结
总结:
本章主要包括进程以及线程的概念和定义,Linux内核如何管理每个进程,他们在内核中如何被列举,如何创建,最终如何消亡。操作系统存在的意义在于运行用户程序,进程管理是所有操作系统的心脏所在。
linux及安全《Linux内核设计与实现》第三章——20135227黄晓妍
原文:http://www.cnblogs.com/angelahxy/p/5389078.html