momo zone

调核人的blog

Monthly Archives: 十二月 2011

64位内核开发需要注意的…

改行后就立即进入了64位 内核开发的工作,虽然之前有了解了一些X86_64架构但实际开发中发现了解的程度还远远不够。

比如,切到X86_64 后我以为int 长度是64位,后来发现和X86一样,还是32位。呵呵, 让我想起当年上学时某人把VC6 的代码copy 到Turbo C 中是发生溢出错误,问我为什么。如果只捧着教材当然不可能知道Turbo C 是16位编译器,编译出来的二进制代码也是16位,int 也是16位长,而VC6 则完全是32位时代的产物。 十几年过去了,同样的事情又发生了,不同的是这次犯傻的是我,而且不像当初预想的那样int 会变成64位。

这里摘一篇IBM的文章,彻底搞明白LLP64,LP64 等问题

将 Linux 应用程序移植到 64 位系统上

平滑迁移的技巧和技术

Harsha S. Adiga, 软件工程师, IBM

简介: 随着 64 位体系结构的普及,针对 64 位系统准备好您的 Linux® 软件已经变得比以前更为重要。在本文中,您将学习如何在进行语句声明、赋值、位移、类型转换、字符串格式化以及更多操作时,防止出现可移植性缺陷。

本文的标签:  codelinux

发布日期: 2006 年 5 月 18 日
级别: 初级
访问情况 : 5369 次浏览
评论: 0 (查看 | 添加评论 – 登录)

平均分 5 星 共 2 个评分 平均分 (2个评分)
为本文评分

Linux 是可以使用 64 位处理器的跨平台操作系统之一,现在 64 位的系统在服务器和桌面端都已经非常常见了。很多开发人员现在都面临着需要将自己的应用程序从 32 位环境移植到 64 位环境中。随着 Intel® Itanium® 和其他 64 位处理器的引入,使软件针对 64 位环境做好准备变得日益重要了。

与 UNIX® 和其他类 UNIX 操作系统一样,Linux 使用了 LP64 标准,其中指针和长整数都是 64 位的,而普通的整数则依然是 32 位的。尽管有些高级语言并不会受到这种类型大小不同的影响,但是另外一些语言(例如 C 语言)却的确会受到这种影响。

将应用程序从 32 位系统移植到 64 位系统上的工作可能会非常简单,也可能会非常困难,这取决于这些应用程序是如何编写和维护的。很多琐碎的问题都可能导致产生问题,即使在一个编写得非常好的高度可移植的应用程序中也是如此,因此本文将对这些问题进行归纳总结,并给出解决这些问题的一些方法建议。

64 位的优点

32 位平台有很多限制,这些限制正在阻碍大型应用程序(例如数据库)开发人员的工作进展,尤其对那些希望充分利用计算机硬件优点的开发人员来说更是如此。科学计算通常要依赖于浮点计算,而有些应用程序(例如金融计算)则需要一个比较狭窄的数字范围,但是却要求更高的精度,其精度高于浮点数所提供的精度。64 位数学运算提供了这种更高精度的定点数学计算,同时还提供了足够的数字范围。现在在计算机业界中有很多关于 32 位地址空间所表示的地址空间的讨论。32 位指针只能寻址 4GB 的虚拟地址空间。我们可以克服这种限制,但是应用程序开发就变得非常复杂了,其性能也会显著降低。

在语言实现方面,目前的 C 语言标准要求 “long long” 数据类型至少是 64 位的。然而,其实现可能会将其定义为更大。

另外一个需要改进的地方是日期。在 Linux 中,日期是使用 32 位整数来表示的,该值所表示的是从 1970 年 1 月 1 日至今所经过的秒数。这在 2038 年就会失效。但是在 64 位的系统中,日期是使用有符号的 64 位整数表示的,这可以极大地扩充其可用范围。

总之,64 位具有以下优点:

  • 64 位的应用程序可以直接访问 4EB 的虚拟内存,Intel Itanium 处理器提供了连续的线性地址空间。
  • 64 位的 Linux 允许文件大小最大达到 4 EB(2 的 63 次幂),其重要的优点之一就是可以处理对大型数据库的访问。

回页首

Linux 64 位体系结构

不幸的是,C 编程语言并没有提供一种机制来添加新的基本数据类型。因此,提供 64 位的寻址和整数运算能力必须要修改现有数据类型的绑定或映射,或者向 C 语言中添加新的数据类型。
表 1. 32 位和 64 位数据模型

ILP32 LP64 LLP64 ILP64
char 8 8 8 8
short 16 16 16 16
int 32 32 32 64
long 32 64 32 64
long long 64 64 64 64
指针 32 64 64 64

这 3 个 64 位模型(LP64、LLP64 和 ILP64)之间的区别在于非浮点数据类型。当一个或多个 C 数据类型的宽度从一种模型变换成另外一种模型时,应用程序可能会受到很多方面的影响。这些影响主要可以分为两类:

  • 数据对象的大小。编译器按照自然边界对数据类型进行对齐;换而言之,32 位的数据类型在 64 位系统上要按照 32 位边界进行对齐,而 64 位的数据类型在 64 位系统上则要按照 64 位边界进行对齐。这意味着诸如结构或联合之类的数据对象的大小在 32 位和 64 位系统上是不同的。
  • 基本数据类型的大小。通常关于基本数据类型之间关系的假设在 64 位数据模型上都已经无效了。依赖于这些关系的应用程序在 64 位平台上编译也会失败。例如,sizeof (int) = sizeof (long) = sizeof (pointer) 的假设对于 ILP32 数据模型有效,但是对于其他数据模型就无效了。

总之,编译器要按照自然边界对数据类型进行对齐,这意味着编译器会进行 “填充”,从而强制进行这种方式的对齐,就像是在 C 结构和联合中所做的一样。结构或联合的成员是根据最宽的成员进行对齐的。清单 1 对这个结构进行了解释。
清单 1. C 结构 

struct test {
	int i1;
	double d;
	int i2;
	long l;
}

表 2 给出了这个结构中每个成员的大小,以及这个结构在 32 位系统和 64 位系统上的大小。
表 2. 结构和结构成员的大小

结构成员 在 32 位系统上的大小 在 64 位系统上的大小
struct test {
int i1; 32 位 32 位
32 位填充
double d; 64 位 64 位
int i2; 32 位 32 位
32 位填充
long l; 32 位 64 位
}; 结构大小为 20 字节 结构大小为 32 字节

注意,在一个 32 位的系统上,编译器可能并没有对变量 d 进行对齐,尽管它是一个 64 位的对象,这是因为硬件会将其当作两个 32 位的对象进行处理。然而,64 位的系统会对 d 和 l 都进行对齐,这样会添加两个 4 字节的填充。

回页首

从 32 位系统移植到 64 位系统

本节介绍如何解决一些常见的问题:

  • 声明
  • 表达式
  • 赋值
  • 数字常数
  • Endianism
  • 类型定义
  • 位移
  • 字符串格式化
  • 函数参数

声明

要想让您的代码在 32 位和 64 位系统上都可以工作,请注意以下有关声明的用法:

  • 根据需要适当地使用 “L” 或 “U” 来声明整型常量。
  • 确保使用无符号整数来防止符号扩展的问题。
  • 如果有些变量在这两个平台上都需要是 32 位的,请将其类型定义为 int。
  • 如果有些变量在 32 位系统上是 32 位的,在 64 位系统上是 64 位的,请将其类型定义为 long。
  • 为了对齐和性能的需要,请将数字变量声明为 int 或 long 类型。不要试图使用 char 或 short 类型来保存字节。
  • 将字符指针和字符字节声明为无符号类型的,这样可以防止 8 位字符的符号扩展问题。

表达式

在 C/C++ 中,表达式是基于结合律、操作符的优先级和一组数学计算规则的。要想让表达式在 32 位和 64 位系统上都可以正确工作,请注意以下规则:

  • 两个有符号整数相加的结果是一个有符号整数。
  • int 和 long 类型的两个数相加,结果是一个 long 类型的数。
  • 如果一个操作数是无符号整数,另外一个操作数是有符号整数,那么表达式的结果就是无符号整数。
  • int 和 doubule 类型的两个数相加,结果是一个 double 类型的数。此处 int 类型的数在执行加法运算之前转换成 double 类型。

赋值

