Linux 發行版/系統長篇分享

Musl libc:為什麼我們會需要另一個 libc?

musl-logo

如果你是一個 Linux 用戶,那你一定至少聽說過 Glibc 的鼎鼎大名,或者甚至在日常使用中碰到不少關於它的問題,例如 Glibc 版本不匹配等問題。而本文的主角—— Musl libc 與之相比就要默默無聞的多,畢竟絕大多數的 Linux 發行版使用的 libc 庫都是 Glibc,只有 Alpine Linux 等極少數 Linux 發行版才會使用 Musl libc,並且還會遇到諸如閉源 JDK 無法正常使用等問題的困擾,那麼,為什麼我們會需要另一個 libc 函數庫呢?

Glibc 並不完美

Glibc 作為目前使用最廣泛的 libc 函數庫,雖然擁有最廣泛的發行版支持和用戶群體,並且在兼容性和性能方面也存在一些優勢,但它並不完美,有三個問題嚴重困擾著它。

代碼庫陳舊

Glibc 擁有悠久的歷史——對於軟體而言這可能並不一定是一句讚譽,尤其是當你需要處理上世紀九十年代就存在的代碼庫時。三十多年來,程序員們編寫 C 程序的方式並不是一成不變的,某些在那個年代被認為是好習慣或者是必須的編程方式在今天看來可能完全不合時宜,諸如疊床架屋的宏等等。這些問題嚴重拖累了 Glibc 的源碼可讀性,例如下面這一段源代碼,摘自 Glibc 的 fopen 函數。

FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
  struct locked_FILE
  {
    struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
    _IO_lock_t lock;
#endif
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

  if (new_f == NULL)
    return NULL;
#ifdef _IO_MTSAFE_IO
  new_f->fp.file._lock = &new_f->lock;
#endif
  _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
  _IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
  _IO_new_file_init_internal (&new_f->fp);
  if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);

  _IO_un_link (&new_f->fp);
  free (new_f);
  return NULL;
}

作為對比,下面是 Musl 庫中同樣對 fopen 函數的實現

FILE *fopen(const char *restrict filename, const char *restrict mode)
{
    FILE *f;
    int fd;
    int flags;

    /* Check for valid initial mode character */
    if (!strchr("rwa", *mode)) {
        errno = EINVAL;
        return 0;
    }

    /* Compute the flags to pass to open() */
    flags = __fmodeflags(mode);

    fd = sys_open(filename, flags, 0666);
    if (fd < 0) return 0;
    if (flags & O_CLOEXEC)
        __syscall(SYS_fcntl, fd, F_SETFD, FD_CLOEXEC);

    f = __fdopen(fd, mode);
    if (f) return f;

    __syscall(SYS_close, fd);
    return 0;
}

兩者的可讀性差距不言自明,筆者在這裡並非是要批判 Glibc 的代碼風格,但對於初次上手閱讀源碼的人來說,顯然是 Musl 的風格更加友好,更便於理解。

體積過大

由於 Glibc 相較體積更加關注性能,因此其鏈接生成的二進位文件相較於 Musl uClibc 等專註於嵌入式等場合的庫來說要大很多,而這些場合往往非常關注幾百 K 大小的區別,因為 SRAM 的大小往往關乎整體開發板的成本。

下面這張表展示了不同 libc 庫編譯的文件大小。

尺寸對比 musl uClibc dietlibc glibc
.a 426k 500k 120k 2.0M
.so 527k 560k 185k 7.9M
靜態最小 1.8k 5k 0.2k 662k
靜態輸出 13k 70k 6k 662k

可以看出 Glibc 在程序大小上明顯大於其他 libc 庫,此外,這裡的靜態也是有水分的,這就引出了 Glibc 的下一個問題,也是最重要的問題之一:靜態鏈接。

對靜態鏈接支持不佳

理論上來說,Glibc 是支持靜態鏈接的。但,這也僅僅是從理論上來說,由於一些歷史遺留問題(當然,也包括對功能實現的考慮)Glibc 的靜態鏈接並不是真正的靜態鏈接:如果你的程序中使用了某些不支持靜態鏈接的特性(這一點在大型軟體中非常常見),那麼即便你在鏈接時選擇靜態鏈接,生成出來的程序實際上仍然是依賴於 Glibc 動態庫的,一旦你嘗試刪除掉它,你立馬就會發現這些「靜態」鏈接的程序統統罷工不幹了。

誰在用 Musl?

在發行版中,主要是 Alpine Linux,作為最特立獨行的 Linux 發行版之一,它選用了 Musl + Busybox 的組合,而非通常的 Glibc + Coreutils,這使得它的最小安裝可以控制在驚人的 5 MB 之內!相比之下,普通的 CentOS 最小安裝則需要 200 MB 左右,這一點使得它在嵌入式等對內存佔用極為敏感的場合佔據了相當的優勢。
alpine-linux-home-page
此外,Musl 從設計之初就很關注靜態鏈接的可用性,因此它完全可以被靜態鏈接進其他程序中,不存在 Glibc 對動態庫的依賴問題,這一點也有助於緩解不同版本 libc 之間的兼容性問題——只要我把萬物都靜態鏈接進去,就不存在版本問題了。當然,這種做法也會帶來體積膨脹等問題,所以並不是一個太好的解決方案。

Musl 的問題與未來

雖然擁有諸多優點,但 Musl 在性能方面遜於 Glibc 也是不爭的事實,畢竟簡化實現的代價就包括犧牲性能,不過這一點並非不可拯救,通過使用開源的 malloc 實現(諸如微軟的實現)替換這些對性能影響較大的熱點函數,就可以在很大程度上解決性能方面的問題。

同時,試圖取代或至少部分取代 Glibc 的庫也並不止 Musl 一個, 例如用於 AOSP 項目上的 bionic,以及廣泛應用於各種嵌入式開發中的 uClibc 等。與它們相比,Musl 背後既沒有 Google 這樣的大公司撐腰,在壓縮體積方面做的也不夠極致,相較之下就沒有那麼受到開發者們的青睞,在新功能和新特性跟進上也不是非常積極。

出於這些原因,也許 Musl 在今後的很長一段時間內會繼續保持這種「小而美」的特點,但這對於我們來說並非就是一件壞事,能夠看到與 GNU Glibc 風格截然不同的另一種實現,對於 Linux 社區的多樣性,以及對於我們這些學習者來說,何嘗不是一件美事?

參考鏈接:
musl libc home page
Alpine Linux
Comparison of C/POSIX standard library implementations for LinuxComparison of C/POSIX standard library implementations for Linux

對這篇文章感覺如何?

太棒了
15
不錯
7
愛死了
2
不太好
0
感覺很糟
1

You may also like

Leave a reply

您的電子郵箱地址不會被公開。 必填項已用 * 標註

此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據