Linux0.11进程切换调度与轨迹跟踪(哈工大OS实验三,四)

Linux0.11进程切换调度与轨迹跟踪

这次将三,四两个实验放在一起讲,这样方便对进程的切换有一个整体流程的了解。按照惯例先放一下两个实验要求。因为博主在做实验的时候,每次都要看很多的资料,实现很多功能时往往没有思路,或者对其他人的实现思路感到一知半解。因此本文就是将大部分的资料整合到一节,讲解一定的实验思路和实现原理,帮助大家完成实验。但并不会给详细的实验代码和流程,还需要大家自己去完成。具体实验细节可以参考蓝桥云和别人的文章:

https://www.lanqiao.cn/courses/115/learning/?id=374&compatibility=false

实验三内容

进程从创建(Linux下调用fork())到结束的整个过程就是进程的生命期,进程在其生命期中的运行轨迹实际上表现为进程状态的多次切换,如进程创建以后会成为就绪态;当该进程被调度以后会切换到运行态;在运行的过程中如果启动一个文件读写操作,操作系统会将该进程切换到阻塞态(等待态)从而让出CPU;当文件读写完毕,操作系统会将其切换成就绪态,等待进程调度算法来调度该进程执行……
本实验内容包括:

  • 基于模板process.c编写多进程的样本程序,实现如下功能:
    • 所有子进程都并行执行,每个子进程的实际运行时间一般不超过30秒
    • 父进程向标准输出打印所有子进程的id,并在所有子进程都退出后才退出
  • 在Linux 0.11上实现进程运行轨迹的跟踪
    基本任务是在内核中维护一个日志文件*/var/process.log*,把操作系统启动到系统关机过程中所有进程的运行轨迹都记录在这一log文件中
    /var/process.log文件的格式必须为:
    pid	X	time
    
    其中:
    • pid是进程的ID
    • X可以是N,J,R,W和E中的任意一个
      • N 进程新建
      • J 进入就绪态
      • R 进入运行态
      • W 进入阻塞态
      • E 退出
    • time表示X发生的时间。这个时间不是物理时间,而是系统的滴答时间(tick)
      三个字段之间用制表符分隔
      例如:
    12    N    1056
    12    J    1057
    4    W    1057
    12    R    1057
    13    N    1058
    13    J    1059
    14    N    1059
    14    J    1060
    15    N    1060
    15    J    1061
    12    W    1061
    15    R    1061
    15    J    1076
    14    R    1076
    14    E    1076
    
    简单来说,实验三就是在每个进程进行切换的节点添加打印语句,方便了解每一个进程进行切换的时机(这在调试的时候十分有效),因此想要完成这个实验,需要了解一个进程是如何诞生,如何调度,调度的策略又是如何。

实验四内容

现在的 Linux 0.11 采用 TSS(后面会有详细论述)和一条指令就能完成任务切换,虽然简单,但这指令的执行时间却很长,在实现任务切换时大概需要 200 多个时钟周期。

而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种 TSS 切换手段,而都是通过堆栈实现的。

本次实践项目就是将 Linux 0.11 中采用的 TSS 切换部分去掉,取而代之的是基于堆栈的切换程序。具体的说,就是将 Linux 0.11 中的 switch_to 实现去掉,写成一段基于堆栈切换的代码。

本次实验包括如下内容:

  1. 编写汇编程序 switch_to:
  2. 在主体框架下依次完成 PCB 切换、内核栈切换、LDT 切换等;
  3. 修改 fork(),由于是基于内核栈的切换,所以进程需要创建出能完成内核栈切换的样子。
  4. 修改 PCB,即 task_struct 结构,增加相应的内容域,同时处理由于修改了 task_struct 所造成的影响。
  5. 用修改后的 Linux 0.11 仍然可以启动、可以正常使用。
  6. (选做)分析实验 3 的日志体会修改前后系统运行的差别。

简单来说,就是将原有的进程切换方法修改为依靠进程内核栈进行进程切换,两者之间的区别会在后续讲解。

实验思路与前置知识

1.进程

进程主要服务于操作系统的并发性,即多个程序同时出发、交替执行。而CPU在不同程序间切换时,仅修改PC指针是不够的,还需要保存程序切换时的状态(进程的上下文)PCB 。PCB可以理解为进程状态的快照,PCB一般包括(所有寄存器的值以及进程栈(进程代码跳转只需要通过读取PCB中的cs:ip即可实现))。
而在Linux中task_struct结构便是用来描述进程的数据结构,除了PCB,还保存了进程的状态与标识信息以及持有的资源信息。
在这里插入图片描述
在这里插入图片描述

在Linux 0.11中,使用task数组管理进程,最多可以有64个进程
在这里插入图片描述
所有的进程信息均被保留在task数组中,task数组为一个指针数组,每一个元素为指向进程结构task_struct的指针。
在Linux0.11中保存TCB则以来了一种名叫tss的结构, IA-32体系结构提供的TSS段可以理解为一种软硬件结合的PCB结构,在进程切换的过程中,CPU可以自动将进程状态保存在对应的TSS段中。因此进程切换时,只需要进行TSS数据之间的互换,就能够实现进程上下文之间的切换,大致的逻辑如下:

OLD.tss = CPU.tss
CPU.tss = NEW.tss

其中tss的结构如下:
在这里插入图片描述

2.多进程(任务)组织与管理

在include/linux/sched.h文件中定义了如下5种进程状态
在这里插入图片描述
其中

  1. 运行状态(TASK_RUNNING)
    ① 进程正在被CPU运行,或已经准备就绪随时可由调度程序运行
    ② 也就是说,操作系统理论课程中的就绪态和运行态,在Linux中均表示为TASK_RUNNING状态

  2. 可中断睡眠状态(TASK_INTERRUPTIBLE)
    ① 当进程处于TASK_INTERRUPTIBLE状态时,操作系统不会调度该进程执行
    ② 当操作系统产生一个中断、释放了进程正在等待的资源、收到一个信号时,都可以唤醒该进程,使其进入TASK_RUNNING状态

  3. 不可中断睡眠状态(TASK_UNINTERRUPTIBLE)
    ① 当进程处于TASK_UNINTERRUPTIBLE状态时,操作系统不会调度该进程执行
    ② 处于该状态的进程只有被wake_up函数明确唤醒时才能转换为TASK_RUNNING状态

  4. 暂停状态(TASK_STOPPED)
    ① 当进程接收到SIGSTOP / SIGTSTP / SIGTTIN / SIGTTOUT信号时会进入暂停状态
    ② 可向进程发送SIGCONT信号,使其转换为TASK_RUNNING状态
    ③ 在Linux 0.11中未实现对暂停状态转换的处理,处于该状态的进程被作为进程终止来处理

  5. 僵死状态(TASK_ZOMBIE)
    当进程已停止运行,但其父进程还没有回收其状态时,则该进程处于僵死状态