由于指针、int 和 long 在 64 位系统上大小不再相同了,因此根据这些变量是如何赋值和在应用程序中使用的,可能会出现问题。下面是有关赋值的一些技巧:

  • 不要交换使用 int 和 long 类型,因为这可能会导致高位数字被截断。例如,不要做下面的事情:
    int i;
    long l;
    i = l;
  • 不要使用 int 类型来存储指针。下面这个例子在 32 位系统上可以很好地工作,但是在 64 位系统上会失败,这是因为 32 位整数无法存放 64 位的指针。例如,不要做下面的事情:
    unsigned int i, *ptr;
    i = (unsigned) ptr;
  • 不要使用指针来存放 int 类型的值。例如,不要做下面的事情;
    int *ptr;
    int i;
    ptr = (int *) i;
  • 如果在表达式中混合使用无符号和有符号的 32 位整数,并将其赋值给一个有符号的 long 类型,那么将其中一个操作数转换成 64 位的类型。这会导致其他操作数也被转换成 64 位的类型,这样在对表达式进行赋值时就不需要再进行转换了。另外一种解决方案是对整个表达式进行转换,这样就可以在赋值时进行符号扩展。例如,考虑下面这种用法可能会出现的问题:
    long n;
    int i = -2;
    unsigned k = 1;
    n = i + k;

    从数学计算上来说,上面这个黑体显示的表达式的结果应该是 -1 。但是由于表达式是无符号的,因此不会进行符号扩展。解决方案是将一个操作数转换成 64 位类型(下面的第一行就是这样),或者对整个表达式进行转换(下面第二行):

    n = (long) i + k;
    n = (int) (i + k);

     

数字常量

16 进制的常量通常都用作掩码或特殊位的值。如果一个没有后缀的 16 进制的常量是 32 位的,并且其高位被置位了,那么它就可以作为无符号整型进行定义。

例如,常数 OxFFFFFFFFL 是一个有符号的 long 类型。在 32 位系统上,这会将所有位都置位(每位全为 1),但是在 64 位系统上,只有低 32 位被置位了,结果是这个值是 0x00000000FFFFFFFF。

如果我们希望所有位全部置位,那么一种可移植的方法是定义一个有符号的常数,其值为 -1。这会将所有位全部置位,因为它采用了二进制补码算法。

long x = -1L;

可能产生的另外一个问题是最高位的设置。在 32 位系统上,我们使用的是常量 0x80000000。但是可移植性更好的方法是使用一个位移表达式:

1L << ((sizeof(long) * 8) - 1);

Endianism

Endianism 是指用来存储数据的方法,它定义了整数和浮点数据类型中是如何对字节进行寻址的。

Little-endian 是将低位字节存储在内存的低地址中,将高位字节存储在内存的高地址中。

Big-endian 是将高位字节存储在内存的低地址中,将低位字节存储在内存的高地址中。

表 3 给出了一个 64 位长整数的布局示例。
表 3. 64 位 long int 类型的布局

低地址 高地址
Little endian Byte 0 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 Byte 7
Big endian Byte 7 Byte 6 Byte 5 Byte 4 Byte 3 Byte 2 Byte 1 Byte 0

例如,32 位的字 0x12345678 在 big endian 机器上的布局如下:
表 4. 0x12345678 在 big-endian 系统上的布局

内存偏移量 0 1 2 3
内存内容 0x12 0x34 0x56 0x78

如果将 0x12345678 当作两个半字来看待,分别是 0x1234 和 0x5678,那么就会看到在 big endian 机器上是下面的情况:
表 5. 0x12345678 在 big-endian 系统上当作两个半字来看待的情况

内存偏移量 0 2
内存内容 0x1234 0x5678

然而,在 little endian 机器上,字 0x12345678 的布局如下所示:
表 6. 0x12345678 在 little-endian 系统上的布局

内存偏移量 0 1 2 3
内存内容 0x78 0x56 0x34 0x12

类似地,两个半字 0x1234 和 0x5678 如下所示:
表 7. 0x12345678 在 little-endian 系统上作为两个半字看到的情况

内存偏移量 0 2
内存内容 0x3412 0x7856

下面这个例子解释了 big endian 和 little endian 机器上字节顺序之间的区别。

下面的 C 程序在一台 big endian 机器上进行编译和运行时会打印 “Big endian”,在一台 little endian 机器上进行编译和运行时会打印 “Little endian”。
清单 2. big endian 与 little endian

#include <stdio.h>
main () {
int i = 0x12345678;
if (*(char *)&i == 0x12)
printf ("Big endian\n");
else if (*(char *)&i == 0x78)
    		printf ("Little endian\n");
}

Endianism 在以下情况中非常重要:

  • 使用位掩码时
  • 对象的间接指针地址部分

在 C 和 C++ 中有位域来帮助处理 endian 的问题。我建议使用位域,而不要使用掩码域或 16 进制的常量。有几个函数可以用来将 16 位和 32 位数据从 “主机字节顺序” 转换成 “网络字节顺序”。例如,htonl (3)ntohl (3) 用来转换 32 位整数。类似地,htons (3)ntohs (3) 用来转换 16 位整数。然而,对于 64 位整数来说,并没有标准的函数集。但是在 big endian 和 little endian 系统上,Linux 都提供了下面的几个宏:

  • bswap_16
  • bswap_32
  • bswap_64

类型定义

建议您不要使用 C/C++ 中那些在 64 位系统上会改变大小的数据类型来编写应用程序,而是使用一些类型定义或宏来显式地说明变量中所包含的数据的大小和类型。有些定义可以使代码的可移植性更好。

  • ptrdiff_t
    这是一个有符号整型,是两个指针相减后的结果。
  • size_t
    这是一个无符号整型,是执行 sizeof 操作的结果。这在向一些函数(例如 malloc (3))传递参数时使用,也可以从一些函数(比如 fred (2))中返回。
  • int32_tuint32_t 等:
    定义具有预定义宽度的整型。
  • intptr_t 和 uintptr_t
    定义整型类型,任何有效指针都可以转换成这个类型。

例 1:

在下面这条语句中,在对 bufferSize 进行赋值时,从 sizeof 返回的 64 位值被截断成了 32 位。

int bufferSize = (int) sizeof (something);

解决方案是使用 size_t 对返回值进行类型转换,并将其赋给声明为 size_t 类型的 bufferSize,如下所示:

size_t bufferSize = (size_t) sizeof (something);

例 2:

在 32 位系统上,int 和 long 大小相同。由于这一点,有些开发人员会交换使用这两种类型。这可能会导致指针被赋值给 int 类型,或者反之。但是在 64 位的系统上,将指针赋值给 int 类型会导致截断高 32 位的值。

解决方案是将指针作为指针类型或为此而定义的特殊类型进行存储,例如 intptr_t 和 uintptr_t

位移

无类型的整数常量就是 (unsigned) int 类型的。这可能会导致在位移时出现被截断的问题。

例如,在下面的代码中,a 的最大值可以是 31。这是因为 1 << a 是 int 类型的。

long t = 1 << a;

要在 64 位系统上进行位移,应该使用 1L,如下所示:

long t = 1L << a;

字符串格式化

函数 printf (3) 及其相关函数都可能成为问题的根源。例如,在 32 位系统上,使用 %d 来打印 int 或 long 类型的值都可以,但是在 64 位平台上,这会导致将 long 类型的值截断成低 32 位的值。对于 long 类型的变量来说,正确的用法是 %ld

类似地,当一个小整数(char、short、int)被传递给 printf (3) 时,它会扩展成 64 位的,符号会适当地进行扩展。在下面的例子中,printf (3) 假设指针是 32 位的。

char *ptr = &something;
printf (%x\n", ptr);

上面的代码在 64 位系统上会失败,它只会显示低 4 字节的内容。

这个问题的解决方案是使用 %p,如下所示;这在 32 位和 64 位系统上都可以很好地工作:

char *ptr = &something;
printf (%p\n", ptr);

函数参数

在向函数传递参数时需要记住几件事情:

  • 在参数的数据类型是由函数原型定义的情况中,参数应该根据标准规则转换成这种类型。
  • 在参数类型没有指定的情况中,参数会被转换成更大的类型。
  • 在 64 位系统上,整型被转换成 64 位的整型值,单精度的浮点类型被转换成双精度的浮点类型。
  • 如果返回值没有指定,那么函数的缺省返回值是 int 类型的。

在将有符号整型和无符号整型的和作为 long 类型传递时就会出现问题。考虑下面的情况:
清单 3. 将有符号整型和无符号整型的和作为 long 类型传递

long function (long l);
int main () {
	int i = -2;
	unsigned k = 1U;
	long n = function (i + k);
}

上面这段代码在 64 位系统上会失败,因为表达式 (i + k) 是一个无符号的 32 位表达式,在将其转换成 long 类型时,符号并没有得到扩展。解决方案是将一个操作数强制转换成 64 位的类型。

在基于寄存器的系统上还有一个问题:系统采用寄存器而不是堆栈来向函数传递参数。考虑下面的例子:

float f = 1.25;
printf ("The hex value of %f is %x", f, f);

在基于堆栈的系统中,这会打印对应的 16 进制值。但是在基于寄存器的系统中,这个 16 进制的值会从一个整数寄存器中读取,而不是从浮点寄存器中读取。

解决方案是将浮点变量的地址强制转换成一个指向整型类型的指针,如下所示:

printf ("The hex value of %f is %x", f, *(int *)&f);

回页首

结束语

主流的硬件供应商最近都在扩充自己的 64 位产品,这是因为 64 位平台可以提供更好的性能、价值和可伸缩性。32 位系统的限制,特别是 4GB 的虚拟内存上限,已经极大地刺激很多公司开始考虑迁移到 64 位平台上。了解如何将应用程序移植到 64 位体系结构上可以帮助我们编写可移植性更好且效率更高的代码。
参考资料

学习

获得产品和技术

  • 在您的下一个 Linux 开发项目中采用 IBM 试用版软件,这可以从 developerWorks 上直接下载。

讨论

关于作者

Harsha Adiga 就职于印度的 IBM Software Group,他参与了很多 Linux 和开放源码社区、工作组的工作。

Advertisements

linux内核定时与测量

linux内核必须完成两种主要的定时测量:

  • 保存当前的时间和日期,以便能通过time, ftime和gettimeofday系统调用把他们返回给用户程序
  • 维持定时器,告诉内核或用户程序,某一时间间隔已经过去了。

定时测量是由基于固定频率振荡器和计数器的几个硬件电路完成的

时钟和定时器电路

实时时钟(RTC, Real Time Clock)

它是独立于CPU和所有其他芯片的。

即使切断电源,RTC还继续工作,靠一个小电池或蓄电池供电。CMOS RAM和RTC被集成在一个芯片上。

RTC能在IRQ8上发周期性的中断。linux只用RTC来获取时间和日期。内核通过0x70和0x71 I/O端口访问RTC。

时间戳计数器(TSC, Time Stamp Counter)

所有的80×86微处理器都包含一条CLK输入引线,接受外部振荡器的时钟信号。包含一个计数器,该计数器利用64位的TSC寄存器来实现,可以通过汇编指令rdtsc读这个寄存器。linux在初始化阶段必须确定时钟信号的频率,编译内核时并不声明这个频率,所以内核映像可以运行在不同时钟频率的CPU上。

初始化完成之后,通过calibrate_tsc函数算一个大约在5ms的时间间隔内产生的时钟信号的个数来算出CPU实际频率。

  1. unsigned long __init calibrate_tsc(void)
  2. {
  3.     mach_prepare_counter();
  4.     {
  5.         unsigned long startlow, starthigh;
  6.         unsigned long endlow, endhigh;
  7.         unsigned long count;
  8.         rdtsc(startlow,starthigh);
  9.         mach_countup(&count);
  10.         rdtsc(endlow,endhigh);
  11.         /* Error: ECTCNEVERSET */
  12.         if (count <= 1)
  13.             goto bad_ctc;
  14.         /* 64-bit subtract – gcc just messes up with long longs */
  15.         __asm__(“subl %2,%0\n\t”
  16.             “sbbl %3,%1”
  17.             :”=a” (endlow), “=d” (endhigh)
  18.             :”g” (startlow), “g” (starthigh),
  19.              “0” (endlow), “1” (endhigh));
  20.         /* Error: ECPUTOOFAST */
  21.         if (endhigh)
  22.             goto bad_ctc;
  23.         /* Error: ECPUTOOSLOW */
  24.         if (endlow <= CALIBRATE_TIME)
  25.             goto bad_ctc;
  26.         __asm__(“divl %2”
  27.             :”=a” (endlow), “=d” (endhigh)
  28.             :”r” (endlow), “0” (0), “1” (CALIBRATE_TIME));
  29.         return endlow;
  30.     }
  31.     /*
  32.      * The CTC wasn’t reliable: we got a hit on the very first read,
  33.      * or the CPU was so fast/slow that the quotient wouldn’t fit in
  34.      * 32 bits..
  35.      */
  36. bad_ctc:
  37.     return 0;
  38. }

可编程间隔定时器(PIT, Programmable Internal Timer)

IBM兼容PC还包含了第三种时间测量设备,就是经典的PIT。这个设备通过发出一个特殊的中断,叫做时钟中断来通知内核又一个时间间隔过去了。PIT通常是使用0x40 ~ 0x43 I/O端口的一个8253或8254 CMOS芯片。

时钟中断的频率取决于硬件体系结构。

linux中,有几个宏产生决定时钟中断频率的常量:

  • HZ产生每秒时钟中断的近似个数,也就是时钟中断的频率。在IBM PC上,这个值为1000
  • CLOCK_TICK_RATE产生的值为1193182,是8254芯片的内部振荡器频率。
  • LATCH产生CLOCK_TICK_RATE和HZ的比值再四舍五入的整数值。这个值用来对PIT编程。
  1. void setup_pit_timer(void)
  2. {
  3.     extern spinlock_t i8253_lock;
  4.     unsigned long flags;
  5.     spin_lock_irqsave(&i8253_lock, flags);
  6.     outb_p(0x34,PIT_MODE);      /* binary, mode 2, LSB/MSB, ch 0 */
  7.     udelay(10);
  8.     outb_p(LATCH & 0xff , PIT_CH0); /* LSB */
  9.     udelay(10);
  10.     outb(LATCH >> 8 , PIT_CH0);   /* MSB */
  11.     spin_unlock_irqrestore(&i8253_lock, flags);
  12. }
  1. #define LATCH  ((CLOCK_TICK_RATE + HZ/2) / HZ)
  1. #define PIT_MODE        0x43
  2. #define PIT_CH0         0x40
  3. #define PIT_CH2         0x42

CPU本地定时器

在最近80×86的本地APIC中,还提供了CPU本地定时器,这个是一种能够提供单步中断和周期性中断的设备。它与可编程间隔定时器不同的是:

  • APIC计数器时32位,而PIC计数器时16位;因此,可以对本地定时器编程来产生很低频率的中断。
  • 本地APIC定时器把中断只发送给自己的处理器,而PIT产生一个全局性中断,系统中的任一CPU都可以对其处理。
  • APIC定时器是基于总线时钟信号的,PIT有其自己的内部时钟振荡器,可以灵活编程。

高精度事件定时器(HPET)

HPET是由Intel和Microsoft联合开发的一种新型定时器芯片。

ACPI电源管理定时器

它的时钟信号拥有大约3.58MHz的固定频率。为了读取计数器的当前值,内核需要访问某个I/O端口,这个I/O端口的地址由BIOS在初始化阶段确定。

linux计时体系结构

基于80×86多处理器机器所具有的计时体系结构和单处理器机器所具有的稍有不同:

  • 在单处理器上,所有的计时活动都是由全局定时器产生的中断触发的。
  • 在多处理器,所有普通的活动都是由全局定时器产生的中断触发的,具体CPU的活动都是由本地APIC定时器产生的中断触发的。

内核使用两种基本的计时函数:一个保持当前最新的时间,另一个计算在当前秒内走过的纳秒数。

计时体系结构的数据结构

定时器对象

它是timer_opts类型的一个描述符。

  1. struct timer_opts {
  2.     char* name; //标识定时器源的一个字符串
  3.     void (*mark_offset)(void); //记录上一个节拍的准确时间,由时钟中断处理程序调用
  4.     unsigned long (*get_offset)(void); //返回自上一个节拍开始所经过的纳秒数
  5.     unsigned long long (*monotonic_clock)(void);//返回自内核初始化开始所经过的纳秒数
  6.     void (*delay)(unsigned long); //等待指定数目的“循环”
  7. };

其中最重要的时mark_offset和get_offset两个字段。由于这两种方法,linux计时体系结构能够达到子节拍的分辨度。内核能以比节拍周期更高的精度来测定当前的时间,这种操作叫做“定时插补(time interpolation)”。cur_timer存放了某个定时器的地址,该定时器时系统可利用的定时器资源中“最好的”。最初cur_timer指向timer_zone,这是一个虚拟的定时器资源对象。内核初始化期间,select_timer函数设置cur_timer指向适当定时器对象的地址。

  1. struct timer_opts *cur_timer = &timer_none;
  2. struct timer_opts* __init select_timer(void)
  3. {
  4.     int i = 0;
  5. //优先选择HPET;否则,将选择ACPI电源管理定时器;再次之使TSC;最后方案选择总是存在PIT。
  6.     /* find most preferred working timer */
  7.     while (timers[i]) {
  8.         if (timers[i]->init)
  9.             if (timers[i]->init(clock_override) == 0)
  10.                 return timers[i]->opts;
  11.         ++i;
  12.     }
  13.     panic(“select_timer: Cannot find a suitable timer\n”);
  14.     return NULL;
  15. }
  1. void __init time_init(void)
  2. {
  3. #ifdef CONFIG_HPET_TIMER
  4.     if (is_hpet_capable()) {
  5.         /*
  6.          * HPET initialization needs to do memory-mapped io. So, let
  7.          * us do a late initialization after mem_init().
  8.          */
  9.         late_time_init = hpet_time_init;
  10.         return;
  11.     }
  12. #endif
  13.     xtime.tv_sec = get_cmos_time();
  14.     xtime.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ);
  15.     set_normalized_timespec(&wall_to_monotonic,
  16.         -xtime.tv_sec, -xtime.tv_nsec);
  17.     cur_timer = select_timer();
  18.     printk(KERN_INFO “Using %s for high-res timesource\n”,cur_timer->name);
  19.     time_init_hook();
  20. }

在time_init中通过select_timer返回值设置cur_timer。

  1. static struct init_timer_opts* __initdata timers[] = {
  2. #ifdef CONFIG_X86_CYCLONE_TIMER
  3.     &timer_cyclone_init,
  4. #endif
  5. #ifdef CONFIG_HPET_TIMER
  6.     &timer_hpet_init,
  7. #endif
  8. #ifdef CONFIG_X86_PM_TIMER
  9.     &timer_pmtmr_init,
  10. #endif
  11.     &timer_tsc_init,
  12.     &timer_pit_init,
  13.     NULL,
  14. };
  15. struct init_timer_opts {
  16.     int (*init)(char *override);
  17.     struct timer_opts *opts;
  18. };

本地APIC定时器没有对应的定时器对象,因为本地APIC定时器仅用来产生周期性中断而从不用来获得子节拍的分辨度

jiffies变量

这是一个计数器,用来记录系统启动以来产生的节拍总数。每次时钟中断发生时,它便加1。80×86体系结构中,jiffies是一个32位的变量,每隔大约50天它的值会回绕到0,。使用了time_after, time_after_eq, time_before和time_before_eq四个宏,内核处理了jiffies变量的溢出。

  1. #define time_after(a,b)     \
  2.     (typecheck(unsigned long, a) && \
  3.      typecheck(unsigned long, b) && \
  4.      ((long)(b) – (long)(a) < 0))
  5. #define time_before(a,b)    time_after(b,a)
  6. #define time_after_eq(a,b)  \
  7.     (typecheck(unsigned long, a) && \
  8.      typecheck(unsigned long, b) && \
  9.      ((long)(a) – (long)(b) >= 0))
  10. #define time_before_eq(a,b) time_after_eq(b,a)

jiffies被初始化为fffb6c20,它是32位有符号值,等于-300000。所以,计数器将会在系统启动后的5分钟内处于溢出状态。这样做,使得那些不对jiffies作溢出检测的内核代码在开发阶段被及时发现,从而不再出现在稳定版本中。

linux需要自系统启动以来产生的系统节拍的真实数目。所以,jiffies变量通过连接器被换算成一个64位计数器的低32位,被称作为jiffies_64。

  1. u64 get_jiffies_64(void)
  2. {
  3.     unsigned long seq;
  4.     u64 ret;
  5.     do {
  6.         seq = read_seqbegin(&xtime_lock);
  7.         ret = jiffies_64;
  8.     } while (read_seqretry(&xtime_lock, seq));
  9.     return ret;
  10. }

xtime变量
xtime变量存放当前时间和日期,是一个timespec类型的数据结构。

  1. struct timespec {
  2.     time_t  tv_sec;     /* seconds 存放自1970年1月1日午夜以来经过的秒数 */
  3.     long    tv_nsec;    /* nanoseconds 存放自上一秒开始经过的纳秒数 */
  4. };

单处理器系统上的计时体系结构
在单处理器上,所有与定时有关的活动都是IRQ线0上的可编程间隔定时器产生的中断触发的。

初始化阶段

初始化阶段,time_init建立计时体系结构。

  1. irqreturn_t timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
  2. {
  3.     /*
  4.      * Here we are in the timer irq handler. We just have irqs locally
  5.      * disabled but we don’t know if the timer_bh is running on the other
  6.      * CPU. We need to avoid to SMP race with it. NOTE: we don’ t need
  7.      * the irq version of write_lock because as just said we have irq
  8.      * locally disabled. -arca
  9.      */
  10.     write_seqlock(&xtime_lock);//保护与定时相关的内核变量
  11.     cur_timer->mark_offset();
  12.     do_timer_interrupt(irq, NULL, regs);
  13.     write_sequnlock(&xtime_lock);
  14.     return IRQ_HANDLED;
  15. }

do_timer_interrupt函数,执行如下操作:

  1. 使jiffies_64的值增1,这时候为写操作持有xtime_lock顺序锁
  2. 调用update_times函数,更新系统日期和时间。
  3. 调用update_process_times函数为本地CPU执行几个与定时相关的计数操作。
  4. 调用profile_tick函数
  5. 如果使用外部时钟来同步系统时钟,则每隔660秒,调用一次set_rtc_mmss调整实时时钟。

多处理器系统上的计时体系结构

多处理器系统可以依赖两种不同的时钟中断源:可编程间隔定时器或高精度事件定时器产生的中断源。

初始化阶段

函数apic_intr_init中,根据LOCAL_TIMER_VECTOR和低级中断处理程序apic_timer_interrupt的地址设置IDT的中断门。每个APIC必须被告知多久产生一次本地时钟中断。函数calibrate_APIC_clock通过正在启动的CPU的本地APIC来计算在一个节拍内收到多少个总线时钟信号。然后用这个值来对本地所有的APIC编程,通过setup_APIC_timer函数完成。

全局时钟中断处理程序

本地时钟中断处理程序

该处理程序执行系统中与特定CPU相关的计时活动,监管内核代码并检测当前进程在特定CPU上的运行时间。

  1. fastcall void smp_apic_timer_interrupt(struct pt_regs *regs)
  2. {
  3.     int cpu = smp_processor_id();//获得CPU逻辑号
  4.     /*
  5.      * the NMI deadlock-detector uses this.
  6.      */
  7.     irq_stat[cpu].apic_timer_irqs++;
  8.     /*
  9.      * NOTE! We’d better ACK the irq immediately,
  10.      * because timer handling can be slow.
  11.      */
  12.     ack_APIC_irq();//应答本地APIC上的中断
  13.     /*
  14.      * update_process_times() expects us to have done irq_enter().
  15.      * Besides, if we don’t timer interrupts ignore the global
  16.      * interrupt lock, which is the WrongThing (tm) to do.
  17.      */
  18.     irq_enter();
  19.     smp_local_timer_interrupt(regs);
  20.     irq_exit();
  21. }
  1. inline void smp_local_timer_interrupt(struct pt_regs * regs)
  2. {
  3.     int cpu = smp_processor_id();
  4.     profile_tick(CPU_PROFILING, regs);
  5.     if (–per_cpu(prof_counter, cpu) <= 0) {
  6.         per_cpu(prof_counter, cpu) = per_cpu(prof_multiplier, cpu);
  7.         if (per_cpu(prof_counter, cpu) !=
  8.                     per_cpu(prof_old_multiplier, cpu)) {
  9.             __setup_APIC_LVTT(
  10.                     calibration_result/
  11.                     per_cpu(prof_counter, cpu));
  12.             per_cpu(prof_old_multiplier, cpu) =
  13.                         per_cpu(prof_counter, cpu);
  14.         }
  15. #ifdef CONFIG_SMP
  16.         update_process_times(user_mode(regs));//检查当前进程运行的时间,并更新一些本地CPU统计数
  17. #endif
  18.     }
  19. }

更新时间和日期
全局时钟中断处理程序调用update_times函数更新xtime变量的值

  1. static inline void update_times(void)
  2. {
  3.     unsigned long ticks;
  4.     ticks = jiffies – wall_jiffies;
  5.     if (ticks) {
  6.         wall_jiffies += ticks;
  7.         update_wall_time(ticks);
  8.     }
  9.     calc_load(ticks);
  10. }
  11. static void update_wall_time(unsigned long ticks)
  12. {
  13.     do {
  14.         ticks–;
  15.         update_wall_time_one_tick();
  16.         if (xtime.tv_nsec >= 1000000000) {
  17.             xtime.tv_nsec -= 1000000000;
  18.             xtime.tv_sec++;
  19.             second_overflow();
  20.         }
  21.     } while (ticks);
  22. }

更新系统统计次数
更新本地CPU统计数
=========================================

  1. void update_process_times(int user_tick)
  2. {
  3.     struct task_struct *p = current;
  4.     int cpu = smp_processor_id();
  5.     /* Note: this timer irq context must be accounted for as well. */
  6.     if (user_tick)//根据当前进程运行在用户态
  7.         account_user_time(p, jiffies_to_cputime(1));
  8.     else
  9.         account_system_time(p, HARDIRQ_OFFSET, jiffies_to_cputime(1));
  10.     run_local_timers();
  11.     if (rcu_pending(cpu))
  12.         rcu_check_callbacks(cpu, user_tick);//检查本地CPU是否经历了静止状态并调用tasklet_schedule来激活本地CPU的rcu_tasklet任务队列
  13.     scheduler_tick();//使当前进程的时间片计数器减1,检查计数器是否到0。
  14. }
  1. void account_user_time(struct task_struct *p, cputime_t cputime)
  2. {
  3.     struct cpu_usage_stat *cpustat = &kstat_this_cpu.cpustat;
  4.     cputime64_t tmp;
  5.     p->utime = cputime_add(p->utime, cputime);
  6.     /* Check for signals (SIGVTALRM, SIGPROF, SIGXCPU & SIGKILL). */
  7.     check_rlimit(p, cputime);
  8.     account_it_virt(p, cputime);
  9.     account_it_prof(p, cputime);
  10.     /* Add user time to cpustat. */
  11.     tmp = cputime_to_cputime64(cputime);
  12.     if (TASK_NICE(p) > 0)
  13.         cpustat->nice = cputime64_add(cpustat->nice, tmp);
  14.     else
  15.         cpustat->user = cputime64_add(cpustat->user, tmp);
  16. }
  1. void run_local_timers(void)
  2. {
  3.     raise_softirq(TIMER_SOFTIRQ);//激活本地CPU上的TIMER_SOFTIRQ队列
  4. }

account_user_time或是account_system_time函数执行:

  1. 更新当前进程描述符的utime字段或stime字段。进程描述符中提供两个cutime和cstime的附加字段,分别用来统计子进程在用户态和内核态下所经过的CPU节拍数。在这里,并不更新这两个字段。只是父进程询问她的其中一个子进程的状态时才对其进行更新。
  2. 检查是否达到总的CPU时限,如果是,向current进程发送SIGXCPU和SIGKILL信号。
  3. 调用account_it_virt和account_it_prof检查进程定时器
  4. 更新一些内核统计数,存放在每CPU变量kstat中。

记录系统负载

  1. static inline void calc_load(unsigned long ticks)
  2. {//计算平均负载
  3.     unsigned long active_tasks; /* fixed-point */
  4.     static int count = LOAD_FREQ;
  5.     count -= ticks;
  6.     if (count < 0) {
  7.         count += LOAD_FREQ;
  8.         active_tasks = count_active_tasks();
  9.         CALC_LOAD(avenrun[0], EXP_1, active_tasks);
  10.         CALC_LOAD(avenrun[1], EXP_5, active_tasks);
  11.         CALC_LOAD(avenrun[2], EXP_15, active_tasks);
  12.     }
  13. }

监管内核代码

linux包含一个readprofiler的最低要求的代码监管器,确定内核的“热点”(hot spot) —执行最频繁的内核代码片段。

  1. <span style=”font-size:13px;”>void profile_tick(int type, struct pt_regs *regs)//为代码监管器采集数据
  2. {
  3.     if (type == CPU_PROFILING && timer_hook)
  4.         timer_hook(regs);
  5.     if (!user_mode(regs) && cpu_isset(smp_processor_id(), prof_cpu_mask))
  6.         profile_hit(type, (void *)profile_pc(regs));
  7. }</span>

这个函数在单处理器系统上被do_timer_interrupt调用,多处理器系统被smp_local_timer_interrupt调用。

当使用oprofile采集数据时,profile_tick调用timer_notify函数来收集这个新监管器所使用的数据。

检查非屏蔽中断(NMI)监视器

多处理器系统上,linux为内核开发者提供了另外一种功能:看门狗系统,对于探测引起系统冻结的内核bug相当有用。必须内核启动时,传递nmi_watchdog参数。

看门狗基于I/O APIC巧妙的硬件特性,能在每个CPU上产生周期性的NMI中断。这个中断不能用汇编语言cli,即使禁止中断,看门狗也能检测到死锁。

一旦每个时钟节拍到来,所有CPU都开始执行NMI中断处理程序,该程序又调用do_nmi。

  1. <span style=”font-size:13px;”>fastcall void do_nmi(struct pt_regs * regs, long error_code)
  2. {
  3.     int cpu;
  4.     nmi_enter();
  5.     cpu = smp_processor_id();//获取CPU的逻辑号n
  6.     ++nmi_count(cpu);//检查irq_stat数组第n项的apic_timer_irqs字段
  7.     if (!nmi_callback(regs, cpu))
  8.         default_do_nmi(regs);
  9.     nmi_exit();
  10. }</span>

当NMI中断处理程序检测到一个CPU冻结时,把引起恐慌的信息记录在系统日志文件中,转储该CPU寄存器的内容和内核栈的内容,并杀死当前进程。

软定时器和延迟函数

每个定时器都包含一个字段,这个字段的初值就是jiffies的当前值加上合适的节拍数。这个字段的值不再改变,每当内核检查定时器时,会比较这个值和当前jiffies的值,如果jiffies大于存放的值,定时器到期。

linux考虑两种类型的定时器,即动态定时器(由内核使用)和间隔定时器(由进程在用户态创建)。

动态定时器

动态定时器存放在timer_list结构中:

  1. <span style=”font-size:13px;”>struct timer_list {
  2.     struct list_head entry;//将软定时器插入双向循环链表队列中
  3.     unsigned long expires;//给出定时器到期时间,用节拍数表示
  4.     spinlock_t lock;
  5.     unsigned long magic;
  6.     void (*function)(unsigned long);//包含定时器到期时执行函数的地址
  7.     unsigned long data;//传给定时器函数的参数
  8.     struct tvec_t_base_s *base;
  9. };</span>

为了创建并激活一个动态定时器,内核必须:

  1. 如果需要,创建一个timer_list对象,可以通过:代码中定义一个静态全局变量;函数内定义一个局部变量,对象存放在内核堆栈;动态分配的描述符中包含这个对象;这几个方式来进行。
  2. 通过init_timer(&t)初始化这个对象
  3. 把定时器到期时激活函数的地址存入function字段。如果需要,把传递给函数的参数值存入data字段。
  4. 如果定时器还没有插入到链表中,给expires字段赋一个值并调用add_timer(&t);
  5. 否则,如果动态定时器已经被插入到链表中,则调用mod_timer函数来更新expires字段。

一旦定时器到期,内核就自动把元素从链表中删除,不过,有时进程需要del_timer, del_timer_sync, del_singleshot_timer_sync函数显示的从定时器链表中删除一个定时器。

动态定时器与竞争条件
在多处理器系统上,del_timer函数有时不安全,如果定时器函数还在其他CPU上运行,定时器函数还作用在资源上时,资源可能被释放,此时应该用del_timer_sync函数,删除定时器时,会检查是否还在其他CPU上运行,如果是,就等待,直到定时器函数结束。

如果内核开发者知道定时器函数从不重新激活定时器,就简单使用del_singleshot_timer_sync是定时器无效,并等待知道定时器结束。

动态定时器的数据结构

动态定时器的主要数据结构是一个叫做tvec_bases的每CPU变量,包含NR_CPUS个元素,每个元素都是tvec_base_t类型的数据结构。

  1. struct tvec_t_base_s {
  2.     spinlock_t lock;
  3.     unsigned long timer_jiffies;//需要检查的动态定时器的最早到期时间;
  4.     struct timer_list *running_timer;//在多处理器中,指向本地CPU当前正处理的动态定时器的timer_list数据结构
  5.     tvec_root_t tv1;//包含一个vec数组,由256个list_head元素组成
  6.     tvec_t tv2;
  7.     tvec_t tv3;
  8.     tvec_t tv4;
  9.     tvec_t tv5;
  10. } ____cacheline_aligned_in_smp;
  11. typedef struct tvec_t_base_s tvec_base_t;

================================
动态定时器处理

run_timer_softirq函数是与TIMER_SOFTIRQ软中断请求相关的可延迟函数。

  1. static void run_timer_softirq(struct softirq_action *h)
  2. {
  3.     tvec_base_t *base = &__get_cpu_var(tvec_bases);//把本地CPU相关的tvec_base_t数据结构的地址存放在base本地变量中
  4.     if (time_after_eq(jiffies, base->timer_jiffies))
  5.         __run_timers(base);
  6. }
  7. static inline void __run_timers(tvec_base_t *base)
  8. {
  9.     struct timer_list *timer;
  10.     spin_lock_irq(&base->lock);//获得lock自旋锁
  11.     while (time_after_eq(jiffies, base->timer_jiffies)) {
  12.         struct list_head work_list = LIST_HEAD_INIT(work_list);
  13.         struct list_head *head = &work_list;
  14.         int index = base->timer_jiffies & TVR_MASK;//计算tv1中链表的索引,保存在index
  15.         /*
  16.          * Cascade timers:
  17.          */
  18.         if (!index &&//如果index为0,说明tv1中所有的链表已经被检查过,调用cascade来过滤动态定时器
  19.             (!cascade(base, &base->tv2, INDEX(0))) &&
  20.                 (!cascade(base, &base->tv3, INDEX(1))) &&
  21.                     !cascade(base, &base->tv4, INDEX(2)))
  22.             cascade(base, &base->tv5, INDEX(3));
  23.         ++base->timer_jiffies;
  24.         list_splice_init(base->tv1.vec + index, &work_list);//执行对应tv1.vec[index]链表上的每一个定时器
  25. repeat:
  26.         if (!list_empty(head)) {
  27.             void (*fn)(unsigned long);
  28.             unsigned long data;
  29.             timer = list_entry(head->next,struct timer_list,entry);
  30.             fn = timer->function;
  31.             data = timer->data;
  32.             list_del(&timer->entry);
  33.             set_running_timer(base, timer);
  34.             smp_wmb();
  35.             timer->base = NULL;
  36.             spin_unlock_irq(&base->lock);
  37.             {
  38.                 u32 preempt_count = preempt_count();
  39.                 fn(data);
  40.                 if (preempt_count != preempt_count()) {
  41.                     printk(“huh, entered %p with %08x, exited with %08x?\n”, fn, preempt_count, preempt_count());
  42.                     BUG();
  43.                 }
  44.             }
  45.             spin_lock_irq(&base->lock);
  46.             goto repeat;
  47.         }
  48.     }
  49.     set_running_timer(base, NULL);//设置running_timer为NULL
  50.     spin_unlock_irq(&base->lock);//释放lock自旋锁并 允许本地中断
  51. }

动态定时器应用之一:nanosleep系统调用

  1. asmlinkage long sys_nanosleep(struct timespec __user *rqtp, struct timespec __user *rmtp)
  2. {
  3.     struct timespec t;
  4.     unsigned long expire;
  5.     long ret;
  6.     if (copy_from_user(&t, rqtp, sizeof(t)))//将包含在timespec结构中的值复制到局部t中
  7.         return -EFAULT;
  8.     if ((t.tv_nsec >= 1000000000L) || (t.tv_nsec < 0) || (t.tv_sec < 0))
  9.         return -EINVAL;
  10.     expire = timespec_to_jiffies(&t) + (t.tv_sec || t.tv_nsec);//timespec_to_jiffies将timespec结构中的时间间隔转换成节拍数,保险起见,加上一个节拍
  11.     current->state = TASK_INTERRUPTIBLE;
  12.     expire = schedule_timeout(expire);
  13.     ret = 0;
  14.     if (expire) {
  15.         struct restart_block *restart;
  16.         jiffies_to_timespec(expire, &t);
  17.         if (rmtp && copy_to_user(rmtp, &t, sizeof(t)))
  18.             return -EFAULT;
  19.         restart = ¤t_thread_info()->restart_block;
  20.         restart->fn = nanosleep_restart;
  21.         restart->arg0 = jiffies + expire;
  22.         restart->arg1 = (unsigned long) rmtp;
  23.         ret = -ERESTART_RESTARTBLOCK;
  24.     }
  25.     return ret;
  26. }
  1. fastcall signed long __sched schedule_timeout(signed long timeout)
  2. {
  3.     struct timer_list timer;
  4.     unsigned long expire;
  5.     switch (timeout)
  6.     {
  7.     case MAX_SCHEDULE_TIMEOUT:
  8.         /*
  9.          * These two special cases are useful to be comfortable
  10.          * in the caller. Nothing more. We could take
  11.          * MAX_SCHEDULE_TIMEOUT from one of the negative value
  12.          * but I’ d like to return a valid offset (>=0) to allow
  13.          * the caller to do everything it want with the retval.
  14.          */
  15.         schedule();//进程挂起,直到定时器到时
  16.         goto out;
  17.     default:
  18.         /*
  19.          * Another bit of PARANOID. Note that the retval will be
  20.          * 0 since no piece of kernel is supposed to do a check
  21.          * for a negative retval of schedule_timeout() (since it
  22.          * should never happens anyway). You just have the printk()
  23.          * that will tell you if something is gone wrong and where.
  24.          */
  25.         if (timeout < 0)
  26.         {
  27.             printk(KERN_ERR “schedule_timeout: wrong timeout “
  28.                    “value %lx from %p\n”, timeout,
  29.                    __builtin_return_address(0));
  30.             current->state = TASK_RUNNING;
  31.             goto out;
  32.         }
  33.     }
  34.     expire = timeout + jiffies;
  35.     init_timer(&timer);
  36.     timer.expires = expire;
  37.     timer.data = (unsigned long) current;//延迟函数接收进程描述符作为参数
  38.     timer.function = process_timeout;
  39.     add_timer(&timer);
  40.     schedule();
  41.     del_singleshot_timer_sync(&timer);
  42.     timeout = expire – jiffies;
  43.  out:
  44.     return timeout < 0 ? 0 : timeout;
  45. }
  1. tatic void process_timeout(unsigned long __data)
  2. {
  3.     wake_up_process((task_t *)__data);//延迟到期,执行这个函数
  4. }

一旦进程唤醒,继续执行sys_nanosleep系统调用,如果schedule_timeout返回的值表明进程延时到期,就结束,否则,将自动重新启动。

当内核需要较短的时间间隔时,内核使用udelay和ndelay函数,前者是微秒级的时间间隔,后者是纳秒级的时间间隔。

  1. void __delay(unsigned long loops)
  2. {
  3.     cur_timer->delay(loops);
  4. }
  5. inline void __const_udelay(unsigned long xloops)
  6. {
  7.     int d0;
  8.     xloops *= 4;
  9.     __asm__(“mull %0”
  10.         :”=d” (xloops), “=&a” (d0)
  11.         :”1″ (xloops),”0″ (cpu_data[_smp_processor_id()].loops_per_jiffy * (HZ/4)));
  12.         __delay(++xloops);
  13. }
  14. void __udelay(unsigned long usecs)
  15. {
  16.     __const_udelay(usecs * 0x000010c7);  /* 2**32 / 1000000 (rounded up) */
  17. }
  18. void __ndelay(unsigned long nsecs)
  19. {
  20.     __const_udelay(nsecs * 0x00005);  /* 2**32 / 1000000000 (rounded up) */
  21. }
  22. #define udelay(n) (__builtin_constant_p(n) ? \
  23.     ((n) > 20000 ? __bad_udelay() : __const_udelay((n) * 0x10c7ul)) : \
  24.     __udelay(n))
  25. #define ndelay(n) (__builtin_constant_p(n) ? \
  26.     ((n) > 20000 ? __bad_ndelay() : __const_udelay((n) * 5ul)) : \
  27.     __ndelay(n))

