当前位置:首页 » 操作系统 » linuxswitchto

linuxswitchto

发布时间: 2023-07-20 05:26:30

linux内核怎么调度系统

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),该实例也是针对每种调度策略的具体实现。调度类封装了不同调度策略的具体实现,屏蔽了各种调度策略的细节实现。
调度器核心函数schele()只需要调用调度类中的接口,完成进程的调度,完全不需要考虑调度策略的具体实现。调度类连接了调度函数和具体的调度策略。

  • 武特师兄关于sche_class和sche_entity的解释,一语中的。

  • 调度类就是代表的各种调度策略,调度实体就是调度单位,这个实体通常是一个进程,但是自从引入了cgroup后,这个调度实体可能就不是一个进程了,而是一个组

  • 4.schele()函数

    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_schele():进行进程切换后的后期处理工作。

    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就是要找的候选进程。
    如果还是不懂,那就来看两个图

    由结果可以看出当nice的值越小的时候,其睡眠时间越短,则表示其优先级升高了。

    7.关于获取和设置优先级的系统调用:sched_getscheler()和sched_setscheler

  • #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 ", policy); if (policy == SCHED_FIFO) printf ("policy = SCHED_FIFO = %d ", policy); if (policy == SCHED_RR) printf ("policy = SCHED_RR = %d ", policy);

  • }int main (int argc, char **argv){ int policy; struct sched_param p; /* obtain current scheling policy for this process */

  • //获取进程调度的策略

  • policy = sched_getscheler (0);

  • printpolicy (policy); /* reset scheling policy */


  • printf (" Trying sched_setscheler... ");

  • policy = SCHED_FIFO;

  • printpolicy (policy);

  • p.sched_priority = 50; //设置优先级为50

  • if (sched_setscheler (0, policy, &p))

  • DEATH ("sched_setscheler:"); printf ("p.sched_priority = %d ", p.sched_priority); exit (0);

  • }

  • 输出结果:

  • [root@wang schele]# ./get_schele_policy policy = SCHED_OTHER = 0


  • Trying sched_setscheler...

  • policy = SCHED_FIFO = 1

  • p.sched_priority = 50

  • 可以看出进程的优先级已经被改变。

Ⅱ linux 怎样安装软件

Linux下软件安装方法

对于Linux初学者来说,安装一个很小的软件恐怕都是一件很让人头疼的事,因为在Linux下安装软件不像在 Windows中那样简单。在Linux中大多数软件提供的是源代码,而不是现成的可执行文件,这就要求用户根据自己系统的实际情况和自身的需要来配置、编译源程序后,软件才能使用。多数初学者往往不知道该如何进行配置和编译就盲目地运行一些有执行属性的文件或者机械地运行“make”、“make install”之类的命令。结果呢?是软件没装成,自己急出一身汗,后果严重的还会破坏系统的稳定性。下面笔者将安装软件方面的一些规律写出来与大家分享。

目前流行的软件包有两种比较常见的形式,一种是RPM包的形式,另一种是压缩成*.tar.gz的形式。本文将讨论这两种形式的软件包在文本环境和图形环境(X Window)下不同的安装方法。

文本环境下的软件安装

一、安装简便的RPM包

RPM是RedHat Package Manager的缩写,它只能使用在安装了RPM软件的系统中,RedHat Linux和Turbo Linux中已经使用了它。这种结构的包使用起来还是很方便的,只要记住几条简单的命令和参数就可以方便地使用:

#〉rpm [options] filename.rpm

其中常用的options包括:

-i: 安装软件

-e: 卸载软件

-q:查看软件安装的信息和状态

-U: 升级现有软件

例如安装软件时,可以使用如下命令:“rpm -i filename.rpm”,软件安装在什么地方、是怎么安装的都不需要用户操心,RPM可以帮助用户管理。由于RPM使用方便,很多软件都有RPM版本。如果想使用RPM形式的软件就要首先下载一个RPM管理软件。在ftp://ftp.rpm.org/pub/rpm/dist/rpm- version网站可以下载最新的版本——rpm-3.-.4.i386。该软件有RPM包和.tar.gz包两种形式,如果你的系统中已经有了RPM管理软件,你可以下载RPM形式的包来升级现有软件,否则就必须了解.tar.gz包的安装方法。

二、安装需要编译的.tar.gz包

由于RPM包使用的局限性(必须安装RPM),目前更多的软件使用的是源代码形式的.tar.gz包。这种软件的安装通常要经过解压缩、软件配置、软件编译及安装的过程。

解压缩通常有两种命令方法:一种是“gunzip filename-VERSION-OS.tar.gz | tar xvf -”,它实质是两条命令“gunzip filename-VERSION-OS.tar.gz”和“tar xvf filename-VERSION-OS.tar”;另一种是“tar xzvf filename-VERSION-OS.tar.gz”。一般来说在软件解压缩后会生成一个目录filename-VERSION-OS。

软件的配置、编译、安装是最让初学者望而生畏的事了,但笔者认为掌握一些规律还是不难的。一般来说,在解压缩生成的目录中都会有名为Readme、 Rnstall或Readme.install之类的文件。这些文件通常会对软件的功能、特性、版权许可、安装以及相关知识加以介绍,并且会提到关于安装的方法和步骤。举例来说:在apache_1.3.6的install文件中说明了如下内容(此处只列出总的条目,具体内容省略):

Installing the Apache 1.3 HTTP server with APACI

==============================

1.Overview for the impatient(概括说明配置的全过程)

$./configure--prefix=PREFIX

$make

$make install

$PREFIX/bin/apachectl start

2.Requirements(需要的条件)

3.Configuring the source tree(配置的参数说明)

4.Building the package(编译软件的方法)

5.Installing the package(安装软件的方法)

6.Testing the package(软件测试)

理解并能熟练使用这些说明文件后,就可以利用一些规律来安装大多数的软件。对于那些没有说明文件的软件(当然这种情况比较少见),这些规律通常也是适用的。一般来说,与安装软件有直接关系的文件只有两个:configure 、Makefile。

其中,configure文件具有可执行的属性,是用来配置软件的,它的参数比较多,用法也比较灵活。当然,不同的软件参数也不相同,这时候就需要借助它的help参数,运行下面的命令就会让你感到豁然开朗:

#〉 ./configure -help

Usage: configure [options]

Options: [defaults in brackets after descriptions]

General options:

--quiet, --silent do not print messages

--verbose,-v print even more messages

--sha [=DIR] switch to a shadow tree (under DIR) for building

Stand-alone options:

--help,-h print this message

--show-layout print installation path layout (check and debug)


Installation layout options:

--with-layout=[F:]ID use installation path layout ID (from file F)

--target=TARGET install name-associated files using basename TARGET

……

接下来,就可以运行“./configure [options]”来配置该软件。注意,命令行中的“./”非常重要,它告诉系统要运行的命令就在当前目录下(否则系统就会到$path变量指定的路径下去执行命令)。执行命令后可以生成Makefile文件或者修改已有的文件配置。

Makefile文件通常是用来编译和安装软件的。运行make命令时系统会自动根据Makefile文件中的设置对软件进行编译和安装。make命令有时还可以带一些参数,如:all、build、config、install等。具体要带哪个参数可以参看Makefile文件。在Linux中绝大部分文件是文本文件,Makefile就是一个shell程序(Linux中shell程序与DOS中的批处理文件有很多相似之处,当然功能要强得多),很容易读懂,尤其是编译时可带的参数都会明确写出,例如:

##========================

## Targets

##========================

# default target

all: build

##------------------------

## Build Target

##------------------------

# build the package

build:

……

# the non-verbose variant for package maintainers

build-quiet:

@$(MAKE) -f $(TOP)/$(MKF) $(MFLAGS) $(MFWD) QUIET=1 build

# build the additional support stuff

build-support:

……

##------------------------

## Installation Targets

## -----------------------

# the install target for installing the complete Apache

# package. This is implemented by running subtargets for the

# separate parts of the installation process.

install:

……

# the non-verbose variant for package maintainers

install-quiet:

@$(MAKE) -f $(TOP)/$(MKF) $(MFLAGS) $(MFWD) QUIET=1 install

# create the installation tree

install-mktree:

……

上面这段代码是apache_1.3.6的Makefile文件的一部分,从这段程序可以看出all参数表示完全编译(缺省参数)。此外,编译时还可以带 build、build-quiet、build-surpport等参数;安装时可以带install、install-quiet、install- surpport等参数。它们的功能分别在“#”表示的注释中进行了说明。需要额外说明的是,有些软件(例如Linux的内核升级程序)不用 configure命令来配置软件,而是用make config来完成这项工作,所以,具体使用哪种方法要具体问题具体分析。

图形界面下的软件安装

在图形环境下,同样可以弹出一个仿真终端以文本的方法来安装软件,但那样就太笨了,因为在X Window中有一些简单的方法可以帮您完成软件安装。下面笔者以RedHat 6.0为例做介绍。

一、图形界面下安装.tar.gz包

在X Window下,安装这种形式的包简化程度并不大,只是在解压缩软件时方便一些。用鼠标左键双击要安装的软件包,系统就会自动生成一个目录—— filename.tar.gz#utar,在这个目录下就有你要解压缩的内容——filename目录。将该目录拷贝到你要解压缩的目录下,解压缩的工作就这样简单地完成了。不过,剩下的工作还要弹出一个仿真终端以文本的方法来完成。

二、图形界面下安装RPM包

在X Window中你要安装、升级、卸载和查询一个RPM软件包实在是太容易了。以Redhat 6.0为例,它的X Window中有一个Gnome RPM软件可以完成上面提到的一切工作。

点击“Start”→“System”→“GnomeRPM”,可以运行该软件。它将安装好的RPM包形式的软件按照功能分在Amusements、 Application、Development、Document、Extension、Extentions、Networking、System Environment、UserInterface、X11等几个树形目录中,每个目录中有相应的文件图标和名称。要安装或升级软件,只要点击工具栏的相应按钮就会弹出打开文件的对话框,选中你要安装的RPM文件,单击“OK”按钮,一切大功告成;卸载软件时,需要根据分类找到该软件的图标,点击右键,选Uninstall就可以了。如果你会在Win 95/98中查找文件的话,在Linux中查找已经安装的软件也就不难了。这个软件可以到下面的站点下载: ftp.gnome.org/pub/GNOME/stable/

sources/gnorpm。

几点注意事项

一、安装方法的适用范围

上面提到的软件安装方法并不是在任何版本的Linux上都适用。笔者认为,.tar.gz包的安装方法适用于各种版本的Linux,而RPM包则有一定的局限性。

目前常见的各种Linux发行版本中,如:Redhat 6.0、Turbo Linux 3.0.2、Xteam 3.0等都支持RPM包。如果你想知道你使用的Linux是否支持RPM包,只要运行一下“rpm --help”命令就知道了。不过,对于不支持RPM包的版本,可以安装一个RPM管理软件。

二、容易出现的问题

在安装软件时,一定要保证你对用到的软件包有访问权限。当然如果你是root就没问题了。但如果你真的是root你就需要注意另一个问题,由于root的权限过高,所以在安装软件时,要防止对系统其它软件造成误操作(在使用rm 等“危险”命令时,尤其要注意)。

另一个容易出现的问题是,在卸载RPM包的软件时要慎重,因为很多软件之间是相互关联的,你卸载的软件很可能是其它软件要用到的,要防止由于卸载了一个软件而影响另一个软件的正常使用。所以笔者建议,初学者对于与系统运行有关的软件尽量不要删除(对于游戏、应用软件一类的软件不必有太多顾虑)。等你成为一名经验丰富的系统管理员时,你就能灵活地处理这些问题了。

Ⅲ 一文读懂Linux任务间调度原理和整个执行过程

在前文中,我们分析了内核中进程和线程的统一结构体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,等等。

由此,我们来总结一下:

有了上述的基本策略和基本调度结构体,我们可以形成大致的骨架,下面就是需要核心的调度流程将其拼凑成一个整体,实现调度系统。调度分为两种,主动调度和抢占式调度。

说到调用,逃不过核心函数schele()。其中sched_submit_work()函数完成当前任务的收尾工作,以避免出现如死锁或者IO中断等情况。之后首先禁止抢占式调度的发生,然后调用__schele()函数完成调度,之后重新打开抢占式调度,如果需要重新调度则会一直重复该过程,否则结束函数。

而__schele()函数则是实际的核心调度函数,该函数主要操作包括选取下一进程和进行上下文切换,而上下文切换又包括用户态空间切换和内核态的切换。具体的解释可以参照英文源码注释以及中文对各个步骤的注释。

其中核心函数是获取下一个任务的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。

至此,我们完成了内核态的切换工作,也完成了整个主动调度的过程。

抢占式调度通常发生在两种情况下。一种是某任务执行时间过长,另一种是当某任务被唤醒的时候。首先看看任务执行时间过长的情况。

该情况需要衡量一个任务的执行时间长短,执行时间过长则发起抢占。在计算机里面有一个时钟,会过一段时间触发一次时钟中断,通知操作系统时间又过去一个时钟周期,通过这种方式可以查看是否是需要抢占的时间点。

时钟中断处理函数会调用scheler_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,下面分析实际抢占的发生。真正的抢占还需要一个特定的时机让正在运行中的进程有机会调用一下 __schele()函数,发起真正的调度。

实际上会调用__schele()函数共有以下几个时机

从系统调用返回用户态:以64位为例,系统调用的链路为do_syscall_64->syscall_return_slowpath->prepare_exit_to_usermode->exit_to_usermode_loop。在exit_to_usermode_loop中,会检测是否为_TIF_NEED_RESCHED,如果是则调用__schele()

内核态启动:内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。在内核态的执行中,有的操作是不能被中断的,所以在进行这些操作之前,总是先调用 preempt_disable() 关闭抢占,当再次打开的时候,就是一次内核态代码被抢占的机会。preempt_enable() 会调用 preempt_count_dec_and_test(),判断 preempt_count 和 TIF_NEED_RESCHED 是否可以被抢占。如果可以,就调用 preempt_schele->preempt_schele_common->__schele 进行调度。

   本文分析了任务调度的策略、结构体以及整个调度流程,其中关于内存上下文切换的部分尚未详细叙述,留待内存部分展开剖析。

1、调度相关结构体及函数实现

2、schele核心函数

