Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error Handling in C #10

Open
Naetw opened this issue Mar 29, 2019 · 0 comments
Open

Error Handling in C #10

Naetw opened this issue Mar 29, 2019 · 0 comments

Comments

@Naetw
Copy link
Owner

Naetw commented Mar 29, 2019

Table Of Contents

Handling out-of-memory conditions in C

本文主要在探討針對 Out-Of-Memory (OOM) 常見的處理方式。因為沒有最正確的方法只有最適合的,Eli 大師檢視一些有名的大型專案,並探討他們的做法。

紀錄下一些有趣的東西。

The policies

Recovery

最不常見的做法,因為 recovery 不好做,且不同領域的作法大相徑庭。Recovery 的動作可能由下列可能組成:

  • 釋放某些資源後再重新嘗試
  • 保存使用者的工作狀態後離開(非終止)
  • 清理暫時資源後離開(非終止)

Abort

最常用的方法,印出錯誤訊息後終止。以 gnulib 為例,裡面的 xmalloc 設計:

void *xmalloc (size_t n) {
    void *p = malloc (n);
    if (!p && n != 0)
        xalloc_die();
    return p;
}

Segfault

沒什麼好講的,就是一個最簡單的方法。

Example

接著 Eli 大師挑了幾個函式庫跟應用程式來探討他們的處理方式,這邊列幾個比較有趣的:

SQLite (recovery policy)

SQLite 提供多個選擇給使用者:

  • 最一般的 malloc-like 方案
  • 利用程式一開始先要好的一塊靜態緩衝區去做分配
  • 給 debug 用的,可以用來偵測記憶體問題
  • 使用者也可以自訂方案

這邊是一般的 malloc-like 方案:

static void *sqlite3MemMalloc(int nByte){
    sqlite3_int64 *p;
    assert( nByte>0 );
    nByte = ROUND8(nByte);
    p = malloc( nByte+8 );
    if( p ){
        p[0] = nByte;
        p++;
    }
    return (void *)p;
}

有趣的地方是,他會多要一塊空間來紀錄這塊 chunk 的大小(並非實際 heap chunk 的大小,而是使用者要求的大小),來讓分配者可以簡單地直接回報 chunk 大小。

Git (recovery policy)

void *xmalloc(size_t size)
{
    void *ret = malloc(size);
    if (!ret && !size)
        ret = malloc(1);
    if (!ret) {
        release_pack_memory(size, -1);
        ret = malloc(size);
        if (!ret && !size)
            ret = malloc(1);
        if (!ret)
            die("Out of memory, malloc failed");
    }
#ifdef XMALLOC_POISON
    memset(ret, 0xA5, size);
#endif
    return ret;
}

Redis (abort policy)

Redis 自己實作了一個 zmalloc 但是當 malloc 回傳是 NULL 時他不會自動結束,而是照舊回傳。Redis 內部的模組都會將 zmalloc 傳遞到使用層,若使用層發現有 NULL 就直接呼叫下面的 oom 函式。

/* Redis generally does not try to recover from out
 * of memory conditions when allocating objects or
 * strings, it is not clear if it will be possible
 * to report this condition to the client since the
 * networking layer itself is based on heap
 * allocation for send buffers, so we simply abort.
 * At least the code will be simpler to read... */
static void oom(const char *msg) {
    fprintf(stderr, "%s: Out of memory\n",msg);
    fflush(stderr);
    sleep(1);
    abort();
}

這裡的註解很清楚地描述了為什麼在應用程式中使用 abort policy 通常是最符合邏輯的。

總結

一個好的函式庫不應該會揭露自己的實作風格 (idiosyncrasy) 給呼叫者,因此一般建議使用 recovery 方式。同時也可以提供一些方法讓使用者自己定義處理的方式,增加彈性。

以應用程式來說,Eli 大師認為 abort 方式是最好的。寫一個 wrapper 把 abort 邏輯包裝起來可以讓整個程式碼的邏輯更清晰,也省下很多麻煩;同時,未來需要修改方式時也很簡單,直接去修改 wrapper。

Using goto for error handling in C

錯誤處理常常是會讓程式碼變得比較複雜且很難做好但卻又是必須的部分 QQ
這篇在講常常為人所厭惡的 goto 在錯誤處理上是能夠帶來很棒的效果。

錯誤處理

錯誤處理經常伴隨著資源的釋放處理,而這常常很令人頭痛。(後面有🌰)不過在 C++ 採用的 RAII (Resource Acquisition Is Initialization) 設計下,這件事可以很漂亮的被解決。這邊討論的是在 C 語言中要怎麼手動處理這樣的情況。

在 RAII wiki 中有這樣的一句話:

C requires significant administrative code since it doesn't support exceptions, try-finally blocks, or RAII at all. A typical approach is to separate the releasing of resources at the end of the function and jump there with gotos in the case of error. This way the cleanup code need not be duplicated.

直接看一個複雜的例子:

int foo(int bar)
{
    int return_value = 0;

    allocate_resources_1();

    if (!do_something(bar))
        goto error_1;

    allocate_resources_2();

    if (!init_stuff(bar))
        goto error_2;

    allocate_resources_3();

    if (!prepare_stuff(bar))
        goto error_3;

    return_value = do_the_thing(bar);

error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

拔掉 goto 後:

int foo(int bar)
{
    int return_value = 0;

    allocate_resources_1();

    if (do_something(bar))
    {
        allocate_resources_2();

        if (init_stuff(bar))
        {
            allocate_resources_3();

            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
            }

            cleanup_3();
        }

        cleanup_2();
    }

    cleanup_1();

    return return_value;
}

整個函式的行為主體在不使用 goto 的情況下被塞進了很深層的巢狀條件中,相較上面使用 goto 的程式碼不使用 goto 造成整體的可讀性是比較低的。

大一還大二時看王垠的編程的智慧裡面的關於 continue, break 還有 if else 的觀念後,風格漸漸變得像是上面沒有 goto 的版本,也就是我變得不習慣只利用單一 if 來處理錯誤,而是習慣反轉 if 的條件,來讓我要做的事情包在 if 的區塊中,之後就常常寫出很多深層的結構,回頭看很令人頭痛啊 QQ
最近讀了 LLVM 的 Coding Standard 還有被 Jserv 糾正才真正意識到這個寫法的還是比較好看 QQ
王垠的方法有他的考量,但是還是不要盲目跟風,應該要自己看看各種方法後思考,來發展或是選擇自己認為適當的風格。

剛好最近看到一句話覺得很適合這心境:「程式就像寫字,從臨摹開始,最後展開自己的風格。」該多寫寫程式來發展自己的風格惹!

最後 Eli 大師也提到上面的例子還算簡單,在 Linux kernel 中有更複雜的例子。這邊提供另一個更複雜的例子 - RAII in C

額外的使用情境

除了錯誤處理外,有些時候多層的迴圈也會需要直接跳離所有迴圈,這時候 goto 也十分適合,相較之下若不使用 goto 就必須要在每個迴圈的條件判斷式中加入某些額外的判斷,不但增加複雜度也讓可維護性降低。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant