三、进程管理

3.1 进程

  进程就是处于执行期的程序。但进程并不仅仅局限于一个可执行程序代码。通常进程还包括其他资源。进程就是正在执行的程序代码的实时结果。

  执行线程,简称线程,是在进程活动中的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程而不是进程。

进程提供两种虚拟机制:虚拟处理器和虚拟内存。虚拟处理器给进程一种假象,让这些进程觉得自己在独享处理器。虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。同一个进程中的线程可以共享虚拟内存,但是每个都拥有各自的虚拟处理器。

Linux系统中,通常是调用fork(),复制一个现有的进程来创建一个全新的进程。创建新的进程都是为了立即执行新的、不同的程序,而接着调用exec()就可以创建新的地址空间,并把新的程序载入其中。最终,程序通过exit()系统调用退出执行。父进程可以通过wait4()系统调用查询子进程是否终结,这其实使得进程拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid()为止。

 

3.2进程描述符及任务结构

  内核把进程的列表存放在叫任务队列的双向循环链表中。链表中的每一项都是类型为task_struct、成为进程描述符(process descriptor)的结构,进程描述符中包含一个具体进程的所有信息。

  task_struct相对较大,在32位机器上,它大约有1.7kB。

3.2.1分配进程描述符

   内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。记住,进程对应的用户栈和内核栈都是进程私有的。当进程在用户空间运行时,cpu堆栈指针寄存器(esp寄存器)里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器(esp寄存器)里面内容是内核栈空间地址,使用内核栈。

linux通过slab分配器分配task_sturct结构,这样能达到对象复用和缓存着色的目的。2.6以前的内核中,各个进程的task_struct存放在各自内核栈的尾端,由于现在使用slab分配器动态生成task_struct,所以只需在栈的尾端创建新的结构thread_info。thread_info结构中的task域存放的是指向该进程的实际task_struct指针。

3.2.2 进程描述符的存放

  内核通过一个唯一的进程标识值(process indentification vale)或PID来标识每个进程。

  内核中访问任务通常需要获得指向其task_struct的指针。在x86系统上,current把栈指针的后13个有效位屏蔽掉,用来计算出thread_info的偏移。然后通过thread_info的task域来获得task_struct的指针。

3.2.3 进程状态

TASK_RUNNING:进程是可执行的;正在执行或者在运行队列中等待执行

TASK_INTERRUPTIBLE:进程正在睡眠,等待某些条件的达成,会因为接收到信号而提前被唤醒并随时准备投入运行。

TASK_UNINTERUPTIBLE:就算是接收到信号也不会被唤醒,其他与上一状态相同

——TASK_TRACED:被其他进程跟踪的进程,例如同构ptrace对调试程序进程跟踪

__TASK_STOPED:进程停止执行;进程没有投入运行也不能投入运行

3.2.4 设置当前进程的状态

  set_task_state(task,state);

  set_current_state(state)=set_tast_state(current,state);

3.2.5 进程上下文。

  可执行程序代码是进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中,current宏是有效的。除非在此间隙有更高优先级的进程需要执行并由调度器做出了相应调整,否则在内核退出的时候,程序恢复在用户空间继续执行。

  系统调用和异常处理是对内核明确定义的接口。用户进程对内核的所有访问必须通过这些接口。

3.2.6 进程家族树

在linux系统中,所有进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本并执行其他的相关程序,最终完成系统启动的整个过程。

  每个task_struct都包含一个指向其父进程task_struct的parent指针,还包含一个成为children的子进程链表。

  struct task_struct *my_parent = current->parent;//获得父进程描述符

  依次访问子进程:

  struct tast_struct *task;

  struct list_head *list;

  list_for_each(list,&current->children){

    task=list_entry(list,struct task_struct,sibling);//task指向当前的某个子进程

  }

  对于给定进程,获取链表中的下一个进程:

  list_entry(task->tasks.next,struct task_struct,tasks);

  获取前一个进程的方法:

  list_entry(task->tasks.prev,struct task_struct,tasks);

  依次访问整个任务队列:

  struct task_struct * task;

  for_each_process(task){
  printk("%s[%d]\n",task->comm,task->pid);

  }

3.3 进程创建

  首先通过fork()拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID、PPID和某些资源、统计量。然后exec()函数负责读取可执行文件并将其载入地址空间开始运行。

3.3.1 写时拷贝

3.3.2fork()

3.3.3 vfork()

3.4 线程在linux中的实现

3.4.1 创建线程

  线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来知名需要共享的资源:  

  clone(clone_vm|clone_fs|clone_files|clone_sighand,0);

线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程。

3.4.2 内核线程

  内核常常需要在后台执行一些操作。这种任务可以通过内核线程完成。内核线程和普通进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被值为null)。他们只在内核空间运行,从来不切换到用户空间去。内核线程只能由其他内核线程创建。内核线程创建后处于不可运行状态,如果不通过调用wake_up_process()明确的唤醒他,它不会主动运行。内核线程启动后就一直运行直到调用do_exit()退出,或内核的其他部分调用kthread_stop()退出。

3.5 进程终结

  当一个进程终结时,内核必须释放它所占有的资源,并告知其父进程。终结的任务大部分由do_exit()完成。

  do_exit()之后,进程占用的所有内存就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向他的父进程提供信息。父进程检索到信息后,由进程持有的剩余内存被释放。

3.5.1 删除进程描述符

3.5.2 孤儿进程

  解决方法是给子进程在当前线程组内找到一个线程作为父亲,如果不行,就让init作为他们的父亲。

  

posted @ 2013-04-05 17:20  shuying1234  阅读(251)  评论(0编辑  收藏  举报