20155322 2017-2018-1《信息安全系統設計》第十一週學習總結

20155322 2017-2018-1《信息安全系統設計》第十一週學習總結

[博客目錄]


教材學習內容總結

本週學習的內容是虛擬內存html

虛擬存儲器的概念和做用

要理解虛擬存儲器,須要理解如下幾個問題:linux

  • 什麼是虛擬地址和物理地址:
    物理地址:主存被組織成一個由M個連續的字節大小的單元組成的數組。而每個字節有一個對應的地址,這樣的地址就被稱做是物理地址。
    虛擬內存:針對物理地址的直接映射的許多弊端,計算機的設計中就採起了一個虛擬化設計,就是虛擬內存。CPU經過發出虛擬地址,虛擬地址再經過MMU翻譯成物理地址,最後得到數據,具體的操做以下所示:

    簡而言之,在每個進程開始建立的時候,都會分配一個虛擬存儲器(就是一段虛擬地址)而後經過虛擬地址和物理地址的映射來獲取真實數據,這樣進程就不會直接接觸到物理地址,甚至不知道本身調用的那塊物理地址的數據。
  • 虛擬存儲器是硬件異常、硬件地址翻譯、主存、磁盤文件和內核軟件的完美交互。
  • 虛擬存儲器的緩存做用
    針對於虛擬存儲器的緩存做用能夠用下圖所示:

    虛擬存儲器中的塊分爲:未分配的,緩存的,未緩存的。有效和無效經過一個valid bit(有效位)來進行判斷
    • 未分配的:顧名思義,這一塊的虛擬存儲器不映射於任何塊。
    • 緩存的:這一塊的虛擬存儲器映射於已經存在於DRAM中的物理頁。
    • 未緩存的:這一塊的虛擬存儲器映射於存在於磁盤中的虛擬頁。(也就是要使用就要把磁盤中的虛擬頁替換到DRAM中的物理頁,會發生Page Fault )
  • 虛擬存儲器的其它做用:

1.簡化共享:利用虛擬地址來映射物理地址,使得可讓多個進程的不一樣虛擬地址映射同一塊物理地址,好比相似於printf,這一類經常使用的庫,不會把printf的代碼拷貝到每個進程,而是讓不一樣進程都使用同一塊printf.。git

  1. 虛擬存儲器做爲存儲器保護的工具,在虛擬存儲器裏面能夠設計該PTE是可讀,可寫,仍是可執行的。若是一旦出現只讀的PTE被寫入了,CPU就會發送出現segmentation fault(段錯誤)但並不會影響到實際存放數據的物理內存。

返回目錄程序員

地址翻譯的概念

要理解地址翻譯,須要理解如下幾個問題:算法

  • 地址空間的概念
    首先,對於32位的計算機,每個地址所對應的數據空間是32位,也就是四個字節。那麼若是一個地址能夠用32位表示,那麼對於這32位地址的全部可能就是:232種可能,那麼32位地址的地址空間就爲232。下面所說的,虛擬地址的地址空間和物理地址的地址空間也就是取決於虛擬地址和物理地址的位數,若是位數分別爲M,N,那麼地址空間也爲:2^M^ 和 2^N^.
  • 地址分頁的概念
    對於一整塊連續的內存,直接連續使用也是不太符合實際的。因而,就有分頁的概念。將1024個地址分紅一頁,經過訪問頁來訪問數據。那麼有了頁就要有如何尋找頁的概念了。咱們經過每一頁的首地址做爲頁入口,即(PTE)來檢索頁。那麼,對於這些PTE,咱們也須要一個專門的數據結構來進行管理,這樣的數據結構就是頁表(page table)。
  • 地址翻譯目的
    地址翻譯的目的是經過MMU將虛擬地址翻譯成物理地址。

下面的轉化圖將說明虛擬地址到物理地址的一個過程
編程

返回目錄api

存儲器映射