与定时测量相关的系统调用
time和gettimeofday系统调用
gettimeofday系统调用由sys_gettimeofday函数实现,这个函数又调用do_gettimeofday

  1. smlinkage long sys_gettimeofday(struct timeval __user *tv, struct timezone __user *tz)
  2. {
  3.     if (likely(tv != NULL)) {
  4.         struct timeval ktv;
  5.         do_gettimeofday(&ktv);
  6.         if (copy_to_user(tv, &ktv, sizeof(ktv)))
  7.             return -EFAULT;
  8.     }
  9.     if (unlikely(tz != NULL)) {
  10.         if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
  11.             return -EFAULT;
  12.     }
  13.     return 0;
  14. }
  1. void do_gettimeofday(struct timeval *tv)
  2. {
  3.     unsigned long seq;
  4.     unsigned long usec, sec;
  5.     unsigned long max_ntp_tick;
  6.     do {
  7.         unsigned long lost;
  8.         seq = read_seqbegin(&xtime_lock);//为读操作获取xtime_lock顺序锁
  9.         usec = cur_timer->get_offset();//确定自上一次时钟中断以来所有过的微秒数
  10.         lost = jiffies – wall_jiffies;
  11.         /*
  12.          * If time_adjust is negative then NTP is slowing the clock
  13.          * so make sure not to go into next possible interval.
  14.          * Better to lose some accuracy than have time go backwards..
  15.          */
  16.         if (unlikely(time_adjust < 0)) {
  17.             max_ntp_tick = (USEC_PER_SEC / HZ) – tickadj;
  18.             usec = min(usec, max_ntp_tick);
  19.             if (lost)
  20.                 usec += lost * max_ntp_tick;
  21.         }
  22.         else if (unlikely(lost))
  23.             usec += lost * (USEC_PER_SEC / HZ);
  24.         sec = xtime.tv_sec;
  25.         usec += (xtime.tv_nsec / 1000);
  26.     } while (read_seqretry(&xtime_lock, seq));
  27.     while (usec >= 1000000) {//检查微秒字段是否溢出,如果必要则调整该字段和秒字段
  28.         usec -= 1000000;
  29.         sec++;
  30.     }
  31.     tv->tv_sec = sec;
  32.     tv->tv_usec = usec;
  33. }

