Thursday, March 17, 2011

Working with Different Coding Styles

问题最近看代码看得相当的 high,有时候也写上几行,再没有了那种不能写代码的郁闷。工作中当然还是没有多少 coding 的机会,不过我现在能挤出时间,尽量增加跟代码的接触 (感谢开明的老板),所以工作心情好多了。这里记录此过程中一个非技术的小问题。

因为接触了好几个开源项目 (Expand Your Bandwidth),而每个都有不同的代码风格,导致我写代码的时候有点不方便。如何缩进 (或者说 tab 键的处理) 是主要的问题,有的用 4 个空格缩进 (比如多数的 python 代码), 有的用 8 个空格 (比如 systemd), 有的用真正的 tab (比如 Linux kernel, packagekit)。但是对于 vim 来说,配置只能有一个,没法照顾到所有的情况。

我能想到的解决办法有这么几个:
  • 在文件头部指定代码风格,比如 "Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8"。这叫 mode line,vim 和 emacs 都支持,但是两者的格式却不一样,上边那个是 emacs 的,要是 vim 的话,得再添一行,"vim: set ts=8 sw=4 tw=0:"。鉴于此,不能指望每个项目在每个文件里为每种编辑器写一行 mode line。systemd 就只考虑了 emacs,因为 Lennart Poettering 用的是 emacs。
  • 上面的问题有两个办法,1) 学习 emacs, 2) 用这个 vim 插件,让 vim 识别 emacs 的 mode line。1) 就算了... 但是 2) 基本上也行不通,因为有相当多的项目根本就没有任何的 mode line。
  • vim 在读取配置的时候,能够识别当前目录下的配置文件。这样就可以为每个工程加一个 .vimrc,在里边指定本工程的代码风格。

    方案

    第一跟第二条属于自己控制不了的,最后一个还比较靠谱。不过 vim 默认不会读取当前目录下的配置文件。因此首先要在 ~/.vimrc 里添加这么两行 (引用):
    set exrc   " enable per-directory .vimrc files
    set secure   " disable unsafe commands in local .vimrc files
    
    第二行是为安全起见 (因为 vimrc 里的东西是被 vim _执行_ 的,所以可以被用作特洛伊木马)。

    然后写各工程的 .vimrc,这些都不会太长,因为只需要指定少数几个选项,比如 expandtab 和 shiftwidth。
    $ cat .vimrc 
    autocmd FileType c set noexpandtab shiftwidth=8
    
    这个配置适合 packagekit,用真正的 tab 进行缩进 (只针对 C 文件;工程里还有 python 文件,仍然使用 vim 的全局配置)。

    剩下的一个问题是,git status 的时候说有 untracked file,看起来实在不够清爽 :-) 本来可以让 git 忽略掉 .vimrc,但是项目里的 .gitignore 不是我能掌控的。不过,记住我们用的是 git :-) 用户可以指定全局的 gitignore 文件 (引用)。
    $ git config --global core.excludesfile '~/.gitignore'
    
    再写好 gitignore,
    $ cat ~/.gitignore 
    .vimrc
    
    这样,就 OK 了!

    Thursday, March 10, 2011

    Git remote add

    今天打算下载 DRM 的源代码,但是公司的网络访问 git.kernel.org 特别慢。而内核的源码树,加上所有的修改历史,大概有 1GB 吧,就算 git 能够先压缩再下载,也得几百兆。可怜下载速度只有十几 KB 每秒。再联想到 git 不能续传...

    公司倒是有自己的内核源码树,就在北京本地的服务器上。从那儿下载能有十几 MB 每秒,所以花几十秒钟就可以了。怎么办呢?毕竟两个源码树肯定是大同小异的。

    这,就是 git 的强大之处了。且看命令:
    $ git clone git://internal/kernel-tree
    $ cd kernel-tree

    $ git remote add airlied-drm git://git.kernel.org/pub/scm/linux/kernel/git/airlied/drm-2.6.git

    $ git fetch airlied-drm drm-next:drm-next
    remote: Counting objects: 284642, done.
    remote: Compressing objects: 100% (55324/55324), done.
    remote: Total 261757 (delta 210979), reused 253047 (delta 203096)
    Receiving objects: 100% (261757/261757), 75.90 MiB | 10 KiB/s, done.
    Resolving deltas: 100% (210979/210979), completed with 8754 local objects.
    From git://git.kernel.org/pub/scm/linux/kernel/git/airlied/drm-2.6
     * [new branch]      drm-next   -> drm-next

    这样就在本地得到了一个 drm-next 分支,用来追踪 airlied 的 DRM 源码树。 同时 master 分支仍然是追踪公司自己的源码树。两者互不干扰,用 git checkout 来切换就行了。

    也就是说,一个 git 仓库可以有多个 remote,而各个 remote 既相互独立,又可以共享数据。用上面的办法只需要下载 75 兆的数据,从输出中可以看到 git 确实 "reuse" 了一些东西。

    再跟直接的 git clone 比较一下,objects 的数量有相当大的区别 (当然,我不确定这跟实际的下载量是不是一回事):
    $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/airlied/drm-2.6.git
    Cloning into drm-2.6...
    remote: Counting objects: 1908681, done.
    remote: Compressing objects: 100% (309630/309630), done.
    ^Cceiving objects:   0% (1198/1908681), 500.00 KiB | 11 KiB/s

    git,真是无比强大...

    Saturday, March 5, 2011

    What does the Mc in McDonald mean?

    今天又遇到一个加拿大人,姓是以 Mc 开头 (McMaster)。于是请教了一下这是为什么,终于明白了这种分两段的姓的意思。

    Mac, Mc, (甚至简写成 M'), 还有 O', 都是 son of 某某人的意思 (O' 其实是 grandson of )。McDonald 这个姓就类似于英语中的 Smithson, Johnson,从某个角度表示了这个家族的发展历史。

    我遇到过的例子有 McMaster, MacDonald, MacLeod, O'Brien搜索一下,有更深入的解释。这几个姓貌似来源于苏格兰语或者爱尔兰语。

    Friday, March 4, 2011

    where are the functions defined?

    0.  Token Concatenation

    在 C 代码中,宏定义中可以进行"文本"层面上的变换,比如在 systemd 的链表实现 (类似 Linux kernel 的链表) 中有这样的用法,
    #define LIST_FIELDS(t,name)                                             \
            t *name##_next, *name##_prev
    
    "##" 能完成所谓的 Token Concatenation, 具体解释看 gnu.org 的手册, 和维基百科,简单的说,## 能把前后两个 token 连为一个。上边定义的 LIST_FIELDS 可以这样使用,
    struct Job {
            LIST_FIELDS(Job, transaction);
            LIST_FIELDS(Job, run_queue);
    }
    
    编译的时候就能扩展为
    struct Job {
            Job *transaction_next, *transaction_prev;
            Job *run_queue_next, *run_queue_prev;
    }
    
    这样确实很聪明,也很省事。但是如果你不熟悉这种用法,初读代码会一头雾水,因为遇到下边这样的代码时,根本找不到这些 _next 和 _prev 变量的定义,无论是用 cscope 还是 grep。
    assert(!j->transaction_next);
    assert(!j->transaction_prev);
    

    1. 没有定义的 to_string 和 from_string 函数

    最近看 systemd 代码,遇到了一个叫 log_target_from_string 的函数。某个头文件有它的声明,
    LogTarget log_target_from_string(const char *s);
    
    但是却找不到它的定义。最后发现是这样的,
    /* src/log.c */
    
    static const char *const log_target_table[] = {
            [LOG_TARGET_CONSOLE] = "console",
            [LOG_TARGET_SYSLOG] = "syslog",
            [LOG_TARGET_KMSG] = "kmsg",
            [LOG_TARGET_SYSLOG_OR_KMSG] = "syslog-or-kmsg",
            [LOG_TARGET_NULL] = "null",
            [LOG_TARGET_AUTO] = "auto"
    };
    
    DEFINE_STRING_TABLE_LOOKUP(log_target, LogTarget);
    
    /* src/util.h */
    
    #define DEFINE_STRING_TABLE_LOOKUP(name,type)                           \
            const char *name##_to_string(type i) {                          \
                    if (i < 0 || i >= (type) ELEMENTSOF(name##_table))      \
                            return NULL;                                    \
                    return name##_table[i];                                 \
            }                                                               \
            type name##_from_string(const char *s) {                        \
                    type i;                                                 \
                    unsigned u = 0;                                         \
                    assert(s);                                              \
                    for (i = 0; i < (type)ELEMENTSOF(name##_table); i++)    \
                            if (name##_table[i] &&                          \
                                streq(name##_table[i], s))                  \
                                    return i;                               \
                    if (safe_atou(s, &u) >= 0 &&                            \
                        u < ELEMENTSOF(name##_table))                       \
                            return (type) u;                                \
                    return (type) -1;                                       \
            }                                                               \
            struct __useless_struct_to_allow_trailing_semicolon__
    
    也就是说有一个通用的宏 DEFINE_STRING_TABLE_LOOKUP,它提供了 to_string 和 from_string 这两个函数的“模板”。

    systemd 需要进行很多的 enum 数值和对应的字符串之间的转换,这些对应关系都是定义在一个个的字符串数组中,比如上面的 log_target_table。转换操作其实很简单,无非就是在数组中进行查找,但是代码树里有很多这样的数组,也有很多的 enum 类型。如果要给每个数组都定义自己的 to_string 和 from_string 函数,那肯定是一大坨重复代码。

    使用 token concatenation 显然是一个很聪明的办法,达到的效果跟 C++ 的模板机制是类似的,能够大幅度地减少代码的重复。DRY! (C vs. C++... 不敢妄下评论,you be the judge)。

    (定义一个通用的查找函数也许是另一种办法)。

    Wednesday, March 2, 2011

    how gnome starts

    关于 Linux 的启动过程,网上有不少资料。比如这个,从按下开机键到创建 TTY 登录界面这个过程讲的很清楚。但是没有讲到这之后发生的故事。gdm 是怎么启动的?X server 又是怎么启动的?

    运行下 pstree,会发现这样一个进程树结构。
    init-
         |-gdm-binary-+-gdm-simple-slav-+-Xorg
         |            |                 |-gdm-session-wor-+-gnome-session-+-applet.py
         |            |                 |                 |               |-gdu-notificatio
         |            |                 |                 |               |-gnome-panel---{gnome-panel}
         |            |                 |                 |               |-gnome-power-man---{gnome-power-man}
         |            |                 |                 |               |-gnome-volume-co
         |            |                 |                 |               |-kerneloops-appl
         |            |                 |                 |               |-metacity---{metacity}
         |            |                 |                 |               |-nautilus---2*[{nautilus}]
         |            |                 |                 |               |-nm-applet---{nm-applet}
         |            |                 |                 |               |-polkit-gnome-au---2*[{polkit-gnome-au}]
         |            |                 |                 |               |-stardict---4*[{stardict}]
         |            |                 |                 |               `-2*[{gnome-session}]
         |            |                 |                 `-{gdm-session-wor}
         |            |                 `-{gdm-simple-slav}
         |            `-{gdm-binary}
    

    这些是如何发生的?

    下边是一个基于 Foresight Linux 的很简单的讲解。但是应该也适用于所有建立在传统的 sysvinit 之上的 Linux 发行版。而至于 upstart 和 systemd 等等就不适用了。

    1. init -> /etc/X11/prefdm

    我们都知道 kernel 在启动之后,会执行 init 程序。而 init 的行为是由 /etc/inittab 定义的。

    我的系统上 inittab 有这样一行:
    $ tail -1 /etc/inittab 
    x:5:respawn:/etc/X11/prefdm -nodaemon
    
    也就是说在进入 runlevel 5 的时候,init 会调用 /etc/X11/prefdm。这是 X11 目录下的一个文件,我们应该能大体猜出它的用处。

    小插曲: 这个文件其实是 initscripts 提供的,
    $ conary q --path /etc/X11/prefdm
    initscripts:runtime=foresight.rpath.org@fl:2-qa/9.12.1-0.3-1
    

    2. /etc/X11/prefdm -> gdm

    /etc/X11/prefdm 是个很短的 shell 脚本。作用很简单,它会依次尝试运行 gdm, kdm 等等。对 GNOME 来说就是相当于 exec gdm。这样 gdm 进程就取代了 prefdm。

    3. gdm -> Xorg

    你可能发现了,直到现在 X server 还没有启动呢。上边的进程树中也体现出来了,gdm 是 init 的子进程,而 Xorg 是 gdm 的子进程。

    gdm 的全称是 GNOME Display Manager,是 X display manager 的一种。维基百科说了,gdm 是对默认的 xdm 的一个更加易用的替换。

    那么什么是 X display manager?还看维基百科
    When the display manager runs on the user's computer, it starts the X server before presenting the user the login screen
    也就是说,gdm 会首先启动 X server (比如在 tty7 上),然后把登录界面显示出来。这样用户就可以登录了。

    其实就是这么简单。再输入密码,gnome-session, gnome-panel 等等就会启动,你就能进入到你的桌面了。