进程切换图如下所示:
在这里插入图片描述
截至目前的讲解,我们就应当对实验三有一个整体的认识。事实上实验三不需要涉及太底层的机制实验,只需要知道进程通过task_struct结构体进行描述,tss_struct用于存储进程上下文。整个的进程底层运行类似于一个自动机,当出现一些例如(定时器触发,进程出现异常,由于资源访问不到而导致拥塞)等问题,从而触发schedule()函数导致进程发生了切换并修改了进程的状态即可。
因此对于实验三,我们只需要了解到底有哪些情况出发了schedule()函数导致了进程的切换以及状态的修改即可,至于schedule()函数,task_struct一切底层的机制实现在这个实验里我们并不需要过多了解。
接下来,我们就需要详细了解一下究竟进程的状态以及转换关系到底是如何。
放个更加具体的图片:
在这里插入图片描述
下面对每个函数进行讲解:

  1. fork()函数
    fork函数表示对一个进程的新建,其内核实现函数为sys_fork,而sys_fork的核心为函数copy_process,其负责完成进程的创建。copy_process就进程状态的切换来说比较简单:先为新建进程申请一页内存存放其PCB,将子进程的状态先设置为不可中断睡眠(TASK_UNINTERRUPTIBLE),开始为子进程复制并修改父进程的PCB数据。完成后将子进程的状态设置为就绪态(TASK_RUNNING)。这个过程对应了进程新建(N)和就绪(J)两种状态。

  2. schedule()调度函数
    schedule()首先对所有任务(不包括进程0)进行检测,唤醒任何一个已经得到信号的进程(调用sys_waitpid等待子进程结束的父进程,在子进程退出后,会在此处被唤醒),所以这里需要记录进程变为就绪(J)。接下来开始选择下一个要运行的进程。首先从末尾开始逆序检查task数组中的所有任务(不包括进程0),在就绪状态的任务中选取剩余时间片(counter值)最大的任务,这里有两种特殊情况:如果有就绪状态的任务但它们的时间片都为0,就根据任务的优先级(priority值)重新设置所有任务(包括睡眠的任务)的时间片值counter,再重新从task数组末尾开始选出时间片最大的就绪态进程;或者当前没有就绪状态的进程,那么就默认选择进程0作为下一个要运行的进程。最后,选出了接下来要运行的进程,其在task数组中的下标为next,调用switch_to(next)进行进程切换。这里需要记录进程变为运行R,以及可能的当前运行态的进程变为就绪(J),当然也可能选出的next仍然是当前进程,那么就不需要进行进程切换。

  3. sys_pause()主动睡觉
    正如上面所提到的,当系统无事可做时(当前没有可以运行的进程)时就会调度进程0执行,所以schedule调度算法不会在意进程0 的状态是不是就绪态(TASK_RUNNING),进程0可以直接从睡眠切换到运行。而进程0会马上调用pause()API主动睡觉,在最终的内核实现函数sys_pause中又再次调用schedule()函数。也就是说,系统在无事可做时会触发这样一个循环:schedule()调度进程0执行,进程0调用sys_pause()主动睡觉,从而引发schedule()再次执行,接下来进程0又再次执行,循环往复,直到系统中有其他进程可以执行。

  4. 不可中断睡眠sleep_on()
    sleep_on()算是内核中比较晦涩难懂的函数了,因为它利用几个进程因等待同一资源而让出CPU都陷入sleep_on()函数的其各自内核栈上的tmp指针,将这些进程隐式地链接起来形成一个等待队列。
    在这里插入图片描述
    下图很好地展示了slepp_on函数中指针的变化
    在这里插入图片描述
    首先先说明一下为什么传参要使用(**p),因为这里的(✳p)需要是一个可变的指针,在函数调用的过程中会不断的发生改变。如果只传入(✳p),则相当于传值操作,函数会复制一个的copy 这里就叫好了(✳q),任何对(✳q)的修改操作都不会改变的(✳p),指向,因此需要传入一个二级指针。
    此外,说明一下(✳p)的作用,这里的(✳p)并不是表示一个进程信息,而是一个阻塞队列的队首。举个例子,当某个IO设备发生阻塞,利用信号量进行了上锁操作,此时信号量的结构体为

typedef struct semaphore{
    	char name[SEM_NAME_LEN];
    	int value;
    	struct task_struct *queue;
	} sem_t;

此时,便需要调用sleep_on(&queue)使当前进程排列在queue为队首的阻塞队列中。
结合sleep_on的代码实现,我们可以看出每一次新的等待(例如r1)进程调用sleep_on进入队列,(✳p)便会指向新的进程r1,而r1进程则会创建一个tmp指向之前的(✳p)进程r0。而tmp是一个位于进程数据段的一个局部变量,因此每一次创建的tmp都是位于每个进程数据段之内的唯一值。因此sleep_on函数实际上创建了一个隐式队列,如下图所示:
在这里插入图片描述
队列头指针指向刚刚进入队列的进程(任务),每个进程的tmp指向了之前的一个任务。每次调用一次sleep_on便会将新的进程加入队首,新的进程的tmp指向原先的队首进程。

  1. 不可中断睡眠interruptible_sleep_on()
    可中断睡眠与不可中断睡眠相比,除了可以用wake_up唤醒外,也可以用信号(给进程发送一个信号,实际上就是将进程PCB中维护的一个向量的某一位置位,进程需要在合适的时候处理这一位。)来唤醒,比如在schedule()中一上来就唤醒得到信号的进程。这样的唤醒会出现一个问题,那就是可能会唤醒等待队列中间的某个进程,此时就需要对和sleep_on中形成机制一样的等待队列进行适当调整:从schedule()调用唤醒的当前进程如果不是等待队列头进程,则将队列头唤醒,并通过goto repeat让自己再去睡眠。后续和sleep_on一样,从队列头进程这里利用tmp变量的链接作用将后续的进程唤醒。由于队列头进程唤醒后,只要依靠tmp变量就可以唤醒后续进程,所以已经不再需要使用队列头指针*p,将其值设置为NULL,从而为再次将其作为interruptible_sleep_on函数的参数做准备

  2. 显式唤醒wake_up()
    wake_up的作用就是显式唤醒队列头进程,所以这里需要记录进程唤醒(J)。唤醒队列头之后,sleep_oninterruptible_sleep_on会将队列的后续进程依次唤醒,所以不再需要该等待队列的头指针*p,将其置为NULL,为后续再次将其作为sleep_oninterruptible_sleep_on函数参数做好初始化。

  3. 进程退出do_exit()
    do_exit将进程的状态设为僵尸态(TASK_ZOMBIE),所以这里需要记录进程的退出(E)。子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才告结束。因此,进程表中代表子进程的表项不会立即释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用。

  4. 父进程等待子进程退出sys_waitpid()
    wait系统调用将暂停父进程直到它的子进程结束为止,它的内核实现函数为sys_waitpidsys_waitpidoptions参数若为WNOHANG就可以阻止sys_waitpid将父进程的执行挂起。这里需要在除去WNOHANG选项的地方记录父进程阻塞睡眠(W)而阻塞的情况。

