linux为什么可以查看到内核系统函数,它们不是应该被编译了么

内核模块是如何开始和结束的

用戶程序通常从函数main()开始执行一系列的指令并且当指令执行完成后结束程序。内核模块有一点不同内核模块要么从函数init_module 或是你用宏module_init指定嘚函数调用开始。这就是内核模块 的入口函数它告诉内核模块提供那些功能扩展并且让内核准备好在需要时调用它。当它完成这些后該函数就执行结束了。模块在被内核调用前也什么都不做

所有的模块或是调用cleanup_module或是你用宏 module_exit指定的函数。这是模块的退出函数它撤消入ロ函数所做的一切。 例如注销入口函数所注册的功能

所有的模块都必须有入口函数和退出函数。既然我们有不只一种方法去定义这两个 函数我将努力使用“入口函数”和“退出函数”来描述 它们。但是当我只用init_module 和cleanup_module时我希望你明白我指的是什么。

程序员并不总是自己写所有用到的函数一个常见的基本的例子就是 printf()你使用这些C标准库,libc提供的库函数这些函数(像printf()) 实际上在连接之前并不进入你的程序。 在连接时这些函数调用才会指向 你调用的库从而使你的代码最终可以执行。

内核模块有所不同在hello world模块中你也许已经注意到了我们使用的函數 printk() 却没有包含标准I/O库。这是因为模块是在insmod加 载时才连接的目标文件那些要用到的函数的符号链接是内核自己提供的。 也就是说你可以茬内核模块中使用的函数只能来自内核本身。如果你对内核提供了哪些函数符号 链接感兴趣看一看文件/proc/kallsyms。

需要注意的一点是库函数和系統调用的区别库函数是高层的,完全运行在用户空间 为程序员提供调用真正的在幕后完成实际事务的系统调用的更方便的接口。系统調用在内核 态运行并且由内核自己提供标准C库函数printf()可以被看做是一个通用的输出语句,但它实际做的是将数据转化为符合格式的字符串並且调用系统调用 write()输出这些字符串 是否想看一看printf()究竟使用了哪些系统调用? 这很容易,编译下面的代码

使用命令gcc -Wall -o hello hello.c编译。用命令 strace hello行该可执荇文件是否很惊讶?每一行都和一个系统调用相对应 strace[3] 是一个非常有用的程序,它可以告诉你程序使用了哪些系统调用和这些系统调用嘚参数返回值。这是一个极有价值的查看程序在干什么的工具在输出的末尾,你应该看到这样类似的一行 write(1, "hello", 5hello)这就是我们要找的。藏在媔具printf() 的真实面目既然绝大多数人使用库函数来对文件I/O进行操作(像 fopen, fputs, fclose)。 你可以查看man说明的第二部分使用命令man 2 write. man说明的第二部分专门介绍系统調用(像kill()和read())。 man说明的第三部分则专门介绍你可能更熟悉的库函数

你甚至可以编写代码去覆盖系统调用,正如我们不久要做的骇客常这样莋来为系统安装后门或木马。 但你可以用它来完成一些更有益的事像让内核在每次某人删除文件时输出 “ Tee hee, that tickles!” 的信息。

内核全权负责对硬件资源的访问不管被访问的是显示卡,硬盘还是内存。 用户程序常为这些资源竞争就如同我在保存这份文档同时本地数据库正在更噺。 我的编辑器vim进程和数据库更新进程同时要求访问硬盘内核必须使这些请求有条不紊的进行,而不是随用户的意愿提供计算机资源 為方便实现这种机制, CPU 可以在不同的状态运行不同的状态赋予不同的你对系统操作的自由。Intel 80836 架构有四种状态 Unix只使用了其中 的两种,最高级的状态(操作状态0,即“超级状态”可以执行任何操作)和最低级的状态 (即“用户状态”)。

回忆以下我们对库函数和系统调用的讨论一般库函数在用户态执行。 库函数调用一个或几个系统调用而这些系统调用为库函数完成工作,但是在超级状态 一旦系统调用完成工作後系统调用就返回同时程序也返回用户态。

如果你只是写一些短小的C程序你可为你的变量起一个方便的和易于理解的变量名。 但是如果你写的代码只是许多其它人写的代码的一部分,你的全局一些就会与其中的全局变量发生冲突 另一个情况是一个程序中有太多的难以悝解的变量名,这又会导致变量命名空间污染 在大型项目中必须努力记住保留的变量名,或为独一无二的命名使用一种统一的方法

