进程、线程小结(2)

来源:互联网 发布:app用户数据分析维度 编辑:程序博客网 时间:2024/05/24 05:13

1. 进程描述符及任务结构
  最初的进程定义都包含程序、资源及其执行三部分,其中程序通常指代码,资源在操作系统层面上通常包括内存资源、IO资源、信号处理等部分,而程序的执行通常理解为执行上下文,包括对cpu的占用等。
  Linux内核把进程列表存放在焦作的任务列表(task list)的双向链表中。链表中的每一项包含类型为task_struct的结构,用于描述进程,包含一个具体进程的所有信息:如进程号,打开的文件,挂起信号,进程状态,父进程等。

struct task_struct {  unsigned long state;  void *stack;  unsigned int flags;  int prio;  unsigned long policy;  struct task_struct *parent;  struct list_head task;  pid_t pid;  struct thread_struct thread;  ……}

  flags定义了很多指示符,表明进程是否正在被创建(PF_STARTING)或退出(PF_EXITING),或是进程当前是否在分配内存(PF_MEMALLOC)。可执行程序的名称(不包含路径)占用 comm(命令)字段。
  每个进程都会被赋予优先级(称为 static_prio),但进程的实际优先级是基于加载以及其他几个因素动态决定的。优先级值越低,实际的优先级越高。
  tasks 字段提供了链接列表的能力。它包含一个 prev 指针(指向前一个任务)和一个 next 指针(指向下一个任务)。
  thread_struct 则用来标识进程的存储状态。此元素依赖于 Linux 在其上运行的特定架构,在 ./linux/include/asm-i386/processor.h 内有这样的一个例子。在此结构内,可以找到该进程自执行上下文切换后的存储(硬件注册表、程序计数器等)。
  
  Linux通过slab分配器分配task_struct结构,在2.6以前的内核中,各进程的task_struct存放在其内核栈的尾端,这样可以那些寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,避免使用额外的专用寄存器。对于现在的slab分配器,在栈底(向下增长栈)或栈顶(向上增长栈)创建一个struct thread_info。每个任务的thread_info存放在其内核栈的尾端,结构中存放着指向该任务实际task_struct的指针。
  内核通过一个唯一的进程标志值(PID)来标识每个进程,为一int型整数。内核把每个进程的PID存放在他们各自的进程描述符中。

2.进程状态
  进程描述符中的state域描述了进程的当前状态,linux有五种进程状态:
  TASK_RUNNING(运行):正在执行或者在运行队列中等待执行;
  TASK_INTERRUPTIBLE(可中断):进程正在睡眠,即被阻塞,等待某些条件的达成,一旦条件达成,内核就把该进程状态置为运行。
  TASK_UNINTERRUPTIBLE(不可中断):接收到信号也不会被唤醒或准备投入运行。
  _TASK_TRACED:被其他进程跟踪的进程,如通过ptrace对调试程序进行跟踪。
  _TASK_STOPPED:进程停止运行,如在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号时。

3.进程家族树
  Linux系统的进程间有明显的继承关系。所有进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。
系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有0个或多个子进程。拥有同一父进程的进程被称为兄弟。进程描述符中包含一个指向父进程的指针,还包含一个子进程链表。

4.进程创建
  Linux通过fork()拷贝当前进程创建子进程,其最终通过clone()系统调用实现。
  其过程大致为:
  1)为新进程创建一个内核栈,thread_info和task_struct结构,这些值与当前进程一致。
  2)子进程着手使自己与父进程区分开来。进程描述符中的很多成员都要清零或设为初始值,大多数数据保持不变。
  3)子进程状态设置为TASK_UNINTERRUPTIBLE,以保证他不会投入运行。
  4)调用copy_flags()更新flags成员。
  5)为新进程分配一个有效的PID。
  6)根据传递给clone()的参数标识,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。一般情况下,这些资源会被给定进程的所有线程共享,这些资源对于每个进程是不同的。
  7)返回一个指向子进程的指针,若成功返回,新建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行,因为一般紫禁城都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程先执行,有可能会开始向地址空间写入。

  写时拷贝(copy-on-write):传统的fork()系统调用直接把所有资源复制给新进程。这样实现简单但效率低下,通过写时拷贝可以推迟甚至免除拷贝数据,内核并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝。只有在需要写入是,数据才会被复制,从而使各进程拥有各自的拷贝。这样是地址空间的拷贝推迟到实际写入时执行,对于一些不会写入的情况(比如fork()后立即调用exec())就无需复制。

