深入源码分析Linux进程模型

  一、操作系统是怎么组织进程的

         1、进程的概念

               在进程模型中,计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程,简称进程。一个进程应该包含如下内容:

              (1)程序的代码,既然进程是一个正在运行的程序,自然需要程序的代码;

              (2)程序的数据;

              (3)CPU寄存器的值,包括通用寄存器,程序计数器;

              (4)堆(heap)是用来保存进程运行时动态分配的内存空间;

              (5)栈(stack)有两个用途,1保存运行的上下文信息。2在函数调用时保存被调用函数的形参或者局部变量;

              (6)进程所占用的一组系统资源,如打开的文件。

        2、进程的组织

                系统为每个进程维护了一个进程控制块(Process Control Block,PCB),用来保存与该进程有关的各种状态信息。PCB只是基本原理中的说法,对于一个真实的操作系统可能不叫PCB,就像我所要讲的Linux中,PCB叫做任务结构体(task struct)。

struct task_struct {

        //进程的运行时状态
        volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
        void *stack;   
        atomic_t usage;

        //进程当前的状态
        /*
        0x00000002表示进程正在被创建;

        0x00000004表示进程正准备退出;

        0x00000040 表示此进程被fork出,但是并没有执行exec;

        0x00000400表示此进程由于其他进程发送相关信号而被杀死 。
        */
        unsigned int flags;     /* per process flags, defined below */


        unsigned int ptrace;
        int on_rq;

        //表示此进程的运行优先级,prio表示动态优先级,根据static_prio和交互性奖罚算出,static_prio是进程的静态优先级,在进程创建时确定,范围从-20到19,越小优先级越高。
        int prio, static_prio, normal_prio;

        //进程的运行优先级
        unsigned int rt_priority;

        //list_head结构体
         struct list_head tasks;

        //mm_struct结构体,描述了进程内存的相关情况
         struct mm_struct *mm, *active_mm;

         /* per-thread vma caching */
         u32 vmacache_seqnum;
         struct vm_area_struct *vmacache[VMACACHE_SIZE];

        /* task state */
        //进程的状态参数
         int exit_state;
         int exit_code, exit_signal;

        //父进程退出后信号被发送
        int pdeath_signal;  /*  The signal sent when the parent dies  */ 

         /* scheduler bits, serialized by scheduler locks */
         unsigned sched_reset_on_fork:1;
         unsigned sched_contributes_to_load:1;
         unsigned sched_migrated:1;
         unsigned sched_remote_wakeup:1;
         unsigned :0; /* force alignment to the next boundary */

         /* unserialized, strictly 'current' */
         unsigned in_execve:1; /* bit to tell LSMs we're in execve */
         unsigned in_iowait:1;

         struct restart_block restart_block;

        //进程号
        pid_t pid;
        //进程组号
        pid_t tgid;


         //进程的亲身父亲
         struct task_struct __rcu *real_parent; /* real parent process */
         //进程的现在的父亲,可能为继父
         struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
         //进程的孩子链表
         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;
         struct list_head thread_node;

        //该进程使用cpu时间的信息,utime是在用户态下执行的时间,stime是在内核态下执行的时间。
        cputime_t utime, stime;

        cputime_t gtime;
        struct prev_cputime prev_cputime;

        //启动时间,,只是时间基准不一样
        u64 start_time;         /* monotonic time in nsec */
        u64 real_start_time;    /* boot based time in nsec */

        struct task_cputime cputime_expires;

        //list_head的CPU时间
        struct list_head cpu_timers[3];

        //保存进程名字的数组,一般数组大小为15位
        char comm[TASK_COMM_LEN];

        /* file system info */
        //文件系统信息
        struct nameidata *nameidata;

        /* 文件系统信息计数*/
        int link_count, total_link_count;

        /* filesystem information */
        //文件系统相关信息结构体
         struct fs_struct *fs;

        /* open file information */
        //打开文件信息的结构体
         struct files_struct *files;

        /* namespaces */
         struct nsproxy *nsproxy;

        /* signal handlers */
        //信号相关信息的句柄
         struct signal_struct *signal;
         struct sighand_struct *sighand;

         struct callback_head *task_works;

         struct audit_context *audit_context;

         struct seccomp seccomp;

        /* Thread group tracking */
         u32 parent_exec_id;
         u32 self_exec_id;

        /* journalling filesystem info */
         void *journal_info;

        /* VM state */
         struct reclaim_state *reclaim_state;

         struct backing_dev_info *backing_dev_info;

