Linux内核设计与实现第三章进程管理.ppt

上传人:wuy****n92 文档编号:80458592 上传时间:2023-03-23 格式:PPT 页数:32 大小:212KB
返回 下载 相关 举报
Linux内核设计与实现第三章进程管理.ppt_第1页
第1页 / 共32页
Linux内核设计与实现第三章进程管理.ppt_第2页
第2页 / 共32页
点击查看更多>>
资源描述

《Linux内核设计与实现第三章进程管理.ppt》由会员分享,可在线阅读,更多相关《Linux内核设计与实现第三章进程管理.ppt(32页珍藏版)》请在得力文库 - 分享文档赚钱的网站上搜索。

1、第二章 进程管理本章讨论了进程的内核抽象,以及进程如何被创建、销毁和管理。由于操作系统最终目的是让用户运行程序,所以这章是最基础的内容。相关章节第三章 进程调度第十五章 进程地址空间作业及实验(一)作业:(1)请分析linux执行程序的ELF格式,并描述其加载执行的过程(2)请分析linux进程调度器的接口,设计并实现一个调度算法,并分析其性能。实验:(1)设计一个linux进程调度器进程与线程执行线程,简称线程:是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。线程模型Linux:线程=进程Windows:线程!=进程进程虚拟

2、机制两种虚拟机制虚拟处理器虚拟内存。虚拟处理器给进程一种假象,让进程觉得自己在独享处理器。虚拟内存让进程在获取和使用内存时觉得自己拥有整个系统的所有内存资源。线程之间(在同一个进程中的线程)可以共享虚拟内存,但拥有各自的虚拟处理器。如何查看进程信息ps aKill-9 PID进程的信息Proc文件系统Proc/PID目录下Pmap内存区域Objdump-x进程树进程树第一个进程Init在Linux系统中,通常调用fork()系统调用创建进程该系统调用通过复制一个现有进程来创建一个全新的进程。通常创建新的进程都是为了执行新的、不同的程序,而接着调用exec()这族函数创建新的地址空间,并把新的程

3、序载入。最终程序通过exit()系统调用退出执行。这个函数会终结进程并将其占用的资源释放掉。2.1 进程描述符及任务队列内核把进程存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct,称为进程描述符(process description)的结构,该结构定义在include/linux/sched.h文件中。进程描述符中包含一个具体进程的所有信息。2.1.1 分配进程描述符Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的。(通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来

4、的资源消耗。)各个进程的task_struct存放在它们内核栈的尾端。从而避免使用额外的寄存器专门记录。所以只需在栈底(对于向下增长的栈)或栈顶(对于向上增长的栈)创建一个新的结构struct thread_info。在x86上,struct thread_info在文件中定义如下:struct thread_info struct task_struct*task;struct exec_domain*exec_domain;unsigned long flags;_u32 cpu;_s32 preempt_count;mm_segment_t addr_limit;u8 superviso

5、r_stack0;每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。内核通过一个唯一的进程标识值(process identification value)或PID来标识每个进程。PID是一个数,最大值默认设置为32767(short int短整型的最大值)。它存放在每个进程的进程描述符中。它实际上就是系统中同时存在的进程的最大数目。也可由系统管理员通过修改/proc/sys/kernel/pid_max来提高上限。在内核中,访问任务通常需要获取指向其task_struct指针。通过current宏查找当前正在运行

6、进程的进程描述符的速度就显得尤其重要。它是随着硬件体系结构不同从而它的实现也不同。2.1.2 进程描述符的存放#define current(get_current()在x86系统上,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移,该操作通过current_thread_info()函数完成。汇编代码如下:movl$-8192,%eaxandl%esp,%eax最后,current 在从thread_info的task域中提取并返回task_struct的地址。static inline struct task_struct*get_current(voi