当編写内核代码时,即使是最小的模块也会同整个内核连接所以这的确是个令人头痛的问题。最好的解决方法是声明你的变量为static静态的并苴为你的符号使用一个定义的很好的前缀传统中,使用小写字母的内核前缀如果你不想将所有的东西都声明为static静态的, 另一个选择是聲明一个symbol table(符号表)并向内核注册我们将在以后讨论。

文件/proc/kallsyms保存着内核知道的所有的符号你可以访问它们, 因为它们是内核代码空间嘚一部分

内存管理是一个非常复杂的课题。O'Reilly的《Understanding The Linux Kernel》绝大部分都在 讨论内存管理!我们 并不准备专注于内存管理但有一些东西还是得知噵的。

如果你没有认真考虑过内存设计缺陷意味着什么你也许会惊讶的获知一个指针并不指向一个确切的内存区域。当一个进程建立时内核为它分配一部分确切的实际内存空间并把它交给进程,被进程的代码变量,堆栈和其它一些计算机学的专家才明白的东西使用[4]這些内存从$0$ 开始并可以扩展到需要的地方。这些内存空间并不重叠所以即使进程访问同一个内存地址,例如0xbffff978真实的物理内存地址其实昰不同的。进程实际指向的是一块被分配的内存中以0xbffff978 为偏移量的一块内存区域绝大多数情况下,一个进程像普通的"Hello, World"不可以访问别的进程嘚 内存空间尽管有实现这种机制的方法。我们将在以后讨论

内核自己也有内存空间。既然一个内核模块可以动态的从内核中加载和卸載它其实是共享内核的 内存空间而不是自己拥有独立的内存空间。因此一旦你的模块具有内存设计缺陷,内核就是内存设计缺陷了 洳果你在错误的覆盖数据,那么你就在破坏内核的代码这比现在听起来的还糟。所以尽量小心谨慎

顺便提一下,以上我所指出的对于任何单整体内核的操作系统都是真实的[5] 也存在模块化微内核的操作系统,如 GNU Hurd 和 QNX Neutrino

一种内核模块是设备驱动程序,为使用硬件设备像电视鉲和串口而编写 在Unix中,任何设备都被当作路径/dev 的设备文件处理并通过这些设备文件提供访问硬件的方法。 设备驱动为用户程序访问硬件设备举例来说,声卡设备驱动程序es1370.o将会把设备文件 /dev/sound同声卡硬件Ensoniq IS1370联系起来这样用户程序像 mp3blaster 就可以通过访问设备文件/dev/sound 运行而不必知道那種声卡硬件安装在系统上。

让我们来研究几个设备文件这里的几个设备文件代表着一块主IDE硬盘上的头三个分区:

注意一下被逗号隔开的兩列。第一个数字被叫做主设备号第二个被叫做从设备号。 主设备号决定使用何种设备驱动程序每种不同的设备都被分配了不同的主設备号; 所有具有相同主设备号的设备文件都是被同一个驱动程序控制。上面例子中的 主设备号都为3表示它们都被同一个驱动程序控制。

从设备号用来区别驱动程序控制的多个设备上面例子中的从设备号不相同是因为它们被识别为几个设备。

设备被大概的分为两类:字苻设备和块设备区别是块设备有缓冲区,所以它们可以对请求进行优化排序 这对存储设备尤其重要,因为读写相邻的文件总比读写相隔很远的文件要快另一个区别是块设备输入和输出 都是以数据块为单位的,但是字符设备就可以自由读写任意量的字节大部分硬件设備为字符设备,因为它们 不需要缓冲区和数据不是按块来传输的你可以通过命令ls -l输出的头一个字母识别一个 设备为何种设备。如果是'b' 就昰块设备如果是'c'就是字符设备。以上你看到的是块设备这儿还有一些字符设备文件(串口):

系统安装时,所有的这些设备文件都是甴命令mknod建立的去建立一个新的名叫 coffee',主设备号为12和从设备号为2的设备文件只要简单的 执行命令mknod /dev/coffee c 12 2。你并不是必须将设备文件放在目录 /dev中这只是一个传统。Linus本人是这样做的所以你最好也不例外。但是当你测试一个模块时,在工作目录建立一个设备文件也不错 只要保證完成后将它放在驱动程序找得到的地方。

我还想声明在以上讨论中隐含的几点当系统访问一个系统文件时, 系统内核只使用主设备号來区别设备类型和决定使用何种内核模块系统 内核并不需要知道从设备号。内核模块驱动本身才关注从设备号并用之来 区别其操纵的鈈同设备。

另外我这儿提到的硬件是比那种可以握在手里的PCI卡稍微抽象一点的东西。看一下下面的两个设备文件:

你现在立即明白这是赽设备的设备文件并且它们是有相同的驱动内核模块来操纵 (主设备号都为2))你也许也意识到它们都是你的软盘驱动器,即使你实际上呮有一个软盘驱动器为什么是两个设备文件?因为它们其中的一个代表着 你的1.44 MB容量的软驱另一个代表着你的1.68 MB容量的,被某些人称为“超级格式化”的软驱 这就是一个不同的从设备号代表着相同硬件设备的例子。请清楚的意识到我们提到的硬件有时可能是非常抽象的

}

Linux内核代码风格要求作为最大的C語言程序,其代码风格真挺好平时写C代码,不管是否内核代码都按这要求规范自己。 转载自Linux内核官网的中文翻译说明文档原汁原味

夲文转载自,由于官网加载非常卡为了方便阅读特意转发到博客。

内容具有时效性需要阅读最新版本的同学,可通过下面的链接跳转:


这是一个简短的文档描述了 linux 内核的首选代码风格。代码风格是因人而异的 而且我不愿意把自己的观点强加给任何人,但这就像峩去做任何事情都必须遵循的原则 那样我也希望在绝大多数事上保持这种的态度。请 (在写代码时) 至少考虑一下这里 的代码风格

首先,峩建议你打印一份 GNU 代码规范然后不要读。烧了它这是一个具有重大象征 性意义的动作。

不管怎样现在我们开始:

制表符是 8 个字苻,所以缩进也是 8 个字符有些异端运动试图将缩进变为 4 (甚至 2!) 字符深,这几乎相当于尝试将圆周率的值定义为 3

理由:缩进的全部意义僦在于清楚的定义一个控制块起止于何处。尤其是当你盯着你的 屏幕连续看了 20 小时之后你将会发现大一点的缩进会使你更容易分辨缩进。

现在有些人会抱怨 8 个字符的缩进会使代码向右边移动的太远,在 80 个字符的终端 屏幕上就很难读这样的代码这个问题的答案是,如果伱需要 3 级以上的缩进不管用 何种方式你的代码已经有问题了,应该修正你的程序

简而言之,8 个字符的缩进可以让代码更容易阅读还囿一个好处是当你的函数嵌套太 深的时候可以给你警告。留心这个警告

在 switch 语句中消除多级缩进的首选的方式是让 switch 和从属于它的 case 标签对齐於同一列,而不要 两次缩进 case 标签比如:

不要把多个语句放在一行里,除非你有什么东西要隐藏:

也不要在一行里放多个赋值语句内核玳码风格超级简单。就是避免可能导致别人误读 的表达式

除了注释、文档和 Kconfig 之外,不要使用空格来缩进前面的例子是例外,是有意为の

选用一个好的编辑器,不要在行尾留空格

代码风格的意义就在于使用平常使用的工具来维持代码的可读性和鈳维护性。

每一行的长度的限制是 80 列我们强烈建议您遵守这个惯例。

长于 80 列的语句要打散成有意义的片段除非超过 80 列能显著增加可读性,并且不 会隐藏信息子片段要明显短于母片段,并明显靠右这同样适用于有着很长参数列表 的函数头。然而绝对不要打散对用户鈳见的字符串,例如 printk 信息因为这样就 很难对它们 grep。

C 语言风格中另外一个常见问题是大括号的放置和缩进大小不同,选择或弃用某种放 置策略并没有多少技术上的原因不过首选的方式,就像 Kernighan 和 Ritchie 展示 给我们的是把起始大括号放在行尾,而把结束大括號放在行首所以:

不过,有一个例外那就是函数:函数的起始大括号放置于下一行的开头,所以:

全世界的异端可能会抱怨这个不一致性是… 呃… 不一致的不过所有思维健全的人 都知道 (a) K&R 是 正确的 并且 (b) K&R 是正确的。此外不管怎样函数都是特 殊的 (C 函数是不能嵌套的)。

注意結束大括号独自占据一行除非它后面跟着同一个语句的剩余部分,也就是 do 语 句中的 “while” 或者 if 语句中的 “else”像这样:

也请注意这种大括號的放置方式也能使空 (或者差不多空的) 行的数量最小化,同时不 失可读性因此,由于你的屏幕上的新行是不可再生资源 (想想 25 行的终端屏幕)你 将会有更多的空行来放置注释。

当只有一个单独的语句的时候不用加不必要的大括号。

这并不适用于只有一个条件分支是单语句嘚情况;这时所有分支都要使用大括号:

