[操作系统实验lab4]实验报告
實驗概況
在開始實驗之前,先對實驗整體有個大概的了解,這樣能讓我們更好地進行實驗。
我們本次實驗需要補充的內容包括一整套以sys開頭的系統調用函數,其中包括了進程間通信需要的一些系統調用如sys_ipc_can_recv等,以及補充完成fork.c函數,當然也不能少填寫syscall_wrap.S.
系統調用
關于系統調用,我們主要是以以下流程來進行的:
- 用戶調用syscall特權指令觸發異常
- 異常觸發,pc值自動被硬件置為0x80000080,轉向異常分發代碼
- trap_init識別是系統調用(8號異常),為其分配處理函數handle_sys
- 將用戶態下的參數拷貝到內核中,根據第1個參數為索引尋找系統調用表syscalltable
- 根據系統調用號在syscalltable中找到相應的函數后,轉向對應函數處理
- 處理完成后,回到用戶態,系統調用完成。
上面是我們這次要填寫的./user/syscall_wrap.S函數,結合注釋與我們上面的講解,應該不難理解。實際上就是如下流程:1、設置syscall的參數;2、執行syscall;3、完成系統調用,返回
而在mips下有如下約定:
在這一階段你可能存在的困惑是,在每個函數中出現的int sysno的參數究竟有什么用?實際上筆者認為這個參數并沒有什么用處,不過筆者最后認為,sysno其實就是a0的值,其實也就是我們的系統調用號。我們可以在./user/syscall_lib.c里面看到其引用:
int syscall_set_pgfault_handler(u_int envid, u_int func, u_int xstacktop) {return msyscall(SYS_set_pgfault_handler,envid,func,xstacktop,0,0); }這就又牽涉到一個問題,syscall_x和sys_x函數有什么關聯和區別呢?
對于這個問題,實際上,在后面填寫fork.c的時候,我們可以發現我們使用的函數全部都是syscall_x類的函數,而不使用sys_x類的函數。實際上根據我們上面的流程,調用關系是這樣的:
填完系統調用后,就可以開始填寫跟系統調用有關的syscalltable中的系統調用子函數了。在Lab4中這些子函數注釋嚴重匱乏,所以我參考了MIT的JOS的注釋來進行理解和填寫。
sys_set_pgfault_handler
第一個要補全的函數是這個,先來看看MIT原生注釋是怎樣的:
// Set the page fault upcall for 'envid' by modifying the corresponding struct // Env's 'env_pgfault_upcall' field. When 'envid' causes a page fault, the // kernel will push a fault record onto the exception stack, then branch to // 'func'. //為envid所對應的進程控制塊設立對應的缺頁處理函數,通過修改進程控制塊通信結構中的 'env_pgfault_upcall'區域 //(在我們的實驗中是 env_pgfault_handler )。當 envid 進程造成頁缺失時,內核將會把頁缺失記錄入異常棧 //(exceptionstack),然后轉向處理函數 'func'。 // Returns 0 on success, < 0 on error. Errors are: // -E_BAD_ENV if environment envid doesn't currently exist, // or the caller doesn't have permission to change envid.實際上面這段注釋已經說明了這個函數的作用,實際上函數應該還是比較好填寫的。結合注釋和指導書中的內容,應該能輕松補全。
sys_mem_alloc
同樣,上手先參考一下MIT-JOS的原生注釋:
// Allocate a page of memory and map it at 'va' with permission // 'perm' in the address space of 'envid'. // The page's contents are set to 0. // If a page is already mapped at 'va', that page is unmapped as a // side effect. // // 分配一頁內存在'envid'進程對應的地址空間中,讓'va'以'perm'的權限位映射它。 // 新分配的那頁內容要清零。如果已有一個va映射到了該頁,那么要解映射。 // // perm -- PTE_U | PTE_P must be set, PTE_AVAIL | PTE_W may or may not be set, // but no other bits may be set. See PTE_SYSCALL in inc/mmu.h. //權限位PTE_U | PTE_P 必須被給予,PTE_AVAIL | PTE_W 給不給都可以,// Return 0 on success, < 0 on error. Errors are: // -E_BAD_ENV if environment envid doesn't currently exist, // or the caller doesn't have permission to change envid. // -E_INVAL if va >= UTOP, or va is not page-aligned. // -E_INVAL if perm is inappropriate (see above). // -E_NO_MEM if there's no memory to allocate the new page, // or to allocate any necessary page tables. //錯誤種類有: //如果傳入的參數envid不存在的話,或者調用者并沒有權限修改envid時,返回-E_BAD_ENV; //如果va>=UTOP或者va沒有對齊時,返回-E_INVAL; //如果權限位不合適的話,返回-E_INVAL; //如果沒有空閑頁用于alloc一個新頁或者新頁表,返回-E_NO_MEM。在這個函數中我覺得很重要的一點就是要判斷關于va是否超過UTOP的問題,在每一個存在傳入參數為va的函數中都應當重視這一點。
關于所有權限位的解釋與說明,我們可以參考MIT-JOS的注釋,可以發現:
#define PTE_P 0x001 // Present //PTE_P和我們的PTE_V的作用一致,表明一個頁表項(或者頁目錄項)是有效的。#define PTE_W 0x002 // Writeable //PTE_W和我們的PTE_R的作用一致,表明該頁表項對應的頁是用戶可寫的。#define PTE_U 0x004 // User #define PTE_D 0x040 // Dirty //PTE_D為什么沒有定義,難道不需要寫回磁盤嗎?這兩個權限位我一直都抱有懷疑的態度,有點奇怪。#define PTE_COW 0x800 //PTE_COW和我們的PTE_COW的作用一樣,也是用于copy on write的一個標志位。下面是我們實驗中的權限位的設置
#define PTE_V 0x0200 // Valid bit #define PTE_R 0x0400 // Dirty bit ,'0' means only read ,otherwise make interrupt #define PTE_COW 0x0001 // Copy On Write #define PTE_LIBRARY 0x0004 // share memmory很多我們實驗中沒有見到卻定義了的的PTE_D,PTE_UC,PTE_G都沒有出現,以上四個權限位是我們貫穿所有實驗的最重要的幾個權限位。
我們實驗中在fork.c中關于PTE_LIBRARY的判斷是極其重要的,當然這是后話,稍后再說。
實際上我們在sys_mem_alloc中所需要的權限位PTE_V是必要的,而PTE_R則不是必須的。所以必須在sys_mem_alloc中判斷PTE_V,而不需要判斷PTE_R.
if((perm & PTE_V) ==0)return -E_INVAL;這一句是必須的,否則我們看到在實驗中可能會出現隱患錯誤。
sys_mem_map
這個函數用于內存映射,那么該如何映射,繼續參考一下MIT-JOS的注釋來看:
// Map the page of memory at 'srcva' in srcenvid's address space // at 'dstva' in dstenvid's address space with permission 'perm'. // Perm has the same restrictions as in sys_page_alloc, except // that it also must not grant write access to a read-only // page. // 利用給定的權限位Perm建立'srcenvid'地址空間中'srcva'映射的內存頁到 //'dstenvid'地址空間中'dstva'虛地址的映射關系。 // Perm有著和sys_mem_alloc一樣的限制,除了以下該點: // 不可以允許只讀頁能以寫的方式訪問(即只讀不可以寫) // // Return 0 on success, < 0 on error. Errors are: // -E_BAD_ENV if srcenvid and/or dstenvid doesn't currently exist, // or the caller doesn't have permission to change one of them. // -E_INVAL if srcva >= UTOP or srcva is not page-aligned, // or dstva >= UTOP or dstva is not page-aligned. // -E_INVAL is srcva is not mapped in srcenvid's address space. // -E_INVAL if perm is inappropriate (see sys_page_alloc). // -E_INVAL if (perm & PTE_W), but srcva is read-only in srcenvid's // address space. // -E_NO_MEM if there's no memory to allocate the new page, // or to allocate any necessary page tables. // 錯誤種類: // 如果 srva 沒有映射在 srcenvid的地址空間里,這一點可以通過 page_lookup的返回值來確定; // 如果 perm & PTE_W 為真,但是 srcva 在srcenvid 地址空間內是只讀的,則返回-E_INVAL;那么,什么地址是只讀的呢?通過閱讀在MIT-JOS里的./inc/memout.h里面可以看到,所以我們在sys_mem_map里所注意的只需要這兩點即可。
sys_mem_unmap
// Unmap the page of memory at 'va' in the address space of 'envid'. // If no page is mapped, the function silently succeeds. // // 解除envid地址空間內'va'與其對應物理頁的映射關系。 // 如果本來就沒有映射頁,就默默地成功了。 // Return 0 on success, < 0 on error. Errors are: // -E_BAD_ENV if environment envid doesn't currently exist, // or the caller doesn't have permission to change envid. // -E_INVAL if va >= UTOP, or va is not page-aligned. // 錯誤種類前面都有所見過,這里不復述了其實unmap函數本身并不難填,難的是理解其在pgfault中的作用。后面談到再寫吧。
sys_env_alloc
終于到這個函數了,這個函數是fork之魂,fork函數因為有了這個函數才能十分厲害地返回兩個返回值,它也是整個系統調用中的核心函數之一。
首先來看其注釋,原本是拆開的,現在合起來以便于理解:
// Allocate a new environment. // Returns envid of new environment, or < 0 on error. Errors are: // -E_NO_FREE_ENV if no free environment is available. // 產生錯誤的原因較少,只有當沒有空閑進程控制塊時才返回非0值。 // Create the new environment with env_alloc(), from kern/env.c. // It should be left as env_alloc created it, except that // status is set to ENV_NOT_RUNNABLE, and the register set is copied // from the current environment -- but tweaked so sys_env_alloc // will appear to return 0. // 使用env_alloc()函數來建立一個新的進程控制塊 // 它除了要被建立外,還需要設置其狀態為ENV_NOT_RUNNABLE // 還需要使用當前環境來設置其寄存器狀態,但是需要調整一些讓函數看起來返回0// install the pgfault upcall to the child // tweak the register eax of the child, // thus, the child will look like the return value // of the the system call is zero. // 為子進程建立頁錯誤處理函數(調用函數),調整child的eax寄存器(2號寄存器) // 因此,子進程的系統調用看起來返回值是0.// but notice that the return value of the parent // is the env id of the child // 但是要注意,父進程的返回值是子進程的ID。我寫的關于sys_env_alloc函數如下:
int sys_env_alloc(void){struct Env *child;1. if (env_alloc(&child, curenv->env_id) < 0)return -E_NO_FREE_ENV; 2. bcopy(KERNEL_SP - sizeof(struct Trapframe), &child->env_tf, sizeof(struct Trapframe));3. child->env_status = ENV_NOT_RUNNABLE; 4. child->env_pgfault_handler = 0; 5. child->env_tf.pc = child->env_tf.cp0_epc;//tweak register exa of JOS(register v0 of MIPS) to 0 for 0-return 6. child->env_tf.regs[2] = 0;7. return child->env_id;}關于這個函數中,我覺得最重要的一點就是理解為何sys_env_alloc在不同的進程可以返回兩個返回值?
1.所做的功能其實就是申請一個空白的進程塊,使用指針child來指向新申請的進程塊,且curenv->env_id為其父進程。
2.將當前的環境中的所有寄存器的狀態全部保存在child->env_tf中,這一步相當于為子進程配置了和父進程完全一眼的進程上下文。
3.在fork結束之前我們不能將子進程狀態設置為RUNNABLE,因為我們還將在父進程中為子進程復制一些資源以及處理一些東西。
4.這里child->env_pgfault_handler其實為0或者為其他均可,因為在子進程實際啟動前我們會主動為子進程設置一個頁錯誤處理函數。
5&6. 5和6搭配才能可以完整地表明為什么在fork函數中使用如下語句:envid = sys_env_alloc()時envid會有兩個值,一個為0,一個非0。事實上是這樣,在fork函數里,在父進程運行到 envid = sys_env_alloc這句話時,實際上變成底層語言,是如下的一個過程:
我們首先要運行sys_env_alloc()函數,其返回值放在了eax寄存器,在mips中稱之為v0寄存器,即regs[2]。然后下一步是將eax寄存器中的值賦給envid。
我們再返回來看一下5&6兩句,可以發現,一個是設置子進程的pc為child->env_tf.cp0_epc,我們知道此時child->env_tf.cp0_epc實際上和父進程的cp0_epc是一樣的,所以實際上當子進程被調度時,子進程運行的第一條指令實際上是代碼段中sys_env_alloc()返回后的第一條指令,即 eax -> envid,又因為我們另一條語句已經設置子進程的eax(v0)寄存器的值為0,所以在子進程中,envid的值是0。
sys_set_env_status
這個函數實際上沒什么難寫,就是為envid對應的進程設置相應的狀態,這個在父進程的fork中將會被調用設置子進程RUNNABLE狀態讓子進程參與調度(因為父進程沒辦法直接操作子進程)。
sys_set_trapframe
這個函數就是為envid對應的進程控制塊設置進程上下文而已,在我們本次實驗中沒有用到,在lab6中將會用到。
后面兩個系統調用和通信有關,等寫完fork之后再敘述。
了解了這個函數,就可以直接講fork的機制了
fork
在講fork的機制之前,首先要談一下關于函數pgfault和函數duppage的填寫及其作用。
pgfault
// Custom page fault handler - if faulting page is copy-on-write, // map in our own private writable copy. // 如果缺頁是copy-on-write的,那么則把它復制一份給子進程。(so...)通過注釋的閱讀,可以發現pgfault實際上就是一個處理頁錯誤時擁有copy-on-write的頁的問題的,正常的缺頁中斷的處理都是有@@@pageout@@@的標識,然后通過tlb進行補頁的。那么來細細觀察一下pgfault的結構:
static void pgfault(u_int va) {int r;int i;va = ROUNDDOWN(va, BY2PG);1. if (!((*vpt)[VPN(va)] & PTE_COW ))user_panic("PTE_COW failed!");2. if (syscall_mem_alloc(0, PFTEMP, PTE_V|PTE_R) < 0)user_panic("syscall_mem_alloc failed!");3. user_bcopy((void*)va, PFTEMP, BY2PG);4. if (syscall_mem_map(0, PFTEMP, 0, va, PTE_V|PTE_R) < 0)user_panic("syscall_mem_map failed!");5. if (syscall_mem_unmap(0, PFTEMP) < 0)user_panic("syscall_mem_unmap failed!"); }首先要搞清楚哪個是父進程的地址空間,哪個又是子進程的地址空間,參考MIT的注釋可以得到如下助攻:
// Allocate a new page, map it at a temporary location (PFTEMP),// copy the data from the old page to the new page, then move the new// page to the old page's address.// Hint:// You should make three system calls.// No need to explicitly delete the old page's mapping.// 分配一頁,把它映射到一個臨時位置(PFTEMP),把舊頁的數據拷到新頁去,然后把新頁再移到舊頁的地址上去。// 提示:// 你應當使用三個系統調用,無需顯式刪除舊頁的映射。(實際上在page_insert里已經做了解除映射)其實我們寫完就能發現,實際上我們所做的事情是很玄妙的,這時候達到的效果就是只有子進程的va可以找到曾經的那個Copy-on-write的頁了!而且我們可以看到,不論是在alloc還是在map的時候這頁都會加上寫權限,并且不會加Copy-on-write。而PFTEMP=Pgfault Temp,實際上用來倒換新舊頁,我們想在改變權限的情況下將copy-on-write的那頁重新弄到父進程相同的地址并且不能破壞子進程的原先頁的屬性,所以就巧妙了用了這樣的機制。
duppage
說到這個函數我真是服了,關于系統調用的那個bug還沒有解決,不過這個函數雖然填寫很坑,但是其內容還是比較有趣的,而且有一些很厲害的東西一直埋伏著,一直到lab6給了我當頭一棒,23333.
首先來看看duppage的注釋:
// Map our virtual page pn (address pn*PGSIZE) into the target envid // at the same virtual address. If the page is writable or copy-on-write, // the new mapping must be created copy-on-write, and then our mapping must be // marked copy-on-write as well. (Exercise: Why do we need to mark ours // copy-on-write again if it was already copy-on-write at the beginning of // this function?) // 把虛擬頁號 pn 映射到目標進程envid 的同樣虛擬地址。 // // Returns: 0 on success, < 0 on error. // It is also OK to panic on error inline static void duppage(u_int envid, u_int pn) {int r;u_int addr;Pte pte;u_int perm;0. perm = ((*vpt)[pn]) & 0xfff;1. if( (perm & PTE_R)!= 0 || (perm & PTE_COW)!= 0){ 2. if(perm & PTE_LIBRARY) {perm = perm | PTE_V | PTE_R;}else{perm = perm | PTE_V | PTE_R | PTE_COW;}3. if(syscall_mem_map(0, pn * BY2PG, envid, pn * BY2PG, perm) == -1)user_panic("duppage failed at 1");4. if(syscall_mem_map(0, pn * BY2PG, 0, pn * BY2PG, perm) == -1)user_panic("duppage failed at 2");}else{ 5. if(syscall_mem_map(0, pn * BY2PG,envid, pn * BY2PG, perm) == -1)user_panic("duppage failed at 3");} }--0. 依舊回環搜索,得到權限位(頁框號為20位,后12位是權限位)
1-2. duppage的含義在于將父進程的所有可以的映射按同樣的方式映射在子進程中,但是對于不同的頁要有不同的處理方式。在父進程中可寫的或者是符合Copy-on-write機制的頁,如果是父子進程共享的(LIBRARY),那么我們就不需要Copy-on-write,但是如果父子進程不可以完全共享的,那么需要為其加上PTE_COW的標志,以便于之后Copy-on-write時使用pgfault進行處理。
3-4. 因為之前修改了權限位,所以在if條件之后我們需要對父子進程都進行重新映射,映射的地址是pn*BY2PG,同樣這里使用到了傳入envid=0時代表父進程的一個特性。
這里有個很有意思的問題,我們在映射要先映射父進程還是先映射子進程呢?
這里父子的先后關系可以直接決定程序是否正確,應該是要先映射子進程,再對父進程自己進行覆蓋映射。原因是這樣,如果先映射父進程的話,就對父進程中的pn*BY2PG的權限位進行了修改,對于fork應當是不要緊的,但是對于進程通信應該會造成比較大的影響。
--5. 這里沒有修改權限,表示父進程中該頁時只讀的或者不是Copy-on-write的,那么則以原先的映射映到子進程即可。
fork
fork中我寫的源碼如下:
int fork(void){// Your code here.u_int envid;int pn;extern struct Env *envs;extern struct Env *env;0. set_pgfault_handler(pgfault);1. if((envid = syscall_env_alloc()) < 0)user_panic("syscall_env_alloc failed!");if(envid == 0){ 2. env = &envs[ENVX(syscall_getenvid())];return 0;} 3. for(pn = 0; pn < ( UTOP / BY2PG) - 1 ; pn ++){ 4. if(((*vpd)[pn/PTE2PT]) != 0 && ((*vpt)[pn]) != 0){ 5. duppage(envid, pn);}} 6. if(syscall_mem_alloc(envid, UXSTACKTOP - BY2PG, PTE_V|PTE_R) < 0)user_panic("syscall_mem_alloc failed~!"); 7. if(syscall_set_pgfault_handler(envid, __asm_pgfault_handler, UXSTACKTOP) < 0)user_panic("syscall_set_pgfault_handler failed~!"); 8. if(syscall_set_env_status(envid, ENV_RUNNABLE) < 0)user_panic("syscall_set_env_status failed~!");return envid;}在0. fork函數中一開始要為父進程設置頁錯誤處理函數為pgfault,這個pgfault其實就是上面所填的那個pgfault函數。
這里的set_pgfault_handler其參數實際上是一個函數指針,即意味著pgfault是作為函數指針的參數傳入set_pgfault_handler函數的。
來觀察一下這個函數,就可以知道其作用了:
這里值得注意的一點就是因為set_pgfault_handler是個用戶態的處理函數(因為注冊的是用戶棧),所以只能使用syscall開頭的系統調用服務。其實能看出,這個函數和系統調用syscall_set_pgfault_handler不同之處在于該函數會判斷當前的錯誤處理函數是否為空。所以我們只能對父進程使用該函數,而對子進程一定要新建錯誤棧并通過系統調用來注冊。
實際上回環搜索的作用是,給定一個虛擬地址,我們可以構造出其頁目錄項和頁表表項。假設我們要查詢的虛擬地址為
va = PDX | PTX | OFFSET
要得到對應的頁目錄項: vaddr = UVPT[31:22] | UVPT[31:22] | PDX | 00;
得到對應的頁表項: vaddr = UVPT[31:22] | PDX | PTX | 00;
實際上我們的vpt和vpd就是發揮了這樣的作用,vpt記載的是對應的頁表項,vpd記載的是對應的頁目錄項,但是這里有一點特殊的地方在于,我們需要使用 *vpt 和 *vpd 來找,因為在entry.S中有如下定義:
實際上vpt是UVPT的一個指針,那么實際上vpt里存著UVPT的首地址,即*vpt=UVPT,所以(*vpt)[N] = UVPT[N]。
6&7&8. 6、7、8三個步驟都是只在父進程里所做的,其為子進程申請了一個新的錯誤棧,并且注冊了錯誤處理函數,然后將子進程的狀態設置為RUNNABLE,子進程就可以參與調度了。注意RUNNABLE應該是只能在父進程結束的末尾來做,否則可能會出現資源沒有配置好,子進程就參與調度的情況出現。
進程通信
其實我們這次的進程通信只是發消息,沒有涉及到共享內存的書寫,所以看起來還是比較好寫。
sys_ipc_can_send
void sys_ipc_recv(int sysno,u_int dstva) {if ((unsigned)dstva >= UTOP || dstva != ROUNDDOWN(dstva, BY2PG)){return -E_INVAL ;}curenv->env_ipc_dstva = dstva;curenv->env_ipc_recving = 1;//Mark Curenv ENV_NOT_RUNNABLE and Give Up CPUcurenv->env_status = ENV_NOT_RUNNABLE;sys_yield();}recv比較好寫,recv就是在等待接收別的進程發送的消息,如果別人發出的消息可以被接收到的話就會調用sys_ipc_recv來接收消息,所以循環阻塞等待這一點是在ipc.c里面完成的,和我們的系統調用沒有半毛錢關系。那么sys_ipc_recv里我們需要置一些消息位,同時將env_ipc_recving=1,以表明自己已經收到了。比較坑的點可能在最后的調度上,調度要使用系統調用的調度函數,不能直接使用sched_yield,因為我們這次系統調用在Kernel_sp處保存進程上下文信息,所以調度一個進程時需要從那里獲取上下文。
不過比較搞笑,lab4在ipc.c里面居然沒有調用這個函數,而是直接就阻塞等待,真是有意思。。。
sys_ipc_can_send
這個系統調用是個大家伙,需要慎重對待。
// Try to send 'value' to the target env 'envid'. // If srcva < UTOP, then also send page currently mapped at 'srcva', // so that receiver gets a duplicate mapping of the same page. // // The send fails with a return value of -E_IPC_NOT_RECV if the // target is not blocked, waiting for an IPC. // // The send also can fail for the other reasons listed below. // // Otherwise, the send succeeds, and the target's ipc fields are // updated as follows: // env_ipc_recving is set to 0 to block future sends; // env_ipc_from is set to the sending envid; // env_ipc_value is set to the 'value' parameter; // env_ipc_perm is set to 'perm' if a page was transferred, 0 otherwise. // The target environment is marked runnable again, returning 0 // from the paused sys_ipc_recv system call. (Hint: does the // sys_ipc_recv function ever actually return?) // // If the sender wants to send a page but the receiver isn't asking for one, // then no page mapping is transferred, but no error occurs. // The ipc only happens when no errors occur. // // Returns 0 on success, < 0 on error. // Errors are: // -E_BAD_ENV if environment envid doesn't currently exist. // (No need to check permissions.) // -E_IPC_NOT_RECV if envid is not currently blocked in sys_ipc_recv, // or another environment managed to send first. // -E_INVAL if srcva < UTOP but srcva is not page-aligned. // -E_INVAL if srcva < UTOP and perm is inappropriate // (see sys_page_alloc). // -E_INVAL if srcva < UTOP but srcva is not mapped in the caller's // address space. // -E_INVAL if (perm & PTE_W), but srcva is read-only in the // current environment's address space. // -E_NO_MEM if there's not enough memory to map srcva in envid's // address space.確實需要的東西比較多,簡略一點說就是如下幾點:
- env_ipc_recving is set to 0 to block future sends;
- env_ipc_from is set to the sending envid;
- env_ipc_value is set to the 'value' parameter;
- env_ipc_perm is set to 'perm' if a page was transferred
值得注意的地方在于這個函數的返回值,很多同學之前都是return ret,ret應當是判斷perm是否要使用的一個參量而已,如果要共享內存,則ret=1,如果沒有共享內存的話,則ret=0,僅此而已,所以在最后只要return 0即可,不需要有別的修飾。
總結
lab4其實還有很多地方沒有搞得特別清楚,也有很多地方沒有講到,可能隨之時間的積淀哪一天會突然有所感悟,哦原來是這樣!
哦,原來是這樣!
乾 2015/7/3
轉載于:https://www.cnblogs.com/SivilTaram/p/os_lab4.html
總結
以上是生活随笔為你收集整理的[操作系统实验lab4]实验报告的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信用卡提额度最快方法:提额成功率90%
- 下一篇: 还呗靠不靠谱?还呗是哪个公司的?