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

2023-05-16

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

难度系数:★★★★☆

实验目的

  • 深入理解进程和进程切换的概念;
  • 综合应用进程、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。这个有待进一步分析。


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • DataHub:通用的元数据搜索和发现工具(开源)

    Get Started With DataHub DataHub 作为世界上最大的专业网络和Economic Graph的运营商 xff0c LinkedIn 的数据团队一直致力于扩展其基础架构 xff0c 以满足我们不断增长的大数据生态系
  • mp4格式视频因为录制到一半断电,导致损坏能修复

    risingresearch com 可以用 xff0c 完全免费 xff0c 只是下载页面有英文 xff0c 安装后是中文的 xff0c 非常小巧 然后可能会出现缺文件头的提示 此时点击是 xff0c 然后导入一个正常录制的MP4视频 x
  • ipscan端口扫描工具

    ip端口扫描工具的英文名字是ipscan 是一款搜索局域网机器的绿色小软件 IPScan在静态IP地址环境下或者DHCP环境下 都提供完善的IP地址管理 用户也可以使用IPScanProbe自带的DHCP服务器 它能提供更高的安全和灵活的D
  • IPFS,HDFS以及http对比笔记

    分布式系统 分布式系统发展至今已有数十年 xff0c 那么分布式系统到底是什么 xff1f 实际上分布式系统并没有标准的定义 分布式系统一般的呈现方式是将硬件或软件分布在不同的网络计算机 xff0c 彼此间通过消息传递进行通信及协调 xff
  • chatgpt注意点

    1 ip地址不能是国内 2 浏览器无痕模式 xff08 浏览器不挑 xff09 3 国外的手机号激活sms 激活 20230213目前还可以注册
  • ASP.NET 连接MySQL数据库 详细步骤

    ASP NET默认的数据库是MS SQL Server xff0c 微软的数据库产品 事实上 xff0c 如果不计成本因素的话 xff0c Windows Server 43 IIS 43 MS SQL Server 43 ASP NET是
  • Apache Skywalking介绍

    Apache Skywalking介绍 1 基本介绍 Apache Skywalking是一款APM工具 xff08 Application Performance Management 应用性能管理 xff09 2 安装部署 官网地址 x
  • 常用的法律检索类网站

    1 中国裁判文书网 xff1a http wenshu court gov cn xff0c 共公布全国各级法院生效裁判文书1 2亿篇 xff0c 可以检索 查看 下载裁判文书 2 中国司法案例网 xff1a http anli court
  • 重要代码备份

    文书 xff1a button 61 document getElementsByClassName 34 a xzBox 34 for let i 61 0 i lt 61 14 i 43 43 setTimeout 61 gt butt
  • 在 Visio 绘图中剪裁线条和形状

    编辑绘图或图表 在 34 开始 34 选项卡上 xff0c 单击 34 编辑 34 组中 34 选择 34 xff0c 然后单击 列表中的 34 全 选 34 单击 34 开发工具 34 选项卡 在 34 形状设计 34 组中 xff0c
  • 定时器/计数器介绍

    第一次在学习定时器的时候模模糊糊 xff0c 在做过一些题目之后对定时器有了更新的理解 xff0c 现在整理一下 xff0c 做笔记使用 目录 一 基础知识 定时器的作用 xff1a 定时器的实质 xff1a 定时器的工作原理 xff1a
  • Win10下安装Framework 3.5

    不同于 VC 43 43 运行库 xff0c NET Framework 是支持向下兼容的 xff0c 即 xff1a NET Framework 4 8 向下兼容至 4 0 NET Framework 3 5 SP1 向下兼容至 2 0
  • linux串口通信

    linux下串口通信与管理 linux下的串口与windows有一些区别 xff0c 下面将介绍一下linux下串口通信管理 查看是否支持USB串口 xff1a lsmod grep usbserial 如果没有信息 xff1a sudo
  • UP-magic的口袋机arm挂载u盘

    查看U盘信息 fdisk l mount t vfat dev mmcblk0p1 mnt sdcard 挂载U盘 mount命令格式 xff1a mount 参数 设备名称 挂载点 其他参数 mount t vfat dev sdb1 m
  • dell t630服务器风扇控制笔记记录(耗时一天)

    1 打开虚拟控制台得用IE xff1b 2 Dell PowerEdge T640 加装显卡之后风扇狂转问题解决 知乎 感谢知乎Billy xff0c 操作步骤 xff1a 1 查看iDrac版本 xff0c 必须在3 30 30 30及以
  • 安装autogpt中出现的问题及安装autogpt的小白教程

    ImportError DLL load failed while importing numpy ops The specified module could not be found 解决方案 xff1a Latest supporte
  • UBUNTU下NFS配置(用于嵌入式开发)

    1 NFS简介 NFS xff08 Network File System xff09 即网络文件系统 xff0c 是FreeBSD支持的文件系统中的一种 xff0c 它允许网络中的计算机之间共享资源 在NFS的应用中 xff0c 本地NF
  • Ubuntu 18.04 下 uhd+gnuradio 安装指南

    sudo apt get y install git swig cmake doxygen build essential libboost all dev libtool libusb 1 0 0 libusb 1 0 0 dev lib
  • 跨网的数据交换解决方案

    一 什么是跨网 跨网是指在互联网与局域网之间不能直接连通的网络 这些局域网可以是保密性较高的单独的局域网 xff0c 也可以是公安网 军网等 二 为什么要跨网传输 以公安网为例 xff0c 公安网对数据安全的要求较高 xff0c 所以不与互
  • urllib2.urlopen超时问题

    原帖地址 xff1a http hi baidu com yss1983 item 933fbe45a09c43e01381da06 问题描述 xff1a 没有设置timeout参数 xff0c 结果在网络环境不好的情况下 xff0c 时常

随机推荐