do_gettimeofday和do_settimeofday修改xtime值时,没有修改RTC寄存器,系统关机时,新的时间会丢失。除非用户用clock程序改变RTC的值。

adjtimex系统调用

把系统配置成能在常规基准上运行时间同步协议,例如网络定时协议(NTP),依赖于adjtimex系统调用。

setitimer和alarm系统调用

linux允许用户态程序激活一种叫做间隔定时器的特殊定时器。间隔定时器由以下两个方面来刻画:

  • 发送信号所必需的频率
  • 在下一个信号被产生以前所剩余的时间

通过setitimer可以激活间隔定时器,第一个参数采取如下:

ITIMER_REAL:真正过去的时间,进程接收SIGALRM信号

ITIMER_VIRTUAL:进程在用户态下花费的时间,进程接收SIGALRM信号

ITIMER_PROF:进程既在用户态下又在内核态下所花费的时间;接收SIGPROF信号。

linux 多网卡共享internet

 |-------|eth0 192.168.5.2               eth1 192.168.5.1|--------------|eth0 192.168.3.136                  |-----------------|eth1 192.168.1.2      192.168.1.1 |--------------------------|
 |host  |--------------------------------------------------------|host share |------------------------------------------|router ddwrt  |------------------------------------------|router internet home|
 |-------|                                                        |--------------|                      br0 192.168.3.1 |-----------------|                                          |--------------------------|

