前言

鉴于目前的研究方向是计算机系统结构,具体是DCN(数据中心网络),平时的学习工作主要是在Linux下进行开发,使用最多的则是C语言。本科阶段的学习中,涉及的C语言比较浅显,而内核C中有许多很有意思的特性。

本文主要是对平时工作中经常遇到的一些知识点进行总结记录,以备查阅温故。

一、inline 内联函数

函数是一种更高级的抽象。它的引入使得编程者只关心函数的功能和使用方法,而不必关心函数功能的具体实现;函数的引入可以减少程序的目标代码,实现程序代码和数据的共享。但是,函数调用也会带来降低效率的问题,因为调用函数实际上将程序执行顺序转移到函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去前要保护现场并记忆执行的地址,转回后先要恢复现场,并按原来保存地址继续执行。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。特别是对于一些函数体代码不是很大,但又频繁地被调用的函数来讲,解决其效率问题更为重要。引入内联函数实际上就是为了解决这一问题。

使用内联函数的注意事项:

  1. 在内联函数内不允许用循环语句和开关语句。如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数(自己调用自己的函数)是不能被用来做内联函数的。内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数实现。  
  2. 内联函数的定义必须出现在内联函数第一次被调用之前。 

  3. 关键字“inline”的添加位置

    (1)在声明是加inline,定义时不加。要求编译器编译时,能看到inline的声明,而且在展开点看到该定义,这样,就将其视为内联函数。

    (2)如果你声明没有inline,却在定义时inline了。这时,如果其它要调用该函数的文件看到了它的声明,就认为该函数不是内联的,所以,到了调用处,转到该函数实现的地方,却意外地看到了inline声明,这时,会导致链接出错。若要改正的话,就要让调用该函数的文件也看到有inline的定义,而不是在调用时才看到.你可以在每个文件都加上有inline的定义.(如果不加inline,则会出现重复定义的错误,因为内联函数才可以被重复定义).或者另一种修改方法,你将定义时的inline去掉,这样就成为普通函数,链接不会出错.如果是前一种改法,仍是内联的,因为符合了看到了inline且随处可见其定义的条件。

    (3)如果你将声明跟定义都放在同一个头文件,而在声明时不内联,在实现时内联,这样编译器也是将该函数内联(符合两个条件,看到inline的声明(虽然是在定义时),随处可见其定义)。

    总结说来,只要编译器看到有inline出现,而且定义随处可见,就能将函数内联(上边已假设你的函数足够简单可以内联),而不必管是定义还是声明加inline的问题。

二、宏定义中的一些特殊符号

根据百科中的解释,计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。解释器或编译器在遇到宏时会自动进行这一模式替换。对于编译语言,宏展开在编译时发生,进行宏展开的工具常被称为宏展开器。这里咱们主要讨论C语言中的宏定义。

C语言中宏定义很常见,尤其是在内核源码中,宏定义大量存在,其中不乏一些灵活有趣的使用方式。

这里简单整理宏定义中常见的一些符号:

符号 作用 示例
# 将后面的 宏参数 进行字符串操作,就是将后面的参数用双引号括起来 例如 #id 为 “id”,id=6 时,#id 为“6”
## 用于连接 例如 initcall_##fn##id 为initcall_fnid,那么,fn = test_init,id = 6时,initcall_##fn##id 为 initcall_test_init6
@# 将后面的 宏参数 进行字符串操作,以一对单引用括起来(注意:只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前) #define makechar(x) @#X, a = makechar(b), 展开后变成了:a= ‘b’;

三、gcc -fgnu89-inline选项

在使用新版本gcc编译旧内核的代码时,内联(inline)函数可能会报警告“warning: inline function xx declared but never defined”。即“内联函数声明但未定义”。

解决办法就是在 编译时加上选项 -fgnu89-inline

四、gcc -E选项

gcc 的 -E 选项只激活预处理,用于在预编译后停下来。使用时,你需要把它重定向到一个输出文件里面,生成预编译文件。看起来这个选项似乎很简单,用的不多,实则在调试定位错误的过程中很有用。

例如,使用gcc编译C语言工程的时候,一个经常遇到的问题是“A.c In function f,Undefined Reference x“。表面看起来很简单,就是在A.c文件的某个函数中使用了未定义的变量或函数(以下简称 b)。然而,当你在该A.c文件的函数f中查看时却找不到这个未定义的b,甚至连包含b的函数f都找不到。这时候,-E选项就派上用场了。

你可以在删除A.c的目标文件后,使用gcc -E命令生成预编译文件:

1
gcc -E A.c -o A

然后vim A打开预编译文件,搜索,你会找到这个隐蔽的b。仔细查看其上下文,你会发现其实b可能是A.c中调用的某个函数调用的或者来源于某个头文件。总之,找到这个b以后,排错就很容易了。

五、C语言中的 extern

在Linux C的开发中,经常会遇到 extern,类似的还有 extern “C”等,这些修饰都是有特别的作用的。之前就是了解个大概,现在在这里简单总结一下。这里参考了网上的一些内容,侵删。

  1. extern用在变量或函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

  2. extern修饰变量的声明。

  举例:若a.c中需引用b.c中的变量int v,可以在a.c中声明extern int v,然后就可以引用变量v;需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到变量v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。这里涉及到另外一个话题—变量的作用域。能够被其他模块以extern引用到的变量通常是全局变量。

  还有一点是,extern int v可以放在a.c中的任何地方,比如可以在a.c中函数func()定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在func()作用域中引用变量v(这还是变量作用域的问题,对于这一点来说,很多人使用时都心存顾虑,好像extern声明只能用于文件作用域似的)。

  1. extern修饰函数的声明。

  本质上讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。如果文件a.c要引用b.c中的函数,比如在b.c中原型是int func(int m),那么就可以在a.c中声明extern int func(int m),然后就能使用func()来做任何事情。就像变量的声明一样,extern int func(int m)可以放在a.c中的任何位置,而不一定非要放在a.c的文件作用域的范围中,

  对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。使用extern和包含头文件来引用函数的区别:extern的引用方式比包含头文件要间接得多。extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这大概是kiss原则的一种体现。这样做的一个明显的好处是,会加速程序的编译(确切地说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。

  1. extern “C”指示调用规范

  extern修饰符可用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器使用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后再目标代码中命名规则不同。

六、Patch 补丁命令的基本使用

  1. 生成补丁文件:
1
2
$ svn diff > patchFile        整个工程的变动生成补丁 
$ svn diff file > patchFile 某个文件单独变动的补丁
  1. svn回滚:
1
2
$ svn revert FILE                     单个文件回滚 
$ svn revert DIR --depth=infinity 整个目录进行递归回滚
  1. 打patch:
1
2
3
4
5
$ patch -p0 < test.patch  -p0   选项要从当前目录查找目的文件 
$ patch -p1 < test.patch -p1 选项要从当前目录查找目的文件,不包含patch中的最上级目录
例如两个版本以a,b开头,而a,b并不是真正有效地代码路径,则这时候需要使用"-p1"参数。
a/src/...
b/src/...

参考

C语言中extern的用法