基于Linux的进程模型分析

关于这个博客:

    首先,作为一名学生,这个博客是应教学课程要求而写的;其次,这个博客是我主动学习关于Linux进程模型的相关知识的一个成果分享。如果有任何错误或者不足之处,欢迎指正,本人必当虚心接受,改正。

 

1、对Linux的介绍

       从标题我们就可以知道,我们要分析的,是关于Linux的进程模型。那在深入了解这个问题时,我们需要先阐明一些基本的问题,以此为基础,方便我们之后对问题的探讨。

1.1、Linux的概念

        Linux是一套类Unix的操作系统,它是一个基于POSIX和Unix的多用户、多任务、支持多线程和多CPU的操作系统,它继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。

        Linux具有许多的版本,但是它们都使用着Linux内核。Linux可安装在各种计算机硬件设备中,如手机、平板电脑、路由器、台式机、超级计算机等等。

        Linux诞生于1991年10月5日(正式公布时间),创始人是林纳斯·托瓦兹,后来有100多名程序员参与了Linux内核代码的编写、修改工作。最终,在1994年3月,具有17万行代码的Linux1.0发布了,当时的Linux是完全自由免费的,这个特点被一直保留到了现在。

1.2、Linux与Unix的区别

       这相当于一个画外音,写这一部分的主要原因是,我在学习Linux操作系统的时候,会发现很多人会把Linux与Unix这两个操作系统混淆,认为这两个是同一概念。而之前的我,虽然知道它们是不同的概念,但却不知道它们的不同之处具体是什么。这里特地说明它们的不同之处,以防止部分读者无法区分二者的差别。

       ①之前的概念有提到,Linux是基于POSIX(可移植操作系统接口)的操作系统,所以,它的诞生在POSIX之后;而Unix早在POSIX之前就已经诞生了。由于年代不同的原因,Unix是命令行下的操作系统,而Linux是具有窗口管理的操作系统。

       ②Unix功能强大、性能全面,可以运用到多种不同的平台上,Linux与它在这方面类似(性能方面甚至优于Unix),但是它们也仅仅止步于“类似”,Linux不源于任何版本的Unix的源代码,它只是一个类似Unix的产品。

       ③之前也有提到,Linux是完全自由免费的,而Unix却是需要付费使用的。我们可以不用花钱就可以得到很多Linux的版本以及为其开发的应用软件。

       它们还有很多不同,这里就不一一例举了,具体完整的资料可以参考博客结尾的分享。

2、对进程的介绍

2.1、进程的定义

       “进程”(Process)有狭义与广义两个定义。

       狭义上来说,进程是正在运行的程序的实例,或者说它是某个正在运行的程序的实体。        广义上来说,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

2.2、进程的特征

       进程具有一些基本的特征:

  • 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生、消亡的;
  • 并发性:任何进程都可以同其他进程一起并发执行;
  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
  • 异步性:由于进程间的相互制约,使得进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进;
  • 结构特征:进程由程序段、数据段和进程控制块三部分组成。

       多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

2.3、进程与程序、线程的区别

2.3.1、进程与程序的区别

       进程(Process)与程序(Procedure)的根本区别是,程序是静态的,本身没有太多的意义,当处理器开始执行它时,它转变成了进程,是动态的。

       由于二者之间的根本性差异,导致了其功能也是大相径庭:

  • 程序是指令和数据的有序集合,可以作为一种软件资料长期存在,是永久性的;而进程是具有一定生命周期的,是暂时性的。
  • 进程能更真实地描述并发,而程序做不到,因为它是静态的。
  • 进程具有创建其他进程的功能,而程序不能创建其他程序,因为它是静态的。
  • 一个程序可以对应多个线程,只要同一个程序运行在不同是数据集合上即可。
  • 在传统的操作系统中,程序不能独立运行,作为资源分配和独立运行的基本单元是进程而非程序。

2.3.2、进程与线程的区别

       线程(Thread)是一种比进程更小的基本单位,通常一个进程会包含若干个线程,它们可以利用进程所拥有的资源。引入线程后,人们把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。由于线程更小,基本不占系统资源,所以调度它所花费的开销更小,也就能更高效地提高多个程序之间并发执行的程度。