Ⅳ 研究生课题 linux内核怎么样 2015

图2-1显示了基于x86计算机Linux系统的启动顺序。第一步是BIOS从启动设备中导入主引导记录(MBR),接下来MBR中的代码查看分区表并从活动分区读取GRUB、LILO或SYSLINUX等引导装入程序,之后引导装入程序会加载压缩后的内核映像并将控制权传递给它。内核取得控制权后,会将自身解压缩并投入运转。
基于x86的处理器有两种操作模式:实模式和保护模式。在实模式下,用户仅可以使用1 MB内存,并且没有任何保护。保护模式要复杂得多,用户可以使用更多的高级功能(如分页)。CPU 必须中途将实模式切换为保护模式。但是,这种切换是单向的,即不能从保护模式再切换回实模式。
内核初始化的第一步是执行实模式下的汇编代码,之后执行保护模式下init/main.c文件(上一章修改的源文件)中的 start_kernel()函数。start_kernel()函数首先会初始化CPU子系统,之后让内存和进程管理系统就位,接下来启动外部总线和 I/O设备,最后一步是激活初始化(init)程序,它是所有Linux进程的父进程。初始化进程执行启动必要的内核服务的用户空间脚本,并且最终派生控制台终端程序以及显示登录(login)提示。

图2-1 基于x86硬件上的Linux的启动过程
本节内的3级标题都是图2-2中的一条打印信息,这些信息来源于基于x86的笔记本 电脑的Linux启动过程。如果在其他体系架构上启动内核,消息以及语义可能会有所不同。

2.1.1 BIOS-provided physical RAM map
内核会解析从BIOS中读取到的系统内存映射,并率先将以下信息打印出来:
BIOS-provided physical RAM map:
BIOS-e820: 0000000000000000 - 000000000009f000 (usable)
...
BIOS-e820: 00000000ff800000 - 0000000100000000 (reserved)
实模式下的初始化代码通过使用BIOS的int 0x15服务并执行0xe820号函数(即上面的BIOS-e820字符串)来获得系统的内存映射信息。内存映射信息中包含了预留的和可用的内存,内核将随后使用这些信息创建其可用的内存池。在附录B的B.1节,我们会对BIOS提供的内存映射问题进行更深入的讲解。

图2-2 内核启动信息
2.1.2 758MB LOWMEM available
896 MB以内的常规的可被寻址的内存区域被称作低端内存。内存分配函数kmalloc()就是从该区域分配内存的。高于896 MB的内存区域被称为高端内存,只有在采用特殊的方式进行映射后才能被访问。
在启动过程中,内核会计算并显示这些内存区内总的页数。

2.1.3 Kernel command line: ro root=/dev/hda1
Linux的引导装入程序通常会给内核传递一个命令行。命令行中的参数类似于传递给C程序中main()函数的argv[]列表,唯一的不同在于它们是传递给内核的。可以在引导装入程序的配置文件中增加命令行参数,当然,也可以在运行过程中修改引导装入程序的提示行[1]。如果使用的是GRUB 这个引导装入程序,由于发行版本的不同,其配置文件可能是/boot/grub/grub.conf或者是/boot/grub/menu.lst。如果使用的是LILO,配置文件为/etc/lilo.conf。下面给出了一个grub.conf文件的例子(增加了一些注释),看了紧接着title kernel 2.6.23的那行代码之后,你会明白前述打印信息的由来。
default 0 #Boot the 2.6.23 kernel by default
timeout 5 #5 second to alter boot order or parameters
title kernel 2.6.23 #Boot Option 1
#The boot image resides in the first partition of the first disk
#under the /boot/ directory and is named vmlinuz-2.6.23. 'ro'
#indicates that the root partition should be mounted read-only.
kernel (hd0,0)/boot/vmlinuz-2.6.23 ro root=/dev/hda1
#Look under section "Freeing initrd memory:387k freed"
initrd (hd0,0)/boot/initrd
#...
命令行参数将影响启动过程中的代码执行路径。举一个例子,假设某命令行参数为bootmode,如果该参数被设置为1,意味着你希望在启动过程中打印一些调试信息并在启动结束时切换到runlevel的第3级(初始化进程的启动信息打印后就会了解runlevel的含义);如果bootmode 参数被设置为0,意味着你希望启动过程相对简洁,并且设置runlevel为2。既然已经熟悉了init/main.c文件,下面就在该文件中增加如下修改:
static unsigned int bootmode = 1 ;
static int __init
is_bootmode_setup( char * str)
{
get_option( & str, & bootmode);
return 1 ;
}

/* Handle parameter "bootmode=" */
__setup( " bootmode= " , is_bootmode_setup);

if (bootmode) {
/* Print verbose output */
/* ... */
}

/* ... */

/* If bootmode is 1, choose an init runlevel of 3, else
switch to a run level of 2 */
if (bootmode) {
argv_init[ ++ args] = " 3 " ;
} else {
argv_init[ ++ args] = " 2 " ;
}

