Linux进程模型简析

1.进程的组织

1.1进程定义

  百度百科中,进程定义如下:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

  一个进程在CPU上运行可以有两种运行模式(进程状态):用户模式和内核模式。如果当前运行的是用户程序(用户代码),那么对应进程就处于用户模式(用户态),如果出现系统调用或者发生中断,那么对应进程就处于内核模式(核心态)。

  在Linux操作系统下,我们通过ps指令实现进程的查看,如下:

1.2进程的组织

   Linux进程通过一个task_struct结构体描述,在linux/sched.h中定义,通过理解该结构,可更清楚的理解linux进程模型。包含进程所有信息的task_struct数据结构是比较庞大的,但是该数据结构本身并不复杂,我们将它的所有域按其功能可做如下划分:

  •    进程状态(State)
  •    进程调度信息(Scheduling Information)
  •    各种标识符(Identifiers)
  •    进程通信有关信息(IPC:Inter_Process Communication)
  •    时间和定时器信息(Times and Timers)
  •    进程链接信息(Links)
  •    文件系统信息(File System)
  •    虚拟内存信息(Virtual Memory)
  •    页面管理信息(page)
  •    对称多处理器(SMP)信息
  •    和处理器相关的环境(上下文)信息(Processor Specific Context)
  •    其他信息