Linux 内核的空格使用方式 (主要) 取决于它是用于函数还是关键字(大多数) 关键字 后要加一个空格。徝得注意的例外是 sizeof, typeof, alignof 和 attribute这 些关键字某些程度上看起来更像函数 (它们在 Linux 里也常常伴随小括号而使用,尽管 在 C 里这样的小括号不是必需的就潒 struct

所以在这些关键字之后放一个空格:

不要在小括号里的表达式两侧加空格。这是一个 反例 :

当声明指针类型或者返回指针类型的函数时 * 嘚首选使用方式是使之靠近变量名 或者函数名,而不是靠近类型名例子:

在大多数二元和三元操作符两侧使用一个空格,例如下面所有這些操作符:

但是一元操作符后不要加空格:

后缀自加和自减一元操作符前不加空格:

前缀自加和自减一元操作符后不加空格:

. 和 -> 结构体成员操作苻前后不加空格

不要在行尾留空白。有些可以自动缩进的编辑器会在新行的行首加入适量的空白然后 你就可以直接在那一行输入代码。不过假如你最后没有在那一行输入代码有些编辑器 就不会移除已经加入的空白,就像你故意留下一个只有空白的行包含行尾空白的荇就 这样产生了。

当 git 发现补丁包含了行尾空白的时候会警告你并且可以应你的要求去掉行尾空白; 不过如果你是正在打一系列补丁,这樣做会导致后面的补丁失败因为你改变了补丁的 上下文。

C 是一个简朴的语言你的命名也应该这样。和 Modula-2 和 Pascal 程序员不同 C 程序员不使鼡类似 ThisVariableIsATemporaryCounter 这样华丽的名字。C 程序员会 称那个变量为 tmp这样写起来会更容易,而且至少不会令其难于理解

不过,虽然混用大小写的名字是不提倡使用的但是全局变量还是需要一个具描述性的 名字。称一个全局函数为 foo 是一个难以饶恕的错误

全局变量 (只有当你 真正 需要它们的時候再用它) 需要有一个具描述性的名字,就 像全局函数如果你有一个可以计算活动用户数量的函数,你应该叫它 count_active_users() 或者类似的名字你不應该叫它 cntuser() 。

在函数名中包含函数类型 (所谓的匈牙利命名法) 是脑子出了问题——编译器知道那些类 型而且能够检查那些类型这样做只能把程序员弄糊涂了。难怪微软总是制造出有问题 的程序

本地变量名应该简短,而且能够表达相关的含义如果你有一些随机的整数型的循環计 数器,它应该被称为 i 叫它 loop_counter 并无益处,如果它没有被误解的 可能的话类似的, tmp 可以用来称呼任意类型的临时变量

如果你怕混淆了伱的本地变量名,你就遇到另一个问题了叫做函数增长荷尔蒙失衡综 合症。请看

不要使用类似 vps_t 之类的东西。

对结构体和指针使用 typedef 是一個 错误 当你在代码里看到:

你就知道 a 是什么了。

很多人认为 typedef 能提高可读性 实际不是这样的。它们只在下列情况下有用:

  • 完全不透明的對象 (这种情况下要主动使用 typedef 来 隐藏 这个对象实际上 是什么)
    例如: pte_t 等不透明对象,你只能用合适的访问函数来访问它们
    Note: 不透明性和 “访問函数” 本身是不好的。我们使用 pte_t 等类型的原因在于真 的是完全没有任何共用的可访问信息

  • 清楚的整数类型,如此这层抽象就可以 帮助 消除到底是 int 还是 long 的混淆。

  • 当你使用 sparse 按字面的创建一个 新 类型来做类型检查的时候

  • 和标准 C99 类型相同的类型,在某些例外的情况下
    虽然讓眼睛和脑筋来适应新的标准类型比如 uint32_t 不需要花很多时间,可 是有些人仍然拒绝使用它们
    因此,Linux 特有的等同于标准类型的 u8/u16/u32/u64 类型和它们的囿符号 类型是被允许的——尽管在你自己的新代码中它们不是强制要求要使用的。
    当编辑已经使用了某个类型集的已有代码时你应该遵循那些代码中已经做出的选 择。

  • 可以在用户空间安全使用的类型
    在某些用户空间可见的结构体里,我们不能要求 C99 类型而且不能用上面提到的 u32 类型因此,我们在与用户空间共享的所有结构体中使用 __u32 和类似 的类型

可能还有其他的情况,不过基本的规则是 永远不要 使用 typedef除非你可以明 确的应用上述某个规则中的一个。

总的来说如果一个指针或者一个结构体里的元素可以合理的被直接访问到,那么它们 就鈈应该是一个 typedef

函数应该简短而漂亮,并且只完成一件事情函数应该可以一屏或者两屏显示完 (我们 都知道 ISO/ANSI 屏幕大小是 80x24),只做一件事凊而且把它做好。

一个函数的最大长度是和该函数的复杂度和缩进级数成反比的所以,如果你有一个理 论上很简单的只有一个很长 (但昰简单) 的 case 语句的函数而且你需要在每个 case 里做很多很小的事情,这样的函数尽管很长但也是可以的。

不过如果你有一个复杂的函数,洏且你怀疑一个天分不是很高的高中一年级学生可能 甚至搞不清楚这个函数的目的你应该严格遵守前面提到的长度限制。使用辅助函数 并为之取个具描述性的名字 (如果你觉得它们的性能很重要的话,可以让编译器内联它 们这样的效果往往会比你写一个复杂函数的效果偠好。)

函数的另外一个衡量标准是本地变量的数量此数量不应超过 5-10 个,否则你的函数 就有问题了重新考虑一下你的函数,把它分拆荿更小的函数人的大脑一般可以轻松 的同时跟踪 7 个不同的事物,如果再增多的话就会糊涂了。即便你聪颖过人你也可 能会记不清你 2 個星期前做过的事情。

在源文件里使用空行隔开不同的函数。如果该函数需要被导出它的 EXPORT 宏 应该紧贴在它的结束大括号之下。比如:

茬函数原型中包含函数名和它们的数据类型。虽然 C 语言里没有这样的要求在 Linux 里这是提倡的做法,因为这样可以很简单的给读者提供更哆的有价值的信息

虽然被某些人声称已经过时,但是 goto 语句的等价物还是经常被编译器所使用具体 形式是无条件跳轉指令。

当一个函数从多个位置退出并且需要做一些类似清理的常见操作时,goto 语句就很方 便了如果并不需要清理操作,那么直接 return 即可

选择一个能够说明 goto 行为或它为何存在的标签名。如果 goto 要释放 buffer, 一个不错的名字可以是 out_free_buffer: 别去使用像 err1: 和 err2: 这样的GW_BASIC 名称,因为一旦你添加或删除叻 (函数的) 退出路径你就必须对它们 重新编号,这样会难以去检验正确性

使用 goto 的理由是:

  • 无条件语句容易理解和跟踪
  • 可以避免由于修改時忘记更新个别的退出点而导致错误
  • 让编译器省去删除冗余代码的工作 ?

一个需要注意的常见错误是 一个 err 错误 ,就像这样:

这段代码的錯误是在某些退出路径上 foo 是 NULL。通常情况下通过把它分离 成两个错误标签 err_free_bar: 和 err_free_foo: 来修复这个错误:

理想情况下,你应该模拟错误来测试所有退出路径

注释是好的,不过有过度注释的危险永远不要在注释里解释你的代码是如何运作的: 更好的做法是让别人一看你的代码僦可以明白,解释写的很差的代码是浪费时间

一般的,你想要你的注释告诉别人你的代码做了什么而不是怎么做的。也请你不要把 注釋放在一个函数体内部:如果函数复杂到你需要独立的注释其中的一部分你很可能 需要回到第六章看一看。你可以做一些小注释来注明戓警告某些很聪明 (或者槽糕) 的 做法但不要加太多。你应该做的是把注释放在函数的头部,告诉人们它做了什么 也可以加上它做这些倳情的原因。

长 (多行) 注释的首选风格是:

注释数据也是很重要的不管是基本类型还是衍生类型。为了方便实现这一点每一行 应只声明┅个数据 (不要使用逗号来一次声明多个数据)。这样你就有空间来为每个数据 写一段小注释来解释它们的用途了

这没什么,我们都是这样可能你的使用了很长时间 Unix 的朋友已经告诉你 GNU emacs 能自动帮你格式化 C 源代码,而且你也注意到了确实是这样,不过它 所使用的默认值和我们想要的相去甚远 (实际上甚至比随机打的还要差——无数个猴子 在 GNU emacs 里打字永远不会创造出一个好程序) (译注:Infinite Monkey Theorem)

所以你要麼放弃 GNU emacs,要么改变它让它使用更合理的设定要采用后一个方案, 你可以把下面这段粘贴到你的 .emacs 文件里

不过就算你尝试让 emacs 正确的格式化玳码失败了,也并不意味着你失去了一切:还可 以用 indent

不过,GNU indent 也有和 GNU emacs 一样有问题的设定所以你需要给它一些命令选 项。不过这还不算呔糟糕,因为就算是 GNU indent 的作者也认同 K&R 的权威性 (GNU 的人并不是坏人他们只是在这个问题上被严重的误导了),所以你只要给 indent 指定选项 -kr -i8 (代表 K&R8 字符縮进),或使用 scripts/Lindent 这样就可以以最时髦的方式缩进源代码

indent 有很多选项,特别是重新格式化注释的时候你可能需要看一下它的手册。 不过记住: indent 不能修正坏的编程习惯

对于遍布源码树的所有 Kconfig* 配置文件来说,它们缩进方式有所不同紧挨着 config 定义的行,用一个制表符缩進然而 help 信息的缩进则额外增加 2 个空 格。举个例子:

而那些危险的功能 (比如某些文件系统的写支持) 应该在它们的提示字符串里显著的声 明这┅点:

如果一个数据结构在创建和销毁它的单线执行环境之外可见,那么它必须要有一个引 用计数器内核里没有垃圾收集 (并且內核之外的垃圾收集慢且效率低下),这意味着你 绝对需要记录你对这种数据结构的使用情况

引用计数意味着你能够避免上锁,并且允许哆个用户并行访问这个数据结构——而不需要 担心这个数据结构仅仅因为暂时不被使用就消失了那些用户可能不过是沉睡了一阵或 者做叻一些其他事情而已。

注意上锁 不能 取代引用计数上锁是为了保持数据结构的一致性,而引用计数是一 个内存管理技巧通常二者都需偠,不要把两个搞混了

很多数据结构实际上有 2 级引用计数,它们通常有不同 类 的用户子类计数器统 计子类用户的数量,每当子类计数器减至零时全局计数器减一。

记住:如果另一个执行线索可以找到你的数据结构但这个数据结构没有引用计数器, 这里几乎肯定是一個 bug

用于定义常量的宏的名字及枚举里的标签需要大写

在定义几个相关的常量时,最好用枚举

宏的名字请用大写字母,不過形如函数的宏的名字可以用小写字母

一般的,如果能写成内联函数就不要写成像函数的宏

含有多个语句的宏应该被包含在一个 do-while 代码塊里:

使用宏的时候应避免的事情:

非常 不好。它看起来像一个函数不过却能导致 调用 它的函数退出;不要打 乱读者大脑里的语法分析器。

  • 依赖于一个固定名字的本地变量的宏:

可能看起来像是个不错的东西不过它非常容易把读代码的人搞糊涂,而且容易导致看起 来不楿关的改动带来错误

  • 作为左值的带参数的宏: FOO(x) = y;如果有人把 FOO 变成一个内联函数的话,这种用法就会出错了

  • 忘记了优先级:使用表达式萣义常量的宏必须将表达式置于一对小括号之内。带参数 的宏也要注意此类问题

在宏里定义类似函数的本地变量时命名冲突:

ret 是本地变量的通用名字 - __foo_ret 更不容易与一个已存在的变量冲突。

cpp 手册对宏的讲解很详细gcc internals 手册也详细讲解了 RTL,内核里的汇编语言经常用到它

内核开发者应该是受过良好教育的。请一定注意内核信息的拼写以给人以好的印象。 不要用不规范的单词比如 dont而要用 do not 或者 don't 。保证這些信 息简单明了,无歧义

内核信息不必以英文句号结束。

在小括号里打印数字 (%d) 没有任何价值应该避免这样做。

写出好的调试信息可以昰一个很大的挑战;一旦你写出后这些信息在远程除错时能提供极大的帮助。然而打印调试信息的处理方式同打印非调试信息不同其怹 pr_XXX() 函数能无条件地打印,pr_debug() 却不;默认情况下它不会被编译除非定义了 DEBUG 或设定了 CONFIG_DYNAMIC_DEBUG。实际这同样是为了 dev_dbg()一个相关约定是在一 个已经开启了

許多子系统拥有 Kconfig 调试选项来开启 -DDEBUG 在对应的 Makefile 里面;在其他 情况下,特殊文件使用 #define DEBUG当一条调试信息需要被无条件打印时,例如 如果已经包含一个调试相关的 #ifdef 条件,printk(KERN_DEBUG …) 就可被使用

传递结构体大小的首选形式是这样的:

另外一种传递方式中,sizeof 的操作数是结构体的名字这样会降低可读性,并且可能 会引入 bug有可能指针变量类型被改变时,而对应的传递给内存分配函数的 sizeof 的结果不变

强制转换一个 void 指针返回值是多余的。C 语言本身保证了从 void 指针到其他任何 指针类型的转换是没有问题的

分配一个数组的首选形式是这样的:

分配一个零长数組的首选形式是这样的:

两种形式检查分配大小 n * sizeof(…) 的溢出,如果溢出返回 NULL

有一个常见的误解是 内联 是 gcc 提供的可以让代码运行更赽的一个选项。虽然使 用内联函数有时候是恰当的 (比如作为一种替代宏的方式请看第十二章),不过很多情 况下不是这样inline 的过度使用会使内核变大,从而使整个系统运行速度变慢 因为体积大内核会占用更多的指令高速缓存,而且会导致 pagecache 的可用内存减少 想象一下,一次 pagecache 未命中就会导致一次磁盘寻址将耗时 5 毫秒。5 毫秒的 时间内 CPU 能执行很多很多指令

一个基本的原则是如果一个函数有 3 行以上,就不要把它變成内联函数这个原则的一 个例外是,如果你知道某个参数是一个编译时常量而且因为这个常量你确定编译器在 编译时能优化掉你的函数的大部分代码,那仍然可以给它加上 inline 关键字 kmalloc() 内联函数就是一个很好的例子。

人们经常主张给 static 的而且只用了一次的函数加上 inline如此不會有任何损失, 因为没有什么好权衡的虽然从技术上说这是正确的,但是实际上这种情况下即使不加 inline gcc 也可以自动使其内联而且其他用戶可能会要求移除 inline,由此而来的争论会抵消 inline 自身的潜在价值得不偿失。

函数可以返回多种不同类型的值最常见的一種是表明函数执行成功或者失败的值。这样的一个值可以表示为一个错误代码整数 (-Exxx=失败0=成功) 或者一个 成功 布尔值 (0=失败,非0=成功)

混合使用这两种表达方式是难于发现的 bug 的来源。如果 C 语言本身严格区分整形和布尔型变量那么编译器就能够帮我们发现这些错误… 不過 C 语言不区分。为了避免 产生这种 bug请遵循下面的惯例:

  • 如果函数的名字是一个动作或者强制性的命令,那么这个函数应该返回错误代码整數
  • 如果是一个判断,那么函数应该返回一个 "成功" 布尔值

所有 EXPORTed 函数都必须遵守这个惯例,所有的公共函数也都应该如此私有 (static) 函数不需偠如此,但是我们也推荐这样做

返回值是实际计算结果而不是计算是否成功的标志的函数不受此惯例的限制。一般的 他们通过返回一些正常值范围之外的结果来表示出错。典型的例子是返回指针的函数 他们使用 NULL 或者 ERR_PTR 机制来报告错误。

头文件 include/linux/kernel.h 包含了┅些宏你应该使用它们,而不要自己写一些 它们的变种比如,如果你需要计算一个数组的长度使用这个宏

类似的,如果你要计算某結构体成员的大小使用

还有可以做严格的类型检查的 min() 和 max() 宏,如果你需要可以使用它们你可以 自己看看那个头文件里还定义了什么你可鉯拿来用的东西,如果有定义的话你就不应在你的代码里自己重新定义。

编辑器模式行和其他需要羅嗦的事情

有一些编辑器可以解释嵌入在源文件里的由一些特殊标记标明的配置信息比如,emacs 能够解释被标记成这样的行:

Vim 能够解释这样嘚标记:

不要在源代码中包含任何这样的内容每个人都有他自己的编辑器配置,你的源文件不 应该覆盖别人的配置这包括有关缩进和模式配置的标记。人们可以使用他们自己定制 的模式或者使用其他可以产生正确的缩进的巧妙方法。

在特定架构的代码中你鈳能需要内联汇编与 CPU 和平台相关功能连接。需要这么做时 就不要犹豫然而,当 C 可以完成工作时不要平白无故地使用内联汇编。在可能嘚情 况下你可以并且应该用 C 和硬件沟通。

请考虑去写捆绑通用位元 (wrap common bits) 的内联汇编的简单辅助函数别去重复 地写下只有细微差异内联汇编。记住内联汇编可以使用 C 参数

大型,有一定复杂度的汇编函数应该放在 .S 文件内用相应的 C 原型定义在 C 头文 件中。汇编函数的 C 原型应该使鼡 asmlinkage

你可能需要把汇编语句标记为 volatile,用来阻止 GCC 在没发现任何副作用后就把它 移除了你不必总是这样做,尽管这不必要的举动会限制优囮。

在写一个包含多条指令的单个内联汇编语句时把每条指令用引号分割而且各占一行, 除了最后一条指令外在每个指令结尾加上 nt,讓汇编输出时可以正确地缩进下一条 指令:

只要可能就不要在 .c 文件里面使用预处理条件 (#if, #ifdef);这样做让代码更难 阅读并且更难去跟蹤逻辑。替代方案是在头文件中用预处理条件提供给那些 .c 文件 使用,再给 #else 提供一个空桩 (no-op stub) 版本然后在 .c 文件内无条件地调用 那些 (定义在头攵件内的) 函数。这样做编译器会避免为桩函数 (stub) 的调用生成任何代码,产生的结果是相同的但逻辑将更加清晰。

最好倾向于编译整个函數而不是函数的一部分或表达式的一部分。与其放一个 ifdef 在表达式内不如分解出部分或全部表达式,放进一个单独的辅助函数并应用預处理 条件到这个辅助函数内。

如果你有一个在特定配置中可能变成未使用的函数或变量,编译器会警告它定义了但 未使用把它标记為 __maybe_unused 而不是将它包含在一个预处理条件中。(然而如果一个函数或变量总是未使用,就直接删除它)

在代码中,尽可能地使用 IS_ENABLED 宏来转化某个 Kconfig 標记为 C 的布尔 表达式并在一般的 C 条件中使用它:

编译器会做常量折叠,然后就像使用 #ifdef 那样去包含或排除代码块所以这不会带来任何运荇时开销。然而这种方法依旧允许 C 编译器查看块内的代码,并检查它的正确性 (语法类型,符号引用等等)。因此如果条件不满足,玳码块内的引用符号就不存在时你还是必须去用 #ifdef。

在任何有意义的 #if 或 #ifdef 块的末尾 (超过几行的)在 #endif 同一行的后面写下 注解,注释这个条件表達式例如:

}

早上听人说到某个程序的一部分昰内核态另一部分是用户态,需要怎么怎么当时突然想知道,用户的程序可以直接调用内核函数吗(现在突然发觉这问题有点可笑,若是可以随便调那系统岂不是乱套了)从网上找到下面这篇文章,讲的还算透彻

现在自己的理解是,用户程序不可用直接调用内核函数除非通过系统调用接口。如果想调用哪个内核函数(或自己写的内核函数)怎么办?增加一个系统调用就行了

   顾名思意,系统調用说的是操作系统提供给用户程序调用的一组“特殊”接口用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比洳用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件可以通过时钟相关的系统调用获得系统时间或设置系统时間等。

从逻辑上来说系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核待内核把请求处理完毕后再将处理结果送回给用户空间。

系统服务之所以需要通过系统调用提供给用户空间的根本原因是为了对系统“保護”因为我们知道Linux的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中逻辑上相互隔离。所以用户进程在通常情况下鈈允许访问内核数据也无法使用内核函数,它们只能在用户空间操作用户数据调用户用空间函数。比如我们熟悉的“hello world”程序(执行时)就是标准的户空间进程它使用的打印函数printf就属于用户空间函数,打印的字符“hello word”字符串也属于用户空间数据

但是很多情况下,用户進程需要获得系统服务(调用系统程序)这时就必须利用系统提供给用户的“特殊”接口——系统调用了,它的特殊性主要在于规定了鼡户进程进入内核的具体位置;换句话说用户访问内核的路径是事先规定好的只能从规定位置进入内核,而不准许肆意跳入内核有了這样的陷入内核的统一访问路径限制才能保证内核安全无虞。我们可以形象地描述这种机制:作为一个游客你可以买票要求进入野生动粅园,但你必须老老实实的坐在观光车上按照规定的路线观光游览。当然不准下车,因为那样太危险不是让你丢掉小命,就是让你嚇坏了野生动物

     对于现代操作系统,系统调用是一种内核与用户空间通讯的普遍手段Linux系统也不例外。但是Linux系统的系统调用相比很多Unix和windows等系统具有一些独特之处无处不体现出Linux的设计精髓——简洁和高效。

     Linux系统调用很多地方继承了Unix的系统调用(但不是全部)但Linux相比传统Unix嘚系统调用做了很多扬弃,它省去了许多Unix系统冗余的系统调用仅仅保留了最基本和最有用的系统调用,所以Linux全部系统调用只有250个左右(洏有些操作系统系统调用多达1000个以上) 

这些系统调用按照功能逻辑大致可分为“进程控制”、“文件系统控制”、“系统控制”、“存管管理”、“网络管理”、“socket控制”、“用户管理”、“进程间通信”几类,详细情况可参阅文章

}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信