根据分析,我们只需要在上述的函数中找到进程切换的地方,增加打印语句即可完成实验三了。

3.进程切换

之前已经介绍过了进程的结构,在Linux中task_struct结构便是用来描述进程的数据结构,除了PCB,还保存了进程的状态与标识信息以及持有的资源信息。其中TSS便是保存了进程的上下文,因此在linux0.11中进程的切换,只需要实现切换TSS即可。
在这里插入图片描述

下面具体讲讲如何实现:
查看schedule()函数可知,进程切换依靠与switch_to()函数来实现

#define switch_to(n) {
    struct{long a,b;} tmp;
    __asm__(
        "movw %%dx,%1"
        "ljmp %0" ::"m"(*&tmp.a), "m"(*&tmp.b), "d"(TSS(n)
    )
 }

#define FIRST_TSS_ENTRY 4

#define TSS(n) (((unsigned long) n) << 4) + (FIRST_TSS_ENTRY << 3))

所以基于 TSS 进行进程/线程切换的 switch_to 实际上就是一句 ljmp 指令,ljmp到了新的进程的TSS段即可。而ljmp指令是一个x86架构提供的一个软硬件治指令,实际上我们也并不知道他的具体的实现方式,只知道一个具体的工作过程:
(1)首先用 TR 中存取的段选择符在 GDT 表中找到当前 TSS 的内存位置,由于 TSS 是一个段,所以需要用段表中的一个描述符来表示这个段,和在系统启动时论述的内核代码段是一样的,那个段用 GDT 中的某个表项来描述,还记得是哪项吗?是 8 对应的第 1 项。此处的 TSS 也是用 GDT 中的某个表项描述,而 TR 寄存器是用来表示这个段用 GDT 表中的哪一项来描述,所以 TR 和 CS、DS 等寄存器的功能是完全类似的。
(2)找到了当前的 TSS 段(就是一段内存区域)以后,将 CPU 中的寄存器映像存放到这段内存区域中,即拍了一个快照。
(3)存放了当前进程的执行现场以后,接下来要找到目标进程的现场,并将其扣在 CPU 上,找目标 TSS 段的方法也是一样的,因为找段都要从一个描述符表中找,描述 TSS 的描述符放在 GDT 表中,所以找目标 TSS 段也要靠 GDT 表,当然只要给出目标 TSS 段对应的描述符在 GDT 表中存放的位置——段选择子就可以了,仔细想想系统启动时那条著名的 jmpi 0, 8 指令,这个段选择子就放在 ljmp 的参数中,实际上就 jmpi 0, 8 中的 8。
(4)一旦将目标 TSS 中的全部寄存器映像扣在 CPU 上,就相当于切换到了目标进程的执行现场了,因为那里有目标进程停下时的 CS:EIP,所以此时就开始从目标进程停下时的那个 CS:EIP 处开始执行,现在目标进程就变成了当前进程,所以 TR 需要修改为目标 TSS 段在 GDT 表中的段描述符所在的位置,因为 TR 总是指向当前 TSS 段的段描述符所在的位置。
其中gdt表的内容为:
在这里插入图片描述
虽然用一条指令就能完成任务切换,但这指令的执行时间却很长,这条 ljmp 指令在实现任务切换时大概需要 200 多个时钟周期。而通过堆栈的方法(基于软件的方法),可以使得CPU更加简单并且更有利于使用指令流水的并行优化技术。下面介绍一下如何去基于堆栈实现进程的切换。同样,只讲思路和相关知识,不讲详细的实现过程。

