深入源码分析进程模型

Posted zjmu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入源码分析进程模型相关的知识,希望对你有一定的参考价值。

1.操作系统是怎么组织进程的

struct task_struct {

......

/* 进程状态 */
volatile long state;
/* 指向内核栈 */
void *stack;
/* 用于加入进程链表 */
struct list_head tasks;
......

/* 指向该进程的内存区描述符 */
struct mm_struct *mm, *active_mm;

........

/* 进程ID,每个进程(线程)的PID都不同 */
pid_t pid;
/* 线程组ID,同一个线程组拥有相同的pid,与领头线程(该组中第一个轻量级进程)pid一致,保存在tgid中,线程组领头线程的pid和tgid相同 */
pid_t tgid;
/* 用于连接到PID、TGID、PGRP、SESSION哈希表 */
struct pid_link pids[PIDTYPE_MAX];

........

/* 指向创建其的父进程,如果其父进程不存在,则指向init进程 */
struct task_struct __rcu *real_parent;
/* 指向当前的父进程,通常与real_parent一致 */
struct task_struct __rcu *parent;

/* 子进程链表 */
struct list_head children;
/* 兄弟进程链表 */
struct list_head sibling;
/* 线程组领头线程指针 */
struct task_struct *group_leader;

/* 在进程切换时保存硬件上下文(硬件上下文一共保存在2个地方: thread_struct(保存大部分CPU寄存器值,包括内核态堆栈栈顶地址和IO许可权限位),内核栈(保存eax,ebx,ecx,edx等通用寄存器值)) */
struct thread_struct thread;

/* 当前目录 */
struct fs_struct *fs;

/* 指向文件描述符,该进程所有打开的文件会在这里面的一个指针数组里 */
struct files_struct *files;

........

  /* 信号描述符,用于跟踪共享挂起信号队列,被属于同一线程组的所有进程共享,也就是同一线程组的线程此指针指向同一个信号描述符 */
  struct signal_struct *signal;
  /* 信号处理函数描述符 */
  struct sighand_struct *sighand;

  /* sigset_t是一个位数组,每种信号对应一个位,linux中信号最大数是64
   * blocked: 被阻塞信号掩码
   * real_blocked: 被阻塞信号的临时掩码
   */
  sigset_t blocked, real_blocked;
  sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
  /* 私有挂起信号队列 */
  struct sigpending pending;


........
}

 

/* wq为某个等待队列的队列头 */
void sleep_on (wait_queue_head_t *wq)
{
    /* 声明一个等待队列结点 */
    wait_queue_t wait;

    /* 用当前进程初始化这个等待队列结点 */
    init_waitqueue_entry (&wait, current);

    /* 设置当前进程状态为TASK_UNINTERRUPTIBLE */
    current->state = TASK_UNINTERRUPTIBLE;

    /* 将这个代表着当前进程的等待队列结点加入到wq这个等待队列 */
    add_wait_queue (wq, &wait);

    /* 请求调度器进行调度,执行完schedule后进程会被移除CPU运行队列,只有等待队列唤醒后才会重新回到CPU运行队列 */
    schedule ();

    /* 这里进程已经被等待队列唤醒,重新移到CPU运行队列,也就是等待的条件已经为真,唤醒后第一件事就是将自己从
等待队列wq中移除 */ remove_wait_queue (wq, &wait); }


   所有处于TASK_RUNNING状态的进程都会被放入CPU的运行队列,它们有可能在不同CPU的运行队列中。

  系统没有为TASK_STOPED、EXIT_ZOMBIE和EXIT_DEAD状态的进程建立专门的链表,因为处于这些状态的进程访问比较简单,可通过PID和通过特定父进程的子进程链表进行访问。

  所有TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE都会被放入相应的等待队列,系统中有很多种等待队列,有些是等待磁盘操作的终止,有些是等待释放系统资源,有些是等待时间经过固定的间隔,每个等待队列它的唤醒条件不同,比如等待队列1是等待系统释放资源A的,等待队列2是等待系统释放资源B的。因此,等待队列表示一组睡眠进程,当某一条件为真时,由内核唤醒这条等待队列上的进程。

  等待队列是等待系统释放资源A,而等待队列中所有的进程都是希望能够占有这个资源A的,就像我们编程中用到的信号量,这时候系统的做法不是将这个等待队列中所有的进程都进行唤醒,而是只唤醒一个。内核区分这种互斥进程的原理就是这个等待队列中所有的等待队列结点wait_queue_t中的flags被设置为1(默认是0)

 

2.进程状态如何转换(给出进程状态转换图)

Linux进程状态有:
R (TASK_RUNNING),可执行状态。
S (TASK_INTERRUPTIBLE),可中断的睡眠状态。
D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。
Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。
T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态。
X (TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁。

         

   只有在该状态的进程才可能在CPU上运行。同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)
被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器从各个CPU的可执行队列中分别选择
一个进程在该CPU上运行。

   正在CPU上执行的进程定义为RUNNING状态、可执行但尚未被调度执行的进程定义为READY状态,这两种状态统一为
TASK_RUNNING状态。

   只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换操作。在内核态下运行的进程不能被其它进程抢占,而且一个进程不能改变另一个进程的状态。为了避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断。

 

进程的三种基本状态

就绪(Ready)状态

当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。

执行(Running)状态

当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。

阻塞(Blocked)状态

正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

 (1) 就绪→执行处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。

 (2) 执行→就绪处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完而不得不让出处理机,于是进程从执行状态转变成就绪状态。

 (3) 执行→阻塞正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。

 (4) 阻塞→就绪处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态。

 

3.进程是如何调度的

 

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;
}

 

  进程提供了两种优先级,一种是普通的进程优先级,第二个是实时优先级,前者使用SCHEED_NORMAL调度策略,后者可选SCHED_FIFO或SCHED_rr调度。任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占,同时实时进程之间是按照FIFO(一次机会做完)或者RR(多次轮转)规则调度的
首先说一下实时进程的调度:
   实时进程,只有静态优先级,因为内核不会根据休眠时间等因素对其静态优先级做调整,默认的实时优先级范围是0~99
不同于普通的进程,系统调度时。实时优先级高的进程总是先于优先级低的进程执行,直到实时优先级高的实时进程无法执行。如果有数个优先级相同的实时进程,那么系统就会按照进程出现在队列上的顺序选择进程。
不同的调度策略的实时进程只有在相同优先级的时候才有可比性:
1)对于FIFO的进程,意味着只有当前进程执行完毕才会轮到其他进程执行。由此可见相当霸道。
2)对于RR进程,一旦时间片消耗完毕,则会将该进程置于队列的末尾,然后运行其他相同优先级的进程,如果没有其他相同优先级的进程,则该进程会继续执行。

  对于实时进程,高优先级的进程先执行,他执行到没法执行,采会轮到优先级低的进程执行。等级制度行当森严。
普通进程:
SCHED_ORHER:基于动态优先级进行调度,其动态优先级可以理解为调度器为每个进程根据多个因素计算出的权值。

4.谈谈自己对该操作系统进程模型的看法

Linux系统是一个具有先天病毒免疫能力的操作系统,很少受到病毒攻击。对于一个开放式系统而言,在方便用户的同时,很可能存在安全隐患。

不过,利用Linux自带防火墙、入侵检测和安全认证等工具,及时修补系统的漏洞,就能大大提高Linux系统的安全性,让黑客们无机可乘。

以上是关于深入源码分析进程模型的主要内容,如果未能解决你的问题,请参考以下文章

第一次作业:深入源码分析进程模型

第一次作业:基于Linux0.01深入源码分析进程模型

第一次作业:深入源码分析进程模型

第一次作业:深入源码分析进程模型(Linux kernel 2.6.32)

第一次作业:深入源码分析进程模型

第一次作业:深入源码分析进程模型