操作系统原理与实践5--内核栈切换的进程切换

【linux0.12】实验楼操作系统原理与实践之实验五:基于内核栈切换的进程切换 简介       早期内核使用intel提供的基于tss切换的机制,这种机制操作十分简单,但是执行速度特别慢,无法充分利用现代cpu的一些指令流水来优化速度。关于tss切换的方式详细可以看实验楼中的描述,和完全剖析这本书。根据老师的实验,要把这种基于tss切换的方式换成现代的基于内核栈切换的方式。       基于堆栈的切换,核心就是要先在切换前保存进程的栈,然后切换到另一个进程的栈,但因 阅读详情

基于内核栈切换的进程切换

难度系数:★★★★☆

实验目的

  • 深入理解进程和进程切换的概念;
  • 综合应用进程、CPU管理、PCB、LDT、内核栈、内核态等知识解决实际问题;
  • 开始建立系统认识。

实验内容

现在的Linux 0.11采用TSS(后面会有详细论述)和一条指令就能完成任务切换,虽然简单,但这指令的执行时间却很长,在实现任务切换时大概需要 200 多个时钟周期。而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得CPU的设计变得简单。所以无论是 Linux还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种TSS切换手段,而都是通过堆栈实现的。

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

本次实验包括如下内容:

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

实验报告

回答下面三个问题:

1. 针对下面的代码片段:

movl tss,%ecx

 addl $4096,%ebx

 movl %ebx,ESP0(%ecx)

回答问题:(1)为什么要加4096;(2)为什么没有设置tss中的ss0。

2. 针对代码片段:

*(--krnstack) = ebp;

 *(--krnstack) = ecx;

 *(--krnstack) = ebx;

 *(--krnstack) = 0;

回答问题:(1)子进程第一次执行时,eax=?为什么要等于这个数?哪里的工作让eax等于这样一个数?(2)这段代码中的ebx和ecx来自哪里,是什么含义,为什么要通过这些代码将其写到子进程的内核栈中?(3)这段代码中的ebp来自哪里,是什么含义,为什么要做这样的设置?可以不设置吗?为什么?

3. 为什么要在切换完LDT之后要重新设置fs=0x17?而且为什么重设操作要出现在切换完LDT之后,出现在LDT之前又会怎么样?

评分标准

  • switch_to(system_call.s),40%
  • fork.c,30%
  • sched.h和sched.c,10%
  • 实验报告,20%

实验提示

  • TSS切换

在现在的Linux 0.11中,真正完成进程切换是依靠任务状态段(Task State Segment,简称TSS)的切换来完成的。具体的说,在设计“Intel架构”(即x86系统结构)时,每个任务(进程或线程)都对应一个独立的TSS,TSS就是内存中的一个结构体,里面包含了几乎所有的CPU寄存器的映像。有一个任务寄存器(Task Register,简称TR)指向当前进程对应的TSS结构体,所谓的TSS切换就将CPU中几乎所有的寄存器都复制到TR指向的那个TSS结构体中保存起来,同时找到一个目标TSS,即要切换到的下一个进程对应的TSS,将其中存放的寄存器映像“扣在”CPU上,就完成了执行现场的切换,如下图所示。

图片描述信息

图1 基于TSS的进程切换