host share:

echo 1 >/proc/sys/net/ipv4/ip_forward

Dekernel:~ # route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.3.1 0.0.0.0 UG 0 0 0 eth0
127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo
192.168.3.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0
192.168.5.0 0.0.0.0 255.255.255.0 U 0 0 0 eth1

route ddwrt

route add -net 192.168.5.0 netmask 255.255.255.0 gw 192.168.3.136

root@router:~# route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
192.168.5.0 192.168.3.136 255.255.255.0 UG 0 0 0 br0
192.168.3.0 * 255.255.255.0 U 0 0 0 br0
192.168.1.0 * 255.255.255.0 U 0 0 0 eth1
169.254.0.0 * 255.255.0.0 U 0 0 0 br0
127.0.0.0 * 255.0.0.0 U 0 0 0 lo
default 192.168.1.1 0.0.0.0 UG 0 0 0 eth1

 

补充:

如果不想在route上添加路由也能共享,就需要host share从二层转发,其实就是NAT 功能 ,可以用iptables实现:

iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -t nat -A POSTROUTING -o eth_s0_0 -j MASQUERADE

linux kernel 固有内核线程

(1)kthreadd:这种内核线程只有一个,它的作用是管理调度其它的内核线程。它在内核初始化的时候被创建,会循环运行一个叫做kthreadd的函数,该函数的作用是运行kthread_create_list全局链表中维护的kthread。可以调用kthread_create创建一个kthread,它会被加入到kthread_create_list链表中,同时kthread_create会weak up kthreadd_task。kthreadd在执行kthread会调用老的接口——kernel_thread运行一个名叫“kthread”的内核线程去运行创建的kthread,被执行过的kthread会从kthread_create_list链表中删除,并且kthreadd会不断调用scheduler 让出CPU。这个线程不能关闭。

