lMIT 6.S081 实验3 笔记与心得

###MIT 6.S081 实验3 笔记与心得

1.打印页表
题目翻译

在你开始编码之前,请阅读xv6书中的第3章,以及相关文件。

kern/memlayout.h,它捕捉了内存的布局。
kern/vm.c,它包含大多数虚拟内存(VM)代码。
kernel/kalloc.c,包含分配和释放物理内存的代码。****

如果你通过了pte printout测试的make grade,你将获得这项作业的全部学分。

现在当你启动xv6时,它应该打印出这样的输出,描述第一个进程在刚刚完成exec()ing init时的页面表。

page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

第一行显示vmprint的参数。之后,每个PTE都有一行,包括指向树中更深的页表页的PTE。每一行PTE都有一个缩进的数字"…",表示它在树中的深度。每个PTE行显示PTE在其页表页中的索引,PTE位,以及从PTE中提取的物理地址。不要打印那些无效的PTEs。在上面的例子中,顶级页表页有条目0和255的映射。下一级的条目0只有索引0的映射,而下一级的索引0有条目0、1和2的映射。

你的代码可能发出的物理地址与上面显示的不同。条目数和虚拟地址应该是一样的。

一些提示:

  • 将vmprint()放在kernel/vm.c中。

  • 使用文件kernel/riscv.h末尾的宏。

  • 函数freewalk可能是鼓舞人心的。

  • 在kernel/defs.h中定义vmprint的原型,以便你可以从exec.c中调用它。

    这里要注意在exec中的第一个进程需要打印,具体代码段为:

    140   if(p->pid==1){
    141       vmprint(p->pagetable);
    142   }
    
  • 在你的printf调用中使用%p来打印出完整的64位十六进制PTE和地址,如例子中所示。

Q&A:用文中的图3-4来解释vmprint的输出。第0页包含什么?第2页中有什么?当以用户模式运行时,该进程能否读/写第1页所映射的内存?

题目解答

主要是对vmprint()递归函数的编写,基于freewalk()的原型。我这里利用局部静态变量的特性进行树结构的表达,具体代码如下:

291 void vmprint(pagetable_t pagetable){
292     static int count=0;
293     if(count==0){
294         printf("page table %p\n",(uint64) pagetable);
295     }
296     char str[3][10] ={"..", ".. ..", ".. .. .."};
297     count++;
298     
299     for(int i=0; i<512;i++){
300         pte_t pte = pagetable[i];
301         if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
302             uint64 child = PTE2PA(pte);
303             printf("%s%d: pte %p pa %p\n", str[count-1], i, (uint64)pte, child);
304             vmprint((pagetable_t)child);
305         }else if(pte & PTE_V){
306             uint64 child = PTE2PA(pte);
307             printf("%s%d: pte %p pa %p\n", str[count-1], i, (uint64)pte, child);
308         }
309 
310     }
311     count = 1;//当一棵树(页表)递归完成后,count置1,进入下一棵树(页表)
312 }

2.每个进程的内核页表
题目翻译

Xv6的内核页表只有一个内核页表,该表在内核中执行时就使用。 内核页表直接映射到物理地址,因此内核虚拟地址x映射到物理地址x。 Xv6还为每个进程的用户地址空间提供了一个单独的页表,仅包含该进程的用户内存的映射,从虚拟地址零开始。 由于内核页表不包含这些映射,因此用户地址在内核中无效。 因此,当内核需要使用在系统调用中传递的用户指针(例如,传递给write()的缓冲区指针)时,内核必须首先将指针转换为物理地址。 本节和下一节的目标是允许内核直接取消引用用户指针。

您的第一项工作是修改内核,以使每个进程在内核中执行时都使用其自己的内核页表副本。 修改struct proc以维护每个进程的内核页表,并修改调度程序以在切换进程时切换内核页表。 对于此步骤,每个进程的内核页表应与现有的全局内核页表相同。 如果usertests正确运行,则可以通过实验室的这一部分。

阅读本作业开始时提到的书籍章节和代码; 了解虚拟内存代码的工作方式会更容易正确地修改它。 页面表设置中的错误可能由于缺少映射而导致陷阱,可能导致加载和存储影响物理内存的意外页面,并可能导致从错误的内存页面执行指令。

