“进程”有诸多的定义,在许多的教材资料上,其定义是一个程序的执行实例,这不无道理,也有的人认为它是程序处理所描述的所有数据结构的集合。这里不深究其定义,如果换个角度而言,进程就好像我们人类,他们被产生,它们有自己的生命周期,尽管生命周期的长短不一,从几毫秒至几秒,甚至几个月,几年。与人类的真正区别就在于它们没有性别之分。
从linux内核观点上来看,进程是内核所分配的系统资源的一个实体(如CPU时间片,内存等)。需要了解的是,早期的linux内核是不支持多线程应用的进程,因为从当初的内核角度来看,多线程应用也仅仅是一个正常的进程。总之早期的应用不太尽如人意,现在linux内核使用轻量级线程来支持多线程应用,实际上,两个轻量级线程可以共享一些资源,如地址空间,打开的文件等等。当其中一个修改了共享资源,另一个马上就能看到它与之间的变化,在访问共享资源时,它们之间需要进行同步操作。
linux系统上运行着许多的进程,内核如何对它们进行高效的管理呢?这是我们想要弄明白的。在阅读了一些linux内核关于进程管理方面的源代码后,作了一些思考与回顾。要想弄明白linux内核对进程的管理,首先应该了解它内部的一些重要的数据结构,这是最基本的。
进程的数据结构--进程描述符
为了管理进程,内核必须有一个非常清晰的关于每个进程正在做什么的宏伟蓝图,比如,它必须知道进程的优先级,它是否在CPU上运行或者在等待什么事件,哪块地址空间被赋予了哪个进程,它允许访问哪些文件等等,所以有关进程的事务内核必须一清二楚。进程描述符(一个task_struct类型的数据结构)内的每个字段包含了每个进程相关的所有信息。也就是说,内核对进程的管理主要是通过从进程描述符获取丰富的信息进行的。可以想象,进程描述符内肯定也包含了其它的数据结构类型,各种数据类型互相穿插,复杂而有序地进行数据信息的传输。说了这么多,不知道进程描述符到底长啥样,庐山真面目如下:
struct task_struct { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack; atomic_t usage; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; int lock_depth; /* BKL lock depth */ int prio, static_prio, normal_prio; unsigned int rt_priority; const struct sched_class *sched_class; struct sched_entity se; struct sched_rt_entity rt; ...... struct list_head tasks; struct plist_node pushable_tasks; struct mm_struct *mm, *active_mm; /* Revert to default priority/policy when forking */ unsigned sched_reset_on_fork:1; pid_t pid; pid_t tgid; /* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->real_parent->pid) */ struct task_struct *real_parent; /* real parent process */ struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */ /* * children/sibling forms the list of my natural children */ struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parent's children list */ struct task_struct *group_leader; /* threadgroup leader */ /* PID/PID hash table linkage. */ struct pid_link pids[PIDTYPE_MAX]; struct list_head thread_group; /* CPU-specific state of this task */ struct thread_struct thread; /* filesystem information */ struct fs_struct *fs; /* open file information */ struct files_struct *files; #ifdef CONFIG_AUDITSYSCALL uid_t loginuid; unsigned int sessionid; #endif struct prop_local_single dirties; #ifdef CONFIG_LATENCYTOP int latency_record_count; struct latency_record latency_record[LT_SAVECOUNT]; #endif /* * time slack values; these are used to round up poll() and * select() etc timeout values. These are in nanoseconds. */ unsigned long timer_slack_ns; unsigned long default_timer_slack_ns; struct list_head *scm_work_list; ........ };
以上我只列出进程描述符中的一小部分代码, 给我的第一感觉是,它很庞大,不知道如何下手。想一下子搞清楚每个字段的含义以及它是怎么工作是不可能的。不过我们可以从list_head task字段开始着手。但是在分析之前必须牢记task_struct进程描述符是描述一个进程所拥有的各种详细信息,按我们正常的理解来看,一个task_struct进程描述符本身就代表了一个进程。它就是一个进程的实体,这样理解我认为也是可以的。
注意以上标注为“红色”的字段,有很大一部分都是struct list_head结构体,这个结构体的作用是非常巨大的,它的作用好比是针线,可以将许多相关的原材料串起来,对,它就是链表结构。list_head结构如下:
struct list_head { struct list_head *next, *prev; };
这只是一个双向链表结点,有一个next和prev指针,这么看来,linux内核对进程描述符的管理方式大概就能猜到了。
所以从上图就可以看到,通过将list_head作为其中一个字段嵌入到task_struct结构体中,便使得task_struct具有双向链表的特性,这样就可以方便地遍历的寻找所有的进程了。完成这个功能的就是list_head task字段。但是,task字段的连接只是逻辑上将所有的进程串起来,此时双向链表结点的总数即为当前进程的总量。它并未考虑进一步的情况,比如,兄弟关系,父子关系等。下面来看看另外两个同样的字段:
struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parent's children list */
children字段的主要作用是将所有的孩子进程连接起来,这时候通过这个字段就可以寻找到自己的进程孩子结点,sibling字段主要作用是将兄弟进程连接起来。举个例子,如进程p0创建了三个子进程分别为P1,P2,P3,然后P3再创建一个子进程为P4,这们之间的关系如下图所示:
P0进程的children字段只记录它的第一个孩子P1的位置,通过这个孩子的sibling.next就可以找着P0的逐个孩子P2,P3,孩子兄弟间是通过slibing来访问的,每个孩子可以通过parent字段寻找到它们的父亲,P0的child.prev可以寻找到它的最后一个孩子进程,即P3,所以它们都是双向链表性质。以P3为例再进行分析,P3创建了进程P4,所以P3的children.next指向P4,由于P4是它唯一一个孩子,P3的chindren.prev还是指向P4,由于P4没有兄弟进程,所以它们的sibing.prev和sibling.next均指向P3进程。
所以稍微总结下,进程描述符中的task字段是将所有的进程串块一块,相当于将某一类型的数据进行归类,但是并未细分,但是children和sibling字段在此基础上进一步加工,将这一类的数据的内部逻辑关系进行了整合。这就好比一群人被集中起来,然后通过血缘的纽带关系我们可以很快地寻找到自己所属的那个羁绊。
进程是如何进行遍历的?
从上面分析可知,遍历当然是靠链表,不过这里主要讲述内核遍历进程时的接口,先来讨论链表的遍历接口,然后再讲述进程的遍历接口,思路基本相同,linux内核对链表的遍历或操作主要通过宏来进行:
list_add(n,p): 从p前面插入结点数据n,所以如果要把n插入到链表头,设置p为头结点
list_add_tail(n,p):与list_add(n,p)基本一致,区别在于插入的位置是在P的后面,所以如果要把n插入到链表尾部,设置p为头结点
list_del(p):删除P所指向的地址结点
list_empty(p):判断P所指向的地址结点是否为空
list_entry(p,t,m):返回数据结构类型t所在的地址,注意,t内包含list_head类型的字段m,并此这个字段的地址为p
list_for_each(p,h):扫描以h为头结点的链表元素,每次迭代,p中存放指向lis_head数据结构字段的地址
list_for_each_entry(p,h,m):同list_for_each有点类似,但是返回的p是包含list_head字段的所在数据结构的地址
linux内核就是凭借以上的链表接口来进行操作。对进程而言,进程的头结点即为init_task描述符,它也是task_struct结构类型,将init_task的task->prev地址作为参数p传入list_add_tail中,就可以将包含task_struct类型的进程描述符插入到链表的末尾。对进程而言SET_LINKS和REMOVE_LINKS宏用于插入或删除一个进程描述符结点。另一个有用的进程宏是for_each_process,用于扫描整个进程列表。使用如下:
#define for_each_process(p) \ for (p = &init_task; ( p=list_entry( (p)->tasks.next, struct task_struct, tasks) ) != &init_task;)
内核如何寻找需要运行的进程?
当内核需要寻找一个新的进程到CPU上运行时,内核必须只考虑状态为可运行(TASK__RUNNING)的进程。
早期linux版本将所有可执行的进程放置于同一个链表运行队列中。但是维护它的代价实在是太高,并且随着进程数量的增多性能下降很快。从linux2.6开始实现了不同的运行队列。它的目标是允许调度器在同一个时间值内寻找到可运行的进程,并且完全不受进程数量的依赖。它的实现技巧为对每个优先级K都有对应的可运行队列,在进程描述符中有一个list_head run_list,这个域将相同优先级的可运行进程链接起来,而且在多核系统中,每个CPU也将有它自己的运行队列。这是一种通过增加数据结构的复杂性来提高性能的经典案例:它使得调度器操作更加高效,运行队列被分散成140个不同的链表。