/* ... */
请重新编译内核并尝试运行新的修改。

2.1.4 Calibrating delay...1197.46 BogoMIPS (lpj=2394935)
在启动过程中,内核会计算处理器在一个jiffy时间内运行一个内部的延迟循环的次数。jiffy的含义是系统定时器2个连续的节拍之间的间隔。正如所料,该计算必须被校准到所用CPU的处理速度。校准的结果被存储 在称为loops_per_jiffy的内核变量中。使用loops_per_jiffy的一种情况是某设备驱动程序希望进行小的微秒级别的延迟的时候。
为了理解延迟—循环校准代码,让我们看一下定义于init/calibrate.c文件中的calibrate_ delay()函数。该函数灵活地使用整型运算得到了浮点的精度。如下的代码片段(有一些注释)显示了该函数的开始部分,这部分用于得到一个 loops_per_jiffy的粗略值:
loops_per_jiffy = ( 1 << 12 ); /* Initial approximation = 4096 */
printk(KERN_DEBUG “Calibrating delay loop...“);
while ((loops_per_jiffy <<= 1 ) != 0 ) {
ticks = jiffies; /* As you will find out in the section, “Kernel
Timers," the jiffies variable contains the
number of timer ticks since the kernel
started, and is incremented in the timer
interrupt handler */

while (ticks == jiffies); /* Wait until the start of the next jiffy */
ticks = jiffies;
/* Delay */
__delay(loops_per_jiffy);
/* Did the wait outlast the current jiffy? Continue if it didn't */
ticks = jiffies - ticks;
if (ticks) break ;
}

loops_per_jiffy >>= 1 ; /* This fixes the most significant bit and is
the lower-bound of loops_per_jiffy */
上述代码首先假定loops_per_jiffy大于4096,这可以转化为处理器速度大约为每秒100万条指令,即1 MIPS。接下来,它等待jiffy被刷新(1个新的节拍的开始),并开始运行延迟循环__delay(loops_per_jiffy)。如果这个延迟循环持续了1个jiffy以上,将使用以前的loops_per_jiffy值(将当前值右移1位)修复当前loops_per_jiffy的最高位;否则,该函数继续通过左移loops_per_jiffy值来探测出其最高位。在内核计算出最高位后,它开始计算低位并微调其精度:
loopbit = loops_per_jiffy;

/* Graally work on the lower-order bits */
while (lps_precision -- && (loopbit >>= 1 )) {
loops_per_jiffy |= loopbit;
ticks = jiffies;
while (ticks == jiffies); /* Wait until the start of the next jiffy */
ticks = jiffies;

/* Delay */
__delay(loops_per_jiffy);

if (jiffies != ticks) /* longer than 1 tick */
loops_per_jiffy &= ~ loopbit;
}
上述代码计算出了延迟循环跨越jiffy边界时loops_per_jiffy的低位值。这个被校准的值可被用于获取BogoMIPS(其实它是一个并非科学的处理器速度指标)。可以使用BogoMIPS作为衡量处理器运行速度的相对尺度。在1.6G Hz 基于Pentium M的笔记本 电脑上,根据前述启动过程的打印信息,循环校准的结果是:loops_per_jiffy的值为2394935。获得BogoMIPS的方式如下:
BogoMIPS = loops_per_jiffy * 1秒内的jiffy数 * 延迟循环消耗的指令数(以百万为单位)
= ( 2394935 * HZ * 2 ) / ( 1000000 )
= ( 2394935 * 250 * 2 ) / ( 1000000 )
= 1197.46 (与启动过程打印信息中的值一致)
在2.4节将更深入阐述jiffy、HZ和loops_per_jiffy。

2.1.5 Checking HLT instruction
由于Linux内核支持多种硬件平台,启动代码会检查体系架构相关的bug。其中一项工作就是验证停机(HLT)指令。
x86处理器的HLT指令会将CPU置入一种低功耗睡眠模式,直到下一次硬件中断发生之前维持不变。当内核想让CPU进入空闲状态时(查看 arch/x86/kernel/process_32.c文件中定义的cpu_idle()函数),它会使用HLT指令。对于有问题的CPU而言,命令行参数no-hlt可以禁止HLT指令。如果no-hlt被设置,在空闲的时候,内核会进行忙等待而不是通过HLT给CPU降温。
当init/main.c中的启动代码调用include/asm-your-arch/bugs.h中定义的check_bugs()时,会打印上述信息。
2.1.6 NET: Registered protocol family 2
Linux套接字(socket)层是用户空间应用程序访问各种网络 协议的统一接口。每个协议通过include/linux/socket.h文件中定义的分配给它的独一无二的系列号注册。上述打印信息中的Family 2代表af_inet(互联网协议)。
启动过程中另一个常见的注册协议系列是AF_NETLINK(Family 16)。网络链接套接字提供了用户进程和内核通信 的方法。通过网络链接套接字可完成的功能还包括存取路由表和地址解析协议(ARP)表(include/linux/netlink.h文件给出了完整的用法列表)。对于此类任务而言,网络链接套接字比系统调用更合适,因为前者具有采用异步机制、更易于实现和可动态链接的优点。
内核中经常使能的另一个协议系列是AF_Unix或Unix-domain套接字。X Windows等程序使用它们在同一个系统上进行进程间通信。
2.1.7 Freeing initrd memory: 387k freed
initrd是一种由引导装入程序加载的常驻内存的虚拟磁盘映像。在内核启动后,会将其挂载为初始根文件系统,这个初始根文件系统中存放着挂载实际根文件系统磁盘分区时所依赖的可动态连接的模块。由于内核可运行于各种各样的存储控制器硬件平台上,把所有可能的磁盘驱动程序都直接放进基本的内核映像中并不可行。你所使用的系统的存储设备的驱动程序被打包放入了initrd中,在内核启动后、实际的根文件系统被挂载之前,这些驱动程序才被加载。使用 mkinitrd命令可以创建一个initrd映像。
2.6内核提供了一种称为initramfs的新功能,它在几个方面较initrd更为优秀。后者模拟了一个磁盘(因而被称为 initramdisk或initrd),会带来Linux块I/O子系统的开销(如缓冲);前者基本上如同一个被挂载的文件系统一样,由自身获取缓冲 (因此被称作initramfs)。
不同于initrd,基于页缓冲建立的initramfs如同页缓冲一样会动态地变大或缩小,从而减少了其内存消耗。另外,initrd要求你的内核映像包含initrd所使用的文件系统(例如,如果initrd为EXT2文件系统,内核必须包含EXT2驱动程序),然而initramfs不需要文件系统支持。再者,由于initramfs只是页缓冲之上的一小层,因此它的代码量很小。
用户可以将初始根文件系统打包为一个cpio压缩包[1],并通过initrd=命令行参数传递给内核。当然,也可以在内核配置过程中通过 INITRAMFS_SOURCE选项直接编译进内核。对于后一种方式而言,用户可以提供cpio压缩包的文件名或者包含initramfs的目录树。在启动过程中,内核会将文件解压缩为一个initramfs根文件系统,如果它找到了/init,它就会执行该顶层的程序。这种获取初始根文件系统的方法对于嵌入式系统而言特别有用,因为在嵌入式系统中系统资源非常宝贵。使用mkinitramfs可以创建一个initramfs映像,查看文档 Documentation/filesystems/ramfs- rootfs-initramfs.txt可获得更多信息。
在本例中,我们使用的是通过initrd=命令行参数向内核传递初始根文件系统cpio压缩包的方式。在将压缩包中的内容解压为根文件系统后,内核将释放该压缩包所占据的内存(本例中为387 KB)并打印上述信息。释放后的页面会被分发给内核中的其他部分以便被申请。
在嵌入式系统开发过程中,initrd和initramfs有时候也可被用作嵌入式设备上实际的根文件系统。
2.1.8 io scheler anticipatory registered (default)
I/O调度器的主要目标是通过减少磁盘的定位次数来增加系统的吞吐率。在磁盘定位过程中,磁头需要从当前的位置移动到感兴趣的目标位置,这会带来一定的延迟。2.6内核提供了4种不同的I/O调度器:Deadline、Anticipatory、Complete Fair Queuing以及NOOP。从上述内核打印信息可以看出,本例将Anticipatory 设置为了默认的I/O调度器。

2.1.9 Setting up standard PCI resources
启动过程的下一阶段会初始化I/O总线和外围控制器。内核会通过遍历PCI总线来探测PCI硬件,接下来再初始化其他的I/O子系统。从图2-3中我们会看到SCSI子系统、USB控制器、视频 芯片(855北桥芯片组信息中的一部分)、串行端口(本例中为8250 UART)、PS/2键盘 和鼠标 、软驱 、ramdisk、loopback设备、IDE控制器(本例中为ICH4南桥芯片组中的一部分)、触控板、以太网控制器(本例中为e1000)以及PCMCIA控制器初始化的启动信息。图2-3中 符号指向的为I/O设备的标识(ID)。

图2-3 在启动过程中初始化总线和外围控制器
本书会以单独的章节讨论大部分上述驱动程序子系统,请注意如果驱动程序以模块的形式被动态链接到内核,其中的一些消息也许只有在内核启动后才会被显示。
2.1.10 EXT3-fs: mounted filesystem
EXT3文件系统已经成为Linux事实上的文件系统。EXT3在退役的EXT2文件系统基础上增添了日志层,该层可用于崩溃后文件系统的快速恢复。它的目标是不经由耗时的文件系统检查(fsck)操作即可获得一个一致的文件系统。EXT2仍然是新文件系统的工作引擎,但是EXT3层会在进行实际的磁盘改变之前记录文件交互的日志。EXT3向后兼容于EXT2,因此,你可以在你现存的EXT2文件系统上加上EXT3或者由EXT3返回到EXT2 文件系统。
EXT3会启动一个称为kjournald的内核辅助线程(在接下来的一章中将深入讨论内核线程)来完成日志功能。在EXT3投入运转以后,内核挂载根文件系统并做好“业务”上的准备:
EXT3-fs: mounted filesystem with ordered data mode
kjournald starting. Commit interval 5 seconds
VFS: Mounted root (ext3 filesystem).