7、d)return current_thread_info()-task;2.1.3 进程状态进程描述符中的state域描述了进程的当前状态。系统中的每个进程都必然处于五种进程状态中的一种。现存的任务调用fork()函数并且创建一个新进程TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE(等待)TASK_RUNNING(正在运行)TASK_ZOMBIE(任务被终止)TASK_RUNNING(准备就绪但还未投入运行)任务forks调度程序将任务投入运行:schedule()函数调用context_switch()函数任务被优先级更高的任务抢占为了等待特定事件,任务在等待

8、队列上睡眠等待的事件发生后任务被唤醒并且被重新置入运行队列中任务通过do_exit()函数退出2.1.4 设置当前进程状态内核经常需要调整某个进程的状态。这时最好使用set_task_state(task,state)函数,该函数将指定的进程设置为指定的状态。必要时,它会设置内存屏障来强制其他处理器作重新排序。(一般只有在SMP系统中有此必要)否则,它等价于:taskstate=state;set_current_state(state)和set_task_state(current,state)含义是等同的。/#define set_current_state(state_value)set

9、_mb(current-state,(state_value)#define set_mb(var,value)do var=value;mb();while(0)2.1.5 进程上下文可执行程序代码是进程的重要组成部分。这些代码从可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序执行了系统调用或触发了某个异常,它就陷入了内核空间。称内核“代表进程执行”并处于进程上下文中。系统调用和异常处理是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行(对内核的所有访问都必须通过这些接口)进程树Linux进程之间存在一个明显的继承关系。所有的进程都是PID为1的init进程的

10、后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscripts)并执行其他的相关程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程。每个进程也可以有一个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。2.1.5 进程树Linux进程之间存在一个明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscripts)并执行其他的相关程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程。每个进程也可以有一个或多个子进程

11、。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。通过下面代码获得其父进程的进程描述符:struct task_stuct*task=current-parent;通过下面代码依次访问子进程struct task_stuct*task;struct list_head*list;/*第一个参数用来指向当前项,第二个参数是需要检索的链表*/list_for_each(list,¤t-children)/*取得包含list_head的结构体*/task=list_entry(list,struct task_struct,sibling);/*task 现在指向当

12、前的某个子进程*/*一个是指向给定的链表元素的指针,一个是其中嵌入了链表的结构体*类型引用,另一个是结构体中链表成员的名称。*list_entry-get the struct for this entry*ptr:the&struct list_head pointer.*type:the type of the struct this is embedded in.*member:the name of the list_struct within the struct.*/#define list_entry(ptr,type,member)container_of(ptr,type,m

13、ember)Init进程的进程描述符是作为init_task静态分配的。以下是演示所有进程之间的关系:struct task_struct*task;for(task=current;task!=&init_task;task=task-parent);/*task 现在指向init*/任务队列是一个双向的循环链表。对于给定的进程,获取链表中的下一个进程:list_entry(task-tasks.next,struct task_struct,tasks)获取前一个进程的方法相同:list_entry(task-tasks.pre,struct task_struct,tasks)这两个例程

14、通过next_task(task)和prev_task(task)宏实现。而实际上,for_each_process(task)宏提供了依次访问整个任务队列的能力。每次访问,任务指针都指向链表中的下一个元素:struct task_struct*task;for_each_process(task)/*它打印出每一个任务的名称和PID*/prink(“%s%dn”,task-comm,task-pid);2.2 进程创建其他操作系统都提供了产生进程的机制。首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先

15、fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。2.2.1 写时拷贝传统的fork()系统调用直接把所有的资源复制给新创建的进程。实现简单但效率低。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制

16、,从而使各个进程拥有各自的拷贝。也就是资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。例:fork()后立即调用exec(),这时它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。2.2.2 fork()Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。do_fork()系统调用成功fork()系统调用clone()系统调用copy_process()函数copy_flags()系统调用dup_task_str