3、操作系统组织进程

       之前提到进程的结构特征时,讲到它由三部分组成,程序段、数据段以及进程控制块,而进程的组织便与这三部分相关联。

3.1、进程控制块(PCB)

       当创建一个进程时,系统为该进程建立一个PCB。当进程执行时,系统通过其PCB 了 解进程的现行状态信息,以便对其进行控制和管理;当进程结束时,系统收回其PCB,该进 程随之消亡。操作系统通过PCB表来管理和控制进程,PCB既是进程的一部分,也是进程存在的唯一标志。

       PCB通常包含如下的内容:

3.2、程序段

       程序段就是能被进程调度程序调度到CPU执行的程序代码段。

       PS:程序可被多个进程共享,就是说多个进程可以运行同一个程序。

3.3、数据段

       一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。

3.4、进程的组织方式

3.4.1、线性方式

       当进程数目不多时,会采用这种方式。即把所有PCB组织在一张线性表中,将该表的首地址存放在内存的一个专用区域中,每次查找是需要扫描全表。

3.4.2、链接方式

       把具有同一状态的 PCB,用其中 的链接字链接成一个队列,PCB存储在一个连续的存区。

       示意图如下:

3.4.3、索引方式

       各个索引表在内存单元中的首地址也记录在内存中的专用单元中,用添加索引表的方式记录具有相应状态下的某个PCB在PCB表中的地址。

       示意图如下:

4、进程状态的转换

       前面有提到,进程执行时具有间断性,由此决定了进程可能具有多种状态。运行中的进程具有以下三种最基本的状态:

(1)就绪状态(Ready):

       进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。

(2)运行状态(Running):

       进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(比如所有进程都处于阻塞状态),通常会自动执行系统的空闲进程。

(3)阻塞状态(Blocked):

       由于进程等待某种条件(比如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。

       关于Linux的进程状态有以下几种:

(1)TASK_RUNNING:就绪态或者运行态,进程就绪且可以运行,但是不一定正在占用CPU;

(2)TASK_INTERRUPTIBLE:睡眠态,当前进程处于浅度睡眠,可以响应信号,一般为进程主动sleep进入的状态;

(3)TASK_UNINTERRUPTIBLE:睡眠态,深度睡眠,不响应信号,典型场景是进程获取信号量阻塞;

(4)TASK_ZOMBIE:僵尸态,进程已退出或者结束;

(5)TASK_STOPED:停止,调试状态。

        示意图:

图片来源:https://img-blog.csdn.net/20160827200931165?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center

5、进程调度

       现代的操作系统都是多任务的操作系统,尽管随着科技的发展,硬件的处理器核心越来越多,但是仍然不能保证一个进程对应一个核心,这就势必需要一个管理单元,负责调度进程,由管理单元来决定下一刻应该由谁使用CPU,这里充当管理单元的就是进程调度器。

 (1)调度器的结构

       在Linux内核中,调度器可以分成两个层级,在进程中被直接调用的成为通用调度器或者核心调度器,他们作为一个组件和进程其他部分分开,而通用调度器和进程并没有直接关系,其通过第二层的具体的调度器类来直接管理进程。每个进程必然属于一个特定的调度器类,Linux会根据不同的需求实现不同的调度器类。

(2)调度器类

       在Linux内核中实现了一个调度器类的框架,其中定义了调度器应该实现的函数,每一个具体的调度器类都要实现这些函数。

       调度器的定义如下:

 struct sched_class 
 {
     const struct sched_class *next;
 
     void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
     void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
     void (*yield_task) (struct rq *rq);
     bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt);
 
     void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags);
 
     struct task_struct * (*pick_next_task) (struct rq *rq);
     void (*put_prev_task) (struct rq *rq, struct task_struct *p);
 
 #ifdef CONFIG_SMP
     int  (*select_task_rq)(struct task_struct *p, int sd_flag, int flags);
     void (*migrate_task_rq)(struct task_struct *p, int next_cpu);
 
     void (*pre_schedule) (struct rq *this_rq, struct task_struct *task);
     void (*post_schedule) (struct rq *this_rq);
     void (*task_waking) (struct task_struct *task);
     void (*task_woken) (struct rq *this_rq, struct task_struct *task);
 
     void (*set_cpus_allowed)(struct task_struct *p,
                  const struct cpumask *newmask);
 
     void (*rq_online)(struct rq *rq);
     void (*rq_offline)(struct rq *rq);
 #endif
 
     void (*set_curr_task) (struct rq *rq);
     void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);
     void (*task_fork) (struct task_struct *p);
 
     void (*switched_from) (struct rq *this_rq, struct task_struct *task);
     void (*switched_to) (struct rq *this_rq, struct task_struct *task);
     void (*prio_changed) (struct rq *this_rq, struct task_struct *task,
                  int oldprio);
 
     unsigned int (*get_rr_interval) (struct rq *rq,
                      struct task_struct *task);
 
 #ifdef CONFIG_FAIR_GROUP_SCHED
     void (*task_move_group) (struct task_struct *p, int on_rq);
 #endif
 };

 (3)调度实体

       Linux中可调度的不仅仅是进程,也可能是一个进程组,所以Linux就把调度对象抽象化成一个调度实体。实际上,调度器直接操作的也是调度实体,只是会根据调度实体获取到其对应的结构。

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

