第一次作业:基于Linux2.6内核源码进程模型分析

时间:2022-09-20 16:46:06

1、概括

  • 进程的基本概念
  • 操作系统是如何组织进程的
  • 进程是如何调度的
  • 对Linux操作系统进程模型的看法

2、什么是进程

一个进程就是一个正在运行的程序。一个进程应该包含以下内容:
(1) 程序的代码,既然进程是一个正在运行的程序,自然需要程序的代码
(2) 程序的数据
(3) CPU寄存器的值,包括通用寄存器,程序计数器
(4) 堆(heap)是用来保存进程运行时动态分配的内存空间
(5) 栈(stack)有两个用途,1保存运行的上下文信息。2在函数调用时保存被调用函数的形参或者局部变量
(6) 进程所占用的一组系统资源,如打开的文件

在 Windows 系统中只要打开任务管理器即可看到当前正在运行的进程的一些基本信息:

第一次作业:基于Linux2.6内核源码进程模型分析

在Linux操作系统下,通过ps指令查看进程:

第一次作业:基于Linux2.6内核源码进程模型分析

3、进程的组织

进程一般由进程控制块(PCB)、程序段和数据段组成。

进程创建时,操作系统就新建一个PCB结构,它之后就常驻内存,任一时刻可以存取, 在进程结束时删除。PCB是进程实体的一部分,是进程存在的唯一标志。
当创建一个进程时,系统为该进程建立一个PCB;当进程执行时,系统通过其PCB 了 解进程的现行状态信息,以便对其进行控制和管理;当进程结束时,系统收回其PCB,该进 程随之消亡。操作系统通过PCB表来管理和控制进程。

对于一个真实的操作系统可能不叫PCB,比如Linux中叫做任务结构体(task struct)

PCB通常包括以下内容:

第一次作业:基于Linux2.6内核源码进程模型分析

1、进程标识符PID

进程标识符:标志各个进程,每个进程都有一个并且是唯一的标识号。

PID的定义:

pid_t pid;

 PID的取值范围是0~32767,即Linux操作系统中可以有32768个进程

#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 :0x8000)

2、进程的状态

#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

     系统中的每个进程都必然处于以上所列进程状态中的一种。

  •     TASK_RUNNING :运行态或就绪态。表示进程要么正在执行,要么正要准备执行。
  •     TASK_INTERRUPTIBLE :浅睡眠状态。表示进程被阻塞。能响应信号,直到被唤醒,进程的状态就被设置为 TASK_RUNNING。
  •     TASK_UNINTERRUPTIBLE :深睡眠状态。的意义与TASK_INTERRUPTIBLE类似,无法响应信号。
  •                                                     该进程等待一个事件的发生或某种系统资源。
  •     __TASK_STOPPED :停止态 。表示进程被停止执行。
  •     __TASK_TRACED : 对于进程本身来说,TASK_STOPPED和TASK_TRACED状态很类似,都是表示进程暂停下来。
  •                                     而TASK_TRACED状态相当于在TASK_STOPPED之上多了一层保护,处于TASK_TRACED状态
  •                                          的进程不能响应SIGCONT信号而被唤醒。只能等到调试进程通过ptrace系统调用执行PTRACE_CONT、
  •                                          PTRACE_DETACH等操作(通过ptrace系统调用的参数指定操作),或调试进程退出,
  •                                           被调试的进程才能恢复TASK_RUNNING状态。
  •     EXIT_ZOMBIE :僵尸态。此时进程不能被调度,但是PCB 未被释放。
  •     EXIT_DEAD :死亡态。表示一个已终止的进程,其PCB被释放。EXIT_ZOMBIE和EXIT_DEAD 也可以存放在 exit_state 成员中。

3、进程状态的切换过程和原因大致如下图(图片来自《Linux Kernel Development》):

第一次作业:基于Linux2.6内核源码进程模型分析

4、进程的调度

4.1 完全公平调度器(Completely Fair Scheduler ,CFS)

CFS在2.6.23版本中首次被集成到内核中,仍然是处理非实时任务的默认调度器。它的主要思想是使用一棵红黑树作为调度队列的数据结构。根据任务在CPU上运行的时间长短而将其有序地排列在树中,这种时间被称为虚拟运行时间(vruntime)。CFS采用ns级的粒度来说明任务的运行时间。

如图所示,树中的每个内部节点对应一个任务。左侧的子节点对应于在CPU上运行时间更少的任务,因此左侧的任务会更早地被调度,右侧的子节点是那些迄今消耗CPU时间较多的任务,叶子节点在调度中不起任何作用。

第一次作业:基于Linux2.6内核源码进程模型分析

 

4.2 调度实体

linux中可调度的不仅仅是进程,也可能是一个进程组,所以LInux就把调度对象抽象化成一个调度实体。就像是很多结构中嵌入list_node用于连接链表一样,这里需要执行调度的也就需要加入这样一个调度实体。实际上,调度器直接操作的也是调度实体,只是会根据调度实体获取到其对应的结构。 vruntime 记录在进程执行期间,在虚拟时钟上流逝的时间,用于CFS调度器

struct sched_entity {
    struct load_weight  load;       /* for load-balancing */
    struct rb_node      run_node;
    struct list_head    group_node;
    unsigned int        on_rq;

    u64         exec_start;    
    u64         sum_exec_runtime; 
    u64         vruntime;   
    u64         prev_sum_exec_runtime;

    u64         last_wakeup;
    u64         avg_overlap;

#ifdef CONFIG_SCHEDSTATS
    u64         wait_start;
    u64         wait_max;
    u64         wait_count;
    u64         wait_sum;

    u64         sleep_start;
    u64         sleep_max;
    s64         sum_sleep_runtime;