要理解存儲器映射,須要理解如下幾個問題:數組

  • 什麼是存儲器映射?
    存儲器映射又叫內存映射,所謂的內存映射就是把物理內存映射到進程的地址空間以內,這些應用程序就能夠直接使用輸入輸出的地址空間,從而提升讀寫的效率。
  • Linux如何實現內存映射?
    Linux提供了mmap()函數,用來映射物理內存。在驅動程序中,應用程序以設備文件爲對象,調用mmap()函數,內核進行內存映射的準備工做,生成vm_area_struct結構體,而後調用設備驅動程序中定義的mmap函數
  • 咱們來了解一下mmap()函數
    • void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset);
      將一個文件或者其它對象映射進內存。文件被映射到多個頁上,若是文件的大小不是全部頁的大小之和,最後一個頁不被使用的空間將會清零。
    • int munmap(void *start, size_t length);
      執行相反的操做,刪除特定地址區域的對象映射。
    • 頭文件:#include <sys/mman.h>
    • 成功執行時,mmap()返回被映射區的指針,munmap()返回0。失敗時,mmap()返回MAP_FAILED[其值爲(void * )-1],munmap返回-1。
    • errno被設爲如下的某個值:
    EACCES:訪問出錯
    EAGAIN:文件已被鎖定,或者太多的內存已被鎖定
    EBADF:fd不是有效的文件描述詞
    EINVAL:一個或者多個參數無效
    ENFILE:已達到系統對打開文件的限制
    ENODEV:指定文件所在的文件系統不支持內存映射
    ENOMEM:內存不足,或者進程已超出最大內存映射數量
    EPERM:權能不足,操做不容許
    ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標誌
    SIGSEGV:試着向只讀區寫入
    SIGBUS:試着訪問不屬於進程的內存區
  • 參數說明:
    1. start:映射區的開始地址。
    2. length:映射區的長度。
    3. prot:指望的內存保護標誌,不能與文件的打開模式衝突。是如下的某個值,能夠經過or運算合理地組合在一塊兒
      PROT_EXEC //頁內容能夠被執行
      PROT_READ //頁內容能夠被讀取
      PROT_WRITE //頁能夠被寫入
      PROT_NONE //頁不可訪問緩存

    4. flags:指定映射對象的類型,映射選項和映射頁是否能夠共享。它的值能夠是一個或者多個如下位的組合體
      MAP_FIXED //使用指定的映射起始地址,若是由start和len參數指定的內存區重疊於現存的映射空間,重疊部分將會被丟棄。若是指定的起始地址不可用,操做將會失敗。而且起始地址必須落在頁的邊界上。
      MAP_SHARED //與其它全部映射這個對象的進程共享映射空間。對共享區的寫入,至關於輸出到文件。直到msync()或者munmap()被調用,文件實際上不會被更新。
      MAP_PRIVATE //創建一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件。這個標誌和以上標誌是互斥的,只能使用其中一個。
      MAP_DENYWRITE //這個標誌被忽略。
      MAP_EXECUTABLE //同上
      MAP_NORESERVE //不要爲這個映射保留交換空間。當交換空間被保留,對映射區修改的可能會獲得保證。當交換空間不被保留,同時內存不足,對映射區的修改會引發段違例信號。
      MAP_LOCKED //鎖定映射區的頁面,從而防止頁面被交換出內存。
      MAP_GROWSDOWN //用於堆棧,告訴內核VM系統,映射區能夠向下擴展。
      MAP_ANONYMOUS //匿名映射,映射區不與任何文件關聯。
      MAP_ANON //MAP_ANONYMOUS的別稱,再也不被使用。
      MAP_FILE //兼容標誌,被忽略。
      MAP_32BIT //將映射區放在進程地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標誌只在x86-64平臺上獲得支持。
      MAP_POPULATE //爲文件映射經過預讀的方式準備好頁表。隨後對映射區的訪問不會被頁違例阻塞。
      MAP_NONBLOCK //僅和MAP_POPULATE一塊兒使用時纔有意義。不執行預讀,只爲已存在於內存中的頁面創建頁表入口。安全

    5. fd:有效的文件描述詞。若是MAP_ANONYMOUS被設定,爲了兼容問題,其值應爲-1。

    6. offset:被映射對象內容的起點。

    • 當使用mmap映射文件到進程後,就能夠直接操做這段虛擬地址進行文件的讀寫等操做,沒必要再調用read,write等系統調用.但需注意,直接對該段內存寫時不會寫入超過當前文件大小的內容.
    • 基於文件的映射,在mmap和munmap執行過程的任什麼時候刻,被映射文件的st_atime可能被更新。若是st_atime字段在前述的狀況下沒有獲得更新,首次對映射區的第一個頁索引時會更新該字段的值。用PROT_WRITE 和 MAP_SHARED標誌創建起來的文件映射,其st_ctime 和 st_mtime在對映射區寫入以後,但在msync()經過MS_SYNC 和 MS_ASYNC兩個標誌調用以前會被更新。