4.基于堆栈的进程切换

直观逻辑:首先当进程发生切换时,需要保存所有的进程上下文,而之前进程上下文(所有寄存器的值)都保留在了TSS上,现在去除了TSS结构,那么进程上下文究竟应当如何保留。
事实上有两种想法,一种就是把所有寄存器的值全部保存在task_struct的结构体上,但是这种方法每次执行一条指令时都需要修改task_struct结构体上寄存器的值,显然不是特别可行。另外一种方式则是当进程发生切换时,将所有的寄存器的值都保存在当前进程的栈上,这样当下次进程切换切换到当前进程时,则只需要读取切换进程栈上的所有寄存器的值,即可完成上下文的切换。
因此,简单的思路想法便是:

//initialize 初始化栈
initialize(current->stack);
//将当前进程上下文保存进栈
current->stack.push(registers);
//执行switch_to
switch_to_stack(next)
//当当前进程又一次被调度,重新运行时
current->stack.pop(registers);
//接着根据读取到的cs:ip跳转到cs:ip处执行当前进程代码段的指令
ret

有了这个简单思路,我们便可以进行基于堆栈的进程切换设计与实现。但在这里,我们有如下几个问题需要解决:

  1. 进程所使用的栈,究竟是位于核心态还是内核态?
  2. 进程保存上下文的寄存器需要满足什么样的顺序,有没有固定的要求?
  3. 如果需要进入内核,用户态和内核态之间要怎么切换,要怎么保存栈信息?
  4. 进程的栈要如何初始化
  5. 因为这个设计是在linux0.11的基础上进行修改,怎么能够进行简单的修改,在能够实现基于堆栈切换的前提下又能够少修改原有的函数,不对原先的机制实现造成过多的影响,从而减少未知的bug?

下面就对基于栈的进程切换做一些细致的讲解:

4.1 线程与进程

多进程之间需要解决地址空间隔离问题。而解决的方法就是每个进程有自己的映射表,实现进程的虚拟地址空间到物理内存的映射,这也就自然导致了在进程切换时,映射表也需要切换2. 线程概念的引入源于这样的想法:能否只切换指令流(即切换PC寄存器),而不切换映射表。出现这样的想法有如下2个原因:
① 只切换指令流,切换的速度更快,代价更小
② 有些资源需要在多个指令流之间共享,比如多个指令流操作同一段内存。如果使用进程实现,还需要额外的进程间通信手段来共享这段内存
因此我们将进程中的资源和指令执行分开,即进程 = 一个资源 + 多个指令执行流,其中的指令执行流就是一个线程
而事实上,进程切换的过程也是类似,只是进程之间不共享系统资源,需要独立的内存空间。我们可以将Linux中的一个进程描述成一个用户级线程与内核级线程的组合,而内核级线程的切换也会设计内存空间(段地址)的改变。

所以,基于堆栈的进程切换类似线程切换的五段执行过程:
在这里插入图片描述

  1. 由于内核级线程的切换需要在内核态进行,因此首先需要从用户态陷入内核态,即用户态线程切换进入核心态线程
  2. 在陷入内核态的中断处理入口,核心工作是保存当前线程在用户态执行的信息。由于处理器硬件只是完成了栈的切换,并且将用户栈 / 用户程序位置 / EFLAGS寄存器的值压栈。因此需要在中断处理入口中进一步保存相关信息,以Linux 0.11内核中处理系统调用的中断处理入口为例,此处进一步保存了段寄存器和通用寄存器的值
  3. 在进一步保存了线程在用户态执行的现场后,进行内核线程与内核线程的切换,将内核态的执行流与栈进行切换
  4. 中断返回,读取新的内核栈的内核线程上下文信息,恢复CPU的状态
  5. 再次中断返回,读取内核栈保存的寄存器信息,实现内核线程到用户线程的切换。··