    u64         block_start;
    u64         block_max;
    u64         exec_max;
    u64         slice_max;

    u64         nr_migrations;
    u64         nr_migrations_cold;
    u64         nr_failed_migrations_affine;
    u64         nr_failed_migrations_running;
    u64         nr_failed_migrations_hot;
    u64         nr_forced_migrations;
    u64         nr_forced2_migrations;

    u64         nr_wakeups;
    u64         nr_wakeups_sync;
    u64         nr_wakeups_migrate;
    u64         nr_wakeups_local;
    u64         nr_wakeups_remote;
    u64         nr_wakeups_affine;
    u64         nr_wakeups_affine_attempts;
    u64         nr_wakeups_passive;
    u64         nr_wakeups_idle;
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
    struct sched_entity *parent;
    /* rq on which this entity is (to be) queued: */
    struct cfs_rq       *cfs_rq;
    /* rq "owned" by this entity/group: */
    struct cfs_rq       *my_q;
#endif
};

4.2  进程的选择过程

1、schedule_tick函数:其主要作用就是根据进程运行时间触发调度;在进程遇到资源等待被阻塞也可以显示的调用调度器函数进行调度。

static void __sched __schedule(void)
 {
     struct task_struct *prev, *next;
     unsigned long *switch_count;
     struct rq *rq;
     int cpu;
 
 need_resched:
     /*禁止内核抢占*/
     preempt_disable();
     cpu = smp_processor_id();
     /*获取CPU 的调度队列*/
     rq = cpu_rq(cpu);
     rcu_note_context_switch(cpu);
     /*保存当前任务*/
     prev = rq->curr;
 
     schedule_debug(prev);
 
     if (sched_feat(HRTICK))
         hrtick_clear(rq);
 
     /*
      * Make sure that signal_pending_state()->signal_pending() below
      * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
      * done by the caller to avoid the race with signal_wake_up().
      */
     smp_mb__before_spinlock();
     raw_spin_lock_irq(&rq->lock);
 
     switch_count = &prev->nivcsw;
      /*  如果内核态没有被抢占, 并且内核抢占有效
         即是否同时满足以下条件:
         1  该进程处于停止状态
         2  该进程没有在内核态被抢占 */
     if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
         if (unlikely(signal_pending_state(prev->state, prev))) {
             prev->state = TASK_RUNNING;
         } else {
             deactivate_task(rq, prev, DEQUEUE_SLEEP);
             prev->on_rq = 0;
 
             /*
              * If a worker went to sleep, notify and ask workqueue
              * whether it wants to wake up a task to maintain
              * concurrency.
              */
             if (prev->flags & PF_WQ_WORKER) {
                 struct task_struct *to_wakeup;
 
                 to_wakeup = wq_worker_sleeping(prev, cpu);
                 if (to_wakeup)
                     try_to_wake_up_local(to_wakeup);
             }
         }
         switch_count = &prev->nvcsw;
     }
 
     pre_schedule(rq, prev);
 
     if (unlikely(!rq->nr_running))
         idle_balance(cpu, rq);
     /*告诉调度器prev进程即将被调度出去*/
     put_prev_task(rq, prev);
     /*挑选下一个可运行的进程*/
     next = pick_next_task(rq);
     /*清除pre的TIF_NEED_RESCHED标志*/
     clear_tsk_need_resched(prev);
     rq->skip_clock_update = 0;
    /*如果next和当前进程不一致,就可以调度*/
     if (likely(prev != next)) {
         rq->nr_switches++;
         /*设置当前调度进程为next*/
         rq->curr = next;
         ++*switch_count;
         /*切换进程上下文*/
         context_switch(rq, prev, next); /* unlocks the rq */
         /*
          * The context switch have flipped the stack from under us
          * and restored the local variables which were saved when
          * this task called schedule() in the past. prev == current
          * is still correct, but it can be moved to another cpu/rq.
          */
         cpu = smp_processor_id();
         rq = cpu_rq(cpu);
     } else
         raw_spin_unlock_irq(&rq->lock);
 
     post_schedule(rq);
   
     sched_preempt_enable_no_resched();
     if (need_resched())
         goto need_resched;
}

2、pick_next_task()函数: 该函数还是处于主调度器的层面,没有涉及到核心逻辑,所以还比较好理解。首先判断当前CPu就绪队列上的可运行进程数和CFS就绪队列上的可运行进程数是否一致,如果一致就说明当前主就绪队列上没有只有CFS调度类的进程,那么这样直接调用CFS调度类的方法挑选下一个进程即可。否则还需要从*的调度类,层层选择。按照stop_sched_class->rt_scheduled_class->fair_schedled_class->idle_sched_class这个顺序,依次调用其pick函数,只有前一个调度类没有找到可运行的进程,才会查找后一个调度类。

static inline struct task_struct *
 pick_next_task(struct rq *rq)
 {
     const struct sched_class *class;
     struct task_struct *p;
 
     /*
      * Optimization: we know that if all tasks are in
      * the fair class we can call that function directly:
      */
      /*如果所有任务都处于完全公平调度类,则可以直接选择下一个任务*/
     if (likely(rq->nr_running == rq->cfs.h_nr_running)) {
         p = fair_sched_class.pick_next_task(rq);
         if (likely(p))
             return p;
     }
     /*从优先级最高的调度器类开始遍历,顺序为stop_sched_class->rt_scheduled_class->fair_schedled_class->idle_sched_class*/
     /*
     #define for_each_class(class) \
    for (class = sched_class_highest; class; class = class->next)
     */
     for_each_class(class) {
         p = class->pick_next_task(rq);
         if (p)
             return p;
     }
 
     BUG(); /* the idle class will always have a runnable task */
 }

 5、对操作系统的看法

操作系统(Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

6、参考资料