# 2019q1 Homework6 (gc) contributed by < `flawless0714` > ## OGC 運作機制 ### reference counting OGC 中實做 reference counting 的資料結構為 `gc_t`,其中的 `ref_count` 會在 `gc_init` (有使用到 OGC 的程式都需在初始化時(或開始動態配置記憶體前)執行此函數以初始化 OGC 的資料結構,倘若有多隻程式在使用 OGC,則自第二次開始 `gc_init` 只會增加 `ref_count`) 時加一,表示多一隻可能會使用到 OGC 所管理的記憶體的程式。 當要關閉 OGC (`gc_destory`) 時,會先透過 `ref_count` 檢查是否還有程式使用到 OGC 所管理的記憶體,若無則開始進行記憶體釋放等善後工作,反之 `gc_destory` 將直接結束。 :::info `gc_destory` 或許可以加個例外處理或警告,以避免失敗時無聲無息的結束。 ::: ### mark and sweep #### gc_mark ```cpp if (start > end) swap_ptr(&start, &end); ``` 確保 `start` 儲存的記憶體位置永遠低於 `end`。 ```cpp while (start < end) { gc_list_t *idx = gc_ptr_index((uintptr_t)(*((void **) start))); if (idx && idx->data.marked != true) { idx->data.marked = true; gc_mark((uint8_t *) (idx->data.start), (uint8_t *) (idx->data.start + idx->data.size)); } start++; } ``` 在 `start` 超越 `end` 前持續檢索 `ptr_map` 中保存的每一塊 heap 記憶體,若檢索到的記憶體為 reachable 且尚未標記(marked),則對其進行標記,反之則繼續往下檢索。值得注意的是若有標記到某塊記憶體,則會以該塊記憶體的開始位置與結束位置繼續遞迴呼叫 `gc_mark`。 ##### gc_ptr_index ```cpp gc_list_t *gc_ptr_index(uintptr_t ptr) { if (ptr > __gc_object.max || ptr < __gc_object.min) return NULL; int h = HASH(ptr) % PTR_MAP_SIZE; gc_list_t *e; if ((e = list_search(ptr, __gc_object.ptr_map[h]))) return e; for (int i = 0; ++i + h < PTR_MAP_SIZE || (h > i);) { if ((h > i) && (e = list_search(ptr, __gc_object.ptr_map[h - i]))) return e; if ((h + i < PTR_MAP_SIZE) && (e = list_search(ptr, __gc_object.ptr_map[h + i]))) return e; } return NULL; } ``` 由 `gc_mark` 呼叫,用於回傳以給定記憶體做完 hash 並取完餘數(`PTR_MAP_SIZE`)後在 `ptr_map` 中找到的 `gc_list_t`。 ##### list_search ```cpp static inline gc_list_t *list_search(uintptr_t ptr, gc_list_t *e) { for (; e; e = e->next) { if (ptr >= e->data.start && e->data.start + e->data.size >= ptr) return e; } return NULL; } ``` 由 gc_ptr_index 呼叫,用於驗證給定的 `gc_list_t` 是否為 reachable 並且 `gc_list_t` 的保存的起始位置加上大小是否大於等於 `ptr`,是則回傳該 `gc_list_t`,反之。==這邊==不太理解為什麼 if statement 裡面要做這兩個檢查,我的理解是拿到 `ptr_map` 中的元素後應該只須驗證其是否為 reachable 就可以回傳了。 > 目前推測是為了避免拿到已經標記過得元素還回傳而造成效率下降,待確認。 ##### HASH() 這個巨集用於對傳入的指標做 hash,位移為三個位元的原因==推測==為為了取得較稀有的記憶體位置,進而在取餘數(`HASH(ptr) % PTR_MAP_SIZE`)時降低拿到已經使用過的 index 的機率。待確認。 #### gc_sweep ```cpp void gc_sweep(void) { for (int i = 0; ++i < PTR_MAP_SIZE;) { gc_list_t *e = __gc_object.ptr_map[i]; int k = 0; while (e) { if (!e->data.marked) { gc_mfree(e); e = e->next; gc_list_del(&__gc_object.ptr_map[i], k); } else { e->data.marked = false; e = e->next; } k++; } } } ``` 走訪整個 `ptr_map`,當遇到沒有 marked 的 `gc_list_t` 時將該節點釋放,並於 linked list 中移除該節點。 ### gc_destory ```cpp void gc_destroy(void) { __gc_object.ref_count--; if (__gc_object.ref_count <= 0) { __gc_object.ref_count = 0; for (int i = -1; ++i < PTR_MAP_SIZE;) { gc_list_t *m = __gc_object.ptr_map[i]; while (m) { gc_list_t *tmp = m; free((void *) (m->data.start)); m = m->next; free(tmp); } __gc_object.ptr_map[i] = 0; } } } ``` 此函數開頭會先做 `ref_count--`,表示有一程式準備要結束,即不再使用 OGC 所管理的記憶體。接下來檢查 `ref_count` 是否為 0,假如為 0 即代表已沒有程式在使用 OGC 管理的記憶體,我們可以開始釋放所有 OGC 使用到的記憶體。 ## OGC 已存在之缺陷 ### unreachable code ```cpp= void gc_mark_stack(void) { uint8_t tmp; gc_mark(__gc_object.stack_start, &tmp); for (gc_list_t *e = __gc_object.globals; e; e = e->next) { gc_mark((uint8_t *) (e->data.start), (uint8_t *) (e->data.start + e->data.size)); } } ``` 於 `util.c` 的 `gc_mark_stack` 這個函數中,第五行的 `__gc_object_globals` 從未 assign 數值,使得這個迴圈永遠不會被執行。 ## 以多執行緒改寫 OGC [主要程式碼](https://github.com/flawless0714/ogc/blob/master/src/gc_thread_funcs.c) 目前尚未完成(僅 `dummy` 通過測試),還差 `gc_mark_stack` (不可用區域變數(thread 沒有共用 stack)) 與將架構改成在 `gc_init` 初始化完 OGC 後每個 user-program (`dummy`, `string-separate`) 以個別一個 thread 為執行單元去執行(以免失去 concurrency 之意義)。 :::warning 先標註 GC 程式中的 critical section,並且依據 CS:APP 第 12 章的手法去視覺化展現 :notes: jserv ::: ## 筆記 ### uintptr_t C99 中定義此資料型態可用於儲存指標(`void*`),並可完好無缺的再轉型回 `void*`。通常要 cast 到 `uintptr_t` 前我們會先轉到 `void**`,再對這個指標的指標做一次 dereference 以取得 `void*` 的內容,最後再轉到 `uintptr_t`。 > 其實不太理解為什麼轉型到 `uintptr_t` 之前要先轉到 `void*` 去做處理,每個平台拿到的記憶體位置(address of operator)與指標中的內容都是該平台最大定址空間的資料型態,這樣應該跟直接轉型為 `uintptr_t` 是一樣的吧?從組合語言來看先轉 `void*` 去做處理的話會多一行以下組語: > ```asm > mov rax, QWORD PTR [rax] > ``` > 像是 x86_64,也是上述 assembly 的平台,指標的內容都是 qword,那再多個轉 `QWORD PTR` 應該是不需要的吧?還是說這麼做其實有其他考量點? :::warning 注意 alignment 的影響 :notes: jserv ::: ### pthread_detach 一般 pthread 須使用 `pthread_join` 來釋放其資源(若未釋放會導致部分資源(exit code...)要等到 process 結束時由 OS 回收。當 thread 多到個程度時會發生 memory leak),而 `pthraed_join` 是個 blocking 的函數,有些時候我們不希望 thread 被 block,因為它可能還要趕著做其他事情。這時候我們就可以用 `pthread_detch` 來讓 child thread 完成工作時自行釋放所有資源。`pthread_detach` 可由 parent thread 呼叫或 child thread 自行呼叫,他的 parameter 是 thread id,在 child thread 中我們可以藉由呼叫 `pthread_self` 來取得自己的 tid,而在 parent thread 中則使用 `pthread_create` 的回傳值(即 tid)即可。 ## 參考資料 - [uintptr_t](https://stackoverflow.com/questions/1845482/what-is-uintptr-t-data-type/1846648#1846648) - [Garbage Collection vs Automatic Reference Counting](https://medium.com/computed-comparisons/garbage-collection-vs-automatic-reference-counting-a420bd4c7c81) - [Multithreaded Programming](https://randu.org/tutorials/threads/) ###### tags: `linux2019`