(2)migration:这种内核线程共有32个,从migration/0到migration/31,每个处理器核对应一个migration内核线程,主要作用是作为相应CPU核的迁移进程,用来执行进程迁移操作,内核中的函数是migration_thread()。属于2.6内核的负载平衡系统,该进程在系统启动时自动加载(每个 cpu 一个),并将自己设为 SCHED_FIFO 的实时进程,然后检查 runqueue::migration_queue 中是否有请求等待处理,如果没有,就在 TASK_INTERRUPTIBLE 中休眠,直至被唤醒后再次检查。migration_queue仅在set_cpu_allowed() 中添加,当进程(比如通过 APM 关闭某 CPU 时)调用set_cpu_allowed()改变当前可用 cpu,从而使某进程不适于继续在当前 cpu 上运行时,就会构造一个迁移请求数据结构 migration_req_t,将其植入进程所在 cpu 就绪队列的migration_queue 中,然后唤醒该就绪队列的迁移 daemon(记录在runqueue::migration_thread 属性中),将该进程迁移到合适的cpu上去在目前的实现中,目的 cpu 的选择和负载无关,而是”any_online_cpu(req->task->cpus_allowed)”,也就是按 CPU 编号顺序的第一个 allowed 的CPU。所以,和 load_balance() 与调度器、负载平衡策略密切相关不同,migration_thread() 应该说仅仅是一个 CPU 绑定以及 CPU 电源管理等功能的一个接口。这个线程是调度系统的重要组成部分,也不能被关闭。

(3)watchdog:这种内核线程共有32个,从watchdog/0到watchdog/31, 每个处理器核对应一个watchdog 内核线程,watchdog用于监视系统的运行,在系统出现故障时自动重新启动系统,包括一个内核 watchdog module 和一个用户空间的 watchdog 程序。在Linux 内核下, watchdog的基本工作原理是:当watchdog启动后(即/dev/watchdog设备被打开后),如果在某一设定的时间间隔(1分钟)内/dev/watchdog没有被执行写操作, 硬件watchdog电路或软件定时器就会重新启动系统,每次写操作会导致重新设定定时器。/dev/watchdog是一个主设备号为10, 从设备号130的字符设备节点。 Linux内核不仅为各种不同类型的watchdog硬件电路提供了驱动,还提供了一个基于定时器的纯软件watchdog驱动。如果不需要这种故障处理机制,或者有相应的替代方案,可以在menuconfig的