Intel架构不仅提供了TSS来实现任务切换,而且只要一条指令就能完成这样的切换,即图中的ljmp指令。具体的工作过程是:(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段的段描述符所在的位置。

上面给出的这些工作都是一句长跳转指令“ljmp 段选择子:段内偏移”,在段选择子指向的段描述符是TSS段时CPU解释执行的结果,所以基于TSS进行进程/线程切换的switch_to实际上就是一句ljmp指令:

#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))

GDT表的结构如下图所示,所以第一个TSS表项,即0号进程的TSS表项在第4个位置上,4<<3,即48,相当于TSS在GDT表中开始的位置(以字节为单位),TSS(n)找到的是进程n的TSS位置,所以还要再加上n<<4,即n16,因为每个进程对应有1个TSS和1个LDT,每个描述符的长度都是8个字节,所以是乘以16,其中LDT的作用就是上面论述的那个映射表,关于这个表的详细论述要等到内存管理一章。TSS(n)=n16+48,得到就是进程n(切换到的目标进程)的TSS选择子,将这个值放到dx寄存器中,并且又放置到结构体tmp中32位长整数b的前16位,现在64位tmp中的内容是前32位为空,这个32位数字是段内偏移,就是jmpi 0, 8中的0;接下来的16位是n16+48,这个数字是段选择子,就是jmpi 0, 8中的8,再接下来的16位也为空。所以swith_to的核心实际上就是“ljmp 空, n16+48”,现在和前面给出的基于TSS的进程切换联系在一起了。

图片描述信息

图2 GDT表中的内容

  • 本次实验的内容

虽然用一条指令就能完成任务切换,但这指令的执行时间却很长,这条ljmp 指令在实现任务切换时大概需要 200 多个时钟周期。而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得CPU的设计变得简单。所以无论是 Linux还是 Windows,进程/线程的切换都没有使用 Intel 提供的这种TSS切换手段,而都是通过堆栈实现的。

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

在现在的Linux 0.11中,真正完成进程切换是依靠任务状态段(Task State Segment,简称TSS)的切换来完成的。具体的说,在设计“Intel架构”(即x86系统结构)时,每个任务(进程或线程)都对应一个独立的TSS,TSS就是内存中的一个结构体,里面包含了几乎所有的CPU寄存器的映像。有一个任务寄存器(Task Register,简称TR)指向当前进程对应的TSS结构体,所谓的TSS切换就将CPU中几乎所有的寄存器都复制到TR指向的那个TSS结构体中保存起来,同时找到一个目标TSS,即要切换到的下一个进程对应的TSS,将其中存放的寄存器映像“扣在”CPU上,就完成了执行现场的切换。

要实现基于内核栈的任务切换,主要完成如下三件工作:(1)重写switch_to;(2)将重写的switch_to和schedule()函数接在一起;(3)修改现在的fork()。

  • schedule与switch_to

    目前Linux 0.11中工作的schedule()函数是首先找到下一个进程的数组位置next,而这个next就是GDT中的n,所以这个next是用来找到切换后目标TSS段的段描述符的,一旦获得了这个next值,直接调用上面剖析的那个宏展开switch_to(next);就能完成如图TSS切换所示的切换了。现在,我们不用TSS进行切换,而是采用切换内核栈的方式来完成进程切换,所以在新的switch_to中将用到当前进程的PCB、目标进程的PCB、当前进程的内核栈、目标进程的内核栈等信息。由于Linux 0.11进程的内核栈和该进程的PCB在同一页内存上(一块4KB大小的内存),其中PCB位于这页内存的低地址,栈位于这页内存的高地址;另外,由于当前进程的PCB是用一个全局变量current指向的,所以只要告诉新switch_to()函数一个指向目标进程PCB的指针就可以了。同时还要将next也传递进去,虽然TSS(next)不再需要了,但是LDT(next)仍然是需要的,也就是说,现在每个进程不用有自己的TSS了,因为已经不采用TSS进程切换了,但是每个进程需要有自己的LDT,地址分离地址还是必须要有的,而进程切换必然要涉及到LDT的切换。

综上所述,需要将目前的schedule()函数做稍许修改,即将下面的代码

if ((*p)->state == TASK\_RUNNING \&\& (*p)->counter > c) c = (*p)->counter, next = i; 

......

 switch_to(next);

修改为

 if ((*p)->state == TASK\_RUNNING \&\& (*p)->counter > c) c = (*p)->counter, next = i, pnext = *p;

 .......

switch_to(pnext, LDT(next));
  • 实现switch_to

这是本次实践项目中最重要的一部分。由于要对内核栈进行精细的操作,所以需要用汇编代码来完成函数switch_to的编写,这个函数依次主要完成如下功能:由于是C语言调用汇编,所以需要首先在汇编中处理栈帧,即处理ebp寄存器;接下来要取出表示下一个进程PCB的参数,并和current做一个比较,如果等于current,则什么也不用做;如果不等于current,就开始进程切换,依次完成PCB的切换、TSS中的内核栈指针的重写、内核栈的切换、LDT的切换以及PC指针(即CS:EIP)的切换。

 switch_to:

     pushl %ebp

     movl %esp,%ebp

     pushl %ecx

     pushl %ebx

     pushl %eax

     movl 8(%ebp),%ebx

     cmpl %ebx,current

     je 1f

     切换PCB

     TSS中的内核栈指针的重写

     切换内核栈

     切换LDT

     movl $0x17,%ecx

     mov %cx,%fs

     cmpl %eax,last_task_used_math //和后面的clts配合来处理协处理器,由于和主题关系不大,此处不做论述

    jne 1f

    clts

 1:    popl %eax

    popl %ebx

    popl %ecx

    popl %ebp

    ret

虽然看起来完成了挺多的切换,但实际上每个部分都只有很简单的几条指令。完成PCB的切换可以采用下面两条指令,其中ebx是从参数中取出来的下一个进程的PCB指针,

movl %ebx,%eax

xchgl %eax,current

经过这两条指令以后,eax指向现在的当前进程,ebx指向下一个进程,全局变量current也指向下一个进程。

TSS中的内核栈指针的重写可以用下面三条指令完成,其中宏ESP0 = 4,struct tss_struct *tss = &(init_task.task.tss);也是定义了一个全局变量,和current类似,用来指向那一段0号进程的TSS内存。前面已经详细论述过,在中断的时候,要找到内核栈位置,并将用户态下的SS:ESP,CS:EIP以及EFLAGS这五个寄存器压到内核栈中,这是沟通用户栈(用户态)和内核栈(内核态)的关键桥梁,而找到内核栈位置就依靠TR指向的当前TSS。现在虽然不使用TSS进行任务切换了,但是Intel的这态中断处理机制还要保持,所以仍然需要有一个当前TSS,这个TSS就是我们定义的那个全局变量tss,即0号进程的tss,所有进程都共用这个tss,任务切换时不再发生变化。

 movl tss,%ecx

addl $4096,%ebx

 movl %ebx,ESP0(%ecx)

定义ESP0 = 4是因为TSS中内核栈指针esp0就放在偏移为4的地方,看一看tss的结构体定义就明白了。

完成内核栈的切换也非常简单,和我们前面给出的论述完全一致,将寄存器esp(内核栈使用到当前情况时的栈顶位置)的值保存到当前PCB中,再从下一个PCB中的对应位置上取出保存的内核栈栈顶放入esp寄存器,这样处理完以后,再使用内核栈时使用的就是下一个进程的内核栈了。由于现在的Linux 0.11的PCB定义中没有保存内核栈指针这个域(kernelstack),所以需要加上,而宏KERNEL_STACK就是你加的那个位置,当然将kernelstack域加在task_struct中的哪个位置都可以,但是在某些汇编文件中(主要是在system_call.s中)有些关于操作这个结构一些汇编硬编码,所以一旦增加了kernelstack,这些硬编码需要跟着修改,由于第一个位置,即long state出现的汇编硬编码很多,所以kernelstack千万不要放置在task_struct中的第一个位置,当放在其他位置时,修改system_call.s中的那些硬编码就可以了。

 KERNEL_STACK = 12

 movl %esp,KERNEL_STACK(%eax)

 movl 8(%ebp),%ebx //再取一下ebx,因为前面修改过ebx的值

 movl KERNEL_STACK(%ebx),%esp

 struct task_struct {

 long state;

 long counter;

 long priority;

  long kernelstack;

......

由于这里将PCB结构体的定义改变了,所以在产生0号进程的PCB初始化时也要跟着一起变化,需要将原来的#define INIT_TASK { 0,15,15, 0,{{},},0,...修改为#define INIT_TASK { 0,15,15,PAGE_SIZE+(long)&init_task, 0,{{},},0,...,即在PCB的第四项中增加关于内核栈栈指针的初始化。

再下一个切换就是LDT的切换了,指令movl 12(%ebp),%ecx负责取出对应LDT(next)的那个参数,指令lldt %cx负责修改LDTR寄存器,一旦完成了修改,下一个进程在执行用户态程序时使用的映射表就是自己的LDT表了,地址空间实现了分离。最后一个切换是关于PC的切换,和前面论述的一致,依靠的就是switch_to的最后一句指令ret,虽然简单,但背后发生的事却很多:schedule()函数的最后调用了这个switch_to函数,所以这句指令ret就返回到下一个进程(目标进程)的schedule()函数的末尾,遇到的是},继续ret回到调用的schedule()地方,是在中断处理中调用的,所以回到了中断处理中,就到了中断返回的地址,再调用iret就到了目标进程的用户态程序去执行,和书中论述的内核态线程切换的五段论是完全一致的。这里还有一个地方需要格外注意,那就是switch_to代码中在切换完LDT后的两句,即:

 切换LDT

 movl $0x17,%ecx

 mov %cx,%fs

这两句代码的含义是重新取一下段寄存器fs的值,这两句话必须要加、也必须要出现在切换完LDT之后,这是因为在实践项目2中曾经看到过fs的作用——通过fs访问进程的用户态内存,LDT切换完成就意味着切换了分配给进程的用户态内存地址空间,所以前一个fs指向的是上一个进程的用户态内存,而现在需要执行下一个进程的用户态内存,所以就需要用这两条指令来重取fs。不过,细心的读者可能会发现:fs是一个选择子,即fs是一个指向描述符表项的指针,这个描述符才是指向实际的用户态内存的指针,所以上一个进程和下一个进程的fs实际上都是0x17,真正找到不同的用户态内存是因为两个进程查的LDT表不一样,所以这样重置一下fs=0x17有用吗,有什么用?

要回答这个问题就需要对段寄存器有更深刻的认识,实际上段寄存器包含两个部分:显式部分和隐式部分,如下图给出实例所示,就是那个著名的jmpi 0, 8,虽然我们的指令是让cs=8,但在执行这条指令时,会在段表(GDT)中找到8对应的那个描述符表项,取出基地址和段限长,除了完成和eip的累加算出PC以外,还会将取出的基地址和段限长放在cs的隐藏部分,即图中的基地址0和段限长7FF。为什么要这样做?下次执行jmp 100时,由于cs没有改过,仍然是8,所以可以不再去查GDT表,而是直接用其隐藏部分中的基地址0和100累加直接得到PC,增加了执行指令的效率。现在想必明白了为什么重新设置fs=0x17了吧?而且为什么要出现在切换完LDT之后?

图片描述信息

图3 段寄存器中的两个部分

  • 修改fork

    修改fork()了,和书中论述的原理一致,就是要把进程的用户栈、用户程序和其内核栈通过压在内核栈中的SS:ESP,CS:IP关联在一起。另外,由于fork()这个叉子的含义就是要让父子进程共用同一个代码、数据和堆栈,现在虽然是使用内核栈完成任务切换,但fork()的基本含义不会发生变化。将上面两段描述联立在一起,修改fork()的核心工作就是要形成如下图所示的子进程内核栈结构。

图片描述信息

图4 fork进程的父子进程结构

不难想象,对fork()的修改就是对子进程的内核栈的初始化,在fork()的核心实现copy_process中,p = (struct task_struct ) get_free_page();用来完成申请一页内存作为子进程的PCB,而p指针加上页面大小就是子进程的内核栈位置,所以语句krnstack = (long ) (PAGE_SIZE + (long) p);就可以找到子进程的内核栈位置,接下来就是初始化krnstack中的内容了。

*(--krnstack) = ss & 0xffff;

 *(--krnstack) = esp;

 *(--krnstack) = eflags;

 *(--krnstack) = cs & 0xffff;

 *(--krnstack) = eip;

这五条语句就完成了上图所示的那个重要的关联,因为其中ss,esp等内容都是copy_proces()函数的参数,这些参数来自调用copy_proces()的进程的内核栈中,就是父进程的内核栈中,所以上面给出的指令不就是将父进程内核栈中的前五个内容拷贝到子进程的内核栈中,图中所示的关联不也就是一个拷贝吗?

接下来的工作就需要和switch_to接在一起考虑了,故事从哪里开始呢?回顾一下前面给出来的switch_to,应该从“切换内核栈”完事的那个地方开始,现在到子进程的内核栈开始工作了,接下来做的四次弹栈以及ret处理使用的都是子进程内核栈中的东西,

 1: popl %eax

 popl %ebx

 popl %ecx

 popl %ebp

 ret

为了能够顺利完成这些弹栈工作,子进程的内核栈中应该有这些内容,所以需要对krnstack进行初始化:

 *(--krnstack) = ebp;

 *(--krnstack) = ecx;

 *(--krnstack) = ebx;

 *(--krnstack) = 0; //这里的0最有意思。

现在到了ret指令了,这条指令要从内核栈中弹出一个32位数作为EIP跳去执行,所以需要弄一个函数地址(仍然是一段汇编程序,所以这个地址是这段汇编程序开始处的标号)并将其初始化到栈中。我们弄的一个名为first_return_from_kernel;的汇编标号,然后可以用语句*(--krnstack) = (long) first_return_from_kernel;将这个地址初始化到子进程的内核栈中,现在执行ret以后就会跳转到first_return_from_kernel去执行了。

想一想 first_return_from_kernel要完成什么工作?PCB切换完成、内核栈切换完成、LDT切换完成,接下来应该那个“内核级线程切换五段论”中的最后一段切换了,即完成用户栈和用户代码的切换,依靠的核心指令就是iret,当然在切换之前应该回复一下执行现场,主要就是eax,ebx,ecx,edx,esi,edi,gs,fs,es,ds等寄存器的恢复,下面给出了 first_return_from_kernel的核心代码,当然edx等寄存器的值也应该先初始化到子进程内核栈,即krnstack中。

 popl %edx

 popl %edi

 popl %esi

 pop %gs

 pop %fs

 pop %es

 pop %ds

 iret

最后别忘了将存放在PCB中的内核栈指针修改到初始化完成时内核栈的栈顶,即:


操作系统原理与实践”实验报告

基于内核栈切换的进程切换

一、tss方式的进程切换

Linux0.11中默认使用的是硬件支持的tss切换,系统为每个进程分配一个tss结构用来存储进程的运行信息(上下文环境),然后通过CPU的一个长跳转指令ljmp来实现进程的切换,这种方式易于实现,但一者不便于管理多CPU进程,二者效率不佳,故此次实验要将系统中使用的tss切换方式修改为栈切换方式。而由于CPU管理方式的原因,tr寄存器必须指向当前正在运行的进程的tss结构,所以系统在运行时必须要有一个tss结构来存储当前运行的进程(线程)的上下文信息,但只需要保留一个全局的tss结构即可(由于这里不考虑多CPU情况,如果考虑则要为每一个CPU准备一个tss结构来储存每个cpu运行时的进程或线程上下文信息)。

从tss结构(见下面)中可以看出,tss结构基本上就是一个全部运行时寄存器保存结构,就是用来保存进程运行时全部的寄存器信息的。当然,也有其他一些进程相关信息,如进程分组链接back_link,IO使用信息trace_bitmap,以及协处理器i387的信息。当进程进行切换时这些信息通过ljmp语句进行交换,被调度到的进程信息进入相应寄存器,而被调度出的进程则使用这个结构保存进程被调度时的寄存器状态及数据,以备下次运行时恢复。而这一切均通过ljmp语句进行。见switch_to宏的代码(如下)。从代码中可以看到这种进程的切换方式简洁方便,但其缺乏效率,而且也不能很好地向多cpu结构扩展。所以要修改为基于栈的切换方式。


struct tss_struct { 
    long    back_link;    /* 16 high bits zero */ 
    long    esp0; 
    long    ss0;        /* 16 high bits zero */ 
    long    esp1; 
    long    ss1;        /* 16 high bits zero */ 
    long    esp2; 
    long    ss2;        /* 16 high bits zero */ 
    long    cr3; 
    long    eip; 
    long    eflags; 
    long    eax,ecx,edx,ebx; 
    long    esp; 
    long    ebp; 
    long    esi; 
    long    edi; 
    long    es;        /* 16 high bits zero */ 
    long    cs;        /* 16 high bits zero */ 
    long    ss;        /* 16 high bits zero */ 
    long    ds;        /* 16 high bits zero */ 
    long    fs;        /* 16 high bits zero */ 
    long    gs;        /* 16 high bits zero */ 
    long    ldt;        /* 16 high bits zero */ 
    long    trace_bitmap;    /* bits: trace 0, bitmap 16-31 */ 
    struct i387_struct i387; 
};

宏switch_to(next):


#define switch_tss(n) {\ 
struct {long a,b;} __tmp; \ 
__asm__("cmpl %%ecx,current\n\t" \  //比较当前要切换的进程是否是当前运行的进程
    "je 1f\n\t" \              //如果是则不调度直接退出
    "movw %%dx,%1\n\t" \      //将要调度的tss指针存到tmp.b中
    "xchgl %%ecx,current\n\t" \      //交换pcb值,要调度的存储在ecx中
    "ljmp *%0\n\t" \            //进行长跳转,即切换tss
    "cmpl %%ecx,last_task_used_math\n\t" \        //进行切换后的处理协处理器
    "jne 1f\n\t" \ 
    "clts\n" \ 
    "1:" \ 
    ::"m" (*&__tmp.a),"m" (*&__tmp.b), \ 
    "d" (_TSS(n)),"c" ((long) task[n])); \ 
}

二、基于内核栈的切换

2.1、关于切换的分析

不管使用何种方式进行进程切换(此次实验不涉及线程),总之要实现调度进程的寄存器的保存和切换,也就是说只要有办法保存被调度出cpu的进程的寄存器状态及数据,再把调度的进程的寄存器状态及数据放入到cpu的相应寄存器中即可完成进程的切换。由于切换都是在内核态下完成的所以两个进程之间的tss结构中只有几个信息是不同的,其中esp和trace_bitmap是必须切换的,但在0.11的系统中,所有进程的bitmap均一样,所以也可以不用切换。

调度进程的切换方式修改之前,我们考虑一个问题,进程0不是通过调度运行的,那进程0的上下文是如何建立的?因为在进程0运行时系统中并没有其他进程,所以进程0的建立模板一定可以为进程栈切换方式有帮助。所以先来分析一下进程0的产生。进程0是在move_to_user_mode宏之后直接进入的。在这之前一些准备工做主要是task_struct结构的填充。


#define move_to_user_mode() \ 
__asm__ ("movl %%esp,%%eax\n\t" \ 
    "pushl $0x17\n\t" \ 
    "pushl %%eax\n\t" \ 
    "pushfl\n\t" \ 
    "pushl $0x0f\n\t" \ 
    "pushl $1f\n\t" \ 
    "iret\n" \ 
    "1:\tmovl $0x17,%%eax\n\t" \ 
    "movw %%ax,%%ds\n\t" \ 
    "movw %%ax,%%es\n\t" \ 
    "movw %%ax,%%fs\n\t" \ 
    "movw %%ax,%%gs" \ 
    :::"ax")

这个宏模拟了一个中断返回,把但却没有设置cs、ss,其原因应该是进程0代码和数据均在内核,所以不需要设置,这样就可以知道,在使用栈切换时我们也只要模仿这种方式,手动造一个中断返回即可。但实际上还要麻烦一点,因为进程0不通过schedule进行第一次运行,但我们fork出来的新进程却要经过schedule,也就是要经过switch_to来进行调度,所以在新fork的进程栈中不仅要模拟中断返回,还要为schedule的返回准备数据。基于此,新fork的进程的内核栈应该更类似于一个被调度出去的一个进程。经过分析代码我们可以知道一个两个进程在调度时内核的栈的状态如下:


            进程A的内核栈            进程B的内核栈
int前
            ss                ss
            esp                esp
            eflags                eflags
            cs                cs
            eip                eip
进入Int之后返回之前
            ds                ds
            es                es
            fs                fs
            edx                edx
            ecs                ecs
            ebx                ebx
            eax                eax
进入schedule的switch后    返回地址retur_from_systemcall    返回地址retur_from_systemcall

所以我们只要把fork的进程的内核栈的状态手工打造成类似的样子就可以了。根据实验指导的内容,此次修改将switch_to宏变成函数,所以需要压栈,故相比上图要做一定的调整,因为c函数的调用参数是通过栈来传递的,其汇编后的代码一般如下:


pushl ebp
movl  esp,ebp
sub     $x,esp
…

此时栈内会放置函数所用的参数,新进程并没有经过schedule的前一部分之直跳进来的,所以fork中的进程栈中也必须要有这些参数数据。此外由于新进程也没有经过int,所以iret返回时所需要的数据也要在栈内准备好。

2.2、基于栈的切换代码

本次要修改的文件如下:

1、schedule.h

这个文件中主要修改如下几处。

第一,task_struct结构的相关部分,由于之前使用tss切换,而此次修改要使用栈切换,tss结构弃之不用(只保留一个进程0的),所以esp指针必须要保存到task_struct中,因此要在task_struct结构中添加一个新的成员long kernelstack; //内核栈指针,实际上此次实验的最后结果中我添加了两个,另一个是long eip; //保存切换时使用的EIP指针,具体原因后面会说明。同时进程0的task_struct也要做相应修改。老师在视频中也讲到成员添加的位置要小心,因为有些成员在系统中被硬编码了,所以不能放在结构的最前面,也最好不要放最后面,因为那样,在汇编程序中计算新添加成员偏移位置时就变得困难了。

此外,为了能在函数中使用汇编中的函数,这里还要声明如下几个函数。


/*添加用来PCB切换的函数声明*/ 
extern void switch_to(struct task_struct * pnext,int next); //这里的参数类型是不重要的,记得C语言
                    //的老师说过编译器只并不对此处的类型进行严格检查。
extern void first_return_from_kernel(void); 
extern void first_switch_from(void); //这里也是在实验指导之外添加的

2、schedule.c

要修改的位置就是schedule函数,因为要将switch_to宏修改为一个汇编函数,所以无法在里面使用宏在查找pcb指针以及ldt指针,所以这两个数据均要使用参数来传递,故在schedule中也添加了一个新成员用来保存当前要调度进cpu的进程的pcb指针。如下:


struct task_struct * pnext=NULL; //保存PCB指针,
…
while (1) { 
        c = -1; 
        next = 0; 
        /*为pnext赋初值,让其总有值可用。*/ 
        pnext=task[next]; //最初我并没有加这句,导致如果系统没有进程可以调度时传递进去的是一个空值,系统宕机,所以加上这句,这样就可以在next=0时不会有空指针传递。
        /**/
i = NR_TASKS; 
        p = &task[NR_TASKS]; 
        while (--i) { 
            if (!*--p) 
                continue; 
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c) 
                c = (*p)->counter, next = i,pnext=*p; //保存要调度到的pcb指针
        } 
        if (c) break; 
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) 
            if (*p) 
                (*p)->counter = ((*p)->counter >> 1) + 
                        (*p)->priority; 
    } 
    /*调度进程到运行态*/ 
    if(task[next]->pid != current->pid) 
    { 
        //判断当前正在运行的进程状态是否为TASK_RUNNING, 
        //如果是,则表明当前的进程是时间片到期被抢走的,这时当前进程的状态还应是TASK_RUNNING, 
        //如果不是,则说明当前进程是主动让出CPU,则状态应为其他Wait状态。 
        if(current->state == TASK_RUNNING) 
        { 
            //记录当前进程的状态为J,在此处,当前进程由运行态转变为就绪态。 
            fprintk(3,"%ld\t%c\t%ld\n",current->pid,'J',jiffies); 
        } 
        fprintk(3,"%ld\t%c\t%ld\n",pnext->pid,'R',jiffies); 
    } 
    /**/ 

    //switch_tss(next); //由于此次实验难度还是挺高的,所以一般不会一次成功,所以我没有将switch_to宏删除,而只是将其改了一个名字,这样,如果下面的切换出问题,就切换回来测试是否是其他代码出问题了。如果换回来正常,则说明问题就出现在下面的切换上。这样可以减少盲目地修改。
    switch_to(pnext,_LDT(next));

3、fork.c

主要修改的是copy_process函数,见下面:


int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, 
        long ebx,long ecx,long edx, 
        long fs,long es,long ds, 
        long eip,long cs,long eflags,long esp,long ss) 
{ 
    /*melon - 添加用来取得内核栈指针*/ 
    long * krnstack; 
    /*melon added End*/ 
    struct task_struct *p; 
    int i; 
    struct file *f; 

    p = (struct task_struct *) get_free_page(); 
    if (!p) 
        return -EAGAIN; 

    /*melon  -取得当前子进程的内核栈指针*/ 
    krnstack=(long)(PAGE_SIZE+(long)p); //实际上进程每次进入内核,栈顶都指向这里。
    /*melon added End*/ 
    task[nr] = p; 
    *p = *current;    /* NOTE! this doesn't copy the supervisor stack */ 
    p->state = TASK_UNINTERRUPTIBLE; 
    p->pid = last_pid; 
    p->father = current->pid; 
    p->counter = p->priority; 
    //初始化内核栈内容,由于系统不再使用tss进行切换,所以内核栈内容要自已安排好 
    //下面部分就是进入内核后int之前入栈内容,即用户态下的cpu现场
    *(--krnstack) = ss & 0xffff; //保存用户栈段寄存器,这些参数均来自于此次的函数调用, 
                      //即父进程压栈内容,看下面关于tss的设置此处和那里一样。 
    *(--krnstack) = esp; //保存用户栈顶指针 
    *(--krnstack) = eflags; //保存标识寄存器 
    *(--krnstack) = cs & 0xffff; //保存用户代码段寄存器 
    *(--krnstack) = eip; //保存eip指针数据,iret时会出栈使用 ,这里也是子进程运行时的语句地址。即if(!fork()==0) 那里的地址,由父进程传递
    //下面是iret时要使用的栈内容,由于调度发生前被中断的进程总是在内核的int中,
    //所以这里也要模拟中断返回现场,这里为什么不能直接将中断返回时使用的
    //return_from_systemcall地址加进来呢?如果完全模仿可不可以呢?
    //有空我会测试一下。
    //根据老师的视频讲义和实验指导,这里保存了段寄存器数据。
    //由switch_to返回后first_return_fromkernel时运行,模拟system_call的返回 
    *(--krnstack) = ds & 0xffff; 
    *(--krnstack) = es & 0xffff; 
    *(--krnstack) = fs & 0xffff; 
    *(--krnstack) = gs & 0xffff; 
    *(--krnstack) = esi; 
    *(--krnstack) = edi; 
    *(--krnstack) = edx; 
    //*(--krnstack) = ecx; //这三句是我根据int返回栈内容加上去的,后来发现不加也可以
                //但如果完全模拟return_from_systemcall的话,这里应该要加上。
    //*(--krnstack) = ebx; 
    //*(--krnstack) = 0; //此处应是返回的子进程pid//eax; 
               //其意义等同于p->tss.eax=0;因为tss不再被使用, 
               //所以返回值在这里被写入栈内,在switch_to返回前被弹出给eax; 

    //switch_to的ret语句将会用以下地址做为弹出进址进行运行 
    *(--krnstack) = (long)first_return_from_kernel; 
    //*(--krnstack) = &first_return_from_kernel; //讨论区中有同学说应该这样写,结果同上
    //这是在switch_to一起定义的一段用来返回用户态的汇编标号,也就是 
    //以下是switch_to函数返回时要使用的出栈数据
    //也就是说如果子进程得到机会运行,一定也是先 
    //到switch_to的结束部分去运行,因为PCB是在那里被切换的,栈也是在那里被切换的, 
    //所以下面的数据一定要事先压到一个要运行的进程中才可以平衡。 
    *(--krnstack) = ebp; 
    *(--krnstack) = eflags; //新添加 
    *(--krnstack) = ecx; 
    *(--krnstack) = ebx; 
    *(--krnstack) = 0; //这里的eax=0是switch_to返回时弹出的,而且在后面没有被修改过。 
            //此处之所以是0,是因为子进程要返回0。而返回数据要放在eax中,
            //由于switch_to之后eax并没有被修改,所以这个值一直被保留。
            //所以在上面的栈中可以不用再压入eax等数据。
            //将内核栈的栈顶保存到内核指针处 
    p->kernelstack=krnstack; //保存当前栈顶 
    //p->eip=(long)first_switch_from;
        //上面这句是第一次被调度时使用的地址 ,这里是后期经过测试后发现系统修改
        //后会发生不定期死机,经分析后认为是ip不正确导致的,但分析是否正确不得
        //而知,只是经过这样修改后问题解决,不知其他同学是否遇到这个问题。
    /*melon added End*/

4、system_call.s

这个文件中主要是添加新的switch_to函数。见下面:


.align 2 
switch_to: 
    pushl %ebp 
    movl %esp,%ebp        #上面两条用来调整C函数栈态 
    pushfl            #将当前的内核eflags入栈!!!! 
    pushl %ecx 
    pushl %ebx 
    pushl %eax 
    movl 8(%ebp),%ebx    #此时ebx中保存的是第一个参数switch_to(pnext,LDT(next)) 
    cmpl %ebx,current    #此处判断传进来的PCB是否为当前运行的PCB 
    je 1f            #如果相等,则直接退出 
    #切换PCB 
    movl %ebx,%eax        #ebx中保存的是传递进来的要切换的pcb 
    xchgl %eax,current    #交换eax和current,交换完毕后eax中保存的是被切出去的PCB 
    #TSS中内核栈指针重写 
    movl tss,%ecx        #将全局的tss指针保存在ecx中 
    addl $4096,%ebx        #取得tss保存的内核栈指针保存到ebx中 
    movl %ebx,ESP0(%ecx)    #将内核栈指针保存到全局的tss的内核栈指针处esp0=4 
    #切换内核栈 
    movl %esp,KERNEL_STACK(%eax)    #将切出去的PCB中的内核栈指针存回去 
    movl $1f,KERNEL_EIP(%eax)    #将1处地址保存在切出去的PCB的EIP中!!!! 
    movl 8(%ebp),%ebx    #重取ebx值, 
    movl KERNEL_STACK(%ebx),%esp    #将切进来的内核栈指针保存到寄存器中 
#下面两句是后来添加的,实验指导上并没有这样做。
    pushl KERNEL_EIP(%ebx)        #将保存在切换的PCB中的EIP放入栈中!!!! 
    jmp  switch_csip        #跳到switch_csip处执行!!!! 
#    原切换LDT代码换到下面
#    原切换LDT的代码在下面 
1:    popl %eax 
    popl %ebx 
    popl %ecx 
    popfl            #将切换时保存的eflags出栈!!!! 
    popl %ebp 
    ret            #该语句用来出栈ip 

switch_csip:            #用来进行内核段切换和cs:ip跳转 
    #切换LDT 
    movl 12(%ebp),%ecx    #取出第二个参数,_LDT(next) 
    lldt %cx        #切换LDT 
    #切换cs后要重新切换fs,所以要将fs切换到用户态内存 
    movl $0x17,%ecx        #此时ECX中存放的是LDT 
    mov  %cx,%fs 
    cmpl %eax,last_task_used_math 
    jne  1f 
    clts         
1: 
    ret            #此处用来弹出pushl next->eip!!!!!!!! 


#第一次被调度时运行的切换代码 
first_switch_from: 
    popl %eax 
    popl %ebx 
    popl %ecx 
    popfl            #将切换时保存的eflags出栈!!!! 
    popl %ebp 
    ret            #该语句用来出栈ip 

#此处是从内核站中返回时使用的代码,用来做中断返回 
first_return_from_kernel: 
    #popl %eax 
    #popl %ebx 
    #popl %ecx 
    popl %edx 
    popl %edi 
    popl %esi 
    popl %gs 
    popl %fs 
    popl %es 
    popl %ds 
    iret

此外还要添加几个常量值:


/*melon 添加用来取内核栈指针的位置宏*/ 
KERNEL_STACK    =16 //内核栈指针在task_struct结构中的位移,指导上这里是12
ESP0        =4         //tss中内核栈指针位移
KERNEL_EIP    =20     //新添加的task_sturct结构中的eip指针位移
/*melon added End*/

这样就修改完毕。编译运行测试即可。

图片描述

图片描述

实验指导提出了几个思考问题如下:

问题1、

针对下面的代码片段:


 movl tss,%ecx
 addl $4096,%ebx
 movl %ebx,ESP0(%ecx)

回答问题:

  • (1)为什么要加4096;
  • (2)为什么没有设置tss中的ss0。

  • 1)该代码片段是用来对全局的tss结构进行进程内核栈指针的保存,虽然不再使用tss进行切换,但由于cpu的设计tr寄存器中必须有一个tss结构的指针指向当前cpu中运行的进程的tss结构,在这个修改版本中仅保留一个全局的tss结构用来存储该指针。而在切换进程时要同时将该指针也切换为相应要运行的进程的内核栈栈项指针,由于进程进入内核态运行时其栈是空的,即由用户态经由int转入内核态时其栈为空,由int等代码对其进行操作,如保存用户栈信息等。所以对于tss结构来说,其内核栈指针应总指向进程内核栈空栈时栈顶,而内核栈栈顶位置就位于分配的内存块的4096位置,即PAGE_SIZE,所以这里要将pcb指针加上该值即是空栈时栈顶指针位置。

  • 2)对于linux0.11来说,内核栈段的选择子永远都是0x10,所以这里不用修改该值。相同意义的代码见fork.c中的p->tss.ss0 = 0x10;,这行代码明确指出对于所有进程ss0都是0x10。

