linux内核进程和用户进程,linux内核进程调度详解

  linux内核进程和用户进程,linux内核进程调度详解

  在内核中,一个进程的所有信息都存储在一个叫做“进程描述符”的结构中,这个结构叫做task_struct,在linux/sched.h文件中定义。此外,内核将所有进程描述符放在一个名为“任务列表”的双向循环列表中。参见《内核中双向列表的实现》(http://blog.csdn.net/gaopenghigh/article/details/8830293)了解双向循环列表的实施。

  task_struct中的state字段描述了流程的当前状态,流程的可能状态必须是以下五种之一:

  1.TASK _ RUNNING-进程正在执行,或者在运行队列中等待。这是用户空间中唯一可能的进程状态。

  2.TASK _ INTERRUPTIBLE——进程正在休眠(阻塞),等待某些条件得到满足。硬件中断的产生、进程正在等待的系统资源的释放以及信号的传输都可以用作唤醒进程的条件。

  3.TASK _ un interruptible——类似于可中断状态,除了即使收到信号也不会唤醒或准备投入运行,也不会对信号做出响应。这种状态通常发生在进程不得不等待时,否则等待事件将很快发生。例如,当一个进程打开一个设备文件,相应的设备驱动程序需要探测一个硬件设备时,该进程将处于这种状态,以确保设备驱动程序在探测完成之前不会被中断。

  5._ _ task _ stopped-进程停止执行。通常,当接收到sigstop、sigtstp、sigttin和sigttou等信号时,会出现这种状态。此外,调试过程中收到的任何信号都会使进程进入这种状态。

  与task_struct结构相关的一个小数据结构是thread_info(线程描述符)。对于每个进程,内核在为该进程单独分配的存储区域中紧凑地存储两个数据结构,即处于内核状态的进程堆栈和对应于该进程的thread_info。在X86上,struct_info在文件linux/thread_info.h中定义如下(Linux _ src/arch/X86/include/ASM/thread _ info . h)

  结构任务_结构*任务;/*主任务结构*/

  struct exec _ domain * exec _ domain/*执行域*/

  __u32标志;/*低电平标志*/

  __u32状态;/*线程同步标志*/

  _ _ u32 cpu/*当前CPU */

  int preempt _计数;/* 0=可抢占,

  0=BUG */

  mm _ segment _ t addr _ limit

  struct restart _ block restart _ block;

  void _ _ user * sysenter _ return

  #ifdef CONFIG_X86_32

  无符号长previous _ esp/*中前一个堆栈的ESP

  嵌套(IRQ)堆栈的情况*/

  _ _ u8 supervisor _ stack[0];

  #endif

  unsigned int SIG _ on _ ua access _ error:1;

  无符号整数u access _ err:1;/* u访问失败*/

  };

  内核使用thread_union来方便地表示一个进程的thread_info和内核堆栈:(linux/sched.h)

  图中esp寄存器是CPU栈指针,用来存储栈顶单元的地址。将它们紧密地存储在一起的主要好处是,内核可以很容易地从esp寄存器的值中获得CPU上当前运行的进程的thread_info的地址(例如,如果thread_union结构的长度为2 ^ 13字节,即8K,则可以通过屏蔽esp的低13位来获得对应thread_info的基址),然后就可以获得该进程的task_struct的地址。对于x86这样寄存器较少的硬件架构,只需要通过堆栈指针就可以获得当前进程的task_struct结构,避免了使用额外的寄存器进行特殊记录。(对于PowerPC结构,它有一个专门的寄存器来保存当前进程的task_struct地址,需要时可以直接访问。)

  内核中处理进程的代码大部分是直接通过task_struct执行的,通过当前宏可以找到当前运行进程的task_struct。这个宏的实现随硬件系统的不同而不同,所以必须根据特殊的影印架构来处理。如果希望PowerPC直接取寄存器的值,对于x86来说,在内核栈的末尾创建thread_info结构,通过计算偏移量间接找到task_struct结构。比如上面说的“那就屏蔽esp的低13位就好了”。

  我们知道进程是由PID标识的,PID的默认最大值是32768。这个值是在linux/threads.h中定义的:

  为了高效地从PID中导出相应的进程描述符指针(而不是顺序扫描链表),内核引入了四个哈希表。(四个的原因是流程描述符包含四个不同类型PID的字段,每种类型的PID都需要自己的哈希表)。这四个哈希表存储在由task_struct结构中名为pids的成员表示的数组中。

  注意,对于内核来说,线程只是一个共享一些资源的进程,它也是由进程描述符来描述的。GetpID(获取进程ID)系统调用也返回tast_struct中的tgid,而tast_struct中的pID则由getid系统调用返回。在不显示子线程的情况下执行ps命令也会出现一些问题。例如,当程序a.out运行时,会创建一个线程。假设主线程的pid是10001,子线程是10002(他们的tgid都是10001)。此时,如果你杀死10002,你就可以一起杀死10001和10002线程,尽管你在执行ps命令时看不到10002的进程。如果你不知道linux线程背后的故事,你一定会觉得自己遇到了灵异。

  一个pid只对应一个进程,但是一个PGID、TGID和SID可以对应多个进程。因此,在PID结构中,具有相同PID(广义PID)的进程被放入由名为tasks的成员表示的数组中。当然,不同类型的id放在相应的数组元素中。

  #定义pid_hashfn(nr,ns) \

  hash_long((无符号长整型)nr(无符号长整型)ns,pidhash_shift)

  这个宏负责将PID转换成索引。关于hash_long函数和内核中的哈希算法,请参考《Linux内核中hash函数的实现》(

  http://blog.csdn.net/gaopenghigh/article/details/8831312

  现在我们可以通过pid_hashfn把PID转换成一个索引。接下来,我们来思考问题。首先,对于内核使用的哈希算法,不同的PID/TGID/PGRP/SESSION id(通常在特殊语句前用PID表示)可能对应同一个哈希表索引,也就是碰撞。因此,索引不是指向单个进程,而是指向一系列进程,它们的PID哈希值都是相同的。task_struct中PID表示的四个列表是具有相同哈希值的进程列表。例如,进程A的task_struct中的PID[PID type _ PID]指向所有PID的hash值都等于PID A的进程列表,PID[PID type _ PGID]指向PGID的hash值等于PGID A的进程列表,需要注意的是,与A同组的进程具有相同的PGID,这在上面有更多解释。这些进程组成的链表存储在A的PID[PID type _ pgid]指向的链表中。pid.tasks

  下图说明了哈希和进程链表的关系,其中TGID=4351和TGID=246的哈希值相同。(图中的字段名比较老,但大意是一样的。只是把pid_chain看成pid_link结构中的节点,把pid_list看成pid结构中的任务。)

  循环作用于PID值等于nr的PID链表。链表的类型由参数type给出,任务指向当前扫描元素的进程描述符。

  通过Linux clone()系统调用实现fork()。这个调用通过一系列参数标志指示父进程和子进程需要共享的资源。fork()、vfork()和_ _ clone()库函数都是根据自己需要的参数标志调用clone(),然后clone()调用do_fork()。

  我们之前讨论过Linux中的线程是共享一些资源的进程,所以你可以通过设置合适的参数,也就是指定一个特殊的clone_flags来创建一个线程,你可以通过clone()系统调用来创建一个线程。

  Linux的fork()是通过“写时复制”实现的,这是一种可以延迟甚至避免复制数据的技术。也就是说,内核此时并没有分配整个进程地址空间,而是让父进程和子进程共享同一个副本。只有需要写的时候,才会复制数据,让每个进程都有自己的副本。在此之前,它只能以只读方式共享。如果页面根本没有写(比如在fork()之后立即调用exec()),就不需要复制。

  CONE_NEWNS为子进程创建一个新的名称空间(即它自己的挂载文件系统视图)。

  CLONE_PARENT指定子进程与父进程具有相同的父进程。即子进程的父进程(进程描述符中的parent和real_parent字段)被设置为调用进程的父进程。

  CLONE_PTRACE继续调试子进程。如果父进程被跟踪,子进程也被跟踪。

  CLONE_VFOK调用vfork(),所以父进程准备休眠,等待子进程唤醒它。

  CLONE_UNTRACED防止跟踪进程在子进程上强制执行CLONE_PTRACE,即使CLONE_PTRACE标志被禁用。

  做一些权限和用户命名空间检查(TODO),调用copy_process函数获取新进程的进程描述符,如果copy_process返回成功,则唤醒新进程并投入运行。内核有意选择首先执行的子进程。一般情况下,子进程会立即调用exec()函数,这样可以避免编写时复制的额外开销。现在让我们看看copy_process()函数实际上是做什么的。

  Copy_process()创建子进程的进程描述符和执行它所需的所有其他内核数据结构,但它并不真正执行子进程。该功能非常复杂,其主要步骤如下:

  1.检查由参数clone_flags传递的标志的一致性。标志必须遵循一定的规则,否则会返回错误的代码。

  2.调用security_task_create()和后来的security_task_alloc()来执行所有附加的安全检查。

  3.调用dup_task_struct()为新进程创建内核栈、thread_info结构和task_struct。这些值与当前流程的值相同。此时,子进程和父进程的进程描述符是相同的。dup_task_struct()的工作如下:

  A.准备_到_复制()。如有必要,将一些寄存器(FPU)2的内容保存到父进程的thread_info结构中。稍后,这些值将被复制到子进程的thread_info结构中;

  b.tsk=alloc _ task _ struct();获取task_struct结构,ti=alloc _ thread _ info();获取task_info结构,将父进程的值测试到新结构中,链接tsk和ti:tsk-thread _ info=ti;ti-task=tsk;

  C.将新的进程描述符的使用计数器(tsk- usage)设置为2,用于指示该进程描述符正在被使用,其对应的进程处于活动状态。

  4.检查并确保当前用户拥有的进程数量没有超过在新创建的子进程之后分配给它的资源限制。

  5.检查系统中的进程数是否超过max_threads变量的值。系统管理员可以通过写入/proc/sys/kernel/threads-max文件来更改该值。

  6.新的过程开始将自己与父过程区分开来。流程描述符中的许多成员必须被清除或设置为初始值。那些不是从父进程继承的成员主要是统计信息,大部分数据保持不变。根据传递给clone()的参数标志,copy_process()复制或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。通常,这些资源将在给定进程的所有线程之间共享,不能共享的资源将被复制到新进程。

  7.调用sched_fork()函数,将子进程的状态设置为TASK_RUNNING,完成进程调度所需的一些初始化。

  8.根据clone_flags初始化一些进程的父子关系。例如,如果clone_flags中有CLONE_PARENTCLONE_THREAD,则子进程的real_parent值等于父进程的值。

  13.做一些收尾工作,修改一些计数器值,返回新进程的进程的进程描述符指针tsk。copy_process()函数成功返回后,do_fork()函数将唤醒新创建的进程,并将其投入运行。

  通过内核线程,完成一些经常需要在后台完成的操作,比如缓存刷机、高级电源管理等等。内核线程与普通进程的区别在于,内核线程没有独立的地址空间,它只在内核状态下运行。

  一个内核只能由其他内核线程创建,内核通过从kthreadd内核进程派生所有新的内核线程来自动处理这个问题。创建新内核线程的方法如下:

  /* linux/kthread.h */

  struct task _ struct * kthread _ create(int(* thread fn)(void * data),

  void *数据,

  const char namefmt[],);

  的新任务是由kthread内核进程通过clone()系统调用创建的。新进程创建后,处于不可操作状态,需要用wake_up_process()唤醒。唤醒后,它以数据为参数运行threadfn函数。新内核线程的名称是namefmt。

  所有进程都是pid=0的祖先的后代,称为进程0。它是Linux在初始化阶段从零开始创建的,它使用静态数据结构(其他进程的数据结构是动态分配的)。

  0将创建一个新进程:进程1,即init进程。init进程被调度后,会运行init()方法,完成内核的初始化,然后用execve()系统调用加载可执行程序init,这样init进程就会从内核线程变成正常进程。

  所有进程终止都由do_exit()函数处理(位于kernel/exit.c中)。这个函数从内核数据结构中删除了大部分对终止进程的引用。注意task_struct和thread_info没有被删除。当父进程调用等待系列函数时,仍然需要这些信息。

  注意,在do_exit()中会调用exit_notify()函数,向父进程发送信号,其子进程会再次找到它的养父。养父是线程组中的另一个进程,找不到的时候会是init进程,进程状态(task_struct结构中的exit_state成员)设置为EXIT_ZOMBIE。然后,do_exit()调用schedule()切换到一个新的进程。

  这样,与进程相关的所有资源都被释放(当然,与其他进程共享的资源不会被释放)。进程无法运行(实际上没有地址空间让它运行),处于EXIT_ZOMBIE状态。它占用的内存都是内核栈,thread_info结构,task_struct结构。只有在父进程调用与终止进程相关的wait()系统后,这些内存才会被释放。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: