第一次作业:基于Linux源代码分析进程模型

作业内容:

挑选一个开源的操作系统,深入源码分析其进程模型,具体包含如下内容:

  • 操作系统是怎么组织进程的
  • 进程状态如何转换(给出进程状态转换图)
  • 进程是如何调度的
  • 谈谈自己对该操作系统进程模型的看法

 

1.进程

           进程的概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。每个进程是有生命周期的,在用户空间,进程是由PID标识。在生命周期中PID是唯一且不变的。在linux内核中,进程是由task_struct结构表示(linux内核存储进程信息的固定格式),此结构中包含此进程所需要的所有数据,如进程的状态、运行的时间、堆栈、父进程、执行的线程等。linux将多个进程放在任务队列的双向链表中。

         进程的创建:当启动linux时,加载完内核后,会创建第一进程init。之后所有的进程都是由init或父进程向内核发起调用fork()自身而来,再而克隆clone()自身的数据给子进程。

          进程的优先级:系统中同时有大量的进程需要执行时,是需要优先级来先后处理的。task_struct中是有记录优先级的,内核是如何知道哪个优先级高哪个低,遍历。内核2.6将进程优先级划分位固定的个数。

Linux环境下分别使用getpid()和getppid()函数来得到进程ID和父进程ID,分别使用getuid()和geteuid()函数来得到进程的用户ID和有效用户ID,分别使用getgid()和getegid()来获得进程的组ID和有效组ID,其函数原型如下:

#include 
pid_t getpid(void);    //获取进程ID
pid_t getppid(void);  //获取父进程ID

uid_t getuid(void);    //获取用户ID
uid_t geteuid(void);    //获取有效用户ID

gid_t getgid(void);    //获取组ID
gid_t getegid(void);    //获取有效组ID

 

      

2.查看进程

第一次作业:基于Linux源代码分析进程模型_第1张图片

 

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

 

             task_struct的 real_parent成员指向父进程,parent成员指向“养父进程”;children成员表示该进程的子进程链表;sibling成员表示该进程的兄弟进程链表。系统启动后创建了第一个进程:进程0(swapper,也叫idle),进程0创建了第一个用户进程(进程1/sbin/init)和第一个内核进程(进程2kthreadd),之后进程0进入idle状态,当没有进程可以被调度的时候运行该进程,不做具体的事情。进程1的主要作用是处理僵尸进程。当某个父进程比子进程提前消亡时,父进程会给子进程重新寻找“养父进程”,一般就是进程1,由进程1负责处理该子进程的消亡。当创建一个新进程时,新进程的父进程为当前进程或者线程,并且把新进程加入到父进程的子链表中。如果使用CLONE_THREAD标志创建一个新进程时,新进程的父进程为当前进程或线程的父进程,并且把新进程加入到父进程的子链表中。CLONE_THREAD标志创建一个新线程时,新线程的父进程为当前线程组长的父进程,所以线程组看起来像是一个独立的进程。此时,因为创建的是线程,所以不需要把新线程加入到任何子链表中。

 

4.进程状态如何转换

          由于进程的不断创建,系统的资源已经不能满足进程运行的要求,这个时候就必须把某些进程挂起(suspend),对换到磁盘镜像区中,暂时不参与进程调度,起到平滑系统操作负荷的目的。 
引起进程挂起的原因是多样的,主要有: 
1. 系统中的进程均处于等待状态,处理器空闲,此时需要把一些阻塞进程对换出去,以腾出足够的内存装入就绪进程运行。 
2. 进程竞争资源,导致系统资源不足,负荷过重,此时需要挂起部分进程以调整系统负荷 ,保证系统的实时性或让系统正常运行。 
3. 把一些定期执行的进程(如审计程序、监控程序、记账程序)对换出去,以减轻系统负荷。 
4. 用户要求挂起自己的进程,以便根据中间执行情况和中间结果进行某些调试、检查和改正。 
5. 父进程要求挂起自己的后代进程,以进行某些检查和改正。 
6. 操作系统需要挂起某些进程,检查运行中资源使用情况,以改善系统性能;或当系统出现故障或某些功能受到破坏时,需要挂起某些进程以排除故障。

        引起进程状态转换的具体原因如下:

  • 等待态—→挂起等待态:如果当前不存在就绪进程,那么至少有一个等待态进程将被对换出去成为挂起等待态;操作系统根据当前资源状况和性能要求,可以决定把等待态进程对换出去成为挂起等待态。
  • 挂起等待态—→挂起就绪态:引起进程等待的事件发生之后,相应的挂起等待态进程将转换为挂起就绪态。
  • 挂起就绪态—→就绪态:当内存中没有就绪态进程,或者挂起就绪态进程具有比就绪态进程更高的优先级,系统将把挂起就绪态进程转换成就绪态。
  • 就绪态—→挂起就绪态:操作系统根据当前资源状况和性能要求,也可以决定把就绪态进程对换出去成为挂起就绪态。
  • 挂起等待态—→等待态:当一个进程等待一个事件时,原则上不需要把它调入内存。但是在下面一种情况下,这一状态变化是可能的。当一个进程退出后,主存已经有了一大块自由空间,而某个挂起等待态进程具有较高的优先级并且操作系统已经得知导致它阻塞的事件即将结束,此时便发生了这一状态变化。
  • 运行态—→挂起就绪态:当一个具有较高优先级的挂起等待态进程的等待事件结束后,它需要抢占 CPU,,而此时主存空间不够,从而可能导致正在运行的进程转化为挂起就绪态。另外处于运行态的进程也可以自己挂起自己。
  • 新建态—→挂起就绪态:考虑到系统当前资源状况和性能要求,可以决定新建的进程将被对换出去成为挂起就绪态。
  • 第一次作业:基于Linux源代码分析进程模型_第2张图片

5.进程是如何调度的

 Linuz 中进程调试都是非强占式的,系统采用相当简单的基于优先级的调试算法。进程只能在核心态等待,当进程执行系统调用时它会从用户态“陷入”核心态,这时内核代表这个进程执行。当进程在核心态等待某事件时系统会将它挂起,而后让另一个进程运行。Liunz 的调度函数是schedule,定义在kernelfsched.c 中。Schedule 函数完成以下工作:
  (1)定义两个指针。prev 是正在运行的进程,next 是调度程序选择下一个将要运行的进程。这两个指针可能会相同。
  (2) 处理调度任务队列。首先检查任务是否在队列中,如果有等待处理的任务则顺序处理其中的每一个任务,然后将队列清空。
  (3) 检查任务是否在中断队列中,如果在,则不做本次调度,直接返回。
  (4) 底半处理。就是中断处理的后半部分。
  (5) 调度运行进程队列。与Minix 不同的是Linuz 没有将可运行进程按其优先级分成多个队列,而是把系统中所有可运行进程(状态为TASK_RUNNING) 都排在同一个队列中,这实际上是一个双向链表。
(6) 选择下一个要运行的进程。从init_task.nezt开始,顺序搜索运行进程队列,对每一个进程计算其weight 值,选择weight 值最大的运行。weight 值的计算规则是: 1.如果当前进程声明要放弃CPU (进程调度策略中设置了SCHED_YIEID 位),则其weigt=0。2.如果进程是实时进程,则weight=rt_priority+1000。3.如果进程的counter'值为0,则weight=0。4.如果进程的counter不为0,则weight=countertpriority; 如果进程是当前进程,则其weight 值再加1,即当前进程优先。
(7) 切换。保存当前进程结构(task_struct )中的上下文,就是当前进程运行到调度程序结束时处理器的上下文。新加载进程的上下文也是它上次运行到调度地的快照,包括进程的程序计数器和各寄存器的内容。

 

1.创建一些局部变量

struct task_struct *prev, *next;//当前进程和一下个进程的进程结构体
unsigned long *switch_count;//进程切换次数
struct rq *rq;//就绪队列
int cpu;

 

2.关闭内核抢占,初始化一部分变量

need_resched:
preempt_disable();//关闭内核抢占
cpu = smp_processor_id();
rq = cpu_rq(cpu);//与CPU相关的runqueue保存在rq中
rcu_note_context_switch(cpu);
prev = rq->curr;//将runqueue当前的值赋给prev

 

3.选择next进程

next = pick_next_task(rq, prev);//挑选一个优先级最高的任务排进队列
clear_tsk_need_resched(prev);//清除prev的TIF_NEED_RESCHED标志。
clear_preempt_need_resched();

 

4.完成进程的调度