17、uct()系统调用get_pid()系统调用新创建的子进程被唤醒并让起投入运行成功返回123copy_process()函数完成的功能:dup_task_struct()系统调用为新进程创建一个内核栈、threa_info结构和task_struct,这些值与当前进程相同。然后检查新创建的子进程的当前用户所拥有的进程数目如果没有超出给它分配的资源的限制,则现在子进程就要与父进程相区别开来。(通过进程描述符中许多成员都要被清0或设为初始值),同时子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。copy_flags()以更新task_struct的flags成员。

18、表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。调用get_pid()为新进程获取一个有效的PID。根据传递给clone()的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。让父进程和子进程平分剩余的时间片。最后,作扫尾工作并返回一个指向子进程的指针。再回到do_fork()函数。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销。2.2.3 vfork()vfork()系统调用和fork()的功能相同,除了不拷贝

19、父进程的页表项。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址空间写入。现以彻底没多大用了。线程机制是现代编程技术中常用的一种抽象。线程机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。线程机制支持并发程序设计技术(concurrent_programming),在多处理器系统上,它也能保证真正的并行处理。2.3 线程在Linux中的实现线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源:clone(CLONE_VM|CLON

20、E_FS|CLONE_FILES|CLONE_SIGHAND,0);传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。Linux实现线程的机制非常独特。从内核的角度来说,它并没有线程这个概念。Linux把所有的线程都当作进程来实现。线程仅仅被视为一个使用某些共享资源的进程。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是该进程和其他一些进程共享某些资源,如地址空间).对于Linux来说,它只是一种进程间共享资源的手段。clone()参数标志参数标志含义CLONE_CLEARTID清除TIDCLONE_DE

21、TACHED父进程不需要子进程退出时发送SIGCHLDCLONE_FILES父子进程共享打开的文件CLONE_FS父子进程共享文件系统信息CLONE_IDLETASK将PID设置为0(只供idle进程使用)CLONE_NEWNS为子进程创建新的命名空间CLONE_PARENT指定子进程与父进程拥有同一个父进程参数标志含义CLONE_PTRACE继续调试子进程CLONE_SETTID将TID回写至用户空间CLONE_SETTLS为子进程创建新的TLSCLONE_SIGHAND父子进程共享信号处理函数CLONE_SYSVSEM父子进程共享System VSEM_UNDO语义CLONE_THREAD

22、父子进程放入相同的线程组CLONE_VFORK调用vfork(),所以父进程准备睡眠等待子进程将其唤醒CLONE_VM父子进程共享地址空间内核线程内核经常需要在后台执行一些操作,这种任务可以通过内核线程(kernel thread)完成,这个内核线程就是独立运行在内核空间的标准进程。内核线程和普通的进程间的区别内核线程和普通的进程间的区别在于内核线程没有独立的地址空间。它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。内核线程只能由其他内核线程创建。在现有内核线程中创建一个新的内核线程的方法如下:int kernel_thread(int(*fn)

23、(void*),void*arg,unsigned long flags)该函数返回后父线程退出,并返回一个指向子线程task_struct指针。子线程开始运行fn指向的函数。一个特殊的clone标志CLONE_KERNEL 定义了内核线程常用到的参数标志:CLONE_FS、CLONE_FILES、CLONE_SIGHAND.大部分的内核线程把这个标志传递给它们的flags参数。一般内核线程会将它创建时得到的函数永远执行下去。2.4 进程终结当一个进程终结时,内核必须释放它所占有的资源并告知父进程。一般进程的析构发生在它调用exit()之后,既可能显示的调用这个系统调用,也可能隐式地从某个程序

24、的主函数返回(如C语言编译器会在main()函数的返回点后面放置调用exit()的代码)。当进程接受到它既不能处理也不能忽略的信号或异常时,它还可能被动的终结。不管进程怎么终结的,该任务大部分都要靠do_exit()来完成。do_exit()完成的工作:(do_exit()的实现再kenel/exit.c文件中可以找到)将task_struct中的标志成员设置为PF_EXITING如果BSD的进程计帐功能是开启的,要调用acct_process()来输出计帐信息。调用_exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们,就彻底释放它们。调用sem_exit()函数