问题2、

针对代码片段:


 *(--krnstack) = ebp;
 *(--krnstack) = ecx;
 *(--krnstack) = ebx;
 *(--krnstack) = 0;

回答问题:

  • (1)子进程第一次执行时,eax=?为什么要等于这个数?哪里的工作让eax等于这样一个数?
  • 回答:子进程第一次执行时要返回0,即eax=0,这样才能和父进程进行分支运行。所以这里要设置eax=0。在使用tss切换时是由p->tss.eax = 0;代码行来设置这个值的。这里只要在switch_to返回时保证eax中的数据为0即可。其实也可不必要在此设置,也可在模仿iret返回时再进行设置。

  • (2)这段代码中的ebx和ecx来自哪里,是什么含义,为什么要通过这些代码将其写到子进程的内核栈中?

  • 回答:代码段中的ebx、ecx均来自于copry_process函数的调用参数,是在sys_fork之前和之中分段进行入栈的。具体到ebx、ecx两个参数是由system_call函数入栈的。是进入内核态前程序在用户态运行时两个寄存器的状态数据。见下面:

system_call: 
    cmpl $nr_system_calls-1,%eax 
    ja bad_sys_call 
    push %ds         #保存用户态时的寄存器
    push %es 
    push %fs 
    pushl %edx 
    pushl %ecx        # 是下面真正的int调用时的参数
    pushl %ebx        # 调用参数
    movl $0x10,%edx        # 设置ds、es寄存器到内核空间
    mov %dx,%ds 
    mov %dx,%es 
    movl $0x17,%edx        # fs指向用户空间
    mov %dx,%fs 
    call sys_call_table(,%eax,4)     #调用系统调用
    pushl %eax             #返回值
    movl current,%eax 
    cmpl $0,state(%eax)        # state 
    jne reschedule 
    cmpl $0,counter(%eax)        # counter 
    je reschedule

就个人理解,这里如果不这么设置似乎也是可以的,但这些寄存器的值必须保存在内核栈中,只要可以保证经由iret再返回到用户态时程序可以正常使用就可以。

  • (3)这段代码中的ebp来自哪里,是什么含义,为什么要做这样的设置?可以不设置吗?为什么?
  • 回答:ebp参数来自于下面程序段中的入栈动作。

sys_fork: 
    call find_empty_process 
    testl %eax,%eax 
    js 1f 
    push %gs 
    pushl %esi 
    pushl %edi 
    pushl %ebp         #入栈,成为copy_process的参数
    pushl %eax 
    call copy_process 
    addl $20,%esp 
1:    ret

观察代码流程,没有发现在这之前对ebp进行过修正,虽然调用过c函数find_empty_process但c函数在返回时会保证ebp值 回到调用之前,所以猜测该值应该是int调用之前进程中的ebp,即进程的用户态的ebp,所以为保证进程在返回用户时能正确处理栈帧,这里要保证该值被正确传递。所以要在这里入栈。在程序切换后时会使用到。

问题3、为什么要在切换完LDT之后要重新设置fs=0x17?而且为什么重设操作要出现在切换完LDT之后,出现在LDT之前又会怎么样?

回答:cpu的段寄存器都存在两类值,一类是显式设置段描述符,另一类是隐式设置的段属性及段限长等值,这些值必须经由movl、lldt、lgdt等操作进行设置,而在设置了ldt后,要将fs显示设置一次才能保证段属性等值正确。

3、对此次实验的思考

1、更换切换方式后cpu运行效率是否改变呢?如何测量呢?在此次实验中对比了两种切换的进程状态改变记录(实验三),数据如下:

tss切换时进程记录分析


(Unit: tick) 
Process   Turnaround   Waiting   CPU I/O 
      0         1929        65           8         406 
      1         1660         0           9        1651 
      2           25         4          21           0 
      3            4         0           4           0 
      4          995       140          40         815 
      5            3         0           3           0 
      6            6         1           5           0 
      7          267        81           1         185 
      8          269       223          46           0 
      9          267       222          45           0 
     10          265       220          45           0 
     11          283       222          61           0 
     12          281       221          60           0 
     13            2         0           2           0 
     14          614       125          40         449 
     15            3         1           2           0 
     16            6         1           5           0 
     17          237        81           1         155 
     18          254       209          45           0 
     19          253       207          46           0 
     20          251       206          45           0 
     21          251       205          46           0 
     22          249       204          45           0 
     23            3         0           3           0 
     24          731         4          39         688 
     25            3         1           2           0 
     26            0         0           0           0 
Average:      337.44     97.89 
Throughout: 1.13/s

栈切换时进程记录分析


(Unit: tick) 
Process   Turnaround   Waiting   CPU   I/O 
      0         2213        65           8         368 
      1         1953         1           8        1944 
      2           24         4          20           0 
      3            4         0           4           0 
      4         1207       127          41        1039 
      5            3         1           2           0 
      6            6         1           5           0 
      7          355        79           1         275 
      8          359       298          61           0 
      9          357       297          60           0 
     10          373       298          75           0 
     11          373       297          76           0 
     12          371       296          75           0 
     13            2         0           2           0 
     14          694       136          43         515 
     15            3         0           2           0 
     16            6         1           5           0 
     17          268        82           1         185 
     18          269       223          46           0 
     19          267       222          45           0 
     20          266       221          45           0 
     21          283       222          61           0 
     22          281       221          60           0 
     23            2         0           2           0 
     24          327         7          37         283 
     25            2         0           2           0 
     26            1         1           0           0 
Average:      380.33    114.81 
Throughout: 1.19/s

tss切换时进程运行记录(部分)


1    N    48 
1    J    48 
0    J    48 
1    R    48 
2    N    49 
2    J    49 
1    W    49 
2    R    49 
3    N    64 
3    J    64 
2    J    64 
3    R    64 
3    W    68 
2    R    68 
2    E    74 
1    J    74 
1    R    74 
4    N    74 
4    J    74 
1    W    74 
4    R    74 
5    N    105 
5    J    105 
4    W    105 
5    R    105 
4    J    107 
5    E    108 
4    R    108 
4    W    113 
0    R    113 
0    W    113

栈切换时进程运行记录(部分)


1    N    48 
1    J    48 
0    J    48 
1    R    48 
2    N    49 
2    J    49 
1    W    49 
2    R    49 
3    N    64 
3    J    64 
2    J    64 
3    R    64 
3    W    68 
2    R    68 
2    E    73 
1    J    73 
1    R    73 
4    N    74 
4    J    74 
1    W    74 
4    R    74 
5    N    104 
5    J    104 
4    W    105 
5    R    105 
4    J    107 
5    E    107 
4    R    107 
4    W    112 
0    R    113 
0    W    113