#ifdef CONFIG_SCHEDSTATS
    struct sched_statistics statistics;
#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

#ifdef CONFIG_SMP
    /* Per-entity load-tracking */
    struct sched_avg    avg;
#endif
};

       说明:

       load用于负载均衡,决定了各个实体占队列中负荷的比例,计算负荷权重是调度器的主要责任,因为选择下一个进程就是要根据这些信息。run_node是一个红黑树节点,用于把实体加入到红黑树,on_rq表明该实体是否位于就绪队列,当为1的时候就说明在就绪队列中,一个进程在得到调度的时候会从就绪队列中摘除,在让出CPU的时候会重新添加到就绪队列(正常调度的情况,不包含睡眠、等待)。

       与时间相关的字段中,exec_start记录进程开始在CPU上运行的时间;sum_exec_time记录进程一共在CPU上运行的时间,pre_sum_exec_time记录本地调度之前,进程已经运行的时间。在进程被调离CPU的时候,会把sum_exec_time的值保存到pre_sum_exec_time,而sum_exec_time并不重置,而是一直随着在CPU上的运行递增。而vruntime 记录在进程执行期间,在虚拟时钟上流逝的时间。

6、个人对于操作系统进程模型的看法

       我在学习Linux操作系统的进程模型的时候,发现许多关于进程的知识都是新颖的,是之前学习计算机类课程所没有涉及的。但是,进程的诞生对于操作系统的历史无疑是至关重要的。如果把整个操作系统的运行比作一场宏大的舞台剧,那么这些进程便是那辛勤工作的幕后人员,平常人们不会怎么注意到它们,也不会去了解它们,但是它们一直存在于此,默默地付出。没有进程,操作系统就不能成功地运行,就像没有幕后人员,舞台剧就不能成功地上演一样。

       随着时代的进步,为了满足人们对于计算机功能的越来越多样化的需求,计算机的内部算法也是越来越复杂,这也导致了作为关键一环的进程调度算法必然非常的复杂,本人在博客中展示的进程调度内容其实都是基础的,也只是其庞大的算法中的一小部分。我自知还没有学习到Linux进程模型更加深层次的地方,我还会不断地去学习,掌握更多相关知识,也能体会到那些世界知名的程序设计师对于改善计算机性能所付出的巨大努力。

 

结尾附上一些分享资料:

1、Linux与Unix的区别:https://www.2cto.com/os/201109/104824.html

2、关于Linux的调度算法:https://www.cnblogs.com/ck1020/p/6089970.html

 

posted @ 2018-05-01 11:10  叶海涵  阅读(145)  评论(0编辑  收藏  举报