Device Drivers —>

Watchdog Timer Support

处取消watchdog功能。

(4)events:这种内核线程共有32个,从events/0到events/31, 每个处理器核对应一个 events 内核线程。用来处理内核事件很多软硬件事件(比如断电,文件变更)被转换为events,并分发给对相应事件感兴趣的线程进行响应。用来处理内核事件的重要线程,不能被去掉

(5)khelper:这种内核线程只有一个,主要作用是指定用户空间的程序路径和环境变量, 最终运行指定的user space的程序,属于关键线程,不能关闭

(6)kblockd:这种内核线程共有32个,从kblockd/0到kblockd/31, 每个处理器核对应一个 kblockd 内核线程。用于管理系统的块设备,它会周期地激活系统内的块设备驱动。如果拥有块设备,那么这些线程就不能被去掉,要是想去掉,需要在.config中直接将CONFIG_BLOCK设成n,同时在menuconfig中取消

Device Drivers   —>

Block devices

(7)kseriod:这种内核线程只有一个,主要作用是管理Serio总线上的设备的各种事件,Serio是一种虚拟总线,是Serial I/O的输写,表示串行的输入输出设备。对应内核中的serio_thread函数,流程大致是这样的:调用serio_get_event()从链表中取出struct serio_event元素,然后对这个元素的事件类型做不同的时候,处理完了之后,调用serio_remove_duplicate_events()在链表中删除相同请求的event。例如:如果要注册新的serio设备,它产生的事件类型是SERIO_REGISTER_PORT,然后流程会转入serio_add_port()。使用Serio总线的主要是标准AT键盘、PS/2鼠标、串口鼠标、Sun键盘,以及一些游戏手柄,不过由于I2C依赖于Serio,所以不关闭I2C就无法关闭Serio,menuconfig中SerialI/O的开关位于

Device Driver  —>

Inputdevice support

HardwareI/O ports

SerialI/O support

(8)pdflush:这种内核线程共有两个,线程名都是pdflush,主要作用是回写内存中的脏页,回收脏页占据的空间。由于页高速缓存的缓存作用,写操作实际上会被延迟。当页高速缓存中的数据比后台存储的数据更新时,那么该数据就被称做脏数据。在内存中累积起来的脏页最终必须被写回。在以下两种情况发生时,脏页被写回:

1.当空闲内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。

2.当脏页在内存中驻留时间超过一个特定的阈值时,内核必须将超时的脏页写回磁盘,以确保脏页不会无限期地驻留在内存中。

对于第一个目标,pdflush线程在系统中的空闲内存低于一个特定的阈值时,将脏页刷新回磁盘。该后台回写例程的目的在于在可用物理 内存过低时,释放脏页以重新获得内存。特定的内存阈值可以通过dirty_background_ratiosysctl系统调用设置。当空闲内存比阈值:dirty_ background_ratio还低时,内核便会调用函数wakeup_bdflush()唤醒一个pdflush线程,随后pdflush线程进一步 调用函数background_writeout()开始将脏页写回磁盘。函数background_ writeout()需要一个长整型参数,该参数指定试图写回的页面数目。函数background_writeout()会连续地写出数据,直到满足以下两个条件:

  1. 已经有指定的最小数目的页被写出到磁盘。
  2. 空闲内存数已经回升,超过了阈值dirty_background_ratio。

上述条件确保了pdflush操作可以减轻系统中内存不足的压力。回写操作不会在达到这两个条件前停止,除非pdflush写回了所有的脏页,没有剩下的脏页可再被写回了。

对于第二个目标,pdflush后台例程会被周期性唤醒(和空闲内存是否过低无关),将那些在内存中驻留时间过长的脏页写出,确保内存中不会有长期存在的脏页。如果系统发生崩溃,由于内存处于混乱之中,所以那些在内存中还没来得及写回磁盘 的脏页就会丢失,所以周期性同步页高速缓存和磁盘非常重要。在系统启动时,内核初始化一个定时器,让它周期地唤醒pdflush线程,随后使其运行函数 wb_kupdate()。该函数将把所有驻留时间超过百分之dirty_expire_centisecs秒的脏页写回。然后定时器将再次被初始化为百 分之dirty_expire_ centisecs秒后唤醒pdflush线程。总而言之,pdflush线程周期地被唤醒并且把超过特定期限的脏页写回磁盘。

系统管理员可以在/proc/sys/vm中设置回写相关的参数,也可以通过sysctl系统调用设置它们

        属于核心的内存管理线程,这个线程也不能被关闭

(9)kswapd0:这种内核线程只有一个,主要作用是用来回收内存。在kswapd中,有2个阀值,pages_hige和pages_low。当空闲内存页的数量低于pages_low的时候,kswapd进程就会扫描内存并且每次释放出32个 free pages,直到freepage的数量到达pages_high。具体回收内存有如下原则:

1. 如果页未经更改就将该页放入空闲队列;

2. 如果页已经更改并且是可备份回文件系统的,就理解将内存页的内容写回磁盘;

3. 如果页已经更改但是没有任何磁盘上的备份,就将其写入swap分区。

同样,属于核心的内存管理线程,这个线程也不能被关闭

(10)aio:这种内核线程共有32个,从aio/0到aio/31, 每个处理器核对应一个 aio 内核线程, 代替用户进程管理I/O,用以支持用户态的AIO(异步I/O),不应该被关闭。

(11)unionfs_siod: 这种内核线程共有32个,但是名称都是 unionfs_siod/,每个处理器核对应一个 unionfs_siod 内核线程

(12)nfsiod:这种内核线程只有一个,主要作用是为nfs提供高效的缓冲机制,从而改善nfs文件系统的性能,如果不需nfs,可以取消这一线程,取消这一线程的方法为menuconfig中取消

File systems  —>

Network File Systems

(13)rpciod:这种内核线程共有32个,从rpciod/0到rpciod/31, 每个处理器核对应一个rpciod 内核线程,主要作用是作为远过程调用服务的守护进程,用于从客户端启动I/O服务,通常启动NFS服务时要用到它,想要关闭它,需要在.config中把CONFIG_SUNRPC, CONFIG_SUNRPC_GSS, CONFIG_SUNRPC_XPRT_RDMA的值设成n

(14)kpsmoused:这种内核线程只有一个,主要作用是支持ps/2接口的鼠标驱动。如要没有鼠标,可以取消,取消方法是menuconfig中取消

DeviceDrivers   —>

Input device support

Mice

[]

Netlink中常用宏定义

在linux/netlink.h中定义了一些方便对消息进行处理的宏,这些宏包括:

#define NLMSG_ALIGNTO 4 #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )

 

宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值。

#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))

 

宏NLMSG_LENGTH(len)用于计算数据部分长度为len时实际的消息长度。它一般用于分配消息缓存。

#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

 

宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存。

#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))

 

宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏。

#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), / (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))

 

宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址,同时len也减少为剩余消息的总长度,该宏一般在一个消息被分成几个部分发送或接收时使用。

#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && / (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && / (nlh)->nlmsg_len <= (len))

 

宏NLMSG_OK(nlh,len)用于判断消息是否有len这么长。

#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len – NLMSG_SPACE((len)))

 

宏NLMSG_PAYLOAD(nlh,len)用于返回payload的长度。

函数close用于关闭打开的netlink socket。

man 无法用了??!!

突然man无法用了,提示:

提示/usr/bin/nroff: Can’t create temp directory,

nroff是个脚本,里面有调用mktemp 在/tmp 里面生成临时目录。看来是/tmp 有问题了?!  第一个想到使用df -h 去看看tmp是不是满了, 但排除掉了。我用mktemp 也可以直接生成一个temp目录。 这是怎么回事??

最后搞了半天发现/tmp 权限为733 ,我改成了777 ,  ok了。 去看看另外系统上的/tmp 确实是777  ,难道谁改了, 很奇怪。不排除其他软件有bug。