一些提示:

  • 在进程的内核页表的struct proc中添加一个字段。
  • 为新进程生成内核页表的合理方法是实现kvminit的修改版本,该版本将创建新的页表,而不是修改kernel_pagetable。 您将要从allocproc调用此函数。
  • 确保每个进程的内核页表都具有该进程的内核堆栈的映射。 在未修改的xv6中,所有内核堆栈都在procinit中设置。 您将需要将部分或全部此功能移至allocproc。
  • 修改scheduler()以将进程的内核页表加载到内核的satp寄存器中(有关灵感,请参阅kvminithart)。 不要忘记在调用w_satp()之后调用sfence_vma()。
  • 当没有进程在运行时,scheduler()应该使用kernel_pagetable。
  • 在freeproc中释放进程的内核页表。
  • 您将需要一种释放页面表而不释放叶子物理内存页面的方法。
  • vmprint可以很方便地调试页表。
  • 可以修改xv6功能或添加新功能。 您可能至少需要在kernel / vm.c和kernel / proc.c中执行此操作。 (但是,请勿修改kernel / vmcopyin.c,kernel / stats.c,user / usertests.c和user / stats.c。)缺少页表映射可能会导致内核遇到页错误。 它将显示包含sepc = 0x00000000XXXXXXXX的错误。 您可以通过在kernel / kernel.asm中搜索XXXXXXXX来找出故障发生的位置。
  • 注意: 在切换进程时,现更新satp为p->kpagetable 这样当前进程的根页表地址就是p->kpagetable 在进程执行结束之后,将satp切换为全局kernel_pagetable
题目解答

任务清单:

  • 修改内核页表的proc 部分,为其添加一个字段内核页表指针kpagetable

     // kernel/proc.h
     ...
     85 // Per-process state
     86 struct proc {
     87   struct spinlock lock;
     ...
    102   pagetable_t kpagetable;      // Kernel page table
     ...
    108 };                                                                                        
    
  • 修改kvminit ->实现proc_kpgtl():为创建一个新进程就创建一个内核页表

    仿照全局内核页表的初始化,得到进程内核页表,这里注释掉的CLINT后面解释

     // kernel/vm.c
     ...
     23 pagetable_t proc_kpgtl(){
     24   pagetable_t pgtl = (pagetable_t) kalloc();
     25   memset(pgtl, 0, PGSIZE);
     26   uvmmap(pgtl,UART0, UART0, PGSIZE, PTE_R | PTE_W);
     27   uvmmap(pgtl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
     28 //  uvmmap(pgtl, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
     29   uvmmap(pgtl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
     30   uvmmap(pgtl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
     31   uvmmap(pgtl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
     32   uvmmap(pgtl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
     33   return pgtl;
     34 } 
    
    
  • 进程内核页表的映射,在allocproc中具体实现

    //kernel/proc.c
    static struct proc*
    allocproc(void)
    {
        ...
    111   // An empty user page table.
    112   p->pagetable = proc_pagetable(p);
    113   if(p->pagetable == 0){
    114     freeproc(p);
    115     release(&p->lock);
    116     return 0;
    117   }
    118     p->kpagetable = proc_kpgtl();
    119     //add kernel stack mappings to kpagetable
    120     char *pa = kalloc();
    121     if(pa == 0)
    122         panic("kalloc");
    123     uint64 va = KSTACK((int) (p-proc));//映射到进程的任意位置
    124     uvmmap(p->kpagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
    125     p->kstack = va;//记录进程内核栈的虚拟地址位置
    126 
    127   // Set up new context to start executing at forkret,
    128   // which returns to user space.
    129   memset(&p->context, 0, sizeof(p->context));
    130   p->context.ra = (uint64)forkret;
    131   p->context.sp = p->kstack + PGSIZE;
    132 
    133   return p;
    134 }
    
    
  • 修改调度程序scheduler

    // kernel/proc.c
    // void scheduler(void)
    ...
    513     for(p = proc; p < &proc[NPROC]; p++) {
    514       acquire(&p->lock);
    515       if(p->state == RUNNABLE) {
    516         // Switch to chosen process.  It is the process's job
    517         // to release its lock and then reacquire it
    518         // before jumping back to us.
    519         p->state = RUNNING;
    520         c->proc = p;
    521         w_satp(MAKE_SATP(p->kpagetable));
    522         sfence_vma();
    523         swtch(&c->context, &p->context); 
    524 //没有用户进程的时候,使用全局内核页表
    525         kvminithart();
    526         // Process is done running for now.
    527         // It should have changed its p->state before coming back.
    528         c->proc = 0;
    529 
    530         found = 1;
    531       }     
    532       release(&p->lock);
    533     }         
    ...
    
  • 在freeproc中释放进程的内核页表

    在kill一个进程时,需要对其用户页表和内核页表进行释放处理,这里有几点需要注意

    首先是顺序问题,要先释放进程的内核栈,在释放对应的进程内核页表,这里要注意参数的设置,看清楚函数原型,到底需要字节数还是页面数

    进程内核栈的释放,这里需要删除对应的物理内存,故最后一个参数设置为1

    进程内核页表的释放,这里只需要取消映射关系即可达到效果,这里需要注意改进freewalk()为kfreewalk(),主要是避免遇到“freewalk: leaf”,其实这里可以直接对三级页表调用kfreewalk(),这里不再展示

139 static void
140 freeproc(struct proc *p)
141 {
142   if(p->trapframe)
143     kfree((void*)p->trapframe);
144   p->trapframe = 0;
145   if(p->kstack){
146      // pte_t *pte = walk(p->kpagetable, p->kstack, 0);
147      // kfree((void*)PTE2PA(*pte));第二种方法需要显示释放内存
148      uvmunmap(p->kpagetable, p->kstack, 1, 1);
149   }
150   if(p->pagetable)
151     proc_freepagetable(p->pagetable, p->sz);                                                   
152   if(p->kpagetable){
153       proc_freekpagetable(p->kpagetable);
154   }
155 
156   p->kstack = 0;
157   p->pagetable = 0;
158   p->kpagetable = 0;
159   p->sz = 0;
160   p->pid = 0;
161   p->parent = 0;
162   p->name[0] = 0;
163   p->chan = 0;
164   p->killed = 0;
165   p->xstate = 0;
166   p->state = UNUSED;
167 }

313 void kfreewalk(pagetable_t pagetable){
314   // there are 2^9 = 512 PTEs in a page table.
315   for(int i = 0; i < 512; i++){
316     pte_t pte = pagetable[i];
317     if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
318       // this PTE points to a lower-level page table.
319       uint64 child = PTE2PA(pte);
320       kfreewalk((pagetable_t)child);
321       pagetable[i] = 0;
322     } else if(pte & PTE_V){
323       pagetable[i] = 0;
324     }
325   }
326   kfree((void*)pagetable);
327 
328 }

211 void
212 proc_freekpagetable(pagetable_t pgtl)
213 {
214 //  kfreewalk(pagetable);第二种方法
215   uvmunmap(pgtl, UART0, 1, 0);
216   uvmunmap(pgtl, VIRTIO0, 1, 0);
217 //  uvmunmap(pgtl, CLINT, 0x10000/PGSIZE, 0);
218   uvmunmap(pgtl, PLIC, 0x400000/PGSIZE, 0);
219   uvmunmap(pgtl, KERNBASE, (PHYSTOP-KERNBASE)/PGSIZE, 0);//合并代码块与数据块
220   uvmunmap(pgtl, TRAMPOLINE, 1, 0);
221   uvmfree(pgtl, 0);
222 }

351 void
352 uvmfree(pagetable_t pagetable, uint64 sz)                                          
353 { 
354   if(sz > 0)
355     uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
356   kfreewalk(pagetable);
357 } 

在调试过程中,总是会遇到一个panic:kvmpa,这一点很奇怪,于是查看kvmpa,确实会有这个panic,触发条件为:(页表中)该虚拟地址不存在。为此,查找调用kvmpa的函数,得到virtio_disk文件中使用了这个函数。

kvmpa() 函数用于将内核虚拟地址转换为物理地址, 其中调用 walk() 函数时使用了全局的内核页表. 此时需要换位当前进程的内核页表. 修改方法有两种, 一种是直接修改 kvmpa() 内部, 将 walk() 的第一个参数改为 myproc()->kpagetable; 第二种则是将 kvmpa() 的参数增加一个 pagetable_t, 在 kernel/virtio_disk.c 的 virtio_disk_rw() 调用时传入 myproc()->kpagetable. 两种方法效果是一样的, 此处选择了第二种, 主要考虑到未来扩展时可能会使用全局内核页表的情况, 则第一种不能适用.

//
205   // buf0 is on a kernel stack, which is not direct mapped,
206   // thus the call to kvmpa().
207   disk.desc[idx[0]].addr = (uint64) kvmpa(myproc()->kpagetable, (uint64) &buf0);

3.简化copyin/copyinstr
题目翻译

内核的copyin函数读取由用户指针指向的内存。它通过将它们转换为物理地址来完成这一工作,内核可以直接对其进行解引用。它通过在软件中walk进程页表来完成这种转换。在这部分实验中,你的工作是向每个进程的内核页表(在上一节中创建)添加用户映射,使copyin(以及相关的字符串函数copyinstr)能够直接解引用用户指针。

将kernel/vm.c中copyin的主体替换为对copyin_new的调用(定义在kernel/vmcopyin.c中);对copyinstr和copyinstr_new做同样的处理。在每个进程的内核页表中添加用户地址的映射,以便copyin_new和copyinstr_new能够工作。如果usertests正确运行,并且make grade都通过,你就通过了这项作业。

这个方案依赖于用户的虚拟地址范围不与内核用于其自身指令和数据的虚拟地址范围重叠。Xv6对用户地址空间使用从零开始的虚拟地址,幸运的是内核的内存从更高的地址开始。然而,这种方案确实限制了用户进程的最大尺寸,使其小于内核的最低虚拟地址。内核启动后,这个地址是xv6中的0xC000000,即PLIC寄存器的地址;参见kernel/vm.c中的kvminit(),kernel/memlayout.h,以及文中的图3-4。你需要修改xv6以防止用户进程的规模超过PLIC地址。

一些提示:

  • 首先用copyin_new的调用代替copyin(),并使其工作,然后再转到copyinstr。
  • 在内核改变进程的用户映射的每个点上,以同样的方式改变进程的内核页表。这些点包括fork(), exec(), 和sbrk().
  • 不要忘记在userinit中把第一个进程的用户页表包括在它的内核页表中。
  • 在一个进程的内核页表中,用户地址的PTE需要什么权限?(一个设置了PTE_U的页不能在内核模式下被访问)。
  • 不要忘记上面提到的PLIC限制。

Linux使用了一种类似于你所实现的技术。直到几年前,许多内核在用户和内核空间都使用相同的每进程页表,并对用户和内核地址进行映射,以避免在用户和内核空间之间切换时必须切换页表。然而,这种设置允许诸如Meltdown和Spectre这样的侧通道攻击。

思考:解释为什么第三个测试srcva + len < srcva在copyin_new()中是必要的:给出srcva和len的值,前两个测试失败(即不会导致返回-1),但第三个测试为真(导致返回1)。

题目大意:内核的copyin函数读取用户指针指向的内存。它先将它们翻译为物理地址(内核可以直接用)。通过代码walk进程页表实现翻译。 在此实验中,你的工作是给每个进程的内核页表添加用户映射,使得copyin可以直接使用用户指针。

题目解答

首先要知道题目意思,刚开始我就自己写了一个copyin_new,原来已经给你写好了,你要做的就是将它替换为copyin,copyinstr也是类似处理

 // kernel/vmcopyin.c:已经实现了对应的函数版本,直接使用即可
...
 26 // Copy from user to kernel.
 27 // Copy len bytes to dst from virtual address srcva in a given page table.
 28 // Return 0 on success, -1 on error.
 29 int
 30 copyin_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
 31 {
 32   struct proc *p = myproc();
 33 
 34   if (srcva >= p->sz || srcva+len >= p->sz || srcva+len < srcva)
 35     return -1;
 36   memmove((void *) dst, (void *)srcva, len);
 37   stats.ncopyin++;   // XXX lock
 38   return 0;
 39 }
...

//kernel/vm.c
...
463 int
464 copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
465 {
466     if(copyin_new(pagetable, dst, srcva, len) == 0)
467         return 0;
468     return -1;
469 }
470 
471 // Copy a null-terminated string from user to kernel.
472 // Copy bytes to dst from virtual address srcva in a given page table,
473 // until a '\0', or max.
474 // Return 0 on success, -1 on error.
475 int
476 copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
477 {
478     if(copyinstr_new(pagetable, dst, srcva, max) == 0)
479         return 0;
480     return -1; 
481 }            

其次,到了最难的部分,当遇到以下几个函数:fork,exec,sbrk,应该如何修改进程的内核页表,这里不能照搬用户页表的方式。先看看在fork里,是怎么处理的。

296 // Create a new process, copying the parent.
297 // Sets up child kernel stack to return as if from fork() system call.
298 int
299 fork(void)                                                                            
300 {
301   int i, pid;
302   struct proc *np;
303   struct proc *p = myproc();
304 
305   // Allocate process.
306   if((np = allocproc()) == 0){
307     return -1;
308   }
309 
310   // Copy user memory from parent to child.
311   if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
312     freeproc(np);
313     release(&np->lock);
314     return -1;
315   }
    //这里不能直接拷贝父进程的内核页表,因为父子进程都是读时共享,写时复制,共享内核页表的物理端,只有需要修改时,才独立分配一片内存,进行独立的写,不影响另一端的数据
    //这里还需要注意的是,父子进程的用户态页表的也不一样,需要使用本进程的用户页表来进行映射,在进行添加用户态映射时,只需要将用户页表端的物理地址映射到内核页表即可。
316   if(kvmcopy(np->pagetable, np->kpagetable, 0, p->sz)<0){
317       freeproc(np);
318       release(&np->lock);
319       return -1;
320   }

365 int
366 kvmcopy(pagetable_t old, pagetable_t new, uint64 begin, uint64 end)
367 {
368   pte_t *pte;
369   uint64 pa, i;
370   uint flags;
371   begin = PGROUNDUP(begin);
    //这里begin的含义是原来的用户页表字节大小(kb)或者理解为起始点复制位置(每一次复制,按照一页来复制),end是进行更改后的用户页表字节大小(kb)或者理解为复制的结束位置。
372   for(i = begin; i < end; i += PGSIZE){
373     if((pte = walk(old, i, 0)) == 0)
374       panic("uvmcopy: pte should exist");
375     if((*pte & PTE_V) == 0)
376       panic("uvmcopy: page not present");
377     pa = PTE2PA(*pte);
378     flags = PTE_FLAGS(*pte);
379     flags = flags & (~PTE_U);
380 // 要注意用户标志的消除,题目中有提到
381     if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
382       goto err;
383     }
384   }
385   return 0;
386 
387  err:
388   uvmunmap(new, 0, i / PGSIZE, 1);
389   return -1;
390 }              

接下来是对exec中进程内核页表的修改,需要注意的几点是,在何处插入内核页表修改代码

,如何修改内核页表。要想解决这两个问题,首先要明确的是exec函数的意义:用一个新的进程取代当前的进程,这就涉及到用户页表的替换(代码段以及数据段)。

在这里面的顺序问题很重要,在手册里提醒道“ 在准备新的内存映像的过程中,如果 exec 检测到一个错误,比如一个无效的程序段, 它就会跳转到标签 bad,释放新的映像,并返回-1。exec 必须延迟释放旧映像,直到它确定exec系统调用会成功:如果旧映像消失了,系统调用就不能返回-1。exec中唯一的错误情况发生在创建映像的过程中。一旦镜像完成,exec就可以提交到新的页表(kernel/exec.c:113)并释放旧的页表 ”。

为此,需要在原来的用户页表被释放后,再进行当前的内核页表的释放,然后再将新的用户页表映射到当前的内核页表(被释放后的)。

// kernel/exec.c
...
125   // Commit to the user image.
126   oldpagetable = p->pagetable;
127   p->pagetable = pagetable;
128   p->sz = sz;
129 
130 
131   p->trapframe->epc = elf.entry;  // initial program counter = main
132   p->trapframe->sp = sp; // initial stack pointer
133   proc_freepagetable(oldpagetable, oldsz);
134 
135   uvmunmap(p->kpagetable, 0, PGROUNDUP(oldsz)/PGSIZE, 0);
136   if(kvmcopy(p->pagetable, p->kpagetable, 0, p->sz) == -1)
137       goto bad;
138
...

接着是sbrk中对页表内容的修改,通过查找,发现这个是一个用户级函数,需要进行系统调用,最后调用的内核函数是growproc,这里需要对growproc进行修改,观察函数内部的功能,发现该函数主要是对用户空间的内存进行扩大或者缩小,这里需要注意的是,是否可以任意扩大用户空间的内存大小,显然是不能的,由于内核页表要引用用户页表的虚拟地址内容,那么用户页表的地址范围不能超过内核页表地址的最小值,为防止重叠,在文中只需要小于PLIC即可,凡是大于该地址的修改项都是错误的,为此修改如下:

//kernel/proc.c
...
270 int
271 growproc(int n)
272 {
273   uint sz;
274   struct proc *p = myproc();
275 
276   sz = p->sz;
277   if(n > 0){
278     if(sz + n > PLIC)
279         return -1;
280     if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
281       return -1;
282     }
    //这里需要特别注意参数的设置后两位参数,这里主要是起始复制点,和末尾点
283     if(kvmcopy(p->pagetable, p->kpagetable, p->sz, sz) == -1)
284         return -1;
285     
286   } else if(n < 0){
287     sz = uvmdealloc(p->pagetable, sz, sz + n);
288     if(PGROUNDUP(sz)<PGROUNDUP(p->sz)){
289         uvmunmap(p->kpagetable,PGROUNDUP(sz), (PGROUNDUP(p->sz)-PGROUNDUP(sz))/PGSIZE,     0);
290     }
291   }
292   p->sz = sz;                                                                         
293   return 0;
294 }
...

最后一个要求是对userinit中第一个进程的用户页表进行映射

239 void
240 userinit(void)
241 {
242   struct proc *p;
243 
244   p = allocproc();
245   initproc = p;
246   
247   // allocate one user page and copy init's instructions
248   // and data into it.
249   uvminit(p->pagetable, initcode, sizeof(initcode));
250   p->sz = PGSIZE;
251   //add user-PGTL into per-proc-kPGTL
252   kvmcopy(p->pagetable, p->kpagetable,0,p->sz);

从上面,几个修改看出,只要不是扩展内存,都是从0开始进行映射,即直接映射,要注意的就是扩展内存里面的大小限制。

4.实验测试与评分
make grade

可能时间比较漫长,耐心等待即可,如果遇到TimeOUT的Error,需要到评分标准gradelib中修改时长限制,这是因为电脑太垃圾了,没有别的原因哈(我这里的虚拟机的配置较差)
这里需要主要在主目录(实验目录上)编写两个文件:
time.txt->内容为完成实验的小时数目(整数)
answers-pgtbl.txt->你对实验的看法与分析,注意要大于10个字符,才会符合评分条件。

== Test pte printout == pte printout: OK (2s) 
== Test answers-pgtbl.txt == answers-pgtbl.txt: OK 
== Test count copyin == count copyin: OK (1.8s) 
== Test usertests == (184.1s) 
== Test   usertests: copyin == 
  usertests: copyin: OK 
== Test   usertests: copyinstr1 == 
  usertests: copyinstr1: OK 
== Test   usertests: copyinstr2 == 
  usertests: copyinstr2: OK 
== Test   usertests: copyinstr3 == 
  usertests: copyinstr3: OK 
== Test   usertests: sbrkmuch == 
  usertests: sbrkmuch: OK 
== Test   usertests: all tests == 
  usertests: all tests: OK 
== Test time == 
time: OK 
Score: 66/66
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值