标签:
第三章 进程管理
姓名:王玮怡 学号:20135116
一、进程
1、进程的含义
进程是处于执行期的程序以及相关资源的总称,程序本身并不是进程,实际上就是正在执行的代码的实时结果。Linux内核通常把进程也叫“任务”。
2、线程的含义
执行线程简称线程,是在进程中互动的对象。内核调度的对象是线程而不是进程。Linux系统不区分线程和进程,线程只是一种特殊的进程。
3、进程的执行过程
(1)clone()调用fork(),通过复制一个现有进程来创建一个全新的进程,进程开始存活。其中调用fork()的进程为父进程,新产生的进程为子进程。在该系统调用结束时,在返回点这个相同位置上,父进程回复执行,子进程开始执行。其中,fork()系统调用从内核返回两次:一次回到父进程,另一次返回到新产生的子进程。子进程和父进程的区别在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量。
(2)新的进程调用exec()这组函数,创建新的地址空间,并把新的程序载入其中。
(3)程序通过exit()系统调用推出执行,终结进程并将其占用的资源释放,进程退出执行后为僵死状态,直到父进程调用wait()或waitpid()为止。其中父进程可以通过wait4()系统调用查询子进程是否终结。
二、进程描述符及任务结构
内核把进程的列表存放在“任务队列”的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符的结构,该结构定义在<linux/sched.h>文件中,进程描述符包含了一个具体进程的所有信息。
1、分配进程描述符
Linux通过slab分配器动态分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的。只需在栈底(向下增长的栈)或栈顶(向上增长的栈)创建一个新的结构struct thread_info。
每个任务的thread_info结构在它的内核栈的尾端分配。结构域中task域存放的是指向该任务实际task_struct的指针。
2、进程描述符的存放
内核通过一个唯一的进程标识值或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型,最大默认值设置为32768(short int短整型的最大值)。最大默认值表示系统中允许同时存在的进程的最大数目,这个值越小,转一圈的速度越快。
在内核中访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。
3、进程状态
进程描述符中的state域描述了进程的当前状态。
(1)进程的五种状态
(2)进程状态转化
4、设置当前进程状态
使用set_task_state(task,state)函数将制定的进程设置为制定的状态。
*注:set_current_state(state)和set_task_state(task,state)含义是等同的。
5、进程上下文
当一个程序调用执行了系统调用或触发了某个异常,它就陷入了内核空间,此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中current宏是有效的。
进程只有通过某些明确定义的接口才能陷入内核执行——对内核的所有访问都必须是必须通过这些接口的。
6、进程家族树
所有的进程都是PID为1的init进程的后代。进程间的关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct、叫做parent的指针,还包含一个称为children的子进程链表。
三、进程创建
1、写时拷贝
(1)Linux的fork()使用写时拷贝(copy-on-write)页实现,内核并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝,而fork()的实际开销就是复制父进程的页表以及给子进程创建唯一地进程描述符。
(2)资源的复制只有在需要写入时才进行,在此之前,只是以只读方式共享。
(3)在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量冗余数据。
2、fork()
(1)fork()、vfork()、__clone()库函数都会根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()。
(2)do_fork()函数调用copy_process()函数,如果copy_process()函数返回成功,新创建的子进程被唤醒并让其投入运行,而内核有意选择子进程先执行。
(3)关于copy_process()函数:
3、vfork()
(1)除了不拷贝父进程的页表项外,vfork()系统调用和fork()的功能相同。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程推出或执行exec()。
(2)理想情况下,系统最好不要调用vfork(),内核也不用实现它。
(3)vfork()系统调用的实现是通过向clone()系统调用传递一个特殊标志来进行的。
四、线程在Linux中的实现
每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程。
1、创建线程
线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:
传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共辜的资源种类。
2、内核线程
(1)内核线程和普通的进程阔的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm 指针被设置为NULL),它们只在内核空间运行,从来不切换到用户空间去。
(2)内核进程和普通进程一样,可以被调度,也可以被抢占。
(3)内核钱程也只能囱其他内核钱程创建
五、进程终结
一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,既可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回(其实C 语言编译器会在main()函数的返回点后面放置惆用exit()的代码)。
进程的终结,大部分依靠do_exit():
至此,与进程相关联的所有资源都被释放掉了,线程不可运行(实际上也没有地址空间让它运行)并处于EXIT_ZOMBIE退出状态。
1、删除进程描述符
wait()这一族函数都是通过唯一(但是很复杂)的一个系统调用wait4()来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。
当最终需要释放进程描述符时,release_task()会被调用,用以完成以下工作:
2、孤儿进程造成的进退维谷
如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白白地艳费内存。
*解决方法:
给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程.在do_exit()中会调用exit_notify(),该函数会调用forget_original_parent(),而后者会调用find_new _reaper() 来执行寻父过程。
当一个进程被跟踪时,它的临时父亲设定为调试进程。寻找一个新的父进程的办法:在一个单独的被ptrace 跟踪的子进程链表中搜索相关的兄弟进程一一用两个相对较小的链袭减轻了遍历带来的消耗。
本章总结:
标签:
原文地址:http://www.cnblogs.com/wwy-20135116/p/5332170.html