ETH官方钱包

前往
大廳
主題 達人專欄

Rust 所有權系統的先修課:記憶體洩漏

解凍豬腳 | 2024-08-16 19:00:03 | 巴幣 138 | 人氣 471

 
本篇文章有不少程式碼,如果這裡的排版讓你感到閱讀困難,請服用 HackMD 好讀版



學 Rust 語言肯定要知道什麼是所有權系統。若只問「該如何打敗 Rust 的借用檢查器」,其實隨便搜一下都有很多相關資料。不過比起能夠通過編譯,我覺得弄懂這些設計的背後緣由更加重要,而這些細節也有不少是在我學 Rust 半年多以來才慢慢領悟到的事情。

今天不直接談所有權系統,先來講講所有程式語言都可能遇上的記憶體洩漏問題吧。



? 程式當中的記憶體洩漏問題

之前稍微提過字串在 stack 和 heap 上面的行為。

我提到,通常 stack 區域用來存放一些比較單純的變數、函數等等,這些記憶體空間的申請和釋放過程都是由編譯器設計、管理,而當變數離開作用域(scope)的時候,它所佔用的空間就會被釋放。

以 C 語言作例:
#include <stdio.h>

void check_var_sum(int x, int y, int z) {
    int sum = x+y+z;
    if (sum % 2 == 0) {
        printf("The sum %d is even.\n", sum);
        return;
    }
    printf("The sum %d is odd.\n", sum);
}

int main() {
    check_var_sum(1, 2, 3);
    return 0;
}

可以看到我在函數 check_var_sum 當中宣告了一個區域變數 sum,接著令系統在 stack 安排一個 int 大小的空間,把 x+y+z 的總和放進去。因為變數 sum 的作用域只在 check_var_sum 範圍當中,所以當這個函數結束(返回)的時候,sum 所佔用的記憶體就會自動被釋放(free),那塊 int 大小的記憶體空間就又可以被使用了。

在一些用例比較複雜的場合下,C 語言允許使用者在 heap 上面要求系統動態分配記憶體:
#include <stdio.h>
#include <stdlib.h>

char *make_dynamic_string(unsigned int capacity) {
char *new_string = malloc(capacity * sizeof(char));
for (unsigned int i=0; i<capacity; i++) {
        new_string[i] = 'Q'; // fill with 'Q'
    }

    return new_string;
}

int main() {
    unsigned int capacity = 0;
    printf("Input the capacity of string: ");
    scanf("%d", &capacity);

    char *my_string = make_dynamic_string(capacity);
    printf("%s", my_string);
    return 0;
}

這麼做的話,分配的長度就可以由使用者來輸入,再經由 malloc(memory allocate)動態分配記憶體。實際執行以上範例就能看見,當輸入 6 的時候,會得到一個長度為 6 的字串 "QQQQQQ";當輸入 8 的時候,會得到一個長度為 8 的字串 "QQQQQQQQ"。這裡為了方便解說,就暫時不考慮 C 語言字串結尾必須是 0x00 的問題了。

既然是我們手動申請的,也就意味著系統並不知道這些空間什麼時候該釋放。當我們不再需要使用這個字串的時候,就必須手動釋放它:
char *my_string = make_dynamic_string(capacity);
printf("%s", my_string);
free(my_string);

為了實現複雜的應用場景,動態分配記憶體的功能是必要的,畢竟本來就不是所有的資料都有固定長度、能夠被 stack 裝得下,就像範例當中的可變長度字串。

然而,這樣的設計很容易遇上工程師忘記釋放資源的問題,一旦工程師忘記手動釋放它,那塊申請來的記憶體空間就會一直被作業系統認為有人使用,直到程式結束為止。假如這樣的問題在程式運作的過程不斷發生,那麼整個程式所佔用的記憶體就會隨著時間越來越大,導致它無法長時間穩定運作,這種對記憶體失去完整控制的情形,就是所謂的記憶體洩漏(memory leak)。



? 預防記憶體洩漏的方案

為了解決這種容易忘記手動釋放的情況,近代常見的程式語言如 Java、JavaScript、Golang、Python、C# 等等都利用垃圾收集(Garbage Collection,簡稱 GC)的機制來解決這樣的麻煩。垃圾收集的思想很簡單,就是在程式語言內建一個機制,定期(或被動地)判斷是否有不再需要的資源,自動把這些資源釋放掉,就像在房間地板隨時備個掃地機器人一樣。

