
在前文中,我们分析了内核中进程和线程的统一结构体task_struct,并分析进程、线程的创建和派生的过程。在本文中,我们会对任务间调度进行详细剖析,了解其原理和整个执行过程。由此,进程、线程部分的大体框架就算是介绍完了。本节主要分为三个部分:Linux内核中常见的调度策略,调度的基本结构体以及调度发生的整个流程。下面将详细展开说明。
Linux 作为一个多任务 *** 作系统,将每个 CPU 的时间划分为很短的时间片,再通过调度器轮流分配给各个任务使用,因此造成多任务同时运行的错觉。为了维护 CPU 时间,Linux 通过事先定义的节拍率(内核中表示为 HZ),触发时间中断,并使用全局变量 Jiffies 记录了开机以来的节拍数。每发生一次时间中断,Jiffies 的值就加 1。节拍率 HZ 是内核的可配选项,可以设置为 100、250、1000 等。不同的系统可能设置不同的数值,可以通过查询 /boot/config 内核选项来查看它的配置值。
Linux的调度策略主要分为实时任务和普通任务。实时任务需求尽快返回结果,而普通任务则没有较高的要求。在前文中我们提到了task_struct中调度策略相应的变量为policy,调度优先级有prio, static_prio, normal_prio, rt_priority几个。优先级其实就是一个数值,对于实时进程来说,优先级的范围是 0 99;对于普通进程,优先级的范围是 100 139。数值越小,优先级越高。
实时调度策略主要包括以下几种
普通调度策略主要包括以下几种:
首先,我们需要一个结构体去执行调度策略,即sched_class。该类有几种实现方式
普通任务调度实体源码如下,这里面包含了 vruntime 和权重 load_weight,以及对于运行时间的统计。
在调度时,多个任务调度实体会首先区分是实时任务还是普通任务,然后通过以时间为顺序的红黑树结构组合起来,vruntime 最小的在树的左侧,vruntime最多的在树的右侧。以CFS策略为例,则会选择红黑树最左边的叶子节点作为下一个将获得 CPU 的任务。而这颗红黑树,我们称之为运行时队列(run queue),即struct rq。
其中包含结构体cfs_rq,其定义如下,主要是CFS调度相关的结构体,主要有权值相关变量、vruntime相关变量以及红黑树指针,其中结构体rb_root_cached即为红黑树的节点
对结构体dl_rq有类似的定义,运行队列由红黑树结构体构成,并按照deadline策略进行管理
对于实施队列相应的rt_rq则有所不同,并没有用红黑树实现。
下面再看看调度类sched_class,该类以函数指针的形式定义了诸多队列 *** 作,如
调度类分为下面几种:
队列 *** 作中函数指针指向不同策略队列的实际执行函数函数,在linux/kernel/sched/目录下,fair.c、idle.c、rt.c等文件对不同类型的策略实现了不同的函数,如fair.c中定义了
以选择下一个任务为例,CFS对应的是pick_next_task_fair,而rt_rq对应的则是pick_next_task_rt,等等。
由此,我们来总结一下:
有了上述的基本策略和基本调度结构体,我们可以形成大致的骨架,下面就是需要核心的调度流程将其拼凑成一个整体,实现调度系统。调度分为两种,主动调度和抢占式调度。
说到调用,逃不过核心函数schedule()。其中sched_submit_work()函数完成当前任务的收尾工作,以避免出现如死锁或者IO中断等情况。之后首先禁止抢占式调度的发生,然后调用__schedule()函数完成调度,之后重新打开抢占式调度,如果需要重新调度则会一直重复该过程,否则结束函数。
而__schedule()函数则是实际的核心调度函数,该函数主要 *** 作包括选取下一进程和进行上下文切换,而上下文切换又包括用户态空间切换和内核态的切换。具体的解释可以参照英文源码注释以及中文对各个步骤的注释。
其中核心函数是获取下一个任务的pick_next_task()以及上下文切换的context_switch(),下面详细展开剖析。首先看看pick_next_task(),该函数会根据调度策略分类,调用该类对应的调度函数选择下一个任务实体。根据前文分析我们知道,最终是在不同的红黑树上选择最左节点作为下一个任务实体并返回。
下面来看看上下文切换。上下文切换主要干两件事情,一是切换任务空间,也即虚拟内存;二是切换寄存器和 CPU 上下文。关于任务空间的切换放在内存部分的文章中详细介绍,这里先按下不表,通过任务空间切换实际完成了用户态的上下文切换工作。下面我们重点看一下内核态切换,即寄存器和CPU上下文的切换。
switch_to()就是寄存器和栈的切换,它调用到了 __switch_to_asm。这是一段汇编代码,主要用于栈的切换, 其中32位使用esp作为栈顶指针,64位使用rsp,其他部分代码一致。通过该段汇编代码我们完成了栈顶指针的切换,并调用__switch_to完成最终TSS的切换。注意switch_to中其实是有三个变量,分别是prev, next, last,而实际在使用时,我们会对last也赋值为prev。这里的设计意图需要结合一个例子来说明。假设有ABC三个任务,从A调度到B,B到C,最后C回到A,我们假设仅保存prev和next,则流程如下
最终调用__switch_to()函数。该函数中涉及到一个结构体TSS(Task State Segment),该结构体存放了所有的寄存器。另外还有一个特殊的寄存器TR(Task Register)会指向TSS,我们通过更改TR的值,会触发硬件保存CPU所有寄存器在当前TSS,并从新的TSS读取寄存器的值加载入CPU,从而完成一次硬中断带来的上下文切换工作。系统初始化的时候,会调用 cpu_init()给每一个 CPU 关联一个 TSS,然后将 TR 指向这个 TSS,然后在 *** 作系统的运行过程中,TR 就不切换了,永远指向这个 TSS。当修改TR的值得时候,则为任务调度。
更多Linux内核视频教程文本资料免费领取后台私信【 内核大礼包 】自行获取。
在完成了switch_to()的内核态切换后,还有一个重要的函数finish_task_switch()负责善后清理工作。在前面介绍switch_to三个参数的时候我们已经说明了使用last的重要性。而这里为何让prev和last均赋值为prev,是因为prev在后面没有需要用到,所以节省了一个指针空间来存储last。
至此,我们完成了内核态的切换工作,也完成了整个主动调度的过程。
抢占式调度通常发生在两种情况下。一种是某任务执行时间过长,另一种是当某任务被唤醒的时候。首先看看任务执行时间过长的情况。
该情况需要衡量一个任务的执行时间长短,执行时间过长则发起抢占。在计算机里面有一个时钟,会过一段时间触发一次时钟中断,通知 *** 作系统时间又过去一个时钟周期,通过这种方式可以查看是否是需要抢占的时间点。
时钟中断处理函数会调用scheduler_tick()。该函数首先取出当前CPU,并由此获取对应的运行队列rq和当前任务curr。接着调用该任务的调度类sched_class对应的task_tick()函数进行时间事件处理。
以普通任务队列为例,对应的调度类为fair_sched_class,对应的时钟处理函数为task_tick_fair(),该函数会获取当前的调度实体和运行队列,并调用entity_tick()函数更新时间。
在entity_tick()中,首先会调用update_curr()更新当前任务的vruntime,然后调用check_preempt_tick()检测现在是否可以发起抢占。
check_preempt_tick() 先是调用 sched_slice() 函数计算出一个调度周期中该任务运行的实际时间 ideal_runtime。sum_exec_runtime 指任务总共执行的实际时间,prev_sum_exec_runtime 指上次该进程被调度时已经占用的实际时间,所以 sum_exec_runtime - prev_sum_exec_runtime 就是这次调度占用实际时间。如果这个时间大于 ideal_runtime,则应该被抢占了。除了这个条件之外,还会通过 __pick_first_entity 取出红黑树中最小的进程。如果当前进程的 vruntime 大于红黑树中最小的进程的 vruntime,且差值大于 ideal_runtime,也应该被抢占了。
如果确认需要被抢占,则会调用resched_curr()函数,该函数会调用set_tsk_need_resched()标记该任务为_TIF_NEED_RESCHED,即该任务应该被抢占。
某些任务会因为中断而唤醒,如当 I/O 到来的时候,I/O进程往往会被唤醒。在这种时候,如果被唤醒的任务优先级高于 CPU 上的当前任务,就会触发抢占。try_to_wake_up() 调用 ttwu_queue() 将这个唤醒的任务添加到队列当中。ttwu_queue() 再调用 ttwu_do_activate() 激活这个任务。ttwu_do_activate() 调用 ttwu_do_wakeup()。这里面调用了 check_preempt_curr() 检查是否应该发生抢占。如果应该发生抢占,也不是直接踢走当前进程,而是将当前进程标记为应该被抢占。
由前面的分析,我们知道了不论是是当前任务执行时间过长还是新任务唤醒,我们均会对现在的任务标记位_TIF_NEED_RESCUED,下面分析实际抢占的发生。真正的抢占还需要一个特定的时机让正在运行中的进程有机会调用一下 __schedule()函数,发起真正的调度。
实际上会调用__schedule()函数共有以下几个时机
从系统调用返回用户态:以64位为例,系统调用的链路为do_syscall_64->syscall_return_slowpath->prepare_exit_to_usermode->exit_to_usermode_loop。在exit_to_usermode_loop中,会检测是否为_TIF_NEED_RESCHED,如果是则调用__schedule()
内核态启动:内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。在内核态的执行中,有的 *** 作是不能被中断的,所以在进行这些 *** 作之前,总是先调用 preempt_disable() 关闭抢占,当再次打开的时候,就是一次内核态代码被抢占的机会。preempt_enable() 会调用 preempt_count_dec_and_test(),判断 preempt_count 和 TIF_NEED_RESCHED 是否可以被抢占。如果可以,就调用 preempt_schedule->preempt_schedule_common->__schedule 进行调度。
本文分析了任务调度的策略、结构体以及整个调度流程,其中关于内存上下文切换的部分尚未详细叙述,留待内存部分展开剖析。
1、调度相关结构体及函数实现
2、schedule核心函数
1.调度器的概述
多任务 *** 作系统分为非抢占式多任务和抢占式多任务。与大多数现代 *** 作系统一样,Linux采用的是抢占式多任务模式。这表示对CPU的占用时间由 *** 作系统决定的,具体为 *** 作系统中的调度器。调度器决定了什么时候停止一个进程以便让其他进程有机会运行,同时挑选出一个其他的进程开始运行。
2.调度策略
在Linux上调度策略决定了调度器是如何选择一个新进程的时间。调度策略与进程的类型有关,内核现有的调度策略如下:
#define SCHED_NORMAL 0#define SCHED_FIFO 1#define SCHED_RR 2#define SCHED_BATCH 3/* SCHED_ISO: reserved but not implemented yet */#define SCHED_IDLE 5
0: 默认的调度策略,针对的是普通进程。
1:针对实时进程的先进先出调度。适合对时间性要求比较高但每次运行时间比较短的进程。
2:针对的是实时进程的时间片轮转调度。适合每次运行时间比较长得进程。
3:针对批处理进程的调度,适合那些非交互性且对cpu使用密集的进程。
SCHED_ISO:是内核的一个预留字段,目前还没有使用
5:适用于优先级较低的后台进程。
注:每个进程的调度策略保存在进程描述符task_struct中的policy字段
3.调度器中的机制
内核引入调度类(struct sched_class)说明了调度器应该具有哪些功能。内核中每种调度策略都有该调度类的一个实例。(比如:基于公平调度类为:fair_sched_class,基于实时进程的调度类实例为:rt_sched_class),该实例也是针对每种调度策略的具体实现。调度类封装了不同调度策略的具体实现,屏蔽了各种调度策略的细节实现。
调度器核心函数schedule()只需要调用调度类中的接口,完成进程的调度,完全不需要考虑调度策略的具体实现。调度类连接了调度函数和具体的调度策略。
武特师兄关于sche_class和sche_entity的解释,一语中的。
调度类就是代表的各种调度策略,调度实体就是调度单位,这个实体通常是一个进程,但是自从引入了cgroup后,这个调度实体可能就不是一个进程了,而是一个组
4.schedule()函数
linux 支持两种类型的进程调度,实时进程和普通进程。实时进程采用SCHED_FIFO 和SCHED_RR调度策略,普通进程采用SCHED_NORMAL策略。
preempt_disable():禁止内核抢占
cpu_rq():获取当前cpu对应的就绪队列。
prev = rq->curr获取当前进程的描述符prev
switch_count = &prev->nivcsw获取当前进程的切换次数。
update_rq_clock() :更新就绪队列上的时钟
clear_tsk_need_resched()清楚当前进程prev的重新调度标志。
deactive_task():将当前进程从就绪队列中删除。
put_prev_task() :将当前进程重新放入就绪队列
pick_next_task():在就绪队列中挑选下一个将被执行的进程。
context_switch():进行prev和next两个进程的切换。具体的切换代码与体系架构有关,在switch_to()中通过一段汇编代码实现。
post_schedule():进行进程切换后的后期处理工作。
5.pick_next_task函数
选择下一个将要被执行的进程无疑是一个很重要的过程,我们来看一下内核中代码的实现
对以下这段代码说明:
1.当rq中的运行队列的个数(nr_running)和cfs中的nr_runing相等的时候,表示现在所有的都是普通进程,这时候就会调用cfs算法中的pick_next_task(其实是pick_next_task_fair函数),当不相等的时候,则调用sched_class_highest(这是一个宏,指向的是实时进程),这下面的这个for()循环中,首先是会在实时进程中选取要调度的程序(p = class->pick_next_task(rq))。如果没有选取到,会执行class=class->next在class这个链表中有三种类型(fair,idle,rt).也就是说会调用到下一个调度类。
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.nr_running)) {
p = fair_sched_class.pick_next_task(rq) if (likely(p)) return p
}//基于实时调度的实时进程
class = sched_class_highest for ( ) {
p = class->pick_next_task(rq) //实时进程的类
if (p) return p /*
* Will never be NULL as the idle class always
* returns a non-NULL p:
*/
class = class->next //rt->next = fair fair->next = idle
}
}
在这段代码中体现了Linux所支持的两种类型的进程,实时进程和普通进程。回顾下:实时进程可以采用SCHED_FIFO 和SCHED_RR调度策略,普通进程采用SCHED_NORMAL调度策略。
在这里首先说明一个结构体struct rq,这个结构体是调度器管理可运行状态进程的最主要的数据结构。每个cpu上都有一个可运行的就绪队列。刚才在pick_next_task函数中看到了在选择下一个将要被执行的进程时实际上用的是struct rq上的普通进程的调度或者实时进程的调度,那么具体是如何调度的呢?在实时调度中,为了实现O(1)的调度算法,内核为每个优先级维护一个运行队列和一个DECLARE_BITMAP,内核根据DECLARE_BITMAP的bit数值找出非空的最高级优先队列的编号,从而可以从非空的最高级优先队列中取出进程进行运行。
我们来看下内核的实现
struct rt_prio_array {DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1)/* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO]
}
数组queue[i]里面存放的是优先级为i的进程队列的链表头。在结构体rt_prio_array 中有一个重要的数据构DECLARE_BITMAP,它在内核中的第一如下:
define DECLARE_BITMAP(name,bits) \unsigned long name[BITS_TO_LONGS(bits)]
5.1对于实时进程的O(1)算法
这个数据是用来作为进程队列queue[MAX_PRIO]的索引位图。bitmap中的每一位与queue[i]对应,当queue[i]的进程队列不为空时,Bitmap的相应位就为1,否则为0,这样就只需要通过汇编指令从进程优先级由高到低的方向找到第一个为1的位置,则这个位置就是就绪队列中最高的优先级(函数sched_find_first_bit()就是用来实现该目的的)。那么queue[index]->next就是要找的候选进程。
如果还是不懂,那就来看两个图
注:在每个队列上的任务一般基于先进先出的原则进行调度(并且为每个进程分配时间片)
在内核中的实现为:
static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq, struct rt_rq *rt_rq){ struct rt_prio_array *array = &rt_rq->active struct sched_rt_entity *next = NULL struct list_head *queue int idxidx = sched_find_first_bit(array->bitmap)//找到优先级最高的位
BUG_ON(idx >= MAX_RT_PRIO) queue = array->queue + idx//然后找到对应的queue的起始地址
next = list_entry(queue->next, struct sched_rt_entity, run_list) //按先进先出拿任务
return next
}
那么当同一优先级的任务比较多的时候,内核会根据
位图:
将对应的位置为1,每次取出最大的被置为1的位,表示优先级最高:
5.2 关于普通进程的CFS算法:
我们知道,普通进程在选取下一个需要被调度的进程时,是调用的pick_next_task_fair函数。在这个函数中是以调度实体为单位进行调度的。其最主要的函数是:pick_next_entity,在这个函数中会调用wakeup_preempt_entity函数,这个函数的主要作用是根据进程的虚拟时间以及权重的结算进程的粒度,以判断其是否需要抢占。看一下内核是怎么实现的:
wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se){
s64 gran, vdiff = curr->vruntime - se->vruntime//计算两个虚拟时间差//如果se的虚拟时间比curr还大,说明本该curr执行,无需抢占
if (vdiff <= 0) return -1
gran = wakeup_gran(curr, se) if (vdiff >gran) return 1 return 0
}
gran为需要抢占的时间差,只有两个时间差大于需要抢占的时间差,才需要抢占,这里避免太频繁的抢占
wakeup_gran(struct sched_entity *curr, struct sched_entity *se){
unsigned long gran = sysctl_sched_wakeup_granularity if (cfs_rq_of(curr)->curr &&sched_feat(ADAPTIVE_GRAN))
gran = adaptive_gran(curr, se)
/*
* Since its curr running now, convert the gran from real-time
* to virtual-time in his units.
*/ if (sched_feat(ASYM_GRAN)) {
/*
* By using 'se' instead of 'curr' we penalize light tasks, so
* they get preempted easier. That is, if 'se' <'curr' then
* the resulting gran will be larger, therefore penalizing the
* lighter, if otoh 'se' >'curr' then the resulting gran will
* be smaller, again penalizing the lighter task.
*
* This is especially important for buddies when the leftmost
* task is higher priority than the buddy.
*/ if (unlikely(se->load.weight != NICE_0_LOAD))
gran = calc_delta_fair(gran, se)
} else { if (unlikely(curr->load.weight != NICE_0_LOAD))
gran = calc_delta_fair(gran, curr)
} return gran
}
6.调度中的nice值
首先需要明确的是:nice的值不是进程的优先级,他们不是一个概念,但是进程的Nice值会影响到进程的优先级的变化。
通过命令ps -el可以看到进程的nice值为NI列。PRI表示的是进程的优先级,其实进程的优先级只是一个整数,它是调度器选择进程运行的基础。
普通进程有:静态优先级和动态优先级。
静态优先级:之所有称为静态优先级是因为它不会随着时间而改变,内核不会修改它,只能通过系统调用nice去修改,静态优先级用进程描述符中的static_prio来表示。在内核中/kernel/sched.c中,nice和静态优先级的关系为:
#define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20)#define PRIO_TO_NICE(prio) ((prio) - MAX_RT_PRIO - 20)
动态优先级:调度程序通过增加或者减小进程静态优先级的值来奖励IO小的进程或者惩罚cpu消耗型的进程。调整后的优先级称为动态优先级。在进程描述中用prio来表示,通常所说的优先级指的是动态优先级。
由上面分析可知,我们可以通过系统调用nice函数来改变进程的优先级。
#include <stdlib.h>#include <stdio.h>#include <math.h>#include <unistd.h>#include <sys/time.h>#define JMAX (400*100000)#define GET_ELAPSED_TIME(tv1,tv2) ( \(double)( (tv2.tv_sec - tv1.tv_sec) \
+ .000001 * (tv2.tv_usec - tv1.tv_usec)))//做一个延迟的计算double do_something (void){ int j double x = 0.0 struct timeval tv1, tv2
gettimeofday (&tv1, NULL)//获取时区
for (j = 0j <JMAXj++)
x += 1.0 / (exp ((1 + x * x) / (2 + x * x)))
gettimeofday (&tv2, NULL) return GET_ELAPSED_TIME (tv1, tv2)//求差值}int main (int argc, char *argv[]){ int niceval = 0, nsched /* for kernels less than 2.6.21, this is HZ
for tickless kernels this must be the MHZ rate
e.g, for 2.6 GZ scale = 2600000000 */
long scale = 1000 long ticks_cpu, ticks_sleep pid_t pid
FILE *fp char fname[256] double elapsed_time, timeslice, t_cpu, t_sleep if (argc >1)
niceval = atoi (argv[1])
pid = getpid () if (argc >2)
scale = atoi (argv[2]) /* give a chance for other tasks to queue up */
sleep (3) sprintf (fname, "/proc/%d/schedstat", pid)//读取进程的调度状态
/*
在schedstat中的数字是什么意思呢?:
*/
/* printf ("Fname = %s\n", fname)*/
if (!(fp = fopen (fname, "r"))) { printf ("Failed to open stat file\n") exit (-1)
} //nice系统调用
if (nice (niceval) == -1 &&niceval != -1) { printf ("Failed to set nice to %d\n", niceval) exit (-1)
}
elapsed_time = do_something ()//for 循环执行了多长时间
fscanf (fp, "%ld %ld %d", &ticks_cpu, &ticks_sleep, &nsched)//nsched表示调度的次数
t_cpu = (float)ticks_cpu / scale//震动的次数除以1000,就是时间
t_sleep = (float)ticks_sleep / scale
timeslice = t_cpu / (double)nsched//除以调度的次数,就是每次调度的时间(时间片)
printf ("\nnice=%3d time=%8g secs pid=%5d"
" t_cpu=%8g t_sleep=%8g nsched=%5d"
" avg timeslice = %8g\n",
niceval, elapsed_time, pid, t_cpu, t_sleep, nsched, timeslice)
fclose (fp) exit (0)
}
说明: 首先说明的是/proc/[pid]/schedstat:在这个文件下放着3个变量,他们分别代表什么意思呢?
第一个:该进程拥有的cpu的时间
第二个:在对列上的等待时间,即睡眠时间
第三个:被调度的次数
由结果可以看出当nice的值越小的时候,其睡眠时间越短,则表示其优先级升高了。
7.关于获取和设置优先级的系统调用:sched_getscheduler()和sched_setscheduler
#include <sched.h>#include <stdlib.h>#include <stdio.h>#include <errno.h>#define DEATH(mess) { perror(mess)exit(errno)}void printpolicy (int policy){ /* SCHED_NORMAL = SCHED_OTHER in user-space */if (policy == SCHED_OTHER) printf ("policy = SCHED_OTHER = %d\n", policy) if (policy == SCHED_FIFO) printf ("policy = SCHED_FIFO = %d\n", policy) if (policy == SCHED_RR) printf ("policy = SCHED_RR = %d\n", policy)
}int main (int argc, char **argv){ int policy struct sched_param p /* obtain current scheduling policy for this process */
//获取进程调度的策略
policy = sched_getscheduler (0)
printpolicy (policy) /* reset scheduling policy */
printf ("\nTrying sched_setscheduler...\n")
policy = SCHED_FIFO
printpolicy (policy)
p.sched_priority = 50 //设置优先级为50
if (sched_setscheduler (0, policy, &p))
DEATH ("sched_setscheduler:") printf ("p.sched_priority = %d\n", p.sched_priority) exit (0)
}
输出结果:
[root@wang schedule]# ./get_schedule_policy policy = SCHED_OTHER = 0Trying sched_setscheduler...
policy = SCHED_FIFO = 1
p.sched_priority = 50
可以看出进程的优先级已经被改变。
一个完整的Linux内核一般由5部分组成,它们分别是内存管理、进程管理、进程间通信、虚拟文件系统和网络接口。
1、内存管理
内存管理主要完成的是如何合理有效地管理整个系统的物理内存,同时快速响应内核各个子系统对内存分配的请求。
Linux内存管理支持虚拟内存,而多余出的这部分内存就是通过磁盘申请得到的,平时系统只把当前运行的程序块保留在内存中,其他程序块则保留在磁盘中。在内存紧缺时,内存管理负责在磁盘和内存间交换程序块。
2、进程管理
进程管理主要控制系统进程对CPU的访问。当需要某个进程运行时,由进程调度器根据基于优先级的调度算法启动新的进程。:Linux支持多任务运行,那么如何在一个单CPU上支持多任务呢?这个工作就是由进程调度管理来实现的。
在系统运行时,每个进程都会分得一定的时间片,然后进程调度器根据时间片的不同,选择每个进程依次运行,例如当某个进程的时间片用完后,调度器会选择一个新的进程继续运行。
由于切换的时间和频率都非常的快,由此用户感觉是多个程序在同时运行,而实际上,CPU在同一时间内只有一个进程在运行,这一切都是进程调度管理的结果。
3、进程间通信
进程间通信主要用于控制不同进程之间在用户空间的同步、数据共享和交换。由于不用的用户进程拥有不同的进程空间,因此进程间的通信要借助于内核的中转来实现。
一般情况下,当一个进程等待硬件 *** 作完成时,会被挂起。当硬件 *** 作完成,进程被恢复执行,而协调这个过程的就是进程间的通信机制。
4、虚拟文件系统
Linux内核中的虚拟文件系统用一个通用的文件模型表示了各种不同的文件系统,这个文件模型屏蔽了很多具体文件系统的差异,使Linux内核支持很多不同的文件系统。
这个文件系统可以分为逻辑文件系统和设备驱动程序:逻辑文件系统指Linux所支持的文件系统,例如ext2、ext3和fat等;设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块。
5、网络接口
网络接口提供了对各种网络标准的实现和各种网络硬件的支持。网络接口一般分为网络协议和网络驱动程序。网络协议部分负责实现每一种可能的网络传输协议。
网络设备驱动程序则主要负责与硬件设备进行通信,每一种可能的网络硬件设备都有相应的设备驱动程序。
扩展资料:
Linux *** 作系统的诞生、发展和成长过程始终依赖着五个重要支柱:UNIX *** 作系统、MINIX *** 作系统、GNU计划、POSIX 标准和Internet 网络。
1981 年IBM公司推出微型计算机IBM PC。
1991年,GNU计划已经开发出了许多工具软件,最受期盼的GNU C编译器已经出现,GNU的 *** 作系统核心HURD一直处于实验阶段,没有任何可用性,实质上也没能开发出完整的GNU *** 作系统,但是GNU奠定了Linux用户基础和开发环境。
1991年初,林纳斯·托瓦兹开始在一台386sx兼容微机上学习minix *** 作系统。1991年4月,林纳斯·托瓦兹开始酝酿并着手编制自己的 *** 作系统。
1991 年4 月13 日在comp.os.minix 上发布说自己已经成功地将bash 移植到了minix 上,而且已经爱不释手、不能离开这个shell软件了。
1993年,大约有100余名程序员参与了Linux内核代码编写/修改工作,其中核心组由5人组成,此时Linux 0.99的代码大约有十万行,用户大约有10万左右。
1994年3月,Linux1.0发布,代码量17万行,当时是按照完全自由免费的协议发布,随后正式采用GPL协议。
1995年1月,Bob Young创办了RedHat(小红帽),以GNU/Linux为核心,集成了400多个源代码开放的程序模块,搞出了一种冠以品牌的Linux,即RedHat Linux,称为Linux"发行版",在市场上出售。这在经营模式上是一种创举。
2001年1月,Linux 2.4发布,它进一步地提升了SMP系统的扩展性,同时它也集成了很多用于支持桌面系统的特性:USB,PC卡(PCMCIA)的支持,内置的即插即用,等等功能。
2003年12月,Linux 2.6版内核发布,相对于2.4版内核2.6在对系统的支持都有很大的变化。
2004年的第1月,SuSE嫁到了Novell,SCO继续顶着骂名四处强行“化缘”, Asianux, MandrakeSoft也在五年中首次宣布季度赢利。3月,SGI宣布成功实现了Linux *** 作系统支持256个Itanium 2处理器。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)