5. 进程销毁
  进程销毁可以通过几个事件驱动 — 通过正常的进程结束、通过信号或是通过对 exit 函数的调用。不管进程如何退出,进程的结束都要借助对内核函数 do_exit(在 ./linux/kernel/exit.c 内)的调用。
  do_exit 的目的是将所有对当前进程的引用从操作系统删除(针对所有没有共享的资源)。销毁的过程先要通过设置 PF_EXITING 标志来表明进程正在退出。内核的其他方面会利用它来避免在进程被删除时还试图处理此进程。将进程从它在其生命期间获得的各种资源分离开来是通过一系列调用实现的,比如 exit_mm(删除内存页)和 exit_keys(释放线程会话和进程安全键)。do_exit 函数执行释放进程所需的各种统计,这之后,通过调用 exit_notify 执行一系列通知(比如,告知父进程其子进程正在退出)。最后,进程状态被更改为 PF_DEAD,并且还会调用 schedule 函数来选择一个将要执行的新进程。请注意,如果对父进程的通知是必需的(或进程正在被跟踪),那么任务将不会彻底消失。如果无需任何通知,就可以调用 release_task 来实际收回由进程使用的那部分内存。

6.线程实现
  在线程概念出现以前,为了减小进程切换的开销,操作系统设计者逐渐修正进程的概念,逐渐允许将进程所占有的资源从其主体剥离出来,允许某些进程共享一部分资源,例如文件、信号,数据内存,甚至代码,这就发展出轻量进程的概念。
  线程机制使同一程序内共享内存地址空间,还可以共享打开的文件和其他资源。
  Linux实现线程非常独特。从内核角度,其没有线程这个概念。Linux把线程视为一个可以与其他进程共享某些资源的进程。每个线程都拥有自己的task_struct。所以在内核中,他看起来就像一个普通的进程,只是在建立进程时指定他们共享某些资源。
  线程的创建和普通进程的创建类似,只不过在调用clone()时需要传递一些参数标识来指明共享的资源。

7.Linux Thread的线程机制
  Linux Threads是目前Linux平台上使用最为广泛的线程库,由XavierLeroy负责开发完成,并已绑定在GLIBC中发行。它所实现的就是基于核心轻量级进程的”一对一”线程模型,一个线程实体对应一个核心轻量级进程,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。
  LinuxThreads定义了一个struct _pthread_descr_struct数据结构来描述线程,并使用全局数组变量__pthread_handles来描述和引用进程所辖线程。在__pthread_handles中的前两项, LinuxThreads定义了两个全局的系统线程:__pthread_initial_thread和__pthread_manager_thread,并用__pthread_main_thread表征__pthread_manager_thread的父线程(初始为__pthread_initial_thread)。
  struct _pthread_descr_struct是一个双环链表结构,__pthread_manager_thread所在的链表仅包括它一个元素,实际上,__pthread_manager_thread是一个特殊线程,LinuxThreads仅使用了其中的errno、p_pid、p_priority等三个域。而__pthread_main_thread所在的链则将进程中所有用户线程串在了一起。
  新创建的线程将首先在__pthread_handles数组中占据一项,然后通过数据结构中的链指针连入以__pthread_main_thread为首指针的链表中。
  “一对一”模型的好处之一是线程的调度由核心完成了,而其他诸如线程取消、线程间的同步等工作,都是在核外线程库中完成的。在LinuxThreads中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程第一次调用pthread_create()创建一个线程的时候就会创建(__clone())并启动管理线程。
  在一个进程空间内,管理线程与其他线程之间通过一对”管理管道(manager_pipe[2])”来通讯,该管道在创建管理线程之前创建,在成功启动了管理线程之后,管理管道的读端和写端分别赋给两个全局变量__pthread_manager_reader和__pthread_manager_request,之后,每个用户线程都通过__pthread_manager_request向管理线程发请求,但管理线程本身并没有直接使用__pthread_manager_reader,管道的读端(manager_pipe[0])是作为__clone()的参数之一传给管理线程的,管理线程的工作主要就是监听管道读端,并对从中取出的请求作出反应。
  在LinuxThreads中,管理线程的栈和用户线程的栈是分离的,管理线程在进程堆中通过malloc()分配一个THREAD_MANAGER_STACK_SIZE字节的区域作为自己的运行栈。
  用户线程的栈分配办法随着体系结构的不同而不同,主要根据两个宏定义来区分,一个是NEED_SEPARATE_REGISTER_STACK,这个属性仅在IA64平台上使用;另一个是FLOATING_STACK宏,在i386等少数平台上使用,此时用户线程栈由系统决定具体位置并提供保护。与此同时,用户还可以通过线程属性结构来指定使用用户自定义的栈。因篇幅所限,这里只能分析i386平台所使用的两种栈组织方式:FLOATING_STACK方式和用户自定义方式。
  在FLOATING_STACK方式下,LinuxThreads利用mmap()从内核空间中分配8MB空间(i386系统缺省的最大栈空间大小,如果有运行限制(rlimit),则按照运行限制设置),使用mprotect()设置其中第一页为非访问区。
  在pthread_create()向管理线程发送REQ_CREATE请求之后,管理线程即调用pthread_handle_create()创建新线程。分配栈、设置thread属性后,以pthread_start_thread()为函数入口调用__clone()创建并启动新线程。pthread_start_thread()读取自身的进程id号存入线程描述结构中,并根据其中记录的调度方法配置调度。一切准备就绪后,再调用真正的线程执行函数,并在此函数返回后调用pthread_exit()清理现场。

0 0
原创粉丝点击