下面具体说说每一个流程:

  1. 由于内核级线程的切换需要在内核态进行,因此首先需要从用户态陷入内核态,在IA-32 + Linux中一般通过如下方式实现,
    1使用int 0x80触发系统调用(int 0x80本质上属于陷阱异常)
    2 通过系统异常
    3 通过外设硬件中断
    这里之前的文章中在系统调用部分也讲过
    在这里插入图片描述
    当硬件检测到进程从用户态切换到内核态,会自动的将SS,SP,EFLAGS,PC,CS保存到内核态栈内,当内核态执行retq指令时,则会自动读取上述五个值从而返回用户态。
  2. 在陷入内核态的中断处理入口,核心工作是保存当前线程在用户态执行的信息。由于处理器硬件只是完成了栈的切换,并且将用户栈 / 用户程序位置 / EFLAGS寄存器的值压栈。因此需要在中断处理入口中进一步保存相关信息.这里是本次实验的难点,也是造成大家很多困扰的地方(说实话到现在为止我也不知道为什么是这样设置的,执行系统调用中的schedule()内核栈的状态和其他时候执行schedule()时候的内核栈是一样的吗?所以猜想后续的内核栈保存寄存器的顺序是为了保证于Linux0.11的系统调用顺序保持一致,这样就不用对system_call函数做过多的修改,可能是一个不得已的操作)。因此我就转载一下别人对于上下文需要保存的信息的理解:

https://blog.csdn.net/qq_42518941/article/details/119182097

在这里插入图片描述
可以看到,在系统调用的时候的,在执行switch_to之前的内核栈如上图所示,在执行内核栈切换时栈如下图所示:
在这里插入图片描述
对于得到CPU的新的进程,我们要修改(kernel/fork.c)中的copy_process()函数,将新的进程的内核栈填写成能进行PC切换的样子。
对于内核级线程的初始创建,需要人为构造内核线程第一次被切换执行时的场景,
在这里插入图片描述

    /* ...... */
    p = (struct task_struct *) get_free_page();
    /* ...... */
    p->pid = last_pid;
    p->father = current->pid;
    p->counter = p->priority;

    long *krnstack;
    krnstack = (long)(PAGE_SIZE +(long)p);
    *(--krnstack) = ss & 0xffff;
    *(--krnstack) = esp;
    *(--krnstack) = eflags;
    *(--krnstack) = cs & 0xffff;
    *(--krnstack) = eip;
    *(--krnstack) = ds & 0xffff;
    *(--krnstack) = es & 0xffff;
    *(--krnstack) = fs & 0xffff;
    *(--krnstack) = gs & 0xffff;
    *(--krnstack) = esi;
    *(--krnstack) = edi;
    *(--krnstack) = edx;
    *(--krnstack) = (long)first_return_from_kernel;
    *(--krnstack) = ebp;
    *(--krnstack) = ecx;
    *(--krnstack) = ebx;
    *(--krnstack) = 0;
    p->kernelstack = krnstack;
    /* ...... */
    }

此时,上下文的保存便完成。

  1. 内核栈切换的操作与用户栈切换类似,伪代码如下,
// current指向当前内核级线程的TCB
 
// next指向下一个要执行的内核级线程的TCB
 
 
 
// 将当前的ESP寄存器值保存在current指向的TCB中
 
current->esp = esp;
 
// 从next指向的TCB中取出esp字段赋值给ESP寄存器
 
esp = next->esp;
  1. 内核态执行流的切换也是通过ret指令完成,在完成内核栈切换后,切换后的内核栈栈顶保存着切换目标线程之前在内核态被切换走时保存的内核现场
    通过ret指令将栈顶元素出栈到EIP寄存器,就实现了内核态执行流的切换
    下面以IA-32 + Linux 2.4内核为例,说明内核态执行流的切换过程。没有选择Linux 0.11内核进行说明,是因为Linux 0.11内核使用TSS段(而不是内核栈)完成进程切换
    在这里插入图片描述
    schedule函数在调用switch_to函数时传递的参数,分别指向切换前后线程的TCB(task_struct结构)。其中prev指向当前线程,next指向下一个要执行的线程。

  2. 中断返回(用户态执行流切换)
    户态执行流切换通过iret指令完成,在调用iret指令之前,需要将内核栈先恢复为如下形式,也就是线程从用户态陷入内核态时处理器保存的栈状态
    在这里插入图片描述

实验具体流程

这里参考如下链接以及蓝桥云手册即可:

https://blog.csdn.net/leoabcd12/article/details/122268321?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168639094316800225524154%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=168639094316800225524154&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-122268321-null-null.142v88control,239v2insert_chatgpt&utm_term=%E5%93%88%E5%B7%A5%E5%A4%A7%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AE%9E%E9%AA%8C&spm=1018.2226.3001.4187

但是对于内核栈的切换好像仍然存在bug。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值