if (likely(prev != next)) {//如果prev和next是不同进程
        rq->nr_switches++;//队列切换次数更新
        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);
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
    struct mm_struct *mm, *oldmm;//初始化进程地址管理结构体mm和oldmm
    prepare_task_switch(rq, prev, next);//完成进程切换的准备工作
    mm = next->mm;
    oldmm = prev->active_mm;
    /*完成mm_struct的切换*/
if (!mm) {
        next->active_mm = oldmm;
        atomic_inc(&oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);
    } else
        switch_mm(oldmm, mm, next);
    if (!prev->mm) {
        prev->active_mm = NULL;
        rq->prev_mm = oldmm;
    }
switch_to(prev, next, prev);//进程切换的核心代码
barrier();
finish_task_switch(this_rq(), prev);
}

 

5.我们看到在context_switch中使用switch_to(prev,next,prev)来切换进程。我们查看一下switch_to的代码。 switch_to是一个宏定义,完成进程从prev到next的切换,首先保存flags,然后保存当前进程的ebp,然后把当前进程的esp保存到prev->thread.sp中,然后把标号1:的地址保存到prev->thread.ip中。 然后把next->thread.ip压入堆栈。这里,如果之前B也被switch_to出去过,那么next->thread.ip里存的就是下面这个1f的标号,但如果next进程刚刚被创建,之前没有被switch_to出去过,那么next->thread.ip里存的将是ret_ftom_fork __switch_canqry应该是现代操作系统防止栈溢出攻击的金丝雀技术。 jmp __switch_to使用regparm call, 参数不是压入堆栈,而是使用寄存器传值,来调用__switch_to eax存放prev,edx存放next。这里为什么不用call __switch_to而用jmp,因为call会导致自动把下面这句话的地址(也就是1:)压栈,然后__switch_to()就必然只能re到这里,而无法根据需要ret到ret_from_fork 当一个进程再次被调度时,会从1:开始执行,把ebp弹出,然后把flags弹出。

 

#define switch_to(prev, next, last)                 \
do {                                    \
    /*                              \
     * Context-switching clobbers all registers, so we clobber  \
     * them explicitly, via unused output variables.        \
     * (EAX and EBP is not listed because EBP is saved/restored \
     * explicitly for wchan access and EAX is the return value of   \
     * __switch_to())                       \
     */                             \
    unsigned long ebx, ecx, edx, esi, edi;              \
                                    \
    asm volatile("pushfl\n\t"       /* save    flags */ \
             "pushl %%ebp\n\t"      /* save    EBP   */ \
             "movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */ \
             "movl %[next_sp],%%esp\n\t"    /* restore ESP   */ \
             "movl $1f,%[prev_ip]\n\t" /* save    EIP   */ \
             "pushl %[next_ip]\n\t" /* restore EIP   */ \
             __switch_canary                    \
             "jmp __switch_to\n"    /* regparm call  */ \
             "1:\t"                     \
             "popl %%ebp\n\t"       /* restore EBP   */ \
             "popfl\n"          /* restore flags */ \
                                    \
             /* output parameters */                \
             : [prev_sp] "=m" (prev->thread.sp),        \
               [prev_ip] "=m" (prev->thread.ip),        \
               "=a" (last),                 \
                                    \
               /* clobbered output registers: */        \
               "=b" (ebx), "=c" (ecx), "=d" (edx),      \
               "=S" (esi), "=D" (edi)               \
                                        \
               __switch_canary_oparam               \
                                    \
               /* input parameters: */              \
             : [next_sp]  "m" (next->thread.sp),        \
               [next_ip]  "m" (next->thread.ip),        \
                                        \
               /* regparm parameters for __switch_to(): */  \
               [prev]     "a" (prev),               \
               [next]     "d" (next)                \
                                    \
               __switch_canary_iparam               \
                                    \
             : /* reloaded segment registers */         \
            "memory");                  \
} while (0)

5.开始抢占

sched_preempt_enable_no_resched();
if (need_resched())
        goto need_resched;

参考文献:https://www.cnblogs.com/xiaomanon/p/4195327.html 

http://dict.youdao.com/w/%E8%BF%9B%E7%A8%8B%E6%A0%87%E8%AF%86%E7%AC%A6/ 

 

 

 

你可能感兴趣的:(第一次作业:基于Linux源代码分析进程模型)