         struct io_context *io_context;

         unsigned long ptrace_message;
         siginfo_t *last_siginfo; /* For ptrace use.  */


         /*
          * time slack values; these are used to round up poll() and
          * select() etc timeout values. These are in nanoseconds.
          */
         //松弛时间值,用来记录select和poll的超时时间,单位为ns
         u64 timer_slack_ns;
         u64 default_timer_slack_ns;


        /* CPU-specific state of this task */
        //该进程在特定CPU下的状态
        struct thread_struct thread;

};

                PID每个经常都有自己的“身份证号码”,即PID号,PID是重要的系统资源,它是用以区分各个进程的基本依据,可以使用ps来查看进程的PID。一个task struct对应一个PID。

 二、进程状态如何转换(给出进程状态转换图)             

状态 描述
TASK_RUNNING 就绪态或者运行态,进程就绪可以运行,但是不一定正在占有CPU,对应进程状态的R
TASK_INTERRUPTIBLE 睡眠态,但是进程处于浅度睡眠,可以响应信号,一般是进程主动sleep进入的状态,对应进程状态S
TASK_UNINTERRUPTIBLE 睡眠态,深度睡眠,不响应信号,典型场景是进程获取信号量阻塞,对应进程状态D
TASK_ZOMBIE 僵尸态,进程已退出或者结束,但是父进程还不知道,没有回收时的状态,对应进程状态Z
TASK_STOPED 停止,调试状态,对应进程状态T


      

 

 

 

 

 

       (1)两状态进程模型

                在该模型中,一个进程要么正在执行,要么没有在执行,没有其他状态,所以进程所处的状态有两种:运行态、未运行态。进程状态的转换方式如1.1图所示。

               

 

                                                                       图1.1 两状态转换图

          (2)三状态进程模型

            在该模型中,进程所处的状态有三种:运行态、就绪态、和阻塞态。进程状态的转换方式如图1.2所示。

                 

                                                          图1.2    三状态转换图

         (3)五状态进程模型相较于三状态进程模型而言,在三状态进程模型的基础上另增加了两个状态:新建态和退出态。进程状态的转换方式如图1.3所示。

                                                           图1.3  五状态转换图

  三、进程是如何调度的

         在Linux操作系统中,有实时进程和普通今天之分,这里我想围绕普通进程的调度展开,对o(1)调度算法和CFS调度算法进行分析。

         1、o(1)调度算法

         在Linux2.6中,o(1)调度被采用,它是对普通进程进行调度的一种调度算法。因为Linux2.6版的调度算法与Linux2.4版相比在性能等方面的改进非常大,且它的时间复杂度为恒定的o(1),故把它称为o(1)调度算法。

        renqueue结构体的部分定义如下:

struct runqueue
{
    unsigned long nr_running;
    task_t *curr;
    prio_array_t *active,*expired,array[2];
}

      上述结构体列举了一些比较重要的字段:

             (1)nr_running:就绪进程的数目,等于活动进程和过期进程之和;

       (2)curr:指向正在运行的进程;

       (3)active:指向表示活动进程集的结构体;

       (4)expired:指向表示过期进程集的结构体;

       (5)arrays[2]:为active和expired分配静态空间。

    调度的核心步骤:

       (1)若当前处理器的运行队列上没有任何可运行态进程,那么为了实现多处理器间的负载平衡,则从其他处理器上调一些可运行态进程进来。若调完后当前处理器的运行队列上还是没有进程可运行,则在处理器上运行idle进程来使处理器处于低功耗模式直到有可运行态进程出现。若运行队列上有可运行态进程则执行步骤(2)

       (2)判断active队列是否为空。当active队列为空,即表示所有活动进程都运行完了,这时就要让expired队列的过期进程再次变为活动进程。但是并不需要将expired队列中的进程一个个移进active队列中去,只需要将active和expired的指针指向的地址互换就行了。具体内核实现代码如下:

struct prop_array *array=rq->active;
if(array->nr_active!=0)
{
    re->active=rq->expired;
    rq->expired=array;
}

 

           (3)步骤(2)执行成功后active队列中必定存在活动进程,这时通过active中的优先级位图bitmap来选择优先级最高且有可运行态进程的优先级队列,具体如图1.4所示。

                                       图1.4 通过bitmap来选择优先级队列的示意图

                   (4)最后选择active的bitmap所指向链表中的第一个进程即可。

          通过以上4个核心步骤,o(1)调度器基本完成了对于可运行态进程的一次调度过程。

          2、CFS调度算法

          从Linux2.6开始,考虑到o(1)调度的一些不足以及公平性方面的缺陷,所以改用完全公平调度(CFS)算法。不同于o(1)调度,CFS调度基于公平的理念,对进程一视同仁,不再对交互式进程进行区别,也不再根据进程的平均睡眠时间来确定奖励bonus并以此来调整动态优先级。取而代之,CFS通过权重使每个进程都能过获得公平的运行时间。更为关键的是,CFS调度算法的设计和实现都很简单,且实际性能非常优越。CFS调度器的整体结构如图1.5所示。

                                                               图1.5 CFS调度器的整体结构

               相对比于o(1)调度,CFS调度没有用运行队列来维护可运行态进程,而是用来红黑树来组织普通进程。红黑树本质上是一颗二叉查找树,它具有以下五个特点:

                        (1)每个叶结点都是空结点,并且他们都是黑色的;

                        (2)根结点是黑色的;

                        (3)红色结点的子结点必定是黑色的;

                        (4)对于任意结点而言,其到叶结点的每条路径上的黑色结点的书目都相同;

                        (5)每个结点不是黑色就是红色;

               这些特点决定了红黑树是自平衡的,虽然红黑树没有达到恒定o(1)的时间复杂度但是它最差的时间复杂度也为o(logn),这就决定了它可以在插入和删除等操作上表现的非常高效。CFS使用的红黑树是以时间为顺序的,它的结点由调度实体来描述,而进程的虚拟运行时间和权重也存放在这个结构中,图1.6描绘了CFS中红黑树的结果。

                                                                  图1.6 CFS红黑树的结构

              内核通过红黑树来对虚拟运行时间进行排序,红黑树的最左侧结点的虚拟运行时间最少,所以该结点所表示的进程将是下一个要被调度的进程。

             3、两种算法的比较

             通过上面对于o(1)调度和CFS调度的分析,我们发现他们的区别归根结底在于两者的设计思想的不同。

             o(1)调度是通过优先级来实现对时间片的绝对映射,而CFS调度对于时间片的映射则是通过权重完成的,CFS调度相比o(1)调度的最主要优势在于它实现了进程间的调度的相对公平,而这也是调度算法设计中非常重要的一个部分。虽然o(1)调度的恒定的时间复杂o(1)也是一大特色,只是CFS的o(logn)的性能已经能够满足于系统的需要,而且CFS的设计与实现简单,实际运行高效,不像o(1)调度有那么多的复杂公式,弥补了o(1)调度的许多不足之处。所以,这些因素最终决定了CFS调度要取代o(1)调度的位置。但是也不能完全否定o(1)调度算法,它对于以后进程调度算法的发展还是有许多值得借鉴的地方。

 四、谈谈自己对Linux操作系统的看法

         我想通过和windows系统的比较来谈我的看法。

          Linux有以下几个优点:

                  (1)追求免费的正版,微软公司开发的平台下的软件都是明码标价的,如果不购买,那么就是属于盗版,为了追求正版,所以只能选择linux,特别是对于公司,则是为了防止出现版权纠纷问题;

                  (2)学习,linux开放源代码,是学习系统和软件开发的平台,也是国外很多研究机构开发系统的盗版源泉;

                  (3)防毒,有许多网站挂马,给客户安全造成了影响,linux作为一个小众平台,感染病毒的概率比较低,因此为了放心地看大片,所以安装了它;

                  (4)最后,也是最重要的一点,装逼,linux是一个高大上的名词,使用linux就和高手挂上了勾,特别是compiz等软件,让windows的客户看得目瞪口呆,然后可以高大上地享受大家对高手地崇拜。

       windows有以下几个优点:

                   (1)windows形成了良好地生态圈,有着丰富实用地软件支持,形成了良好地盈利模式,在windows平台下,你的需要可以得到贴心的维护,当然你要付出你的money;

                   (2)维护成本低,技术文档资料详实,不想linux什么都要靠自己、靠社区;

                   (3)最重要的是对我们这些初学者比较友善。

        说真的,作为一个计算机菜鸟,如果要我选择的话,我更喜欢windows,不过想成为大佬,还是应该使用Linux。

、参考资料

https://wenku.baidu.com/view/2003f071cbaedd3383c4bb4cf7ec4afe04a1b1c8.html?qq-pf-to=pcqq.c2c

https://blog.csdn.net/xxpresent/article/details/71023637

 

posted @ 2018-05-01 11:18  一如当初ゝ  阅读(363)  评论(0编辑  收藏  举报