• Linux内核设计与实现 第20章 补丁、开发和社区


    20.1社区

    如果一定要让Linux内核社区在现实世界中找到它的位置,那它也许会叫做内核邮件列表(Linux Kernel Mailing List)之家。内核邮件列表(或者简写成lkml)是对内核进行发布、讨论、争辩和打口水仗的主战场。

    在做任何实际的动作之前,新特性会在此处被讨论,新代码的大部分也会在此处张贴。这个列表每天发布的消息超过300条,所以决不适合心血来潮的玩主。任何想踏踏实实研究、认认真真开发内核的人都应该订阅它(至少要订阅它的摘要或者是它的归档资料)。单单看看这些奇才们使出的一招一式,也能让你受益匪浅了。
    你可以通过向majordomo@vger.kermel.org发送下面的纯文本消息订阅这个邮件列表:
    subacribe linux-kernel

    关于这方面更为详细的信息可以在htp://ger.kermel.org/中找到,此外在htp:/:/ww.tx.orglkm/

    还有一个专门的FAQ.网上还有无数与内核相关或与普通的Lioux使用相关的资源。
    htp://kermelnewbies.org/ 是一方适合内核开发初级黑客的乐土——该网站几乎能够满足所有磨刀霍霍向内核的新手的需求。

    还有两个网站也是不错的资源,
    包括htp://www.1wn.net, Linux 新闻周刊,它有一个专区报道有关内核的重要新闻;
    htp:/kermelhewbies.org/,内核直通车,提供关于内核开发-针见血的评论。

    20.2 Linux编码风格

    社区给我们提供了学习和贡献内核的地方, 但是为了避免不必要的麻烦(被别人指责或者无人理睬), 首先得好好了解一些内核代码的编码风格.

    linux的编码风格都记录在 Documentation/CodingStyle 内核开发前要好好研读以下

    1)缩进

    缩进风格是用制表位(Tab)每次缩进8个字符长度。这不是说用8个空格缩进就行了。这里的规定很明确,每次缩进通过制表位进行,每个制表位8个字符长度。例如:

    static void get_new_ship(const char * name)
    {
            if {!name)
            name = DEFAULT_SHIP_NAME;
            get_new_ship_with_name (name);
    }
    

    不知为什么,虽然违反它会对可读性带来非常大的冲击,但这个规定还是最容易被人们违反。八个字符长度的缩进能让不同的代码块看起来一目了然,特别是在连续几个小时的开发之后,效果更加明显。当然,随着缩进层数的增加,八字符制表位的左侧可用空间就所剩不多了。
    这是因为每行最多有80个字符(参见20.2.2节)。Linus极其反对这样做,他认为代码不应当复杂、费解到需要两级或者三级缩进。如果真的需要多层缩进,他建议,应当重构你的代码,把复杂的层次关系(为此形成多层缩进)分解为独立的功能。

    2)switch语句

    Switch语句下属的case标记,应该缩进到和switch声明对齐,这样将有助于减少8个字符的
    tab键带来的排版缩进,比如

    switch (animal){
    case ANIMAL_ CAT;
           handle_ cats();
           break;
    case ANIMAL_ WOLP;
           handle_ wolves() ;
           /* fall through */
    case ANIMAL DOG:
            handle_ dogs{);
            break;
    default :
            printk (KERN _WARNING "Unknown animal $d!\n", animal);
    }        
    

    当执行逻辑需要有意地从一个case声明尾部进入另外一个case声明时,对其进行评注。无疑是一个普遍的(良好的)实践经验。如示例中所见。

    3)空格

    这一节讨论给符号和关键字加空格,而不涉及在缩进中加空格(这将在后面两节中讨论)。
    一般来说,Linux 的编码风格规定,空格放在关键字周围,函数名和圆括号之间无空格。例如:

    if (foo)
    while (foo)
    for(i=0;i<NR_CPUS;i++)
    switch (foo)
    

    相反,函数、宏以及与函数相像的关键字(例如sizeof、Typeof 以及alignof)在关键字和圆括号之间没有空格。

    wake_up_process(task);
    size_t nlongs = BITS_TO_LONG(nbits);
    int len = sizeof(struct task_ struct);
    typeof (*p)
    __alignof__(struct sockaddr *)
    __attribute__((packed))
    

    在括号内,如前所示,参数前后也不加空格。例如,下 面这是禁止的:

    int prio = task_prio( task ); /* BAD STYLE! */
    

    对于大多数二元或者三元操作符,在操作符的两边加上空格。例如:

    intsum=a+b;
    int product = a * b;
    int mod =a % b;
    intret=(bar)?bar:0;
    return (ret ? 0 : size);
    intnr=nr?:1;/*allowedshortcut,sameas"nr?nr:1"*/
    if(x<y)
    if (tsk->flags & PF_ SUPERPRIV)
    mask = POLLIN
    POLLRDNORM;
    

    相反,对于大多数一元操作符,在操作符和操作数之间不加空格:

    if (!foo)
    int len = foo.len;
    struct work_struct *work = &dwork->work;
    f00++;
    -bar;
    unsigned 1ong inverted = -mask;
    

    在提领运算符的周围加上合适的空格尤为重要。正确的风格是:

    char *strcpy(char *dest, const char *src)
    

    在提领运算符的一边加上空格是不良的风格:

    char * strcpy(char * dest, const char * src) /* BAD STYLE */
    

    把提领运算符放在紧挨类型的地方也是借用C++风格的一种不良作风:

    char* strcpy(char* dest, const char* src) /* BAD STYLE ◆/
    

    4)花括号

    花括号的使用不存在技术上的差异,完全是个人喜好问题,但我们还是必须宣传一致的风格。内核选定的风格是左括号紧跟在语句的最后,与语句在相同的一行。而右括号要新起一行,作为该行的第一个字符。如下例:

    if (strncmp(buf, "NO_". 3) = 0) {
            neg = 1;
            cmp += 3;
    }
    

    注意,如果接下来的标识符是相同语句块的一部分,那么右花括号就不单独占一行,而是与那个标识符在同一行,例如:

    if (ret) {
            sysct1_sched_rt_period = old_period;
            sysct1_sched_rt_runtime = old_runtime;
    } else {
            def_rt_bandwidth.rt_runtime = global_rt_runtime();
            def_rt_bandwidth.rt. period = ns_t_ktime{global_rt_period));
    }       
    

    还有,

    do {
            percpu_ counter_add(&ca->cpustat[idx],val);
            ca = ca->parent;
    } while (ca);
    

    函数不采用这样的书写方式,因为函数不会在内部嵌套定义:

    unsigned 1ong func (void)
    {
    /* ... */
    }
    

    最后,不需要一定使用括号的语句可以忽略它:

    if (cnt > 63)
            cnt = 63;
    

    所有这些方法原理都源自K&R日。大多数编码风格都遵循K&R风格,这是在那本著名的书中所使用的C编码风格。

    5)每行代码的长度

    源代码中要尽可能地保证每行代码长度不超过80个字符,因为这样做可使代码最适合在标准的80X24的终端上显示。事实上,并不存在一个广 泛接受的标准——如果代码行超过80应该折到下一行。有些开发者也许根本不理会代码跨行问题,而是让编辑器以可读的方式处理代码的显示;而有些开发者会手动插入断行符来分割代码行,他们也许会在新行头插入两个tab键以便和原先行错开。类似的,有些开发者会在圆括号内来分行,对齐排列函数参数,比如:

    static void get_new_parrot(const char *name,
            unsigned 1ong di sposition,
            unsigned long feathe_quality)
    

    而另一些开发者虽然也会将参数分行输入,但却不会把它们对齐排列,而是在开头简单的加入两个标准tab。比如:

    int find_pirate_flag_by_color(const char *color,
            const char *name,int len)
    

    因为分行没有确定的规则,所以开发者在这点上可采取自由行动。大多数内核贡献者(包括我在内)更愿意采用前-一个例子中的方式:把大于80个字符的行进行拆分,尽量让新产生的行与前一行对齐。

    6)命名规范

    名称中不允许使用骆驼拼写法(CamelCase)、 Studly Caps或者其他混合的大小写字符。局部变量如果能够清楚地表明它的用途,那么选取idx甚至是i这样的名称都是可行的。而像theLoopIndex这样冗长繁复的名字不在接受之列。匈牙利命名法(在变量名称中加入变量的类别)是不必要的,绝对不允许使用——要知道这里是C,不是Java ;用的是Unix, 不是Windows。

    而全局变量和函数应该选择包含描述性内容的名称,并且使用小写字母,必要时加上下划线区分单词。给一个全局函数起名为atty()会使人迷惑;而像get_active_tty()这样就比较容易让人接受了。这里是Linux,不是BSD。

    7)函数

    根据经验,函数的代码长度不应该超过两屏,局部变量不应超过10个。一个函数应该功能单一并且实现精准。将一个函数分解成一些更短小的函数的组合不会带来危害。如果你担心函数调用导致的开销,可以使用inline关键字。

    8)注释

    代码的注释非常重要,但注释必须按照正确的方式进行。一般情况下,你应该描述的是你的代码要做什么和为什么要做,而不是具体通过什么方式实现的。怎么实现应该由代码本身展现。
    如果你不是这样做的,那么应该回过头去考虑一下你写的东西了。此外,注释不应该包含谁写了哪个函数、修改日期和其他那些琐碎而无实际意义的内容。这些信息应该集中在文件最开头的地方。
    虽然gcc也支持C++风格的注释符号,但内核只使用C风格的注释符号。内核中一条注释看起来像是这样:

    /* get_ ship_ speed() - return the current speed of the pirate ship
    We need this to calculate the ship coordinates.As this function can sleep,
    当do not call while holding a spinlock
    */
    

    在注释中,重要信息常常以“XXX:”开头,而bug通常以“FIXME:”开头,就像:

    /*
    FIXME: We assume dog == cat which may not be true in the future
    */
    

    内核包含一套自动文档生成工具。它源自GNOME-doc,略加修改后命名为Kemnel-doc.如果想要生成独立的HTML格式文档,运行
    make htmldocs
    如果想要postscript格式的话,用下列命令:
    make psdocs
    工具GNOME-doc可以按照你需要的特定的格式对你的函数进行注解,这样该工具也可以为你的函数服务:

    /**
    *find_ treasure find 'X marks the spot'
    *@map _treasure map
    *@time - time the treasure was hidden
    *Must ca1l while holding the pirate_ ship_ 1ock .
    */
    void find_ treasure (int map,struct timeval time)
    {
    /*...*/
    }
    

    有关此方面更多的细节请参看Documentation/kernel-doc -nano-HOWTO.txt文件。

    9)typedef

    内核开发者们强烈反对使用typedef语句。他们的理由是:
    ●typedef掩盖了数据的真实类型。
    ●由于数据类型隐藏起来了,所以很容易因此而犯错误,比如以传值的方式向栈中推人结构。
    ●使用typedef往往是因为想要偷懒。
    有些程序员往往是为了少敲打几次健盘而使用typedef,比如typedef unsigned char uchar.而这种缩写可能会引发理解和一致性上的问题,所以仅仅出于此目的而使用typedef被作者视为懒情行为

    无论如何,就算是为了别惹人耻笑吧,尽量少用typedef。
    当然,typedef 也有它施展身手的时候:当需要隐藏变量与体系结构相关的实现细节的时候,当某种类型将来有可能发生变化,而现有程序必须要考虑到向前兼容问题的时候,都需要typedef。使用typedef要谨慎,只有在确实需要的时候再用它;如果仅仅是为了少敲打几下键盘,别使用它。

    10)多用现成的东西

    请勿闭门门造车。内核本身就提供了字符串操作函数、压缩函数和一个链表接口,所以请使用它们。
    不要为了使现存接口更通用化而对它们进行新的封装,请直接使用内核提供的接口。
    你经常会发现,当把一段代码从某个操作系统移植到Linux上的时候,表面好像看起来根本没什么问题,可是隐藏在接口下面复杂的函数调用却往往是与它原有的内核相关的。没人愿意面对这些问题,所以请直接使用内核提供的接口。

    11)在源码中减少使用ifdef

    我们不赞成在源码中使用ifdef 预处理指令。你绝不应该在自己的函数中使用如下的实现方法:

       .....
    #ifdef CONFIG_ FOO
            foo();
    #endif
       .....
    

    相反,应该采取的方法是在CONFIG_ FOO没定义的时候让foo() 函数为空。

    #ifdef CONFIG_FOO
    static int foo (void)
    /*.....*/
    #else
    static inline int foo(void) { }
    #endif /*CONFIG_FOO*/
    

    这样,你在任何情况下都能调用foo()了。让编译器去做这些工作好了。

    12)结构初始化

    结构初始化的时候必须在它的成员前加上结构标识符。这种初始化能避免错误地使用其他结构而引发一个初始化错误。它也支持使用忽略值。不幸的是,C99标准改用了一种丑陋的格式来表示这种标识符,于是gcc就再也不支持原来GNU风格的标识符了,尽管它看起来确实要更帅一些。结果,内核代码现在必须都要使用新的C99标识符格式了,不管它有多难看:

    struct foo my_foo = {
            .a = INITIAL_A,
            .b= INITIAL B,
    };        
    

    其中a和b是结构体foo的成员,而INITIAL_ A和INITIAL_B 是它们对应的初始值。如果一个字段没有给初始值,那么它就会被设置为ANSI C规定的默认值( 如指针被设为NULL,整型被设为0,浮点数被设置为0.0)。
    举例来说,如果foo结构体还有一个int型的c成员,那么上面的初始化语句执行之后c会被设置为0。.

    13)代码的事后修正

    即使你得到了一段与内核编码风格风马牛不相及的代码,也不用发愁。只消抬抬手,indent工具就能帮你解决它。indent 是一个在大多数Linux系统中都能找到的好工具,它可以按照指定的方式对源代码进行格式化。默认情况下它按照不怎么好看的GNU编码风格格式化代码。想要用Linux内核编码风格,执行下列命令:

    indent -kr -i8 -ts8 -sob -180 -ss -bs -psl <file>
    

    这样就能调用该工具按内核编码风格对你的代码进行格式化了。此外,还可以通过scripts/Lindent自动按照所需的格式调用indent。

    20.3管理系统

    内核黑客就是那些从事内核开发工作的人。做这些工作有些人是因为钱,有些人是因为嗜好,但几乎所有人都是为了从中找到快乐。所有做出卓越贡献的黑客都能在源代码树根目录上的CREDITS文件中留名。

    内核中几乎每个部分都对应一个维护者。维护者是指一个或几个对内核特定部分负责的人。
    比如,每个单独的驱动程序都对应一个维护者。 每个内核子系统( 如网络)也有一个维护者。驱动程序和子系统的维护者也能在源代码树根目录上的MAINTAINERS文件中找到。

    还有一类特殊的维护者称作内核维护者。这些人负责维护的实际上就是代码树本身。以前,由Linus自己负责维护开发版的内核(乐趣尽在此中),稳定版最开始的一段时间也由他来维护。等到该内核稳定下来了,他就会把火炬传递给最好的内核开发者中的一些人手上,由这些人负责维护该代码树,而Linus会转身启动下一开发版本的内核开发工作。在2.6内核继续保持稳定的“新世界秩序”前提下,Linus仍然维护着2.6系列的内核。另外的开发者以严格的“发现bug-修订bug”模式维护2.4系列内核。

    20.4提交错误报告

    如果碰到了一个bug,最理想的应对无疑是写出修正代码,创建补丁,测试后提交它,这个流程在20.5节会仔细介绍。当然,也可以报告这个问题,然后让其他人替你解决。

    提交一个错误报告最重要的莫过于对问题进行清楚的描述。要讲清楚症状、系统输出信息、完整并经过解码的oops(如果有的话)。更重要的是,你应该尽可能地提供能够准确地重现这个错误的步骤,并提供你的机器的硬件配置基本信息。

    然后再来考虑把这个错误报告发送给谁。在内核源代码书的根目录中,MAINTAINERS 文件列举出了每个相关的设备驱动程序和子系统的单独信息——接收关于其所维护的代码的所有问题。如果找不到对此问题感兴趣的人,那么就把它报告给位于linux kernel@vger.kernel.org的内核邮件列表。即使你已经找到维护者了,贴一份副本在那里也不会有什么坏处。

    文档REPORTING-BUGS和Documentation/oops -tracing.txt中有更多相关信息。

    20.5补丁

    1)通过两份内核源代码创建补丁

    创建补丁最简单的办法是通过两份内核源代码进行,一份源码,另一份是加进了所修改部分的源代码。一般会给原来的内核代码起名linux-x.y.z (其实就是把源代码包解压缩后所得到的文件夹),而修改过的就起名为linux。然后利用下面的命令通过这两份代码创建补丁:

    diff -urN 1inux-x.y.z/ 1inux/ > my-patch
    

    你可以在自己的目录下运行该命令,一般都是在home目录下,而不是在/usr/src/inux目录下进行这种操作,所以不一定必须具备超级用户权限。
    -u参数指定使用特殊的diff输出格式。否则得到的patch格式怪异,一般人都无法看懂。
    -r参数保证会遍历所有子目录进行操作,
    -N参数指明做出修改的源代码中所有新加入的文件在diff操作时会包含在内。
    另外,如果想对一个单独的文件进行diff,你也可以这么做:

    diff -u 1inux-x.y. z/some/file linux/some/file > my-patch
    

    注意,在你自己代码所在的目录下执行diff很重要。这样创建的补丁别人用起来更方便,哪怕他们的目录名字叫differ也没问题。执行一个这样生成的补丁,只需要在你自己代码树的根目录执行下列命令就可以了:

    patch -p1 < ... /my-patch
    

    在这个例子中,补丁的名字叫my-patch,它位于当前目录的上一级目录中。
    -p1参数用来剥去补丁中头-一个目录的名称。这么做的好处是可以在打补丁的时候忽略创建补丁的人的目录命名
    习惯。
    diffstat是一个很有用的工具,它可以列出补丁所引起的变更的统计(加入或移去的代码行)。输出关于补丁的信息,执行:

    diffstat -p1 my-patch
    

    在向lkml贴出自己的补丁时,附带上这份信息往往会很有用。由于patch(1)会忽略第一个diff之前的所有内容,所以你甚至可以在patch的最前面直接加上简短的说明。

    2)用Git创建补丁

    # 提交修改的或新增的代码
    git commit -a   # 提交所有修改的代码
    #OR
    git commit linux-src/some/file.c  # 提交某个修改的代码
    #OR
    git add linux-src/some/new-file.c   # 把新增的文件加入版本库
    git commit -a       # 提交新增的文件
    
    # 生成补丁(patch)
    git format-patch -N  # N 是正整数, 这条命令生成最后N次提交产生的补丁
    #OR
    git format-patch -1  # 最后1次提交产生的补丁
    
    # 应用补丁(patch): 和第一种diff命令方法一样
    

    3)提交补丁

    补丁可以按照20.5.2节描述的方式创建。
    如果补丁涉及了某个特定的驱动程序或子系统,应该把它发给MAINTAINER中列举的相关部分的维护者。此外,还应该向Linux 内核邮件列表linux _kermel@vger.kernel.org发送一份拷贝。只有在经过广泛的讨论之后,或者是补丁所做的修改很细微并且很容易就能保证正确的时候,才应该向内核维护者(比如说Linus)提交。

    一般包含一份补丁的邮件,它的主题一栏内容应该以“[PATCH]简要说明”的格式写出。
    邮件的主体部分应该描述所做的改变的技术细节,以及要做这些的原因,越详细越准确越好。在E-mail中还要注明补丁对应的内核版本。

    内核开发者们都希望能通过邮件阅读补丁,并且能够将其保存为一个单独文件。因此,最好把补丁直接插人邮件,放在所有信息的最后。还要小心一些,差劲的邮件客户端工具,它们会加入信息或者改变邮件的格式;这会导致补丁出错,从而引起其他开发者的不满。如果你用的邮件客户端工具也有类似表现,就检查一下, 看它是否有“Insert Inline"、“Preformat” 或类似功能。
    如果有的话,就用纯文本方式把你的补丁贴到邮件上作为附件,不要对它做什么编码工作。

    如果你的补了很大或者包含对几个不同的逻辑的修改,那么应该将你的补丁分成几块,每块对应一个逻辑。
    比如你在补丁中引入了一个新的API,并且同时对几个驱动程序进行了修改以便利用它,那么应该把该补丁一分为二(先是新的API,然后是对驱动程序的修改),邮件也写成两份。如果任何一个部分需要其他的补丁先行,要明确地注明这一点。

    提交之后,保持耐心,等待答复。别因为某些反对言论而灰心——至少你还是得到回应了嘛!和其他人讨论这个问题并且在需要的时候应该提交修正过的新补丁。如果你压根就没听到回声,想想是什么出了问题,然后着手解决它。多请邮件列表和维护者们提出宝贵意见。运气好的话,未来版本的内核发行时,你可能就会看到自己做出的修改了——那可就真的该恭喜你了!

  • 相关阅读:
    idea中使用git创建分支与标签
    Vue 之 Toast 消息提示插件的简单封装
    【Conda】Conda多环境管理和环境切换教程
    No.7软件需求规格说明书及UML
    关于电影的HTML网页设计-威海影视网站首页-电影主题HTM5网页设计作业成品
    【战斗吧,青春!】
    .net----泛型
    SpringCloud中注册中心Nacos的下载与使用步骤
    公司缺人自己搞了vue又搞koa,熬夜把架子搭起来
    SQL Server实战二:创建、修改、复制、删除数据库表并加以数据处理
  • 原文地址:https://blog.csdn.net/weixin_55255438/article/details/127082924