Linus Torvalds 无疑是开源软件界最具影响力的人物之一。作为 Linux 内核的创始人,他因技术贡献赢得了尊敬,但也常因口无遮拦的言辞引发争议。

Linus 对代码质量的要求极其严苛,也许正是因为自信能够写出完美的代码,才让他有底气挖苦和讽刺其他开发者吧。

Linus 写出的代码到底能有多么精简、多么高深、多么优雅、多么健壮……?可能很多程序员都对此充满好奇。Linux 内核的代码显然过于复杂,不适合“鉴赏”。不过,好在 Linus 还在 2005 年 4 月(和 BitKeeper 闹翻之后)编写过 Git 的原型,而且 Git 最初版本的源代码(https://github.com/git/git/commit/e83c5163316f89bfbde7d9ab23ca2e25604af290)只有千行左右,算上 README 和 Makefile 才只有 11 个文件。这可真是揭晓答案的好机会。

 $ wc -l *.c *.h | sort
   23 cat-file.c
   43 read-tree.c
   51 init-db.c
   66 write-tree.c
   81 show-diff.c
   93 cache.h
  172 commit-tree.c
  248 update-cache.c
  259 read-cache.c
 1036 total

不过,要是把这些源文件通读一遍还是需要花些功夫的。那就再从中挑一个最简单的来看一看。毕竟齐白石画只虾、画个萝卜也是名作。

最简单的应该是 init-db.c 了(别光看 cat-file.cread-tree.c 的行数少,那里面调用的函数可复杂了)。经过编译后,init-db.c 形成了一个叫作 init-db 的命令。

试着运行一下就会发现,该命令的功能非常单一,类似现在的 git init,仅仅是先在当前目录中创建一个隐藏目录 .dircache,然后在里面创建 objects 目录,以及以两位十六进制数命名、名称依次为 00ff 的 256 个子目录。

$ tree -a .
.
`-- .dircache
    `-- objects
        |-- 00
        |-- 01
        |-- 02
        ...
        |-- fc
        |-- fd
        |-- fe
        `-- ff

对照着功能再来欣赏下 Linus 亲自操刀的 init-db.c 的代码,

... ...
int main(int argc, char **argv)
{
    char *sha1_dir = getenv(DB_ENVIRONMENT), *path;
    int len, i, fd;

    if (mkdir(".dircache", 0700) < 0) {    // ① 创建 .dircache 目录
        perror("unable to create .dircache");
        exit(1);
    }

    ... ...

    // #define DEFAULT_DB_ENVIRONMENT ".dircache/objects" in `cache.h`
    sha1_dir = DEFAULT_DB_ENVIRONMENT;
    fprintf(stderr, "defaulting to private storage area\n");
    len = strlen(sha1_dir);
    if (mkdir(sha1_dir, 0700) < 0) {    // ② 创建 .dircache/objects 目录
        if (errno != EEXIST) {
            perror(sha1_dir);
            exit(1);
        }
    }
    path = malloc(len + 40);    // ⚠️注意
    memcpy(path, sha1_dir, len);
    for (i = 0; i < 256; i++) {
        sprintf(path+len, "/%02x", i);
        if (mkdir(path, 0700) < 0) {    // ③ 创建 .dircache/objects/xx 目录
            if (errno != EEXIST) {
                perror(path);
                exit(1);
            }
        }
    }
    return 0;
}

代码非常直观,①先调用 mkdir() 创建了 .dircache,然后创建了 .dircache/objects②,最后利用 for 循环,③创建了 256 个名字依次为 00ff 的、.dircache/objects 中的子目录。

但请注意这一行 path = malloc(len + 40);。这里动态分配了一块内存,大小为 len + 40,用于存储以十六进制数命名的子目录的路径。

稍有 C 语言编程经验的程序员都会牢记一条法则,如果使用 malloc() 分配了内存,但忘记调用 free() 来释放,就会造成内存泄漏。只要大学还在使用谭浩强的 C 语言教材开蒙,就是毫无项目经验的大一新生,也会因多次听到老师强调这一点,而囫囵吞枣地记住要 free() 吧。大名鼎鼎的 Linus 竟然忘了?

而且,搜索整个项目,可以发现不止一处调用了 malloc(), calloc(), realloc() 来申请内存,

$ egrep '(m|c|re)alloc' --color -rn . 
./update-cache.c:65:        active_cache = realloc(active_cache, active_alloc * sizeof(struct cache_entry *));
./update-cache.c:80:    void *out = malloc(max_out_bytes);
./update-cache.c:81:    void *metadata = malloc(namelen + 200);
./update-cache.c:138:    ce = malloc(size);

...

却只 free() 过 1 次!

$ egrep 'free' -C1 --color -rn . 
./show-diff.c-77-        show_differences(ce, &st, new, size);
./show-diff.c:78:        free(new);
./show-diff.c-79-    }

Linus 是真忘了?这不妥妥地内存泄露吗!

等一等,malloc() 之后真的一定要 free() 吗?

其实,如果程序运行时间很短(就像 init-db 这样简单的工具),程序结束后操作系统就会回收所有已分配的内存(包括未释放的堆内存),所以不手动释放也不会对系统产生显著影响(但确实是不好的编程习惯)。

而 Linus 在初版 Git 中唯一一次 free() 是在 for (i = 0; i < entries; i++) { 这样一个循环中调用的。这说明即使程序不会长时间运行,但如果分配了大量内存或通过循环多次申请内存,还是应该及时 free(),及时释放资源给其他进程。


总之,malloc() 之后并不是一定需要 free(),这取决于具体的场景和需求。下回面试时万一忘记写 free(),就可以辩解了,“Linus 当初创造 Git 时也没写,我们这是特意不写的”。但结果嘛……祝你好运。

不过,还有个好奇的点,path = malloc(len + 40) 这里为啥要 + 40 呢,追加的 /[0-f][0-f] 明明只占 3 个字节呀。


da_miao_zi
1 声望0 粉丝

软件工程师、技术图书译者。译有《图解云计算架构》《图解量子计算机》《计算机是怎样跑起来的》《自制搜索引擎》等。