這些語言的 GC 具體實作細節因不同語言而異:有些語言會給物件維護一個引用計數器,有些會把不同長短生命週期的物件分類,有些會對所有物件一個一個檢查引用狀態,它們都有各自的優缺點。由於它們要嘛在許多物件內部都封裝了一個額外的引用計數器,要嘛必須定期把這些引用物件遍歷一次,這些做法都難免需要犧牲一點性能或是資源,不然就是需要使用比較複雜的演算法。

比方說,實作引用計數器策略的 Python 在以下的場景:
def my_func_inner(str_to_print):
    print(str_to_print)

def my_func():
    my_str = "ABCDE"
    my_func_inner(my_str)

def main():
    my_func()

於是依序發生了這樣的過程:
  1. 當我們產生 "ABCDE" 字串並存入 my_str 的時候,這個字串本身內部的引用計數從 0 變成 1
  2. 當字串被傳入 my_func_inner 的時候,引用計數從 1 變成 2
  3. 當字串被傳入 print 的時候,引用計數從 2 變成 3,而 print 函數結束時它又從 3 變成 2
  4. 當 my_func_inner 函數結束,引用計數從 2 變成 1
  5. 當 my_func 函數結束,字串內部的引用計數從 1 變成 0
  6. 引用計數器因為到達 0,系統藉此得知這個字串不再有人需要使用,因而釋放字串所佔用的記憶體空間

除此之外,也有一派的程式語言不依賴 GC 策略,而是資源取得即初始化(Resource Acquisition Is Initialization,簡稱 RAII),最具代表性的是 C++。

這個詞我覺得無論中英文都不是很好懂,就不要太細究這個名稱了。首先資源的取得和釋放應該要是成雙成對的,這個取得、準備資源的流程叫做「建構」(所以產生物件的那些 new() 函式,我們稱它為建構子),而釋放資源的時候則有「解構」。C++ 的策略是讓物件本身可以定義解構函數,把釋放資源的過程定義在解構函數裡面,這樣當變數離開作用域、生命週期結束的時候,系統自動嘗試呼叫物件的解構函式,那麼資源就會自動釋放掉了。

像前面 C 語言實作簡單字串的例子當中,如果換成是 C++ 的話,我們就能給這個自訂的 String 物件做更完善的封裝,定義它在變數離開作用域的時候由系統自動呼叫 free(self) 之類的行為來把這些佔用的記憶體空間釋放掉,也就能更好地避免記憶體空間可能會忘記被釋放的問題了。

之後再接著說說 Rust 是怎麼透過所有權系統和 RAII 的思想,更優雅地解決各種常見的安全問題。



HackMD 好讀版:https://hackmd.io/@upk1997/rust-ownership-0

縮圖素材原作者:Karen Rustad T?lva(CC0 1.0)
送禮物贊助創作者 !
0
留言

創作回應

露米諾斯 Lumynous
其實所有權不是新概念,在 C 中更要強調呼叫者有沒有回傳(指標指向的)物件的所有權。另外,sizeof(char) 永遠是 1,是多餘的
2024-08-16 20:04:20
解凍豬腳
原來是多餘的嗎 [e17]
不是很熟 C 語言的慣例,有點強迫癥就全寫上了 [e5]
2024-08-24 19:39:49
紅色雨燕
「sizeof(char)」雖然永遠應該是 1,但是加個 static_assert 總是安全的。
2024-08-16 21:35:06
解凍豬腳
感謝補充 [e12]
2024-08-24 19:40:07
Pnerd
工作也是用C的,C的記憶體管理如果出了Bug,非常難追;路過留個言,看了一下大大的文,真是厲害,全方面的創作XD。
2024-08-17 12:29:21
解凍豬腳
記憶體安全的語言應該會是未來趨勢,Golang 和 Rust 都是值得學習的
感謝支持,我很享受在不同領域的興趣取得成果或進步 [e12]
2024-08-24 19:45:16
露米諾斯 Lumynous
回 B3,不只是「應該是」而是「一定是」,並不會比較安全,這點是標準保證的,即使 char 有 64 位元寬也是 1
2024-08-25 17:29:39
%%鼠 拒收病婿
RAII可以簡單用smart pointer實現https://i2.bahamut.com.tw/editor/emotion/38.gif
2024-08-26 08:42:27

更多創作