本週學習目標:①理解虛擬存儲器的概念和做用;②理解地址翻譯的概念;③理解存儲器映射;④掌握動態存儲器分配的方法;⑤瞭解C語言中與存儲器有關的錯誤。html
『一 虛擬存儲器的概念和做用』linux
虛擬存儲器git
地址空間程序員
虛擬存儲器做爲緩存的工具編程
頁表數組
『二 地址翻譯』緩存
『三 存儲器映射』安全
『四 動態存儲器分配的方法』數據結構
『五 C語言中與存儲器有關的錯誤』多線程
『問題一』:教材P565提到,能夠利用Linux的getrusage函數監測缺頁的數量以及許多其餘的信息,具體如何使用這個函數呢?
『問題一解決』:
首先使用「man -k getrusage」獲取函數的初步信息:
得知getrusage在手冊的第二節,接下來使用「man 2 getrusage」查看函數基本格式和需包含的頭文件:
getrusage函數有兩個參數。第一個參數能夠設置爲RUSAGE_SELF或者RUSAGE_CHILDREN。若是設置成 RUSAGE_SELF,那麼將會以當前進程的相關信息來填充rusage(數據)結構。反之,若是設置成RUSAGE_CHILDREN,那麼 rusage結構中的數據都將是當前進程的子進程的信息。
咱們還注意到,裏面介紹了一個很是重要的結構體:struct rusage,其中包含的內容以下所示
各個字段的解釋以下:
執行成功返回0,發生錯誤返回-1,同時設置errno的值。
下面使用一個簡單的程序測試函數的功能:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/time.h> #include <sys/resource.h> int main(int argc, char **argv) { struct rusage buf; if(argc == 2) { system(argv[1]); }else { fprintf(stderr,"./getrusage \"ls -l > /dev/null\"\n"); exit(0); } int err = getrusage(RUSAGE_CHILDREN, &buf); //int err = getrusage(RUSAGE_SELF, &buf); printf("ERR=%d\n", err); printf("%20s:%ld/%ld\t%s\n", "ru_utime", buf.ru_utime.tv_sec, buf.ru_utime.tv_usec, "user time used (secs/usecs)"); printf("%20s:%ld/%ld\t%s\n", "ru_stime", buf.ru_stime.tv_sec, buf.ru_stime.tv_usec, "system time used (secs/usecs)"); printf("%20s:%-10ld\t%s\n", "ru_maxrss", buf.ru_maxrss, "maximum resident set size"); printf("%20s:%-10ld\t%s\n", "ru_ixrss", buf.ru_ixrss, "integral shared memory size"); printf("%20s:%-10ld\t%s\n", "ru_idrss", buf.ru_idrss, "integral unshared data size"); printf("%20s:%-10ld\t%s\n", "ru_isrss", buf.ru_isrss, "integral unshared data stack size"); printf("%20s:%-10ld\t%s\n", "ru_minflt", buf.ru_minflt, "page reclaims"); printf("%20s:%-10ld\t%s\n", "ru_majflt", buf.ru_majflt, "page faults"); printf("%20s:%-10ld\t%s\n", "ru_nswap", buf.ru_nswap, "swaps"); printf("%20s:%-10ld\t%s\n", "ru_inblock", buf.ru_inblock, "block input operations"); printf("%20s:%-10ld\t%s\n", "ru_oublock", buf.ru_oublock, "block output operations"); printf("%20s:%-10ld\t%s\n", "ru_msgsnd", buf.ru_msgsnd, "messages sent"); printf("%20s:%-10ld\t%s\n", "ru_msgrcv", buf.ru_msgrcv, "messages received"); printf("%20s:%-10ld\t%s\n", "ru_nsignals", buf.ru_nsignals, "signals received"); printf("%20s:%-10ld\t%s\n", "ru_nvcsw", buf.ru_nvcsw, "voluntary context switches"); printf("%20s:%-10ld\t%s\n", "ru_nivcsw", buf.ru_nivcsw, "involuntary context switches"); exit(0); }
測試結果以下:
『問題二』:教材P567提到,每一個PTE中添加了三個許可位,其中SUP位表示進程是否必須運行在內核(超級用戶)模式下才能訪問該頁。什麼是內核模式呢?與之相對應的是什麼模式呢?
『問題二解決』:
查閱資料瞭解到,Linux操做系統使用了雙模式(內核模式和用戶模式),能夠有效地實現時間共享。
在Linux機器上,CPU要麼處於受信任的內核模式,要麼處於受限制的用戶模式。除了內核自己處於內核模式之外,全部的用戶進程都運行在用戶模式之中。
內核模式的代碼能夠無限制地訪問全部處理器指令集以及所有內存和I/O空間。若是用戶模式的進程要享有此特權,它必須經過系統調用向設備驅動程序或其餘內核模式的代碼發出請求。另外,用戶模式的代碼容許發生缺頁,而內核模式的代碼則不容許。
Linux簡化了分段機制,使得虛擬地址與線性地址老是一致,所以,Linux的虛擬地址空間也爲0~ 4G。Linux內核將這4G字節的空間分爲兩部分。將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲「內核空間」。而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲「用戶空間「)。由於每一個進程能夠經過系統調用進入內核,所以,Linux內核由系統內的全部進程共享。因而,從具體進程的角度來看,每一個進程能夠擁有4G字節的虛擬空間。
當一個任務(進程)執行系統調用而陷入內核代碼中執行時,咱們就稱進程處於內核運行態(或簡稱爲內核態)。此時處理器處於特權級最高的(0級)內核代碼中執行。當進程處於內核態時,執行的內核代碼會使用當前進程的內核棧。每一個進程都有本身的內核棧。
當進程在執行用戶本身的代碼時,則稱其處於用戶運行態(用戶態)。即此時處理器在特權級最低的(3級)用戶代碼中運行。當正在執行用戶程序而忽然被中斷程序中斷時,此時用戶程序也能夠象徵性地稱爲處於進程的內核態。由於中斷處理程序將使用當前進程的內核棧。這與處於內核態的進程的狀態有些相似。
處理器總處於如下狀態中的一種:
一、內核態,運行於進程上下文,內核表明進程運行於內核空間;
二、內核態,運行於中斷上下文,內核表明硬件運行於內核空間;
三、用戶態,運行於用戶空間。
用戶空間的應用程序,經過系統調用,進入內核空間。這個時候用戶空間的進程要傳遞不少變量、參數的值給內核,內核態運行的時候也要保存用戶進程的一些寄存器值、變量等。所謂的「進程上下文」,能夠看做是用戶進程傳遞給內核的這些參數以及內核要保存的那一整套的變量和寄存器值和當時的環境等。
硬件經過觸發信號,致使內核調用中斷處理程序,進入內核空間。這個過程當中,硬件的一些變量和參數也要傳遞給內核,內核經過這些參數進行中斷處理。所謂的「中斷上下文」,其實也能夠看做就是硬件傳遞過來的這些參數和內核須要保存的一些其餘環境(主要是當前被打斷執行的進程環境)。
『問題三』:如何理解malloc/brk/mmap函數?
『問題三解決』:
結合以前學習過的fork相關知識,參考如下代碼:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/wait.h> #include<signal.h> #include<sys/mman.h> int ga = 1; int main() { int a = 10; int *pa = malloc(sizeof(int)); *pa = 100; int *ma = mmap(0,4,PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED/*MAP_PRIVATE*/,0,0); *ma = 1000; int *spa = sbrk(4); *spa = 10000; if(fork()) { printf("parent:ga = %d\n",ga); printf("parent:a = %d\n",a); printf("parent:*pa = %d\n",*pa); printf("parent:*ma = %d\n",*ma); printf("parent:*spa = %d\n",*spa); a = 7; ga = 77; *pa = 777; *ma = 7777; *spa = 77777; } else { sleep(5); printf("\nchild:ga = %d\n",ga); printf("child:a = %d\n",a); printf("child:*pa = %d\n",*pa); printf("child:*ma = %d\n",*ma); printf("child:*spa = %d\n",*spa); } return 0; }
運行結果以下:
咱們知道,經過fork建立的子進程克隆父進程的內存區域(全局區、棧區、堆區、代碼區),但內存區域經過映射以後指向不一樣的物理空間,因此,儘管子進程克隆了父進程的內存區域,但他們的實際內存是獨立. 不能相互訪問。能夠看出,malloc/brk/mmap對fork建立的子進程的操做並不徹底相同。
brk系統調用,可讓進程的堆指針增加必定的大小,邏輯上消耗掉一塊本進程的虛擬地址區間,malloc向OS獲取的內存大小比較小時,將直接經過brk調用獲取虛擬地址,結果是將本進程的brk指針推高。
mmap系統調用,可讓進程的虛擬地址區間裏切分出一塊指定大小的虛擬地址區間vma_struct,並返回給用戶態進程,被mmap映射返回的虛擬地址,邏輯上被消耗了,直到用戶進程調用munmap,纔回收回來。malloc向系統獲取比較大的內存時,會經過mmap直接映射一塊虛擬地址區間。mmap系統調用用處很是多,好比一個進程的全部動態庫文件.so的加載,都須要經過mmap系統調用映射指定大小的虛擬地址區間,而後將.so代碼動態映射到這些區域,以供進程其餘部分代碼訪問;另外,多進程通信,也可使用mmap,這塊另開文章詳解。
不管是brk仍是mmap返回的都是虛擬地址,在第一次訪問這塊地址的時候,會觸發缺頁異常,而後內核爲這塊虛擬地址申請並映射物理頁框,創建頁表映射關係,後續對該區間虛擬地址的訪問,經過頁表獲取物理地址,而後就能夠在物理內存上讀寫了。
malloc是 libc實現的庫函數,主要實現了一套內存管理機制,當其管理的內存不夠時,經過brk/mmap等系統調用向內核申請進程的虛擬地址區間,若是其維護的內存能知足malloc調用,則直接返回,free時會將地址塊返回空閒鏈表。
malloc(size) 的時候,這個函數會多分配一塊空間,用於保存size變量,free的時候,直接經過指針前移必定大小,就能夠獲取malloc時保存的size變量,從而free只須要一個指針做爲參數就能夠了calloc 庫函數至關於 malloc + memset(0)
『問題四』:mmap() vs read()/write()?
『問題四解決』:
系統調用mmap()能夠將某文件映射至內存(進程空間),如此能夠把對文件的操做轉爲對內存的操做,以此避免更多的lseek()與read()、write()操做,這點對於大文件或者頻繁訪問的文件而言尤爲受益。但有一點必須清楚:mmap的addr與offset必須對齊一個內存頁面大小的邊界,即內存映射每每是頁面大小的整數倍,不然maaped_file_size%page_size內存空間將被閒置浪費。
如下代碼分別使用mmap與read/write兩種方法,將test.txt文件中的字符轉換成大寫:
/* * @file: t_mmap.c */ #include <stdio.h> #include <ctype.h> #include <sys/mman.h> /*mmap munmap*/ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd; char *buf; off_t len; struct stat sb; char *fname = "test.txt"; fd = open(fname, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); if (fd == -1) { perror("open"); return 1; } if (fstat(fd, &sb) == -1) { perror("fstat"); return 1; } buf = mmap(0, sb.st_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if (buf == MAP_FAILED) { perror("mmap"); return 1; } if (close(fd) == -1) { perror("close"); return 1; } for (len = 0; len < sb.st_size; ++len) { buf[len] = toupper(buf[len]); /*putchar(buf[len]);*/ } if (munmap(buf, sb.st_size) == -1) { perror("munmap"); return 1; } return 0; }
使用strace運行程序:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <ctype.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd, len; char *buf; char *fname = "/tmp/file_mmap"; ssize_t ret; struct stat sb; fd = open(fname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR); if (fd == -1) { perror("open"); return 1; } if (fstat(fd, &sb) == -1) { perror("stat"); return 1; } buf = malloc(sb.st_size); if (buf == NULL) { perror("malloc"); return 1; } ret = read(fd, buf, sb.st_size); for (len = 0; len < sb.st_size; ++len) { buf[len] = toupper(buf[len]); /*putchar(buf[len]);*/ } lseek(fd, 0, SEEK_SET); ret = write(fd, buf, sb.st_size); if (ret == -1) { perror("error"); return 1; } if (close(fd) == -1) { perror("close"); return 1; } free(buf); return 0; }
使用strace運行程序:
能夠看出:read()/write()在頻繁訪問大文件時,須要調用多個lseek()來肯定位置。每次編輯read()/write(),在物理內存中的雙份數據。而mmap內存映射文件以後,操做內存便是操做文件,能夠省去很多系統內核調用(lseek, read, write)。
『問題五』:fork/寫時複製是如何使用內存空間的?
『問題五解決』:
咱們知道,一個進程在地址空間上的表現形式主要是:正文段,數據段,堆,棧。內核爲其分配相應的數據結構來表示它們,其看作是進程在地址空間的實體,也能夠想象爲靈魂。隨後內核會爲這四部分分配相應的載體,即真正的物理存儲,那麼這些物理存儲就是進程的真正實體。就像靈魂要附之於身體同樣。
有一個父進程P1,這是一個主體(有靈魂也身體)。如今在其虛擬地址空間(有相應的數據結構表示)上有:正文段,數據段,堆,棧這四個部分。相應的,內核要爲這四個部分分配各自的物理塊,即:正文段塊,數據段塊,堆塊,棧塊。
代碼行數(新增/累積) | 博客量(新增/累積) | 學習時間(新增/累積) | 重要成長 | |
---|---|---|---|---|
目標 | 5000行 | 20篇 | 400小時 | |
第一週 | 50/50 | 1/1 | 8/8 | 瞭解計算機系統、靜態連接與動態連接 |
第三週 | 451/501 | 2/3 | 27/35 | 深刻學習計算機算術運算的特性 |
第四周 | 503 / 1004 | 1/4 | 20/55 | 掌握程序崩潰處理、Linux系統編程等知識,利用所學知識優化myod,並實現head和tail命令 |
第五週 | 315 / 1319 | 3/7 | 29/84 | 掌握「進程」的概念,並學習應用相關函數;瞭解程序如何在機器上表示 |
第七週 | 264 / 1583 | 1/8 | 15/99 | 瞭解處理器的體系結構,學習多線程的概念 |
第九周 | 634 / 2217 | 2/10 | 10/109 | 學習存儲器系統的層次結構,以及改善程序性能的方法 |
第十一週 | 486 / 2703 | 2/12 | 23/132 | 理解虛擬內存的工做原理和特性 |