25、。如果进程排队等候IPC信号,它则离开队列。调用_exit_files()、_exit_fs()、exit_namespace()和exit_sighand(),用分别递减文件描述符、文件系统数据,进程名字空间和信号处理函数的引用计数。如果其中某些引用计数的数值将为0,那么就代表没有进程在使用相应的资源,此时可以释放。把存放在task_struct的exit_code成员中的任务退出代码置为exit()提供的代码中,或者去完成任何其他由内核机制规定的退出动作。2.4.1 删除进程描述符调用exit_notify()向父进程发送信号,将子进程的父进程重新设置为线程组中的其他线程或init进程,并

26、把进程状态设置成TASK_ZOMBIE.最后调用schedule()切换到其他进程。因为处于TASK_ZOMBIE状态的进程不会再被调度,所有以这是进程所执行的最后一段代码。至此,与进程相关的所有资源都被释放掉了。进程不可运行并处于TASK_ZOMBIE状态。它所占有的所有资源就是保存thread_info的内核栈和保存task_struct结构的那一小片slab。这时进程存在的唯一目的就是向它的父进程提供信息。在调用了do_exit()之后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。这样做可以让系统有办法在子进程终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程

27、描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,子进程的task_struct结构才能被释放。调用free_uid()来减少该进程拥有者的进程使用计数。Linux用一个单用户高速缓存统计和记录每个用户占用的进程数目、文件数目。如果这些数目都将为0,表明这个用户没有使用任何进程和文件,那么这块缓存就可以销毁了。调用unhash_process()从pidhash上删除该进程,同时也要从task_list中删除该进程。如果这个进程正在被ptrace跟踪,将跟踪进程的父进程重设为其最初的父进程并将它从ptract list上删除。wait()这一族函数都是通过唯一的一个系统调用wait4

28、()实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。当最终需要释放进程描述符时,release_task()会被调用,用以完成以下工作:2.4.2 孤儿进程造成的进退维谷最后,调用put_task_struct释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存。至此,进程描述符和所有进程独享的资源就被释放掉了。如果父进程在子进程之前退出,必须有一种机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白

29、白消耗内存。解决的方法就是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。在do_exit()中会调用notify_present(),该函数会通过forget_original_parent来执行寻父过程。struct task_struct *p,*reaper=father;struct list_head*list;if(father-exit_signal!=-1)reaper=prev_thread(reaper);elsereaper=child_reaper;if(reaper=father)reaper=child_reaper;这段代码将re

30、aper设置为该进程所在的线程组内的其他进程。如果线程组内没有其他的进程,它就将reaper设置为child_reaper,也就是init进程。找到合适的父进程后,需要遍历所有子进程并为它们设置新的父进程:list_for_each(list,&father-children)p=list_entry(list,struct task_struct,sibling);reparent_thread(p,reaper,child_reaper);list_for_each(list,&father-ptrace_children)p=list_entry(list,struct task_struct,ptrace_list);reparent_thread(p,reaper,child_reaper);以上代码遍历了两个链表:子进程链表和ptrace子进程链表,给每个子进程设置新的父进程。在2。6内核中,当一个进程被跟踪时,它被暂时设定为调试进程的子进程。此时如果它的父进程退出了,系统会为它和它的所有兄弟重新找一个父进程。现在是在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程用两个相关链表减轻了遍历带来的消耗。一旦系统给进程成功的找到和设置了新的父进程,就不会再出现驻留僵死进程的危险了。Init进程会例行调用wait()来等待其子进程,清除所有与其相关的僵死进程。

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 教育专区 > 大学资料

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知得利文库网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号-8 |  经营许可证:黑B2-20190332号 |   黑公网安备:91230400333293403D

© 2020-2023 www.deliwenku.com 得利文库. All Rights Reserved 黑龙江转换宝科技有限公司 

黑龙江省互联网违法和不良信息举报
举报电话:0468-3380021 邮箱:hgswwxb@163.com