从数据上看进程切换时间在缩短。但不是很量化,我尝试去记录切换时间,但未成功,因为无论是哪种切换方式,在switch_to前后记录的时间都无变化,即都是同一个tick。这个有待进一步分析。


高级操作系统——XV6进程管理 源代码阅读: type.h:用于声明一些数据类型的简化名称,和声明页表指针的数据类型。 param.h:主要用于声明基本的一些常量,包括内核栈大小等。 memlayout.h:主要用于声明一些和内存与地址相关的常量与方法,包括虚拟内存转物理内存的方法以及物理内存转虚拟内存的方法等。 defs.h:声明在之后文件中要用到的函数。 x86.h:让c代码使用特殊的x86汇编的一些函数包括outb等,并声... 阅读详情

相关推荐

哈工大操作系统实验5 基于内核栈的进程切换 实验报错:no father found

遇到一个很严重的问题 system_call.s 需要添加以下代码 state = 0 # these are offsets into the task-struct. counter = 4 priority = 8 kernelstack = 12 signal = 16 sigaction = 20 # MUST be 16 (=len of sigaction) blocked = (37*16) 否则会报错 经过试验过后,发现在前面已经定义过这些值,只要修改signal = 16 ,就没

weixin_43987915的博客 869

哈工大操作系统实验---lab4:基于内核栈的进程切换

实验目的 深入理解进程和进程切换的概念; 综合应用进程、CPU管理、PCB、LDT、内核栈、内核态等知识解决实际问题; 开始建立系统认识。 实验内容: 将Linux0.11中采用的基于TSS进程切换去掉,取而代之的是基于堆栈的切换程序,具体地说,也就是将进程切换函数schedule()函数中的switch_to()函数从原本的基于TSS切换改写成基于堆栈的切换。 编写汇编程序switch_t...

东瓜blog 4646

操作系统实验五 基于内核栈切换的进程切换(哈工大李治军)

实验5 基于内核栈切换的进程切换 实验目的 深入理解进程和进程切换的概念; 综合应用进程、CPU 管理、PCB、LDT、内核栈、内核态等知识解决实际问题; 开始建立系统认识。 实验内容 现在的 Linux 0.11 采用 TSS 和一条指令就能完成任务切换,虽然简单,但这指令的执行时间却很长,在实现任务切换时大概需要 200 多个时钟周期。 而通过堆栈实现任务切换可能要更快,而且采用堆栈的切换还可以使用指令流水的并行优化技术,同时又使得 CPU 的设计变得简单。所以无论是 Linux 还是 Window

leoabcd12的博客 3085

【操作系统】实验楼实验五——基于内核栈的进程切换

载入实验环境 首先配置环境 cd /home/shiyanlou/oslab tar -zxvf hit-oslab-linux-20110823.tar.gz -C /home/shiyanlou 实验过程截图 分别对/kernel/fork.c、kernel/system_call.s、/kernel/sched.c、/include/linux/sched.h 进行修改,截图如下: 注释掉原来switch_to宏函数,将新的switch_to()函数作为一个系统调用函数重写在汇编文件system

qq_46612803的博客 1353

pthread.h sched.h semaphore.h 三个头文件

windows下安装cygwin,编译darknet时缺少的三个头文件,下载后复制到darknet下include文件夹内即可

将sched.h下载,然后保存到工程include文件夹

VS编译时无法打开 源 文件 "sched.h"。是因为微软VS没有sched. 项目--属性--配置属性--c/c++常规--附加包含目录添加pthread.h所在的文件夹

我的学习之旅(33) sched.h

sched.h主要定义了时钟频率、任务数据结构、和sleep宏。sleep宏的实现主要功能是设置当前任务的sleep_time,然后通过软中断0x80做任务软切换。 #ifndef __SCHED_H__ #define __SCHED_H__ #define HZ 100 #define delay(ticks) __asm__ __volatile__ ("cli"::); \

baidu_25474831的专栏 1066

手把手教你用Docker快速部署ZLMediaKit流媒体服务器(含WebRTC配置)

本文详细介绍了如何使用Docker快速部署ZLMediaKit流媒体服务器,并配置WebRTC功能。通过容器化部署,实现环境一致性、资源隔离和快速扩展,同时提供WebRTC专项配置、多协议推拉流实战测试及生产环境优化建议,帮助开发者高效搭建企业级流媒体平台。

agile9scrum的博客 202

sched.h

#ifndef _LINUX_SCHED_H #define _LINUX_SCHED_H #include &lt;uapi/linux/sched.h&gt; struct sched_param { int sched_priority; }; #include &lt;asm/param.h&gt; /* for HZ */ #include &lt;linux/cap...

刘星燎 2138

风力发电机并网模型建立及仿真验证

传统能源日益匮乏,环境污染日趋严重,全球向安全、可靠和环保的可持续能源体系转型迫在眉睫,风电迎来了重要的发展契机。近年来,全球风电迅猛发展,我国已成为世界风电新增装机容量第一的国家。然而,随着风电规模的不断增大,风电场接入对系统影响不容忽视。随着变速恒频风力发电系统优越的性能突出和大容量电力电子技术的成熟,风速随机性引起的功率波动及故障情况下的动态响应,对电能质量,系统电压、频率的稳定性产生的影响需要深入研究。因而,本文结合实际项目,针对变速恒频风力发电系统建模,端口动态特性及对电网电能质量、运行稳定性的影

毕业设计区博客 1639

电力电子论文(TPE、TTE)投稿经验分享

电力电子顶刊论文投稿经验

橙子的博客 2万+

进程并发究竟是如何进行进程切换的?Linux内核原理解析