1.3标识符(Identifiers

  进程标识符在task_struct类中如下定义:

pid_t pid;//内核中用以标识进程的id
    pid_t tgid;//用来实现线程机制

 

struct pid
{
    atomic_t count;
    unsigned int level;
    /* lists of tasks that use this pid */
    struct hlist_head tasks[PIDTYPE_MAX];
    struct rcu_head rcu;
    struct upid numbers[1];
};
* What is struct pid?
*
* A struct pid is the kernel's internal notion of a process identifier.
* It refers to individual tasks, process groups, and sessions. While
* there are processes attached to it the struct pid lives in a hash
* table, so it and then the processes that it refers to can be found
* quickly from the numeric pid value. The attached processes may be
* quickly accessed by following pointers from struct pid.

 

  每个进程有进程标识符、用户标识符、组标识符,如下:

域名 含义
Pid 进程标识符
Uid、gid 用户标识符、组标识符
Euid、egid 有效用户标识符、有效组标识符
Suid、sgid 备份用户标识符、备份组标识符
Fsuid、fsgid

文件系统用户标识符、文件系统组标识符

 

  不管对内核还是普通用户来说,怎么用一种简单的方式识别不同的进程呢?这就引入了进程标识符(PID:process identifier),每个进程都有一个唯一的标识符,内核通过这个标识符来识别不同的进程,同时,进程标识符PID也是内核提供给用户程序的接口,用户程序通过PID对进程发号施令。PID是32位的无符号整数,它被顺序编号:新创建进程的PID通常是前一个进程的PID加1。然而,为了与16位硬件平台的传统Linux系统保持兼容,在Linux上允许的最大PID号是32767,当内核在系统中创建第32768个进程时,就必须重新开始使用已闲置的PID号。

2..进程状态

  为了对进程从产生到消亡的整个过程进行跟踪和描述,就需要定义各种进程的各种状态并制定相应的状态转换策略,以此来控制进程的运行。

  Linux系统中,进程状态在 task_struct 中定义如下:

volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */

  其状态取值如下:

#define TASK_RUNNING        0
#define TASK_INTERRUPTIBLE  1
#define TASK_UNINTERRUPTIBLE    2
#define __TASK_STOPPED      4
#define __TASK_TRACED       8
/* in tsk->exit_state */
#define EXIT_ZOMBIE     16
#define EXIT_DEAD       32
/* in tsk->state again */
#define TASK_DEAD       64
#define TASK_WAKEKILL       128
#define TASK_WAKING     256
#define TASK_STATE_MAX      512
 

 

对进程每个状态简析如下:

  • TASK_RUNNING (可运行状态):处于这种状态的进程,要么正在运行、要么正准备运行。正在运行的进程就是当前进程(由current所指向的进程),而准备运行的进程只要得到CPU就可以立即投入运行,CPU是这些进程唯一等待的系统资源。
  • TASK_INTERRUPTIBLE(可中断的等待状态):表示进程被阻塞(睡眠),直到某个条件达成,进程的状态就被设置为TASK_RUNNING。处于该状态的进程正在等待某个事件(event)或某个资源,而被挂起。对应的task_struct结构被放入对应事件的等待队列中。处于可中断等待态的进程可以被信号(外部中断触发或者其他进程触发)唤醒,如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度。
  • TASK_UNINTERRUPTIBLE(不可中断的等待状态):该状态与 TASK_INTERRUPTIBLE 状态类似,也表示进程被阻塞,处于睡眠状态。当进程等待的某些条件被满足了之后,内核也会将该进程的状态设置为 TASK_RUNNING。但是,处于这个状态下的进程不能在接收到某个信号之后立即被唤醒。这时该状态与 TASK_INTERRUPTIBLE 状态唯一的区别。
  • __TASK_STOPPED(暂停状态):此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。
  • __TASK_TRACED(跟踪状态):当前进程正在被另一个进程所监视。
  • EXIT_ZOMBIE(僵死状态):进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。
  • EXIT_DEAD:一个进程的最终状态。

以下是LINUX进程间状态转换和内核调用图解

 

3.进程的调度

3.1 进程调度概述

  内存中保存了对每个进程的唯一描述,,并通过若干结构与其他进程连接起来。调度器面对的情形就是这样, 其任务是在程序之间共享CPU时间, 创造并行执行的错觉, 该任务分为两个不同的部分,,其中一个涉及调度策略, 另外一个涉及上下文切换。

3.2Linux调度器的发展

  一开始的调度器是复杂度为O(n)的始调度算法(实际上每次会遍历所有任务,所以复杂度为O(n)),这个算法的缺点是当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从linux2.5开始引入赫赫有名的O(1)调度器。可没过多久,一个优秀的调度器就被开发出来了,它就是CFS调度器Completely Fair Scheduler。 这个是在2.6内核中引入的,具体为2.6.23,即从此版本开始,内核使用CFS作为它的默认调度器,O(1)调度器被抛弃了。

3.3 O(1)调度算法

3.3.1优先级数组

  O(1)算法的一个核心数据结构即为prio_array结构体。该结构体中有一个用来表示进程动态优先级的数组queue,它包括了每一种优先级进程所形成的链表。

#define
 MAX_USER_RT_PRIO        100
#define
 MAX_RT_PRIO             MAX_USER_RT_PRIO
#define
 MAX_PRIO                (MAX_RT_PRIO + 40)
typedef struct prio_array
 prio_array_t;
struct prio_array
 {
        unsigned int nr_active;
        unsigned long bitmap[BITMAP_SIZE];
        struct list_head
 queue[MAX_PRIO];
};

 

  因为进程优先级的最大值为139,因此MAX_PRIO的最大值取140(详细的是,普通进程使用100到139的优先级。实时进程使用0到99的优先级)。因此,queue数组中包括140个可执行状态的进程链表,每一条优先级链表上的进程都具有同样的优先级,而不同进程链表上的进程都拥有不同的优先级。除此之外,prio_array结构中还包含一个优先级位图bitmap。该位图使用一个位(bit)来代表一个优先级,而140个优先级最少须要5个32位来表示,因此BITMAP_SIZE的值取5。起初。该位图中的全部位都被置0,当某个优先级的进程处于可执行状态时。该优先级所相应的位就被置1。因此,O(1)算法中查找系统最高的优先级就转化成查找优先级位图中第一个被置1的位。与2.4内核中依次比较每一个进程的优先级不同,因为进程优先级个数是定值,因此查找最佳优先级的时间恒定。它不会像曾经的方法那样受可运行进程数量的影响。假设确定了优先级。那么选取下一个进程就简单了,仅仅需在queue数组中相应的链表上选取一个进程就可以。

3.3.2活动进程和过期进程

  当处于执行态的进程用完时间片后就会处于就绪态。此时调度程序再从就绪态的进程中选取一个作为即将要执行的进程。而在详细Linux内核中,就绪态和执行态统一称为可执行态(TASK_RUNNING)。

  对于系统内处于可执行状态的进程,我们能够分为三类。首先是正处于执行状态的那个进程;其次,有一部分处于可执行状态的进程则还没实用完他们的时间片。他们等待被执行;剩下的进程已经用完了自己的时间片,在其它进程没实用完它们的时间片之前,他们不能再被执行。据此,我们将进程分为两类,活动进程,那些还没实用完时间片的进程。过期进程。那些已经用完时间片的进程。因此,调度程序的工作就是在活动进程集合中选取一个最佳优先级的进程,假设该进程时间片恰好用完,就将该进程放入过期进程集合中。

  在可执行队列结构中,arrays数组的两个元素分别用来表示刚才所述的活动进程集合和过期进程集合,active和expired两个指针分别直接指向这两个集合。

关于可执行队列和两个优先级数组的关系可參考以下的图:

3.4全然公平(CFS)调度算法

3.4.1概述

  CFS 调度程序使用安抚(appeasement)策略确保公平性。当某个任务进入运行队列后,将记录当前时间,当某个进程等待 CPU 时,将对这个进程的 wait_runtime 值加一个数,这个数取决于运行队列当前的进程数。当执行这些计算时,也将考虑不同任务的优先级值。 将这个任务调度到 CPU 后,它的 wait_runtime 值开始递减,当这个值递减到其他任务成为红黑树的最左侧任务时,当前任务将被抢占。通过这种方式,CFS 努力实现一种理想 状态,即 wait_runtime 值为 0。

3.4.2CFS调度器类fair_sched_class

  CFS完全公平调度器的调度器类叫fair_sched_class, 其定义在kernel/sched/fair.c,line 8521, 它是我们熟知的是struct sched_class调度器类类型, 将我们的CFS调度器与一些特定的函数关联起来.

/*
 * All the scheduling class methods:
 */
const struct sched_class fair_sched_class = {
        .next                   = &idle_sched_class,  /*  下个优先级的调度类, 所有的调度类通过next链接在一个链表中*/
        .enqueue_task           = enqueue_task_fair,
        .dequeue_task           = dequeue_task_fair,
        .yield_task             = yield_task_fair,
        .yield_to_task          = yield_to_task_fair,

        .check_preempt_curr     = check_preempt_wakeup,

        .pick_next_task         = pick_next_task_fair,
        .put_prev_task          = put_prev_task_fair,

#ifdef CONFIG_SMP
        .select_task_rq         = select_task_rq_fair,
        .migrate_task_rq        = migrate_task_rq_fair,

        .rq_online              = rq_online_fair,
        .rq_offline             = rq_offline_fair,

        .task_waking            = task_waking_fair,
        .task_dead              = task_dead_fair,
        .set_cpus_allowed       = set_cpus_allowed_common,
#endif

        .set_curr_task          = set_curr_task_fair,
        .task_tick              = task_tick_fair,
        .task_fork              = task_fork_fair,

        .prio_changed           = prio_changed_fair,
        .switched_from          = switched_from_fair,
        .switched_to            = switched_to_fair,

        .get_rr_interval        = get_rr_interval_fair,

        .update_curr            = update_curr_fair,

#ifdef CONFIG_FAIR_GROUP_SCHED
        .task_move_group        = task_move_group_fair,
#endif
};

 

  下面就这调度器类成员进行介绍:

  • enqueue_task:向就绪队列中添加一个进程, 某个任务进入可运行状态时,该函数将得到调用。它将调度实体(进程)放入红黑树中,并对 nr_running 变量加 1。
  • dequeue_task:将一个进程从就就绪队列中删除, 当某个任务退出可运行状态时调用该函数,它将从红黑树中去掉对应的调度实体,并从 nr_running 变量中减 1。
  • yield_task:在进程想要资源放弃对处理器的控制权的时, 可使用在sched_yield系统调用, 会调用内核API yield_task完成此工作. compat_yield sysctl 关闭的情况下,该函数实际上执行先出队后入队;在这种情况下,它将调度实体放在红黑树的最右端。
  • check_preempt_curr:该函数将检查当前运行的任务是否被抢占。在实际抢占正在运行的任务之前,CFS 调度程序模块将执行公平性测试。这将驱动唤醒式(wakeup)抢占。
  • pick_next_task:该函数选择接下来要运行的最合适的进程。
  • put_prev_task:用另一个进程代替当前运行的进程。
  • set_curr_task:当任务修改其调度类或修改其任务组时,将调用这个函数。
  • task_tick:在每次激活周期调度器时, 由周期性调度器调用, 该函数通常调用自 time tick 函数;它可能引起进程切换。这将驱动运行时(running)抢占。
  • task_new:内核调度程序为调度模块提供了管理新任务启动的机会, 用于建立fork系统调用和调度器之间的关联, 每次新进程建立后, 则用new_task通知调度器, CFS 调度模块使用它进行组调度,而用于实时任务的调度模块则不会使用这个函数。

3.4.3CFS的就绪队列

  就绪队列是全局调度器许多操作的起点, 但是进程并不是由就绪队列直接管理的, 调度管理是各个调度器的职责, 因此在各个就绪队列中嵌入了特定调度类的子就绪队列(cfs的顶级调度就队列struct cfs_rq, 实时调度类的就绪队列struct rt_rq和deadline调度类的就绪队列struct dl_rq)。

/* CFS-related fields in a runqueue */
/* CFS调度的运行队列,每个CPU的rq会包含一个cfs_rq,而每个组调度的sched_entity也会有自己的一个cfs_rq队列 */
struct cfs_rq {
    /* CFS运行队列中所有进程的总负载 */
    struct load_weight load;
    /*
     *  nr_running: cfs_rq中调度实体数量
     *  h_nr_running: 只对进程组有效,其下所有进程组中cfs_rq的nr_running之和
    */
    unsigned int nr_running, h_nr_running;

    u64 exec_clock;

    /*
     * 当前CFS队列上最小运行时间,单调递增
     * 两种情况下更新该值: 
     * 1、更新当前运行任务的累计运行时间时
     * 2、当任务从队列删除去,如任务睡眠或退出,这时候会查看剩下的任务的vruntime是否大于min_vruntime,如果是则更新该值。
     */

    u64 min_vruntime;
#ifndef CONFIG_64BIT
    u64 min_vruntime_copy;
#endif
    /* 该红黑树的root */
    struct rb_root tasks_timeline;
     /* 下一个调度结点(红黑树最左边结点,最左边结点就是下个调度实体) */
    struct rb_node *rb_leftmost;

    /*
     * 'curr' points to currently running entity on this cfs_rq.
     * It is set to NULL otherwise (i.e when none are currently running).
     * curr: 当前正在运行的sched_entity(对于组虽然它不会在cpu上运行,但是当它的下层有一个task在cpu上运行,那么它所在的cfs_rq就把它当做是该cfs_rq上当前正在运行的sched_entity)
     * next: 表示有些进程急需运行,即使不遵从CFS调度也必须运行它,调度时会检查是否next需要调度,有就调度next
     *
     * skip: 略过进程(不会选择skip指定的进程调度)
     */
    struct sched_entity *curr, *next, *last, *skip;

#ifdef  CONFIG_SCHED_DEBUG
    unsigned int nr_spread_over;
#endif

#ifdef CONFIG_SMP
    /*
     * CFS load tracking
     */
    struct sched_avg avg;
    u64 runnable_load_sum;
    unsigned long runnable_load_avg;
#ifdef CONFIG_FAIR_GROUP_SCHED
    unsigned long tg_load_avg_contrib;
#endif
    atomic_long_t removed_load_avg, removed_util_avg;
#ifndef CONFIG_64BIT
    u64 load_last_update_time_copy;
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
    /*
     *   h_load = weight * f(tg)
     *
     * Where f(tg) is the recursive weight fraction assigned to
     * this group.
     */
    unsigned long h_load;
    u64 last_h_load_update;
    struct sched_entity *h_load_next;
#endif /* CONFIG_FAIR_GROUP_SCHED */
#endif /* CONFIG_SMP */

#ifdef CONFIG_FAIR_GROUP_SCHED
    /* 所属于的CPU rq */
    struct rq *rq;  /* cpu runqueue to which this cfs_rq is attached */

    /*
     * leaf cfs_rqs are those that hold tasks (lowest schedulable entity in
     * a hierarchy). Non-leaf lrqs hold other higher schedulable entities
     * (like users, containers etc.)
     *
     * leaf_cfs_rq_list ties together list of leaf cfs_rq's in a cpu. This
     * list is used during load balance.
     */
    int on_list;
    struct list_head leaf_cfs_rq_list;
    /* 拥有该CFS运行队列的进程组 */
    struct task_group *tg;  /* group that "owns" this runqueue */

#ifdef CONFIG_CFS_BANDWIDTH
    int runtime_enabled;
    u64 runtime_expires;
    s64 runtime_remaining;

    u64 throttled_clock, throttled_clock_task;
    u64 throttled_clock_task_time;
    int throttled, throttle_count;
    struct list_head throttled_list;
#endif /* CONFIG_CFS_BANDWIDTH */
#endif /* CONFIG_FAIR_GROUP_SCHED */
};

成员描述:

  • nr_running:队列上可运行进程的数目。
  • load:就绪队列上可运行进程的累计负荷权重。
  • min_vruntime:跟踪记录队列上所有进程的最小虚拟运行时间. 这个值是实现与就绪队列相关的虚拟时钟的基础。
  • tasks_timeline:用于在按时间排序的红黑树中管理所有进程。
  • rb_leftmost:总是设置为指向红黑树最左边的节点, 即需要被调度的进程. 该值其实可以可以通过病例红黑树获得, 但是将这个值存储下来可以减少搜索红黑树花费的平均时间。
  • curr:当前正在运行的sched_entity(对于组虽然它不会在cpu上运行,但是当它的下层有一个task在cpu上运行,那么它所在的cfs_rq就把它当做是该cfs_rq上当前正在运行的sched_entity。
  • next:表示有些进程急需运行,即使不遵从CFS调度也必须运行它,调度时会检查是否next需要调度,有就调度next。
  • skip:略过进程(不会选择skip指定的进程调度)。

  3.4.4红黑树

  红黑树 (Red–black tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构。在 CFS 调度器中将 sched_entity 存储在以时间为顺序的红黑树中,vruntime 最低的进程存储在树的左侧,vruntime 最高的进程存储在树的右侧。 为了公平,调度器每次都会选取红黑树最左端的节点调度以便保持公平性。这样,整颗红黑树最左侧的进程就被给予时间运行了,树的内容从右侧迁移到左侧以保持公平。同时,红黑树的时间复杂度为 O(log n),可以快速高效地执行插入或是删除操作。如下图:

 

4.小结

  进程是一种数据结构,进程的引入为了使程序在多道程序环境下能并发执行。尽管进程只是计算机操作系统中很小的一方面,但它却为了整个系统运行提供了极大地帮助。我们完全可以将进程比拟成我们现今纷繁复杂的世界,我们人类便是一个个程序,如何更好的调整社会朝着安稳和谐快速发展,这是我们要努力的。在如今计算机越来越复杂的情况下,我们急需对进程进行更深入的研究。

5.参考资料

1.https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503?fr=aladdin

2.http://blog.chinaunix.net/uid-24203478-id-3130713.html

3.https://blog.csdn.net/jinkang_zhao/article/details/71367924

4.https://blog.csdn.net/gatieme/article/details/51701149

5.https://blog.csdn.net/gatieme/article/details/52067518

 

posted @ 2018-04-27 21:08  我无我  阅读(2382)  评论(0编辑  收藏  举报