linux内核原理02——有关C语言的注记
本帖最后由 楚门 于 2014-10-16 21:48 编辑C语言一直是实现各类操作系统的的首选编程语言,linux内核的主要部分,除了少量汇编语言(和平台相关)之外,都是用C语言编写的,这篇帖子将介绍C语言在内核程序设计领域一些很少使用但是非常有用的方面。
过程调用:
内核中既有汇编语言也有C语言,那么汇编语言和C语言的互操作性是用过程调用(也就是函数调用)
来实现,那么kernel为了实现两者的互相调用不会乱掉就必须知道函数调用背后的机制,程序执行时的活动记录(函数调用)保存在程序的栈空间中,在Intel的CPU架构中栈是自顶向下增长的(arm平台提供了满递减,满递增,空递减,空递增4种指令来操作栈空间),下图是函数的调用在栈空间的示意图
stack pointer(栈指针)始终指向栈的顶部,它与frame pointer(帧指针)之间的范围就是一个活动记录(称为一个frame帧),一个frame的细节可以看上图的右半部分,在函数执行的过程中访问局部变量(静态局部变量除外)和函数的实参是通过frame pointer+偏移地址得到的,因为局部变量和函数参数的个数是在编译时就能够确定的,但这并不是强制的,完全可以忽略帧指针(frame pointer),因为在没有帧指针的情况下完全可生成等效的代码,而且效率还会高一点,但是取消帧指针的缺点在于在查找内核oops的原因时不太有效。这里说一个题外话,很多新手对函数的调用的值传递和引用传递弄不清出,其实没拿么复杂,在C语言中所有函数的参数传递都是值传递,也就是所有的参数传递方式都是通过把参数(无论这个参数是全局变量还是局部变量)的值copy一个副本到frame空间保存函数参数的地方,不同的是如果这个副本是一个值,那么函数就只能更改这个副本的值,如果这个副本是一个指针(本质上指针也是一个值),那么函数就能通过这个指针值更改指针所指向的内存空间。最后函数执行完之后把frame pointer切换回原来的值(旧的帧空间),并通过ret指令来返回,总之在内核操作内存空间时必须非常小心,内核必须明白编译器到底做了什么工作。
内联函数:
因为函数的调用会有额外的开销(压栈及出栈),因此内核中一些频繁调用的小功能函数实现为内联函数。
属性:
GCC支持很多属性,这些属性通过对C语言的函数和变量的申明增加前缀或后缀来指导编译器更精准的优化,这些属性大部分都是GCC扩展的。
内联汇编:
kernel中有很多代码片段都是用的内联汇编,因为这些代码不同于C语言调用汇编函数,这些代码是嵌入到C语言中,作为C函数的一部分。有些为了提高性能,有些纯粹是C语言实现不了,必须要用汇编实现。对于这块内容只提两点,1、输入和输出部分是针对C语言和汇编语言的交互来说,输入是把C中变量(位于内存中,栈也好,堆也罢)写到汇编语言的寄存器(具体那个寄存器可以自己指定,也可以由编译器来选择)中,同理输出是把汇编语言寄存器中的值写到C语言的变量(内存)中,破坏部分告诉编译器哪些寄存器或者内存的值可能会被修改。2、对以上部分,仅仅只是告诉编译器,内存单元和寄存器的值如何传递,哪些寄存器或者内存单元会被这部分汇编代码修改,对这些寄存器或内存单元的保护(备份和恢复)和操作都由编译器根据你提供给他的信息自动完成。
对齐:
在用户空间编程中很少遇到对齐的问题,即使有编译器也能帮我们很好的解决。但在内核编程中有时候为了高效的利用cache的特性需要主动的插入一些空白的内存空间,但是又有些时候为了尽可能的节约内存,在自然对齐(一个基本类型数据所存储的位置必须能够被数据类型的宽度整除)的内存中会有一些空隙,内核要利用这个空隙来尽可能的多存储一些信息。那么对于未对齐(利用对齐的空隙来存储)的数据,必须用内核中的get_unaligned(ptr)和put_unaligned(val, ptr)这两个宏来访问,其中ptr是指向未对齐数据的指针。我们可以做一个实验,代码如下
#include <stdio.h>
int main(int argc, char **argv)
{
int a = -1;
int b = 0;
char *p = (char *)&a;
printf("0x%x\n", *(int *)(p+1));
printf("0x%x\n", *(int *)(p+2));
printf("0x%x\n", *(int *)(p+3));
return 0;
}
输出结果如下:# ./test
0xffffff
0xffff
0xff
#
我们可以看到printf只输出了指针指向的字节到对齐地址的值,而不会把从指针开始的地方向后4字节的内容都打印出来。由此可以得出,访问非对齐的变量必须要借助特殊的操作,具体怎么操作,对C语言很熟的人都应该知道怎么去操作,如果你不知道,可以去看上面两个宏在内核中的实现。
基本数据类型:
为什么还要讲基本数据类型,不就是int,char这些吗?还真不是这些,尽管这些都是C语言的基本数据类型,但是在除了用到这个基本数据类型,内核还有大量的使用链表这种操作,由于内核代码的复杂性,不可能每种新的数据结构一旦用到链表都重新实现一次,这无疑增加了内核的冗余度,所以内核把双向循环链表这种操作实现成了一个标准(暂时可以这么理解)的数据类型,需要用到双向循环链表的地方就使用内核的标准操作,内核的链表实现堪称经典,除此之外,内核还实现了红黑树,hash表,基树这些数据结构的标准操作,我把这些类型都归为内核的标准数据类型。
综上所述,内核在使用C语言时是非常注重细节的,使用了很多C语言的非标准特性,在内核开发中要利用这些特性来压榨出硬件最后1%的性能。内核还实现了一些标准的数据结构,其源代码大量使用了这些标准数据结构。
感谢分享1 再看一遍:loveliness: {:5_106:} 读万卷书,不错 路过看看 。。。。谢谢分享 谢谢分享! 学习了 感谢分享{:5_105:} 楼主写的不错,赞 看的好玄乎,不过还是谢谢啦 为什么不更新了呢
页:
[1]