课本第三章读书笔记
进程管理
进程是unix操作系统抽象概念中的最基本的一种
- 进程的定义及相关的概念
- 讨论Linux内核如何管理每个进程
3.1 进程
进程
是处于执行期的程序,(以及相关资源的总称)不仅仅局限于一段可执行的代码,通常还要包括其他资源。实际上进程就是正在执行的程序代码的实时结果。内核需要有效而透明的管理所以细节
线程
是在进程中活动的对象,每个线程都拥有一个独立的程序计数器,进程栈和一组进程计数器。内核调度的对象是线程,而不是进程
进程提供的两种虚拟机制
虚拟处理器:实际上有多个进程分享一个处理器,但虚拟处理器给进程一种假象,让进程觉得自己在独享处理器
虚拟内存:让进程在分配和管理内存是觉得自己拥有整个系统所有内存资源
实际上完全可能存在两个或多个进程执行的是同一个程序,并且两个或多个并存的进程还可以共享许多资源。进程在创建的时候存活调用fork()
fork()
复制一个现有进程来创建一个全新进程。
调用的进程称为父进程,新产生的进程称为子进程
fork()进程从内核返回两次:一次回到父进程,另一次回到子进程
实际是由clone()系统调用实现的
创建的新进程都是为了立即执行新的,不同的程序。而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。
程序通过exit()系统调用退出执行。终结进程并释放其占用的资源
wait4():查询子进程是否终结
进程退出执行后备设置成为僵死状态,直到他的父进程调用wait()或waitpid()为止
3.2 进程描述符及任务结构
内核把进程的列表存放在叫做任务队列的双向循环链表中。链表的每一项都是类型为task_struct、称为进程描述符的结构。进程描述符中包含了一个具体进程的所有信息:
他打开的文件、进程的地址空间、挂起的信号、进程的状态、以及其他的一些信息
3.2.1 分配进程描述符
Linux通过slab分配器分配task _ struct结构,这样能达到对象复用和缓存着色的目的。slab分配器动态生成task _ struct结构,所以只需在栈底(向下增长)或栈顶(向上增长)创建一个新的结构struct thread _ info
每个任务的thread _ info结构在他的内核栈的尾端。结构中task域存放的是指向该任务实际的task _ struct的指针
3.2.2 进程描述符的存放
内核通过一个唯一的进程标识值或PID来标识每个进程。内核把每个进程的PID存放的各自的进程描述符中。
PID是一个是,表示为pid_t隐含类型,实际上就是提高int类型
PID的默认最大值设置为32768,即系统允许同时存在的进程的最大数目。可以通过
/proc/sys/kernel/pid_max来提高上限
内核中访问任务通常需要获得指向其的task_struct指针。通过current宏查找到当前正在运行的进程描述符的速度就显得尤为重要。
x86体系只能在内核栈的尾端创建thread_info结构,通过计算偏移间接查找task结构
movl $-8192,%eax
andl %esp,%eax
current_thread_info()->task //从thread_info域中提取并返回task_struct的地址
3.2.3 进程状态
进程描述符state域描述了进程的当前状态
- TASK_RUNNING(运行):进程是可执行的、正在执行的、等待执行的
- TASK_INTERRUPTABLE(可中断):正在睡眠(阻塞)等待触发条件达成
- TASK_UNINTERRUPTABLE(不可中断):除了就算收到触发信号也不会被唤醒之外与可打断状态一样
- TASK_TRACED:被其他进程跟踪的进程
- TASSK_STOPPED(停止):停止执行、进程没有投入运行也不能投入运行
3.2.4 设置当前进程的状态
set _ task_state(task,state) //将任务task的状态设置为state
等价于task-> = state
3.2.5 进程上下文
系统调用和异常处理是对内核明确定的接口
3.2.6 进程家族树
进程之间存在明显的继承关系。系统中每个进程必有一个父进程,相应的每个进程也可以拥有0个或多个子进程
进程之间的关系存放在进程描述符中。包含一个指向父进程task_state叫做parent的指针,还包括一个称为children的子进程链表
获得父进程的进程描述符:
struct task _ struct *my_parent = current->parent;
访问子进程:
struct task_struct *task;
struct list_head *list;
list_for_each(list ¤t->children){
task = list_entry(list,struck task_struct,sibling); //task指向当前的某个子进程
}
依次访问进程队列:
struct task_struct *task;
for_each_process(task){
printk("%s[%d]\n",task->comm,task->pid);
//打印出每一个任务的名称和PID
}
3.3 进程创建
Unix独特的进程创建方式:
- fork():拷贝当前进程创建一个子进程
- exec():读取可执行文件并将其载入地址空间开始执行
3.3.1 写时拷贝
是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个内存空间,而是让父进程好和子进程共享一个拷贝。只有在需要的时候数据才会被复制
fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。一般情况下,进程在创建后会立即执行一个可执行文件
3.3.2 fork( )
Linux通过clone()系统调用实现fork()
fork()——>clone()——do_fork()——>copy _process()
copy_process的工作过程:
1、调用dup_task_struct()为进程创建一个内核栈、thread_info结构等与当前进程的值相同
2、检查并确保创建这个进程后系统进程数不会超过限制
3、子进程着手使自己与父进程区别开来:许多描述符被清零或初始化
4、子进程的状态被设置为不可中断模式以保证不会投入运行
5、copy_process调用copy_flags()以更新task_struct的flags成员
6、调用alloc_pid()为新进程分配一个有效的PID
7、根据传递给clone()的参数标志,copy_process拷贝或共享打开的文件等
8、copy_process()做扫尾工作并返回一个指向子进程的指针
3.3.3 vfork()
- 除了不拷贝父进程的页表项外,vfork()和fork()的功能相同
3.4 线程在Linux中的实现
线程机制提供了在同一程序内共享内存地址空间、打开的文件和一些其他资源的一组线程
在Linux中所以的线程都当做进程来处理,线程被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的task_struct
3.4.1 创建线程
线程的创建于进程的创建类似,只不过在调用clone()的时候需要传递一些参数来指明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0);
3.4.2 内核线程
独立运行在内核空间的标准进程称为内核线程。他与普通进程的区别在于内核线程没有独立的地址空间。内核线程和普通进程一样可以被调度或抢占。
内核线程也只能由其他内核线程创建
从现有内核线程中创建一个新的内核线程:
新的任务kthread内核进程通过clone()系统调用创建的
创建一个新的进程并让他运行起来
3.5 进程终结
当一个进程终结时,内核必须释放他所占有的资源并把这一不幸告知其父进程。
进程的终结大部分都要靠do_exit()函数来完成:
3.5.1 删除进程描述符
调用了do_exit()之后进程线程已经僵死不能运行,但是系统还是保留了他的进程描述符(在子进程终结后仍有可能获得他的信息)
只有在父进程获得已终结的子进程的信息后或通知内核他并不关注那些消息之后子进程的状态描述符再回别释放。
系统通过调用release_task ()释放需要释放的进程描述符
3.5.2 孤儿进程造成进退维谷
父进程在子进程之前退出,成为孤儿的进程就会在哎退出时永远处于僵死状态。所以必须保证子进程能找到一个新的父进程(当前进程组中的一个线程或者init)
一旦进程成功的找到和设置了新的父进程,就不会再有出现驻留僵死进程的危险了
init进程会例行调用wait()来检查其子进程清楚所有与其相关的僵死进程
总结
- 如何存放和表示进程:用task _struct和thread _info
- 如何创建进程:通过fork(),实际上是通过clone()
- 如何把新的执行映象装入到地址空间:通过exec()系统调用族
- 如何表示进程层次关系父进程又是如何收集其后代的信息:通过wait()调用族
- 进程最终如何消亡:强制或自愿的调用exit()