返回目錄

動態存儲器分配的方法

咱們須要瞭解的是存儲器以及其在內存中實現動態分配的方法

  • 隨機訪問存儲器(RAM):
    • 靜態RAM:用來做爲高速緩存存儲器,每一個位存儲在一個雙穩態的存儲器單元裏。雙穩態:電路能夠無限期的保持在兩個不一樣的電壓配置或者狀態之一。只要供電,就會保持不變。
    • 動態RAM:用來做爲主存以及圖形系統的幀緩衝區。將每一個位存儲爲對一個電容的充電,當電容的電壓被擾亂以後,他就永遠都不會再恢復了。暴露在光線下會致使電容電壓改變。優點是密集度低,成本低。
    • 傳統的DRAM:DRAM芯片中的單元(位)被分紅了d個超單元,每一個超單元都由w個DRAM單元組成, 一個d*w的DRAM共存儲dw位信息。超單元被組織成一個r行c列的長方形陣列,rc=d。每一個超單元的地址用(i,j)來表示(從零開始)。設計成二維矩陣是爲了下降芯片上地址引腳的數量。息經過稱爲引腳的外部鏈接器流入/流出芯片,每一個引腳攜帶一個1位信號。DRAM芯片包裝在存儲器模塊中,它是插到主版的擴展槽上的。
    • 內存模塊:DRAM芯片封裝在內存模塊中,它插到主板的擴展槽上。經過將多個內存模塊鏈接到內存控制器,可以聚合成主存。
  • 加強的DRAM:
    • 快頁模式DRAM(FPM DRAM):許對同一行連續的訪問能夠直接從行緩衝區獲得服務。

    • 擴展數據輸出DRAM(EDO DRAM):容許單獨的CAS信號在時間上靠的更緊密一點。

    • 同步DRAM(SDRAM):用驅動存儲控制器相同的外部時鐘信號的上升沿來替代許多的異步信號,比異步的更快。

    • 雙倍數據速率同步DRAM(DDR DRAM):經過使用兩個時鐘沿做爲控制信號,使得DRAM的速度翻倍。

    • 視頻RAM(VRAM):用在圖形系統的緩衝中。輸出經過以此對內部緩衝區的整個內容進行移位獲得的;容許對內存並行地讀和寫。

    • 非易失性存儲器:若是斷電,DRAM和SRAM都會丟失信息。非易失型存儲器:即便在關電後,也仍然保存着它們的信息;稱爲ROM。
    • PROM:只能被編程一次。
    • 可擦寫可編程ROM(EPROM):紫外線光照射過窗口,EPROM就被清除爲0,被擦除和重編程的次數爲1000次。
    • 電子可擦除ROM(EEPROM):不須要一個物理上獨立的編程設備,所以能夠直接在印製電路卡上編程,可以編程的次數爲10^3。
    • 閃存:基於EEPROM,爲大量的電子設備提供快速而持久的非易失性存儲。當一個計算機系統通電以後,它會運行存儲在ROM中的固件。
  • 接下來談談在Linux下實現動態分配的方法:
    ANSI C說明了三個用於存儲空間動態分配的函數
    • malloc 分配指定字節數的存儲區。此存儲區中的初始值不肯定
    • calloc 爲指定長度的對象,分配能容納其指定個數的存儲空間。該空間中的每一位(bit)都初始化爲0
    • realloc 更改之前分配區的長度(增長或減小)。當增長長度時,可能需將之前分配區的內容移到另外一個足夠大的區域,而新增區域內的初始值則不肯定

