Linux內核剖析 之 進程地址空間(三)
本節主要講述缺頁異常處理程序和堆的管理等內容。
缺頁異常處理程序
觸發缺頁異常程序的兩種情況:
1. 由編程錯誤引起的異常(如訪問越界,地址不屬於進程地址空間)。
2. 地址屬於線性地址空間,但內核還未分配相應的物理頁,導致缺頁異常。
缺頁異常處理程序總體方案:
線性區描述符可以讓缺頁異常處理程序非常有效的完成它的工作。
do_page_fault()函數是80x86上的缺頁中斷服務程序,它把引起缺頁的線性地址和當前進程的線性區相比較,從而根據具體方案選擇適當的方法處理此異常。
標識符vmalloc_fault、good_area、do_sigbus、bad_area、no_context、survive、out_of_memory和bad_area_nosemaphore對應的代碼段對不同的缺頁異常進行不同的處理操作。
接收參數:
fastcall void do_page_fault(struct pt_regs *regs, unsigned long error_code)pt_regs結構的地址regs,該結構包含異常發生時的微處理器寄存器的值。
3位的error_code:
===>>error_code:
*第0位被清零,訪問一個不存在的頁——異常;第0位被置位,無效的訪問權限——異常。
*第1位被清零,讀訪問或者執行訪問——異常;第1位被置位,寫訪問——異常。
*第2位被清零,處理器內核態——異常;第2位被置位,處理器用戶態——異常。
執行步驟:
*讀取引起缺頁的線性地址。當異常發生時,CPU控製單元將此線性地址存放在cr2控製寄存器中。
__asm__("movl %%cr2,%0":"=r" (address)); if (regs->eflags & (X86_EFLAGS_IF|VM_MASK)) /*X86_EFLAGS_IF|VM_MASK)=0x00020200*/ local_irq_enable(); tsk = current;pt_regs結構指針regs指向異常發生前CPU中各寄存器內容的一份副本,這是由內核的中斷響應機製保存下來的“現場”,而error_code則進一步指明映射失敗的具體原因。如果缺頁發生之前或CPU運行在80x86模式時就打開了本地中斷,則使能local_irq_enable(),並將指向current進程描述符的指針保存在tsk局部變量中。
*接下來:
對此圖進行說明:
do_page_fault()首先檢查引起缺頁的線性地址是否在內核地址空間:
如果是,則當內核試圖訪問不存在的頁,跳轉執行非連續內存區地址訪問代碼,即vmalloc_fault標記處後的代碼,否則,執行bad_area_ nosemaphore標記處後的代碼。
如果不是,則引起缺頁的線性地址在用戶地址空間。此時,判斷缺頁是否發生中斷處理程序、可延遲函數、臨界區或內核線程中:
如果是,由於中斷處理程序等不使用小於TASK_SIZE的地址,故轉而執行bad_area_nosemaphore標識處代碼。
if (in_atomic() || !mm) goto bad_area_nosemaphore;如果沒有,即卻也沒有發生在中斷處理程序、可延遲函數、臨界區或者內核線程中,則函數檢查進程所擁有的線性區以決定引起缺頁的線性地址是否包含在進程的地址空間中,為此必須獲得進程的mmap_sem讀寫信號量。
if (!down_read_trylock(&mm->mmap_sem)) { if ((error_code & 4) == 0 && !search_exception_tables(regs->eip)) goto bad_area_nosemaphore; down_read(&mm->mmap_sem); }當函數獲取了mmap_sem信號量,do_page_fault()開始搜索錯誤線性地址所在的線性區,並根據vma的值,跳轉到相應標誌代碼段。
vma = find_vma(mm, address); if (!vma) goto bad_area; if (vma->vm_start <= address) goto good_area; if (!(vma->vm_flags & VM_GROWSDOWN)) goto bad_area; if (error_code & 4) { /* accessing the stack below %esp is always a bug. * The "+ 32" is there due to some instructions (like * pusha) doing post-decrement on the stack and that * doesn't show up until later.. */ if (address + 32 < regs->esp) goto bad_area; } if (expand_stack(vma, address)) goto bad_area;
處理地址空間以外的錯誤地址
如果address(引起缺頁的線性地址)不屬於進程的地址空間,則do_page_fault()函數執行bad_area標記處的語句。
/* Something tried to access memory that isn't in our memory map.. * Fix it, but check if it's kernel or user first.. */ bad_area: up_read(&mm->mmap_sem); /*退出臨界區*/ bad_area_nosemaphore: /* User mode accesses just cause a SIGSEGV */ if (error_code & 4) { /*用戶態*/ /* Valid to do another page fault here because this one came from user space.*/ if (is_prefetch(regs, address, error_code)) return; tsk->thread.cr2 = address; /* Kernel addresses are always protection faults */ tsk->thread.error_code = error_code | (address >= TASK_SIZE); tsk->thread.trap_no = 14; info.si_signo = SIGSEGV; info.si_errno = 0; /* info.si_code has been set above */ info.si_addr = (void __user *)address; force_sig_info(SIGSEGV, &info, tsk); return; } no_context: /*內核態*/ if (fixup_exception(regs)) return; if (is_prefetch(regs, address, error_code)) return;如果異常發生在用戶態,則發送一個SIGSEGV信號給current進程並結束函數;
其中,force_sig_info()確信進程不忽略或阻塞SIGSEGV信號,並通過info局部變量傳遞附加信息的同時把該信號發送給用戶態進程。
如果異常發生在內核態(error_code的第二位被清零),有兩種可選的情況(在no_context代碼段實現):
*異常的引起是由於把某個線性地址作為係統調用的參數傳給內核;
*異常是因一個真正的內核缺陷引起的。
對於第一種情況,代碼跳轉到一段“修正代碼”處,這段代碼的典型操作就是向當前進程發送SIGSEGV信號,或用一個適當的出錯碼終止係統調用處理程序。
對於第二種情況,函數把CPU寄存器和內核態堆棧的全部轉儲打印到控製台,並輸出到係統消息緩衝區,然後調用do_exit()殺死當前進程。——內核漏洞“Kernel Oops”。
處理地址空間以內的錯誤地址
如果address地址屬於進程的地址空間,則do_page_fault()轉到good_area標記處執行程序:
/*Ok, we have a good vm_area for this memory access, so we can handle it.. */ good_area: info.si_code = SEGV_ACCERR; write = 0; switch (error_code & 3) { default: /* 寫引起,在內存中 */ /* fall through :失敗(下麵的情況)*/ case 2: /* 寫引起,不在內存中 */ if (!(vma->vm_flags & VM_WRITE)) /*線性區不可寫*/ goto bad_area; write++; /*線性區可寫*/ break; case 1: /* 讀或</span>執行訪問引起,在內存中 */ goto bad_area; case 0: /* 讀或執行訪問引起,不在內存中 */ if (!(vma->vm_flags & (VM_READ | VM_EXEC))) goto bad_area; } survive: switch (handle_mm_fault(mm, vma, address, write)) { case VM_FAULT_MINOR:/*次缺頁*/ tsk->min_flt++; break; case VM_FAULT_MAJOR:/*主缺頁*/ tsk->maj_flt++; break; case VM_FAULT_SIGBUS:/*其他任何錯誤*/ goto do_sigbus; case VM_FAULT_OOM:/*沒有足夠的內存*/ goto out_of_memory; default: BUG(); } if (regs->eflags & VM_MASK) { unsigned long bit = (address - 0xA0000) >> PAGE_SHIFT; if (bit < 32) tsk->thread.screen_bitmap |= 1 << bit; } up_read(&mm->mmap_sem); return;對error_code&3===>>>
case 2:如果異常由寫訪問引起,函數檢查這個線性區是否可寫。如果不可寫(!(vma->vm_flags & VM_WRITE)),跳到bad_area代碼處;如果可寫,把write局部變量置為1.
case 1 and case 0:如果異常是由讀或執行訪問引起,函數檢查這一頁是否已經存在於RAM中。在存在的情況下,異常發生是由於進程試圖訪問用戶態下的一個有特權的頁框,因此函數跳轉到bad_area代碼處。在不存在的情況下,函數還將檢查這個線性區是否可讀或可執行。
default and case2(write==1):如果這個線性區的訪問權限於引起異常的訪問類型相匹配,調用handle_mm_fault()函數分配一個新的頁框(survive代碼段):
關鍵:handle_mm_fault()函數
/* By the time we get here, we already hold the mm semaphore */ int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma, unsigned long address, int write_access) { pgd_t *pgd; pud_t *pud; pmd_t *pmd; pte_t *pte; __set_current_state(TASK_RUNNING); inc_page_state(pgfault); if (is_vm_hugetlb_page(vma)) return VM_FAULT_SIGBUS; /* mapping truncation does this. */ /* We need the page table lock to synchronize with kswapd * and the SMP-safe atomic PTE updates. */ pgd = pgd_offset(mm, address); spin_lock(&mm->page_table_lock); pud = pud_alloc(mm, pgd, address); if (!pud) goto oom; pmd = pmd_alloc(mm, pud, address); if (!pmd) goto oom; pte = pte_alloc_map(mm, pmd, address); if (!pte) goto oom; return handle_pte_fault(mm, vma, address, write_access, pte, pmd); oom: spin_unlock(&mm->page_table_lock); return VM_FAULT_OOM; }參數:異常發生時正在CPU上運行的進程的內存描述符指針mm,引起異常的線性地址所在線性區的描述符指針vma,引起異常的線性地址address,write_access(如果tsk試圖向address寫則置位,如果tsk試圖向address讀或執行則清零)。
步驟:
*函數首先檢查發生異常的原因,然後檢查用來映射address的頁目錄和頁表是否存在,再執行分配頁目錄和頁表的任務;
*handle_pte_fault()函數檢查address地址對應的頁表項,並決定如何為進程分配一個新頁框:
static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct * vma, unsigned long address,int write_access, pte_t *pte, pmd_t *pmd) { pte_t entry; /*請求調頁*/ entry = *pte; if (!pte_present(entry)) { /* * If it truly wasn't present, we know that kswapd * and the PTE updates will not touch it later. So * drop the lock. */ if (pte_none(entry)) return do_no_page(mm, vma, address, write_access, pte, pmd); if (pte_file(entry)) return do_file_page(mm, vma, address, write_access, pte, pmd); return do_swap_page(mm, vma, address, pte, pmd, entry, write_access); } /*寫時複製*/ if (write_access) { if (!pte_write(entry)) return do_wp_page(mm, vma, address, pte, pmd, entry); entry = pte_mkdirty(entry); } entry = pte_mkyoung(entry); ptep_set_access_flags(vma, address, pte, entry, write_access); update_mmu_cache(vma, address, entry); pte_unmap(pte); spin_unlock(&mm->page_table_lock); return VM_FAULT_MINOR;===>>>
#如果訪問的頁在內存中不存在,也就是說,這個頁還沒有被存放在任何一個頁框中,則內核分配一個新的頁框並適當初始化。這種技術稱為請求調頁(Demand Paging);
#如果被訪問的頁存在但是標記為隻讀,也就是說,它已經被存放在一個頁框中,則內核分配一個新的頁框,並把舊的頁框的數據拷貝到新頁框來初始化它的內容。這種技術稱為寫時複製(Copy On Write,COW)。
*如果線性區的訪問權限與引起異常的訪問類型相匹配,handle_mm_fault()函數分配一個新的頁框:
當handle_mm_fault()成功地給進程分配一個頁框,則返回VM_FAULT_MINOR或VM_FAULT_MAJOR。值VM_FAULT_MINOR表示在沒有阻塞當前進程的情況下處理了缺頁,這種缺頁叫做次缺頁(minor fault)。值VM_FAULT_MAJOR表示缺頁迫使當前進程睡眠,阻塞當前進程的缺頁叫做主缺頁(major fault)。
當沒有足夠內存時,函數返回VM_FAULT_OOM,此時函數不分配新的頁框,內核通常殺死當前進程。不過,如果當前進程是init進程,則隻是把它放在運行隊列的末尾並調用調度程序,一旦init恢複執行,則handle_mm_fault又執行:
case VM_FAULT_OOM: goto out_of_memory;out_of_memory標記處代碼(過程如上所述):
/* We ran out of memory, or some other thing happened to us that made * us unable to handle the page fault gracefully. */ out_of_memory: up_read(&mm->mmap_sem); if (tsk->pid == 1) { yield(); down_read(&mm->mmap_sem); goto survive; } printk("VM: killing process %s\n", tsk->comm); if (error_code & 4) do_exit(SIGKILL); goto no_context;
請求調頁
請求調頁指的是一種動態內存分配技術,它把頁框的分配推遲到不能再推遲為止,也就是說,一直推遲到進程要訪問的頁不再內存中時為止,由此引起缺頁異常。
請求調頁技術的動機是:請求調頁能增加係統中的空閑頁框的平均數,從而更好地利用空閑內存,從總體上能使係統有更大的吞吐量。
付出的代價是係統額外的開銷:由請求調頁所引發的每個“缺頁”異常必須由內核處理。
有關請求調頁的代碼:
entry = *pte; /* 如果頁不在主存中*/ if (!pte_present(entry)) { /* 頁表項內容為0,表明進程未訪問過該頁 */ if (pte_none(entry)) return do_no_page(mm, vma, address, write_access, pte, pmd); /* 屬於非線性文件映射且已被換出 */ if (pte_file(entry)) return do_file_page(mm, vma, address, write_access, pte, pmd); /* 頁不在主存中,但是頁表項保存了相關信息, * 則表明該頁被內核換出,則要進行換入操作 */ return do_swap_page(mm, vma, address, pte, pmd, entry, write_access); }pte_present()宏指明entry頁是否在主存中。如果entry頁不在主存中,其原因或是進程從未訪問過該頁,或是內核已經回收了相應的頁框。
在這兩種情況下,缺頁處理程序必須為進程分配新的頁框。不過,如何初始化這個頁框有三種特殊情況:
*entry頁從未被進程訪問到且沒有映射到磁盤文件:
pte_none()宏==>do_no_page()函數;
*entry頁屬於非線性磁盤文件的映射:
pte_file()宏==>do_file_page()函數;
*entry頁已經被進程訪問過,但是其內容被臨時保存在磁盤中(present=dirty=0):
==> do_swap_page()函數。
handle_pte_fault()函數通過檢查address對應頁表項的標誌能夠區分這三種情況,並根據不同標誌來進行不同的函數處理。
===>>>匿名頁和映射頁:
在Linux虛擬內存中,如果頁對應的vma映射的是文件,則稱為映射頁;如果不是映射的文件,則稱為匿名頁。兩者最大的區別體現在頁和vma的組織上,因為在頁框回收處理時要通過頁來逆向搜索映射了該頁的vma。對於匿名頁的逆映射,vma都是通過vma結構體中的vma_anon_node(鏈表節點)和anon_vma(鏈表頭)組織起來,再把該鏈表頭的信息保存在頁描述符中;而映射頁和 vma的組織是通過vma中的優先樹節點和頁描述符中的mapping->i_mmap優先樹樹根進行組織的。
寫時複製
原始的進程創建:
當發出fork()係統調用時,內核原樣將父進程的整個地址空間複製一份給子進程。這種方式非常耗時:
1. 為子進程的地址空間分配頁框
2. 為子進程的頁表分配頁框
3. 初始化子進程的頁表
4. 將父進程的頁複製到子進程相應的頁
缺點:耗費CPU周期。
現在linux係統采用一種寫時複製的技術;
原理:父子進程共享頁框而不是複製;共享意味著不能被修改,父子進程無論何時試圖寫頁框,就會產生異常;這時內核將這個物理頁複製到一個新的頁框,標記為可寫。
頁描述符的_count字段用於跟蹤共享相應頁框的進程數目。隻要進程釋放一個頁框或者在它上麵執行寫時複製,它的_count字段就減小;隻有當_count變為-1時,此頁框才被釋放。
寫時複製相關代碼:
if(pte_present(entry)) { if (write_access) { if (!pte_write(entry)) return do_wp_page(mm, vma, address, pte, pmd, entry); entry = pte_mkdirty(entry); } entry = pte_mkyoung(entry); ptep_set_access_flags(vma, address, pte, entry, write_access); update_mmu_cache(vma, address, entry); pte_unmap(pte); spin_unlock(&mm->page_table_lock); return VM_FAULT_MINOR; }核心函數:do_wp_page()函數
該函數首先獲取與缺頁異常相關的頁框描述符。接下來,函數確定頁的複製是否真正必要。具體說來,函數讀取頁描述符的_count字段,如果它等於0(隻有一個所有者),寫時複製就不必進行。如果多個進程通過寫時複製共享頁框,那麼函數就把舊頁框的內容複製到新分配的頁框中(copy_page()宏)。然後,新頁框的物理地址最終被寫進頁表項,且使對應的TLB寄存器無效。同時,lru_cache_add_active()函數把新頁框插入到與交換相關的數據結構中。最後,do_wp_page()把old_page的使用計數器減少兩次(pte_unmap()函數),第一次減少是取消複製頁框內容之前進行的安全性增加;第二次減少是反映當前進程不再擁有該頁框的事實。
處理非連續內存區訪問
異常發生在內核態且產生缺頁的線性地址大於TASK_SIZE。此時,do_page_fault()檢查相應的主內核頁全表項:
vmalloc_fault: { int index = pgd_index(address); unsigned long pgd_paddr; pgd_t *pgd, *pgd_k; pud_t *pud, *pud_k; pmd_t *pmd, *pmd_k; pte_t *pte_k; asm("movl %%cr3,%0":"=r" (pgd_paddr)); pgd = index + (pgd_t *)__va(pgd_paddr); pgd_k = init_mm.pgd + index; if (!pgd_present(*pgd_k)) goto no_context; pud = pud_offset(pgd, address); pud_k = pud_offset(pgd_k, address); if (!pud_present(*pud_k)) goto no_context; pmd = pmd_offset(pud, address); pmd_k = pmd_offset(pud_k, address); if (!pmd_present(*pmd_k)) goto no_context; set_pmd(pmd, *pmd_k); pte_k = pte_offset_kernel(pmd_k, address); if (!pte_present(*pte_k)) goto no_context; return; }do_page_fault()把存放在cr3寄存器中的當前進程頁全局目錄的物理地址賦給局部變量pgd_paddr,把與pgd_paddr對應的線性地址賦給局部變量pgd,並且把主內核頁全局目錄的線性地址賦給pgd_k局部變量。
如果產生缺頁的線性地址所對應的主內核頁全局目錄項為空,則函數跳到標號為no_context的代碼處。否則,函數檢查與錯誤線性地址相對應的主內核頁上級目錄項和主內核頁中間目錄項。如果它們中間有一個為空,就再次跳轉到no_context處。否則,就把主目錄項複製到進程頁中間目錄的相應項中。隨後對主頁表項重複上述操作。
創建和刪除進程的地址空間
進程獲得一個新線性區的六種典型情況:
程序執行
exec()函數
缺頁異常處理程序
內存映射
IPC共享內存
malloc()函數
fork()係統調用要求為子進程創建一個完整的新地址空間。相反,當進程結束是,內核撤銷它的地址空間。
創建進程的地址空間
vfork()/fork()/clone()係統調用=====>>>:
copy_mm()函數:
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk) { struct mm_struct * mm, *oldmm; int retval; tsk->min_flt = tsk->maj_flt = 0; tsk->nvcsw = tsk->nivcsw = 0; tsk->mm = NULL; tsk->active_mm = NULL; oldmm = current->mm; if (!oldmm) return 0; if (clone_flags & CLONE_VM) { atomic_inc(&oldmm->mm_users); mm = oldmm; /* * There are cases where the PTL is held to ensure no * new threads start up in user mode using an mm, which * allows optimizing out ipis; the tlb_gather_mmu code * is an example. */ spin_unlock_wait(&oldmm->page_table_lock); goto good_mm; } retval = -ENOMEM; mm = allocate_mm(); if (!mm) goto fail_nomem; /* Copy the current MM stuff.. */ memcpy(mm, oldmm, sizeof(*mm)); if (!mm_init(mm)) goto fail_nomem; if (init_new_context(tsk,mm)) goto fail_nocontext; retval = dup_mmap(mm, oldmm); if (retval) goto free_pt; mm->hiwater_rss = mm->rss; mm->hiwater_vm = mm->total_vm; good_mm: tsk->mm = mm; tsk->active_mm = mm; return 0; free_pt: mmput(mm); fail_nomem: return retval; fail_nocontext: /* * If init_new_context() failed, we cannot use mmput() to free the mm * because it calls destroy_context() */ mm_free_pgd(mm); free_mm(mm); return retval; }如果flag參數的CLONE_VM標誌被置位,copy_mm()函數把父進程(current)地址空間給子進程。
如果沒有設置CLONE_VM標誌,copy_mm()函數創建新的地址空間,分配一個新的內存描述符,複製父進程的mm內容到新的進程描述符中。
然後調用函數init_new_context()和init_mm()函數進行初始化工作;
最後調用dup_mmap()函數複製父進程的線性區和頁表。
刪除進程地址空間
當進程結束時,內核調用exit_mm()釋放進程的地址空間。
void exit_mm(struct task_struct * tsk) { struct mm_struct *mm = tsk->mm; mm_release(tsk, mm); if (!mm) return; /*判斷是否為內核線程*/ /* Serialize with any possible pending coredump. * We must hold mmap_sem around checking core_waiters * and clearing tsk->mm. The core-inducing thread * will increment core_waiters for each thread in the * group with ->mm != NULL. */ down_read(&mm->mmap_sem); if (mm->core_waiters) { up_read(&mm->mmap_sem); down_write(&mm->mmap_sem); if (!--mm->core_waiters) complete(mm->core_startup_done); up_write(&mm->mmap_sem); wait_for_completion(&mm->core_done); down_read(&mm->mmap_sem); } atomic_inc(&mm->mm_count); if (mm != tsk->active_mm) BUG(); /* more a memory barrier than a real lock */ task_lock(tsk); tsk->mm = NULL; up_read(&mm->mmap_sem); enter_lazy_tlb(mm, current); task_unlock(tsk); mmput(mm); }mm_release()函數喚醒tsk->vfork_done補充原語上睡眠的任一進程。
如果正在被終止的進程不是內核線程,exit_mm()函數釋放內存描述符和所有相關的數據結構。首先,它檢查mm->core_waiters標誌是否置位:如果是,進程就把內存的所有內容卸載到一個轉儲文件中。為了避免轉儲文件的混亂,函數利用mm->core_done和mm->core_startup_done補充原語使共享同一內存描述符mm的輕量級進程的執行串行化。
接下來,函數遞增內存描述符的主使用計數器,重新設置進程描述符的mm字段,並使處理器處於懶惰TLB模式。
最後,調用mmput()函數釋放局部描述符表、線性區描述符和頁表。由於exit_mm()函數已經遞增了主使用計數器,所以並不釋放內存描述符本身。當要把正在被終止的進程從本地CPU撤銷時,將由finish_task_switch()函數釋放內存描述符。
堆的管理
每個進程都有一個特殊的線性區,即堆(heap)。用於滿足進程的動態內存請求。內存描述符中的start_brk與brk字段分別表示堆的起始地址和結束地址。
操作函數 |
說明 |
malloc(size) |
請求size個字節的動態內存,如果成功,則返回第一個字節的線性地址 |
calloc(n, size) |
請求n個大小為size的元素的內存,如果成功,返回第一個元素的線性地址 |
realloc(ptr,size) |
改變由前麵malloc\calloc分配的內存大小 |
free(addr) |
釋放由malloc、calloc分配的起始地址為addr的線性區 |
brk(addr) |
直接修改堆的大小。Addr參數指定current->mm->brk的新值,返回值是線性區新的結束地址。 |
sbrk(incr) |
類似brk(),不過其中的incr參數指定是增加還是減少以字節為單位的堆大小 |
至此,進程地址空間一章節結束。
遺留問題:
1、在寫時複製時,對於父進程和子進程,其分配頁框的具體機製如何?
2、每個線性區是否都會劃分代碼段、數據段、堆棧等線性地址空間?
3、線性區的分配是動態分配還是靜態分配,即線性區的分配會不會隨著使用內存的增加而動態添加分配?還是說程序執行的時候,係統會提前分配好線性區?
4、前麵提到過一句在寫時複製時,do_wp_page()把old_page的使用計數器減少兩次(pte_unmap()函數),第一次減少是取消複製頁框內容之前進行的安全性增加;第二次減少是反映當前進程不再擁有該頁框的事實。這裏,為什麼需要進行安全性增加?
最後更新:2017-04-03 05:39:42