实时操作系统(Real Time Operating System,简称RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统做出快速响应,调度一切可利用的资源完成实时任务,并控制所有实时任务协调一致运行的操作系统。此时操作系统会在进程被剥离前,将当前进程的运行相关信息保存到PCB中,然后将该进程的PCB从活跃队列中移除,加载到过期队列。至于新产生的需要被调度的进程,则是直接添加到过期队列中。活跃进程、过期进程。

Zhenyu_Coder的博客 2013

Appsflyer Web2App实战指南:OneLink与Web SDK深度对比

本文深入对比了Appsflyer实现Web2App的两大核心工具:OneLink智能链接与Web SDK。OneLink提供开箱即用的智能跳转,部署快捷,适合快速推广;Web SDK则支持高度自定义的用户行为追踪,适合深度分析与精细化运营。文章提供了详细的配置指南、场景化选型建议及实战避坑技巧,帮助营销与开发团队根据自身需求选择最佳方案,有效提升从网页到应用的转化率与归因精度。

weixin_29213655的博客 458

详解操作系统之进程切换

详解操作系统的进程切换

Bruce 4698

Python编程基础05:运算符与表达式

文章目录一、算术运算符1、分类说明表2、注意事项3、案例演示任务1、计算成绩差与平均成绩课堂练习:编程处理带余除法。任务2、打印购物小票 运算符就是专门用于告诉程序执行特定运算或逻辑操作的符号。表达式是指由常量、变量和运算符组成的一个算式。编写程序对数据进行处理,其实处理就是对数据实施各种各样的运算,搞懂运算符与表达式是我们编写程序的基础性工作,本次讲课笔记通过案例讲解各种运算符的用法以及正确书写Python表达式的注意事项。 一、算术运算符 Python中的算术运算符就是用来处理四则运算的符号,也是

howard2005的专栏 3119

【电动汽车再生制动控制技术(万字长文,图文并茂)】

电动汽车再生制动的基本原理是:通过具有可逆作用的电动机/发电机来实现电动汽车动能和电能的转化。在汽车减速或制动时,可逆电机以发电机形式工作,汽车行驶的动能带动发电机将汽车动能转化为电能并储存在储能器(蓄电池或超级电容器)中;在汽车起动或加速时,可逆电机以电动机形式工作,将储存在储能器中的电能转化为机械能给汽车。这对于改善汽车的能量利用效率,延长电动汽车的行驶距离是至关重要的。

优快云-新能源汽车研发测试专业博主 1577

哈工大-基于内核栈切换的进程切换

1. 课程说明 难度系数:★★★★☆ 本实验是操作系统之进程与线程 - 网易云课堂的配套实验,推荐大家进行实验之前先学习相关课程: L10 用户级线程 L11 内核级线程 L12 核心级线程实现实例 L13 操作系统的那棵树 Tips:点击上方文字中的超链接或者输入https://mooc.study.163.com/course/1000002008#/info进入理论课程的学习。 如果网易云上的课程无法查看,也可以看 Bilibili 上的操作系统哈尔滨工业大学李治军老师。 ...

INGNIGHT的专栏 942

[Dify实战] OA 公文排版助手:自动格式化、编号、生成摘要-带配套资源

摘要: Dify 公文排版助手通过自动化解决公文处理中的格式混乱、编号错漏等问题。支持标题层级自动编号、摘要生成、要素补全等功能,输出标准化Markdown格式。核心流程包括输入原文、智能识别、自动排版与要素输出,适用于通知、纪要等场景。实施需统一模板规则,规范输入格式,并通过试跑优化纠错率与处理效率。关键价值在于减少人工返工,提升排版一致性,未来可对接OA系统实现全流程自动化。

技术小甜甜:传播可落地技术,帮助个人和企业降低解决真实场景问题的成本 472

哈工大操作系统实验4---基于内核栈切换的进程切换

前置知识 关于栈桢 关于栈栈帧详解https://blog.csdn.net/ylyuanlu/article/details/18947951 进程切换流程 首先先了解一下进程切换的流程。开始的时候一个进程(or线程,在这里不加以区分,简单的认为二者的区别只是在是否产生内存映射表的切换,其他方面相同)A正在运行,CPU通过取指执行的方式一点点的去执行它的代码,突然遇到一个中断(系统调用、外部中断、异常),然后就要进入到A进程的内核态去执行相应的中断服务例程,在这期间突然不小心发生一个意外,程序的执行由于缺

weixin_42326997的博客 1693

梯度提升决策树GBDT原理与代码实例讲解

梯度提升决策树GBDT原理与代码实例讲解 1.背景介绍 1.1 机器学习发展历程 机器学习是人工智能领域的一个重要分支,旨在使计算机具备学习和改进自身性能的能力。随着大数据时代的到来,海量的数据为机器学习算法提供了广阔的应用空间。在

AI天才研究院 1086

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

HIT Linux-0.11 实验五 基于内核栈切换的进程切换 实验报告

实验要求与实验指导见 实验楼。 实验环境为 配置本地实验环境。 一、实验目标 深入理解进程和进程切换的概念; 综合应用进程、CPU 管理、PCB、LDT、内核栈、内核态等知识解决实际问题; 开始建立系统认识。 二、实验内容和结果 0. 进程切换过程   基于内核栈实现进程切换的大致过程如下:   当系统发生中断从用户态进入内核态时,CPU 通过 TR 寄存器找到 TSS 的位置,根据 TSS ...

laoshuyudaohou的博客 2115

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

哈工大操作系统实验4:基于内核切换的进程切换

前言 对于这个实验来说,说实话的其实代码基本上蓝桥实验楼上的指导书已经给全了,当然依旧存在一些特意留下的陷阱留着自己来解决。但是做实验也好还是做习题也好,其实本质上都是都是增加自己的理解,而做实验,正如李治军教授说的一样,纸上得来终觉浅,绝知此事要躬行,是给我们更深入理解其本质的绝佳做法。所以,在真正去做这个实验必须要搞懂整个流程是如何进行的。这三天时间,我也是查阅了一些博客,以及看了好多遍视频,才大致把整个流程搞懂来。接下来这篇博客,我会尽力把我所理解的流程讲解一遍。 关于整个进程切换的思考 对于整个核心

qq_50271250的博客 1024

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

【操作系统】探究进程奥秘:显示进程列表的解密与实战

《【操作系统】探究进程奥秘:显示进程列表的解密与实战》深入探讨了操作系统中显示进程列表的研究。首先,文章详细介绍了获取和展示进程列表的基础概念,包括系统调用和API的运用,以及命令行工具的实际操作。其次,深入剖析了进程调度和优先级的机制,强调了这一过程对系统性能和响应时间的直接影响。最后,文章聚焦于进程间通信和同步机制,阐述了共享内存、消息传递和信号等关键概念的应用。

朝花夕拾 1万+

写给Java程序员看的,CPU 上下文切换、用户态、内核态、进程与线程上下文切换

概述 JDK源码中很多Native方法,特别是多线程、NIO部分,很多功能需要操作系统功能支持,作为Java程序员,如果要理解和掌握多线程和NIO等原理,就需要对操作系统的原理有所了解。 CPU 上下文切换 多任务操作系统中,多于CPU个数的任务同时运行就需要进行任务调度,从而多个任务轮流使用CPU。 从用户角度看好像所有的任务同时在运行,实际上是多个任务你运行一会,我运行一会,任务切换的速度很快,我们感觉不到而已。 而每个任务运行前,CPU需要知道从哪里加载这个任务的程序,还需要知道从程序哪行开始执行,这

javaThinker的专栏 4252

【操作系统基础】进程切换 进程调度 PCB 进程队列 中断过程

文章目录并发的进程切换中断技术中断源中断的过程特权指令和非特权指令进程调度 并发的进程切换 要了解并发进程的切换,首先要清楚知道什么是并发的进程!! 并发的进程最主要的特点就是走走停停的状态,从运行的开始到结束不是一步运行到底的,是走走,然后cup被调度走就停停,等到需要进程继续执行的时候,又可以被分配到cpu资源,然后走走;如此反复,知道进程运行结束! (单cpu的讨论)如上图的两个进程process1和process2,虚线部分表示进程停止,实线部分表示进程运行,当process1停的时候,pro

呋喃吖的博客 5269

哈工大操作系统之进程与线程(Lab4 基于内核栈的进程切换)(转载)

文章目录前引链接 前引 下面就贴了几个写的比较详细且收藏比较多的链接吧 Forgive me 周末玩的太开心 打断了我现在做这个Lab的勇气 确实我感觉我汇编学习的并不是那么好 毕竟是自学看书 而且也只过了一遍 现在我也明白了一个事情 我认为很多事情 如果是具有挑战或者有一定困难的 千万千万要一口气做下去 不能半途休息或者放缓 因为当一口气下去之后 再想起来 那是真的真的很困难的 本来这8个Lab我是打算每个都自己一个步骤一个步骤慢慢调试的 但是到第4个 我也就打算转载了 后面反正我也打算按照 《操作系

Love 6's Private Blog 820
上一篇: 操作系统原理与实践10--proc文件系统实现
下一篇: 操作系统原理与实践4--进程运行轨迹的跟踪与统计
Watson2016
博客等级 码龄11年 342粉丝 88原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值