返回目錄

垃圾收集的概念

垃圾收集須要完成的三件事情

  • 哪些內存須要回收?
  • 何時回收?
  • 如何回收?

在網上我沒有找到C語言的垃圾收集知識,我找到了Java的,這裏我就以Java內存垃圾收集機制來談談垃圾收集的概念

  1. 垃圾收集器在對堆進行回收以前須要判斷這些對象中哪些還「存活着」,哪些已經「死去」。

  2. 判斷算法
  • 引用計數法(Reference Counting):許多教科書上判斷對象是否存活都是這個算法,可是在主流的Java虛擬機裏沒有選用這個算法來管理內存,下面來簡單介紹一下此算法,其實就是爲對象中添加一個引用計數器,每當一個地方引用它時,計數器就加1;當引用失效時,計數器就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的
  • 可達性分析算法(Reachability Analysis):在主流的商用程序語言的實現中,都是經過可達性分析法來斷定對象是否存活的。這個算法的基本思路就是經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的
  1. 垃圾收集算法
  • 標記-清除算法
    最基本的收集算法「標記-清除」(Mark-Sweep)算法,算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象,之因此說它是最基本的收集算法,是由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。它的主要不足有兩個:
    一是效率問題,標記和清除效率都不高
    二是空間問題,標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後程序在運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做
  • 複製算法
    爲了解決效率問題,一種稱爲「複製」(Copying)的收集算法出現了,他將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這塊的內存用完了,就將還存活這的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲了原來的一半,未免過高了一點。
  • 標記-整理算法
    複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。更關鍵的是若是不想浪費50%的空間就要使用額外的空間進行分配擔保(Handle Promotion當空間不夠時,須要依賴其餘內存),以應對被使用的內存中全部對象都100%存活的極端狀況.對於「標記-整理」算法,標記過程仍與「標記-清除」算法同樣,可是後續步驟不是直接對可回收對象進行清理,而是讓全部的存活對象都向一端移動,而後直接清理掉端邊界之外的內存.
  • 分代收集算法
    當前的商業虛擬機的垃圾收集都是採用「分代收集」(Generational Collection)算法,這種算法並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊。通常是把堆劃分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適合的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就採用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清理」或者「標記-整理」算法來進行回收

返回目錄

C語言中與存儲器有關的錯誤

  • 內存泄漏
    內存泄漏一般是緩慢的隱形的,當咱們不當心忘記釋放咱們分配的內存就會發生這樣的問題,看下面的這段代碼:
/*mem_leak*/
void mem_leak(int n)
{
 int *gb_n = (int *)malloc(sizeof(int) * n);
 //gb_n 在這裏沒有被釋放,也沒有被引用
 return ;
}

這段代碼分配了一個內存快,在釋放以前程序就返回了,若是這是一個服務器或守護進程的程序就很糟糕了,它會形成內存空間被佔滿的狀況,通常現象是程序運行變慢最終終止。

  • 間接引用不良指針
    間接引用不良指針是一個常見的錯誤,其中最爲經典的應該是scanf錯誤了,假設咱們想要是用scanf從stdin讀取一個整數到一個變量,正確的方法應該是傳遞給scanf一個格式串和變量的地址:
void scanf()
{
 scanf("%d",$val);
}
   然而有些程序員特別是初學者,很容易出現如下錯誤:
/*scanf_bug*/
void scanf_bug()
{
 scanf("%d",val);
}

傳遞的是val的內容而不是他的地址,在這種狀況下,scanf把變量的內容解釋爲一個地址,試圖將一個字寫到這個位置,最想的狀況是程序當即終止,而在糟糕的狀況下阿拉的內容被寫到對應的內存中某個合法的讀寫區域,因而就會覆蓋原來地址中的值,一般這樣會形成災難性的或使人困惑的後果.

  • 讀未出世化的地址或存儲器
    雖然.bss存儲器的位置如未初始化的變量老是被初始化爲零,可是對於堆來講就不是這樣了,這種錯誤中最多見的是認爲堆被初始化爲零了:
/*uninit*/
int uninit_bug(int **array, int *p, int num)
{
 int i = 0;
 int j = 0;
 int *temp_p = (int *)malloc(sizeof(int) * num);
 for(i = 0; i < num; i++)
 {
  //temp_p[i] = NULL;
  for(j = 0; j  > num; j++)
  {
      temp_p[i] = array[i][j] * p[j];
  }
  return temp_p;
 }
}

上面的這短代碼中顯示的是:
不正確地認爲指針temp_p被初始化爲零,正確的應該在程序中把

temp_p[i]設置爲零:
temp_p[i] = NULL;

或者使用另外一個方法calloc來動態分配內存:
int *temp_p = (int *)calloc(1, sizeof(int) * num);
方法calloc來動態分配內存時自動的將指針temp_p被初始化爲零.

  • 錯誤的認爲指針和它們指向的對象是相同大小的
    這種錯誤是錯誤的認爲錯誤的認爲指針和它們指向的對象是相同大小的:
/*pioter_to_obj_size*/
int **poiter_to_obj_size(int num1,  int num2)
{
 int i = 0;
 int **p_array = (int **)malloc(sizeof(int) * num1);
 //
 *p_array = NULL;
 for(i = 0; i < num1; i++)
 {  
     p_array[i] = (int *)malloc(sizeof(int) * num2);
 }
 return p_array;
}

如今咱們來分析上面這段代碼,這段代碼的目的是建立一個由num1個指針組成的數組,每一個指針都指向一個包含num2個int類型的數組.
在程序中咱們把:
int **p_array = (int **)malloc(sizeof(int*) * num1);
寫成了:
int **p_array = (int **)malloc(sizeof(int) * num1);
這時程序實際上建立的是一個int類型的數組,這段代碼會在int和指向int的指針大小相同的機器上運行良好,若是把這段代碼放在int和指向int的指針大小不一樣的機器上運行就會出現一些使人困惑的奇怪的錯誤.

  • 錯位錯誤
    這種錯誤是一種很常見的覆蓋錯誤看下面的這段代碼:
/*off_by_one*/
int **off_by_one(int num 1, int num2)
{
 int i = 0;
 int **p_array = (int **)malloc(sizeof(int*) * num1);
 *p_array = NULL;
 //for(i = 0; i < num1; i++)
 for(i = 0; i <= num1; i++)
 {
     p_array[i] = (int *)malloc(sizeof(int) * num2);
 }
 return p_array;
}

這段代碼建立了一個num1個元素的指針數組,可是後面的代碼卻試圖初始化數組的num1+1個元素,這樣最後一個就會覆蓋數組的後面的某個地址中的數據.

  • 錯誤地引用指針而不是它所指的對象
    錯誤地引用指針而不是它所指的對象,這種錯誤通常是因爲C操做符的優先級和結合性引發的,這時咱們會錯誤地操做指針而不是他所指向的對象先看一下下面的這個小的程序代碼:
/*use_another_obj*/
int *use_another_obj(int **binheap, int *size)
{
 int *pac = binheap[0];
 binheap[0] = binheap[*size - 1];
 *size--;
 heapify(binheap, *size, 0);
 return pac;
}

本意是減小size指針指向的整數的值,卻由於運算符的優先級出現了減小指針本身的值!正確的寫法應該是這樣的:

binheap[0] = binheap[*size - 1];
    (*size)--;
  • 誤解指針運算
    首先看一下代碼:
/*wrong_poiter_op*/
int *wrong_poiter_op(int *poiter, int num)
{
 while(*poiter && * != num)
 {
     poiter += sizeof(int);
 }
 return poiter;
}

想用這個程序遍歷一個int類型的數組,返回一個指針指向num的首次出現,可是結果卻不是我指望的那樣,由於每次循環時poiter += sizeof(int);都把指針加上了4,這樣代碼就遍歷了數組中的每4個整數了,正確的應該是這樣的:
poiter++ += sizeof(int);

  • 容許棧緩衝區溢出
    假設一個程序不見查輸入串的大小就寫入棧的目標緩衝區,那麼這個程序就會有緩衝區溢出的錯誤:
/*bufoverflow*/
void bufoverflow_bug()
{
    char buf[256];
    gets(buf);
}

上面的這段程序代碼就出現了緩衝區錯誤,由於gets函數拷貝一個任意長度的串到緩衝區了,因此咱們必須使用fgets函數:
fgets(buf);
由於這個函數限制了輸入串的大小.

  • 引用已經釋放掉的堆中的數據
    引用已經釋放掉的堆中的數據,看下面的代碼:
/*ref_freeed_heap_data*/
int *ref_freeed_heap_data(int n, int m)
{
    int i = 0;
    int *x = NULL;
    int *y = NULL;
    x = (int *)malloc(sizeof(int) * n);
    free(x);
    y = (int *)malloc(sizeof(int) * m);
    for(i = 0; i < m; i++)
    {
            y[i] = x[i]++;
            //x freeed!!!!!!
    }
    return y;
}

程序引用了已經釋放的數據:free(x);

  • 引用不存在的變量
    下面的這個函數返回的是一個地址,指向棧裏的一個局部變量而後彈出棧幀
/*ref_no_val*/
int *ref_no_val()
{
 int num;
 return &num;
}

這裏他已經再也不指向一個合法的變量了.
返回目錄

教材學習中的問題和解決過程

  • 問題1:咱們的程序是否是能夠直接能夠接觸到物理地址,就是是否是能夠直接從物理地址當中獲取數據?若是不是,爲何?
  • 解答:
  1. 主存的容量有限,可是咱們的進程是無限,若是計算機上的每個進程都獨佔一塊物理存儲器(即物理地址空間)。那麼,主存就會很快被用完。實際上,每一個進程在不一樣的時刻都是隻會用同一塊主存的數據,這就說明了其實只要在進程想要主存數據的時候咱們把須要的主存加載上就好,換進換出。針對這樣的需求,直接提供一整塊主存的物理地址就明顯不符合。
  2. 進程間通訊的需求。若是每一個進程都 獨佔一塊物理地址,這樣就只能經過socket這樣的手段進行進程通訊,但若是進程間能使用同一塊物理地址就能夠解決這個問題。
  3. 主存的保護問題。對於主存來講,須要說明這段內存是可讀的,可寫的,仍是可執行的。針對這點,光用物理地址也是很難作到的。
  • 問題2:如何理解垃圾回收機制?
  • 解答:
    以Java爲例,內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域會隨着線程而生,隨線程而滅;棧中的棧幀隨着方法的進行有條不紊地執行着出棧和入棧操做。每個棧幀中分配多少內存基本上是在類結構肯定下來時就已知得,所以這幾個區域的內存分配回收都具有肯定性,在這幾個區域就不須要過多的考慮回收的問題,由於在方法結束或線程結束時內存就被回收了。
    而Java堆和方法區則不同,一個接口中的實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間才能知道會建立那些對象,這部份內存的分配和回收都是動態的,垃圾收集器所關注的是這部份內存。
    返回目錄

    代碼調試中的問題和解決過程


    返回目錄

    本週結對學習狀況

  • 結對學習博客
    20155302
  • 結對學習圖片
  • 結對學習內容
    • 教材第九章

返回目錄

代碼託管

返回目錄

學習進度條

代碼行數(新增/累積) 博客量(新增/累積) 學習時間(新增/累積) 重要成長
目標 5000行 30篇 400小時
第一週 0/0 1/1 10/10
第三週 200/200 2/3 10/20
第四周 100/300 1/4 10/30
第五週 200/500 3/7 10/40
第六週 500/1000 2/9 30/70
第七週 500/1500 2/11 15/85
第八週 223/1723 3/14 15/100
第九周 783/2506 3/17 15/115
第九周 0/2506 3/20 12/127
第十週 620/3126 2/22 20/147
第十一週 390/3516 2/24 17/164
  • 計劃學習時間:17小時

  • 實際學習時間:17小時

返回目錄

參考資料

返回目錄

相關文章
相關標籤/搜索