第二章进程管理本章讨论了进程的内核抽象,以及进程如何被创建、销毁和管理。由于操作系统最终目的是让用户运行程序,所以这章是最基础的内容。相关章节第三章进程调度第十五章进程地址空间作业及实验(一)作业:(1)请分析linux执行程序的ELF格式,并描述其加载执行的过程(2)请分析linux进程调度器的接口,设计并实现一个调度算法,并分析其性能。实验:(1)设计一个linux进程调度器进程与线程执行线程,简称线程:是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。线程模型Linux:线程=进程Windows:线程!=进程进程虚拟机制两种虚拟机制虚拟处理器虚拟内存。虚拟处理器给进程一种假象,让进程觉得自己在独享处理器。虚拟内存让进程在获取和使用内存时觉得自己拥有整个系统的所有内存资源。线程之间(在同一个进程中的线程)可以共享虚拟内存,但拥有各自的虚拟处理器。如何查看进程信息ps–aKill-9PID进程的信息Proc文件系统Proc/PID目录下Pmap内存区域Objdump-x进程树进程树第一个进程Init在Linux系统中,通常调用fork()系统调用创建进程该系统调用通过复制一个现有进程来创建一个全新的进程。通常创建新的进程都是为了执行新的、不同的程序,而接着调用exec()这族函数创建新的地址空间,并把新的程序载入。最终程序通过exit()系统调用退出执行。这个函数会终结进程并将其占用的资源释放掉。2.1进程描述符及任务队列内核把进程存放在叫做任务队列(tasklist)的双向循环链表中。链表中的每一项都是类型为task_struct,称为进程描述符(processdescription)的结构,该结构定义在include/linux/sched.h文件中。进程描述符中包含一个具体进程的所有信息。2.1.1分配进程描述符Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的。(通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗。)各个进程的task_struct存放在它们内核栈的尾端。从而避免使用额外的寄存器专门记录。所以只需在栈底(对于向下增长的栈)或栈顶(对于向上增长的栈)创建一个新的结构structthread_info。在x86上,structthread_info在文件asm/thread_info.h中定义如下:structthread_info{structtask_struct*task;structexec_domain*exec_domain;unsignedlongflags;__u32cpu;__s32preempt_count;mm_segment_taddr_limit;u8supervisor_stack[0];};每个任务的thread_info结构在它的内核栈的尾端分配。结构中task域中存放的是指向该任务实际task_struct的指针。内核通过一个唯一的进程标识值(processidentificationvalue)或PID来标识每个进程。PID是一个数,最大值默认设置为32767(shortint短整型的最大值)。它存放在每个进程的进程描述符中。它实际上就是系统中同时存在的进程的最大数目。也可由系统管理员通过修改/proc/sys/kernel/pid_max来提高上限。在内核中,访问任务通常需要获取指向其task_struct指针。通过current宏查找当前正在运行进程的进程描述符的速度就显得尤其重要。它是随着硬件体系结构不同从而它的实现也不同。2.1.2进程描述符的存放#definecurrent(get_current())在x86系统上,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移,该操作通过current_thread_info()函数完成。汇编代码如下:movl$-8192,%eaxandl%esp,%eax最后,current在从thread_info的task域中提取并返回task_struct的地址。staticinlinestructtask_struct*get_current(void){returncurrent_thread_info()-task;}2.1.3进程状态进程描述符中的state域描述了进程的当前状态。系统中的每个进程都必然处于五种进程状态中的一种。现存的任务调用fork()函数并且创建一个新进程TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE(等待)TASK_RUNNING(正在运行)TASK_ZOMBIE(任务被终止)TASK_RUNNING(准备就绪但还未投入运行)任务forks调度程序将任务投入运行:schedule()函数调用context_switch()函数任务被优先级更高的任务抢占为了等待特定事件,任务在等待队列上睡眠等待的事件发生后任务被唤醒并且被重新置入运行队列中任务通过do_exit()函数退出2.1.4设置当前进程状态内核经常需要调整某个进程的状态。这时最好使用set_task_state(task,state)函数,该函数将指定的进程设置为指定的状态。必要时,它会设置内存屏障来强制其他处理器作重新排序。(一般只有在SMP系统中有此必要)否则,它等价于:task-state=state;set_current_state(state)和set_task_state(current,state)含义是等同的。////////////#defineset_current_state(state_value)\set_mb(current-state,(state_value))#defineset_mb(var,value)\do{var=value;mb();}while(0)2.1.5进程上下文可执行程序代码是进程的重要组成部分。这些代码从可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序执行了系统调用或触发了某个异常,它就陷入了内核空间。称内核“代表进程执行”并处于进程上下文中。系统调用和异常处理是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行(对内核的所有访问都必须通过这些接口)进程树Linux进程之间存在一个明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscripts)并执行其他的相关程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程。每个进程也可以有一个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。2.1.5进程树Linux进程之间存在一个明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscripts)并执行其他的相关程序,最终完成系统启动的整个过程。系统中的每个进程必有一个父进程。每个进程也可以有一个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。通过下面代码获得其父进程的进程描述符:structtask_stuct*task=current-parent;通过下面代码依次访问子进程structtask_stuct*task;structlist_head*list;/*第一个参数用来指向当前项,第二个参数是需要检索的链表*/list_for_each(list,¤t-children){/*取得包含list_head的结构体*/task=list_entry(list,structtask_struct,sibling);/*task现在指向当前的某个子进程*/}//*一个是指向给定的链表元素的指针,一个是其中嵌入了链表的结构体*类型引用,另一个是结构体中链表成员的名称。*list_entry-getthestructforthisentry*@ptr:the&structlist_headpointer.*@type:thetypeofthestructthisisembeddedin.*@member:thenameofthelist_structwithinthestruct.*/#definelist_entry(ptr,type,member)\container_of(ptr,type,member)Init进程的进程描述符是作为init_task静态分配的。以下是演示所有进程之间的关系:structtask_struct*task;for(task=current;task!=&init_task;task=task-parent);/*task现在指向init*/任务队列是一个双向的循环链表。对于给定的进程,获取链表中的下一个进程:list_entry(task-tasks.next,structtask_struct,tasks)获取前一个进程的方法相同:list_entry(task-tasks.pre,structtask_struct,tasks)这两个例程通过next_task(task)和prev_task(task)宏实现。而实际上,for_each_process(task)宏提供了依次访问整个任务队列的能力。每次访问,任务指针都指向链表中的下一个元素:structtask_struct*task;for_each_process(task){/*它打印出每一个任务的名称和PID*/prink(“%s[%d]\n”,task-comm,task-pid);}2.2进程创建其他操作系统都提供了产生进程的机制。首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如挂起的信号,它没有必要被继承)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。2.2.1写时拷贝传统的fork()系统调用直接把所有的资源复制给新创建的进程。实现简单但效率低。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。例:fork()后立即调用exec(),这时它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。2.2.2fork()Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。do_fork()系统调用成功fork()系统调用clone()系统调用copy_process()函数copy_flags()系统调用dup_task_struct()系统调用get_pid()系统调用新创建的子进程被唤醒并让起投入运行成功返回123copy_process()函数完成的功能:dup_task_struct()系统调用为新进程创建一个内核栈、threa_info结构和task_struct,这些值与当前进程相同。然后检查新创建的子进程的当