2.1.11 INIT: version 2.85 booting
所有Linux进程的父进程init是内核完成启动序列后运行的第1个程序。在init/main.c的最后几行,内核会搜索一个不同的位置以定位到init:
if (ramdisk_execute_command) { /* Look for /init in initramfs */
run_init_process(ramdisk_execute_command);
}

if (execute_command) { /* You may override init and ask the kernel
to execute a custom program using the
"init=" kernel command-line argument. If
you do that, execute_command points to the
specified program */
run_init_process(execute_command);
}

/* Else search for init or sh in the usual places .. */
run_init_process( " /sbin/init " );
run_init_process( " /etc/init " );
run_init_process( " /bin/init " );
run_init_process( " /bin/sh " );
panic( " No init found. Try passing init= option to kernel. " );
init会接受/etc/inittab的指引。它首先执行/etc/rc.sysinit中的系统初始化脚本,该脚本的一项最重要的职责就是激活对换(swap)分区,这会导致如下启动信息被打印:
Adding 1552384k swap on /dev/hda6
让我们来仔细看看上述这段话的意思。Linux用户进程拥有3 GB的虚拟地址空间(见2.7节),构成“工作集”的页被保存在RAM中。但是,如果有太多程序需要内存资源,内核会释放一些被使用了的RAM页面并将其存储到称为对换空间(swap space)的磁盘分区中。根据经验法则,对换分区的大小应该是RAM的2倍。在本例中,对换空间位于/dev/hda6这个磁盘分区,其大小为1 552 384 KB。
接下来,init开始运行/etc/rc.d/rcX.d/目录中的脚本,其中X是inittab中定义的运行级别。runlevel是根据预期的工作模式所进入的执行状态。例如,多用户文本模式意味着runlevel为3,X Windows则意味着runlevel为5。因此,当你看到INIT: Entering runlevel 3这条信息的时候,init就已经开始执行/etc/rc.d/rc3.d/目录中的脚本了。这些脚本会启动动态设备命名子系统(第4章中将讨论 udev),并加载网络、音频、存储设备等驱动程序所对应的内核模块:
Starting udev: [ OK ]
Initializing hardware... network audio storage [Done]
...
最后,init发起虚拟控制台终端,你现在就可以登录了。

Ⅳ 有没有懂linux内核源码中的汇编代码的#define switch_to(n){struct {long a, b;}__tmp; __asm__("cmpl "

用的at&t汇编,也就是Linux下的汇编语言,跟Intel x86汇编翻译成i386指令是一样的,就是写法和符号不同。
里面应该是__asm__()后面跟的是一个字符串,包含大量转义字符,你把转移字符翻译成对于的格式再看.

Ⅵ Linux 进程管理之进程调度与切换

我们知道,进程运行需要各种各样的系统资源,如内存、文件、打印机和最

宝贵的 CPU 等,所以说,调度的实质就是资源的分配。系统通过不同的调度算法(Scheling Algorithm)来实现这种资源的分配。通常来说,选择什么样的调度算法取决于资源分配的策略(Scheling Policy)。

有关调度相关的结构保存在 task_struct 中,如下:

active_mm 是为内核线程而引入的,因为内核线程没有自己的地址空间,为了让内核线程与普通进程具有统一的上下文切换方式,当内核线程进行上下文切换时,让切换进来的线程的 active_mm 指向刚被调度出去的进程的 active_mm(如果进程的mm 域不为空,则其 active_mm 域与 mm 域相同)。

在 linux 2.6 中 sched_class 表示该进程所属的调度器类有3种:

进程的调度策略有5种,用户可以调用调度器里不同的调度策略:

在每个 CPU 中都有一个自身的运行队列 rq,每个活动进程只出现在一个运行队列中,在多个 CPU 上同时运行一个进程是不可能的。

运行队列是使用如下结构实现的:

tast 作为调度实体加入到 CPU 中的调度队列中。

系统中所有的运行队列都在 runqueues 数组中,该数组的每个元素分别对应于系统中的一个 CPU。在单处理器系统中,由于只需要一个就绪队列,因此数组只有一个元素。

内核也定义了一下便利的宏,其含义很明显。

Linux、c/c++服务器开发篇-------我们来聊聊进程的那些事

Linux内核 进程间通信组件的实现

学习地址:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括 C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),免费分享

在分析调度流程之前,我们先来看在什么情况下要执行调度程序,我们把这种情况叫做调度时机。

Linux 调度时机主要有。

时机1,进程要调用 sleep() 或 exit() 等函数进行状态转换,这些函数会主动调用调度程序进行进程调度。

时机2,由于进程的时间片是由时钟中断来更新的,因此,这种情况和时机4 是一样的。

时机3,当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查 need_resched 的值,如果必要,则调用调度程序 schele() 主动放弃 CPU。

时机4 , 如前所述, 不管是从中断、异常还是系统调用返回, 最终都调用 ret_from_sys_call(),由这个函数进行调度标志的检测,如果必要,则调用调用调度程序。那么,为什么从系统调用返回时要调用调度程序呢?这当然是从效率考虑。从系统调用返回意味着要离开内核态而返回到用户态,而状态的转换要花费一定的时间,因此,在返回到用户态前,系统把在内核态该处理的事全部做完。

Linux 的调度程序是一个叫 Schele() 的函数,这个函数来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等。

从代码分析来看,Schele 主要完成了2个功能:

进程上下文切换包括进程的地址空间的切换和执行环境的切换。

对于 switch_mm 处理,关键的一步就是它将新进程页面目录的起始物理地址装入到寄存器 CR3 中。CR3 寄存器总是指向当前进程的页面目录。

switch_to 把寄存器中的值比如esp等存放到进程thread结构中,保存现场一边后续恢复,同时调用 __switch_to 完成了堆栈的切换。

在进程的 task_struct 结构中有个重要的成分 thread,它本身是一个数据结构 thread_struct, 里面记录着进程在切换时的(系统空间)堆栈指针,取指令地址(也就是“返回地址”)等关键性的信息。

关于__switch_to 的工作就是处理 TSS (任务状态段)。

TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。

linux 为每一个 CPU 提供一个 TSS 段,并且在 TR 寄存器中保存该段。

linux 中之所以为每一个 CPU 提供一个 TSS 段,而不是为每个进程提供一个TSS 段,主要原因是 TR 寄存器永远指向它,在任务切换的适合不必切换 TR 寄存器,从而减小开销。

在从用户态切换到内核态时,可以通过获取 TSS 段中的 esp0 来获取当前进程的内核栈 栈顶指针,从而可以保存用户态的 cs,esp,eip 等上下文。

TSS 在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。

在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到 TR(任务寄存器)所指定的任务的 TSS 中;然后,下一任务的 TSS 被装入 TR;最后,从 TR 所指定的 TSS 中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在 TSS 中保存任务现场各寄存器状态的完整映象,实现任务的切换。

因此,__switch_to 核心内容就是将 TSS 中的内核空间(0级)堆栈指针换成 next->esp0。这是因为 CPU 在穿越中断门或者陷阱门时要根据新的运行级别从TSS中取得进程在系统空间的堆栈指针。

thread_struct.esp0 指向进程的系统空间堆栈的顶端。当一个进程被调度运行时,内核会将这个变量写入 TSS 的 esp0 字段,表示这个进程进入0级运行时其堆栈的位置。换句话说,进程的 thread_struct 结构中的 esp0 保存着其系统空间堆栈指针。当进程穿过中断门、陷阱门或者调用门进入系统空间时,处理器会从这里恢复期系统空间栈。

由于栈中变量的访问依赖的是段、页、和 esp、ebp 等这些寄存器,所以当段、页、寄存器切换完以后,栈中的变量就可以被访问了。

因此 switch_to 完成了进程堆栈的切换,由于被切进的进程各个寄存器的信息已完成切换,因此 next 进程得以执行指令运行。

由于 A 进程在调用 switch_to 完成了与 B 进程堆栈的切换,也即是寄存器中的值都是 B 的,所以 A 进程在 switch_to 执行完后,A停止运行,B开始运行,当过一段时间又把 A 进程切进去后,A 开始从switch_to 后面的代码开始执行。

schele 的调用流程如下:





热点内容
我的世界手机版非常好玩的服务器推荐 发布:2025-03-15 22:04:48 浏览:177
怎样解压手机文件 发布:2025-03-15 22:04:47 浏览:524
我的世界手机基岩版怎么做服务器 发布:2025-03-15 22:04:11 浏览:99
邮件发送压缩文件 发布:2025-03-15 22:04:06 浏览:818
数据库中的数据特征 发布:2025-03-15 21:56:20 浏览:28
账号密码可以用什么替换 发布:2025-03-15 21:55:43 浏览:698
主板自带什么配置好 发布:2025-03-15 21:49:57 浏览:698
交换空间linux 发布:2025-03-15 21:49:57 浏览:84
剪映怎么添加安卓手机里面的录音 发布:2025-03-15 21:45:01 浏览:696
查询网站服务器mac地址 发布:2025-03-15 21:45:00 浏览:41