我们组此次课设的选取的题目是:阅读 Linux 内核代码,任务 4: 时钟中断与进程调度。
一.导言
其中含 3 个部分:
(1)kernel/sched.c
(2)arch/i386/kernel/time.c
(3)kernel/timer.c
在课设期间,我们参考了有关 linux 内核代码的书籍,以及在网上查阅了很多相关的资料进
行阅读分析初步以有助于我们对程序的理解。我们用的阅读软件是:Source Insight3.5。
二.相关知识简介
2.1什么是Linux
Linux是一类Unix计算机操作系统的统称。Linux操作系统的内核的名字也是“Linux”。
Linux操作系统也是自由软件和开放源代码发展中最著名的例子。严格来讲,Linux这个词本
身只表示Linux内核,但在实际上人们已经习惯了用Linux来形容整个基于Linux内核,并且
使用GNU 工程各种工具和数据库的操作系统。
2.2 什么是进程调度
进程:计算机中进程分为两种,一为普通进程,它的优先级用进程控制块中的counter表
示,二为实时进程,实时进程的优先级用rt_priority表示,它在进程控制块中有定义,当就绪
队列中同时存在两种进程的时候,实时进程总是先于普通进程运行,它的实现机制是实时进
程的权值以1000做为基础值,也就是说实时进程的权值是1000加上它的进程优先级,而普通
进程的优先级就是它的进程优先级.
2.3 Intel 8254 PIT芯片简介
Intel 8254 PIT有3个计时通道,每个通道都有其不同的用途:
(1) 通道0用来负责更新系统时钟。每当一个时钟滴答过去时,它就会通过IRQ0向系统产
生一次时钟中断。
(2) 通道1通常用于控制DMAC对RAM的刷新。
(3) 通道2被连接到PC机的扬声器,以产生方波信号。
1
每个通道都有一个向下减小的计数器,8254
PIT的输入时钟信号的频率是1193181HZ,
也即一秒钟输入1193181个clock-cycle。每输入一个clock-cycle其时间通道的计数器就向
下减1,一直减到0值。因此对于通道0而言,当他的计数器减到0时,PIT就向系统产生一次
时钟中断,表示一个时钟滴答已经过去了。当各通道的计数器减到0时,我们就说该通道处
于“Terminal count”状态。
2.4 时间系统一些基本概念
1、实时时钟(RTC时钟)
也叫CMOS时钟,是PC主板上的一块芯片(或叫做时钟电路),靠电池供电,即使系统断电,
也可以维持日期和时间。独立于操作系统,也被称为硬件电路,为整个计算机提供一个计时
标准,是最原始最底层的时钟数据。Linux只用RTC来获取时间和日期,也允许进程对RTC编
程。内核通过访问I/O端口存取RTC,同时,系统管理员可以配置时钟。
2、操作系统时钟(软时钟、系统时钟)
产生于PC主板上的定时/计数芯片,由操作系统控制这个芯片的工作,基本单位是该芯
片的计数周期。开机时操作系统去的RTC中的时间数据来初始化OS时钟,通过计数芯片的向
下计数形成OS时钟。
3、时钟运作机制
RTC是OS时钟的基准,操作系统通过读取RTC来初始化OS时钟,此后二者保持同步运行,
每隔一个固定时间会刷新或校正RTC中的信息,共同维持着系统时间。
4、时间基准:1970-01-01 00:00:00
5、时间系统
用全局变量jiffies表示系统自启动以来的时钟滴答数目。
2.5 动态定时器
动态定时器是指内核的定时器队列是可以动态变化的,内核函数init_timer()用来初始
化一个定时器。
假定一个定时器要经过interval个时钟滴答后才到期(interval=expires-jiffies),
则Linux采用了下列思想来实现其动态内核定时器机制:对于那些0≤interval≤255的定时
器,Linux严格按照定时器向量的基本语义来组织这些定时器,也即Linux内核最关心那些在
接下来的255个时钟节拍内就要到期的定时器,因此将它们按照各自不同的expires值组织成
256个定时器向量。而对于那些256≤interval≤0xffffffff的定时器,由于他们离到期还有
一段时间,因此内核并不关心他们,而是将它们以一种扩展的定时器向量语义
2.6 时钟中断
可编程定时器/计数器产生输出脉冲,送入CPU,引发一个中断请求信号,我们称之为
时钟中断。每产生时钟中断,时钟中断处理程序要完成的工作,处理与时间相关的所有信息
(系统时间、进程的时间片、延时、使用CPU的时间、各种定时器),检查是否需要调度以
及处理bottom-half,在创建进程之前,必须保证已初始化好时钟中断处理程序。
三.代码分析之逐步深入
3.1 代码之中的一些变量及函数的语义分析
3.1.1 变量
2
kernel/sched.c
1 counter是进程的动态优先级,它定义了一个在就绪队列的进程当它得到CPU后可运行的
时间,用静态优先级初始化,当然计算机是以时钟中断做为时间的计数器,每发送一个时钟中
断,动态优先级上的时间片就减少一个时钟中断的时间,时间片减到0的时候就退出该进程而
调度另一个进程获得CPU。
2 nice是进程的静态优先级,当一个在就绪队列中的进程获得CPU之后,它被赋予此进程可
占有CPU的时间.这个时间被称为时间片.
3
mm_struct *active_active 内核线程用来指向调用它的普通进程的内存地址空间.当普
通进程在运行时如果发生系统调用,程序就会从用户态转为内核态,内核态中执行的是内核
线程,内核线程没有内存空间地址结构mm_struct,当他需要内存空间地址的时候就会调用用
户态对应进程的用以空间地址结构mm_struct.内核线程就是通过active_mm指针来指向用户
态进程的mm_struct结构
pid是进程标志符,操作系统每创建一个新的进程就要为这个新进程分配
5 pid_t pid
一个进程控制快(PCB),那么系统内核是怎样区分这些进程的呢?就是通过进程标志符pid,系
统在为新的进程分配进程控制块的候,它不是自己去创建,而是直接从上一个进程中复制它
的进程控制块,其中里面的大部分东西保留下来,只做少量的改动,然后它的进程标志符加1
赋值给新的进程控制块.
6 SMP(Symmetrical Multi-Processing)平时所说的双CPU系统,实际上是对称多处理机系
统中最常见的一种,通常称为2路对称多处理。
arch/i386/kernel/time.c
kernel/timer.c
(1):8253/8254 PIT的本质就是对由晶体振荡器产生的时钟周期进行计数,
(2)时钟滴答(clock tick):当PIT通道0的计数器减到0值时,它就在IRQ0上产生一次时钟
中断,也即一次时钟滴答。PIT通道0的计数器的初始值决定了要过多少时钟周期才产生一次
时钟中断,因此也就决定了一次时钟滴答的时间间隔长度。
(3) 时钟滴答的频率(HZ):1秒时间内PIT所产生的时钟滴答次数,i386平台HZ的值是100。
根据HZ的值,可以知道一次时钟滴答的具体时间间隔应该是(1000ms/HZ)=10ms。
(4) 时钟滴答的时间间隔:用全局变量 tick 来表示时钟滴答的时间间隔长度,定义在
kernel/timer.c 文件中,long tick = (1000000 + HZ/2) / HZ;单位微妙(μs),在不同
平台上宏 HZ 的值会有所不同,方程式 tick=1000000÷HZ 的结果可能会是个小数,因此将
其进行四舍五入成一个整数,所以 Linux 将 tick 定义成(1000000+HZ/2)/HZ,其中被
除数表达式中的 HZ/2 的作用就是用来将 tick 值向上圆整成一个整型数。另外,Linux 还
3
用 宏 TICK_SIZE 来 作 为 tick 变 量 的 引 用 别 名 ( alias ) , 其 定 义 如 下 ( arch /
i386/kernel/time.c): #define TICK_SIZE tick
(5) 宏 LATCH:Linux 用宏 LATCH 来定义要写到 PIT 通道 0 的计数器中的值,它表示 PIT 将
每隔多少个时钟周期产生一次时钟中断。显然 LATCH 应该由下列公式计算:
LATCH=(1秒之内的时钟周期个数)÷(1秒之内的时钟中断次数)=(CLOCK_TICK_RATE)
÷ ( HZ ) 。 Linux 将 LATCH 定 义 为 ( include/linux/timex.h ) : #define LATCH
((CLOCK_TICK_RATE + HZ/2) / HZ)
(6) 全局变量jiffies:这是一个32位的无符号整数,用来表示自内核上一次启动以来的时
钟滴答次数。每发生一次时钟滴答,内核的时钟中断处理函数timer_interrupt()都要将
该全局变量jiffies加1
(7) 全局变量xtime:它是一个timeval结构类型的变量,用来表示当前时间距UNIX时间基准
1970-01-01 00:00:00的相对秒数值。结构timeval是Linux内核表示时间的一种格式
(Linux内核对时间的表示有多种格式,每种格式都有不同的时间精度),其时间精度是微
秒。该结构是内核表示时间时最常用的一种格式,如下所示:
/从1970年1月1号开始计算的秒数
struct timeval {
time_t tv_sec;
suseconds_t tv_usec; //微秒:百万分之一秒,当前秒内的微秒数
};
其中,成员tv_sec表示当前时间距UNIX时间基准的秒数值,而成员tv_usec则表示一秒
之内的微秒值,且1000000>tv_usec>=0。
(8) 全局变量sys_tz:它是一个timezone结构类型的全局变量,表示系统当前的时区信息。
结构类型timezone定义在include/linux/time.h头文件中,如下所示:
struct timezone {
int tz_minuteswest;
int tz_dsttime;
};
基于上述结构,Linux在kernel/time.c文件中定义了全局变量sys_tz表示系统当前所处
//格林尼治时间往西方的时差
/*时间修正方式*/
的时区信息,如下所示:
struct timezone sys_tz;
(9)timer.h中内核定时器的定义
struct timer_list{
struct list_headlist;
unsigned long expires;
unsigned long data;
void (*function)(unsignedlong);/*定时器到期时执行的函数*/
};
/*动态定时器链表*/
/*定时器到期的时间*/
/*function的参数*/
(10)双向链表元素list:用来将多个定时器连接成一条双向循环队列。
(11) expires:指定定时器到期的时间,这个时间被表示成自系统启动以来的时钟滴答计
4
数(也即时钟节拍数)。当一个定时器的expires值小于或等于jiffies变量时,我们就说这
个定时器已经超时或到期了。在初始化一个定时器后,通常把它的 expires 域设置成当前
expires 变量的当前值加上某个时间间隔值(以时钟滴答次数计)。
(12) 函数指针 function:指向一个可执行函数。当定时器到期时,内核就执行 function
所指定的函数。 而 d a 域则被内核用作 function 函数的调用参数。
(13) 全局变量wall_jiffies:Linux内核定义了一个类似于jiffies的全局变量
wall_jiffies,来保存内核上一次更新xtime时的jiffies值。时钟中断的底半部分每一次更
新xtime的时侯都会将wall_jiffies更新为当时的jiffies值。全局变量wall_jiffies定义在
kernel/timer.c文件中:
unsigned long wall_jiffies;
(14)
unsigned long fast_gettimeoffset_quotient;
全局变量,无符号长整型,与可编程定时器 PIT 比,用 TSC 寄存器可获得更精确的时间
度量。但使用 TSC 前,它必须精确地确定 1 个 TSC 计数值到底代表多长的时间间隔,即到底
要 过 多 长 时 间 间 隔 TSC 寄 存 器 才 会 加 1 。 Linux 内 核 用 全 局 变 量
fast_gettimeoffset_quotient 来 表 示 这 个 值 . 这 个 变 量 的 值 是 通 过 下 述 公 式 来 计
算:fast_gettimeoffset_quotient = (2^32) / (每微秒内的时钟周期个数)
(15) unsigned long cpu_khz:
全局变量,无符号长整型,在校准 TSC 时,检测到的 CPU 频率。
(16) static int delay_at_last_interrupt:
静 态 全 局 变 量 , 整 型 , 从 产 生 时 钟 中 断 的 那 个 时 刻 到 内 核 时 钟 中 断 服 务 函 数
timer_interrupt 真正在 CPU 上执行的那个时刻之间的时间延迟间隔。
(17) static unsigned long last_tsc_low:
静态全局变量,无符号长整型,表示中断服务 timer_interrupt 真正在 CPU 上执行时刻
的 TSC 寄存器值的低 32 位(LSB),即表示上一次时钟中断服务函数 timer_interrupt()执
行时刻的 CPU TSC 寄存器的值。
(18) static long last_rtc_update:
静态全局变量,长整型,表示内核最近一次成功地对 RTC 进行更新的时间(单位是秒数),
每一次成功地调用 set_rtc_mmss()函数后,内核都会马上将 last_rtc_update 更新为当前
时间。
(19) static struct irqaction irq0:
静态全局变量,时钟中断请求的中断服务描述符,结构 irq0 中的 next 指针被设置为
NULL,IRQ0 所对应的中断服务队列中只有 irq0 这唯一的一个元素,且 IRQ0 不允许中断共
享。
(20) 进程用 task_struct 表示,所有进程被组织到以 init_task 为表头的双向链表中
struct task_struct {
volatile long state;
unsigned long flags;
/* 进程的状态*/
/* 进程标志 */
5
int sigpending;
mm_segment_t addr_limit;
/* 线性地址空间:0-0xBFFFFFFF 为用户线性空间地址;
0-0xFFFFFFFF 为内核线性空间地址 */
struct exec_domain *exec_domain;
volatile long need_resched;
/*标志下一次有调度机会的时候是否调用此进程.
如果此进程没有没有被用户关闭或者其代码全被执行完了,在下一次调度机会应该还被调用.
如果被用户关闭则直接退出该进程.*/
unsigned long ptrace;
int lock_depth;
long counter;
/* 进程的动态优先级,定义了一个在就绪队列的进程当它得到
CPU 后可运行的时间,用静态优先级初始化,当然计算机是以时钟中断做为时间的计数器,每
发送一个时钟中断,动态优先级上的时间片就减少一个时钟中断的时间,时间片减到 0 的时
候就退出该进程而调度另一个进程获得 CPU.*/
long nice;
/* 进程的静态优先级,当一个在就绪队列中的进程获得 CPU 之
后,它被赋予此进程可占有 CPU 的时间.这个时间被称为时间片.*/
unsigned long policy;
struct mm_struct *mm;
/* 进程采用的调度策略,在代码后面有说明 */
/* 进程属性中指向内存管理的数据结构 mm_struct 的指
针,mm_struct 数据结构是描述内存存储信息的数据结构,进程控制块 task_struct 中用 mm
指针指向 mm_struct 数据结构.也就是在进程的属性中通过 mm 指针来管理起对应的内存区*/
int processor;
};
(21)runqueue_head: 以 runqueue_head 为表头的链表记录了所有处于就绪态的进程(当前
正在运行的进程也在其中,但 idle_task 除外),调度器总是从中选取最适合调度的进程投
入运行。
(22)IDLE 进程 系统最初的引导进程(init_task)在引导结束后即成为 cpu 0 上的 idle
进程。在每个 cpu 上都有一个 idle 进程,正如上文所言,这些进程登记在 init_tasks[]数
组中,并可用 idle_task()宏访问。idle 进程不进入就绪队列,系统稳定后,仅当就绪队列
为空的时候 idle 进程才会被调度到。
init_tasks 调度器并不直接使用 init_task 为表头的进程链表,而仅使用其中的
"idle_task"。该进程在引导完系统后即处于 cpu_idle()循环中。SMP 系统中,每个 CPU 都
分别对应了一个 idle_task,它们的 task_struct 指针被组织到 init_tasks[NR_CPUS]数组
中,调度器通过 idle_task(cpu)宏来访问这些"idle"进程。
6
3.1.2 完成功能的主要函数
函数说明:
(1) Calibrate_tsc
功能: 根据上述公式 fast_gettimeoffset_quotient = (2^32) / (每微秒内的时钟周期
个数)来计算 fast_gettimeoffset_quotient 的值,只被初始化函数 time_init()所调用
参数:无
返回值:TSC 的每一次计数真正代表多长的时间间隔(单位为 us),即一个时钟周期的真正
时间间隔长度
(2) Do_fast_gettimeoffset
功能:通过 delay_at_last_interrupt、last_tsc_low 和时刻 x 处的 TSC 寄存器值计算时刻
x 距上一次时钟中断产生时刻的时间间隔偏移
参数:无
返回值:offset_usec 的值
(3) Do_gettimeofday
功能:完成实际的当前时间检索工作,执行步骤如下:
( 1 ) 调 用 函 数 do_gettimeoffset() 计 算 从 上 一 次 时 钟 中 断 发 生 到 执 行
do_gettimeofday()函数的当前时刻之间的时间间隔 offset_usec。
(2)通过 wall_jiffies 和 jiffies 计算 lost_usec 的值。
(3)然后,令 sec=xtime.tv_sec,usec=xtime.tv_usec+lost_usec+offset_usec。显
然,sec 表示系统当前时间在秒数量级上的值,而 usec 表示系统当前时间在微秒量级上的
值。
(4)用一个 while{}循环来判断 usec 是否已经溢出而超过 106us=1 秒。如果溢出,则
将 usec 减去 106us 并相应地将 sec 增加 1,直到 usec 不溢出为止。
(5)最后,用 sec 和 usec 分别更新参数指针所指向的 timeval 结构变量。至此,整个
查询过程结束。
参数: struct timeval *tv
返回值:无
7
(6)Do_settimeofday
功能:设定实际的当前时间
1>调用 do_gettimeoffset()函数计算上一次时钟中断发生时刻到当前时刻之间的时间
间隔值。
2>通过 wall_jiffies 与 jiffies 计算二者之间的时间间隔 lost_usec。
3>从 tv->tv_usec 中减去 fixed_usec,即:tv->tv_usec-=(lost_usec+offset_usec)。
4> 用 一 个 while{} 循 环 根 据 tv->tv_usec 是 否 小 于 0 来 调 整 tv 结 构 变 量 。 如 果
tv->tv_usec 小于 0,则将 tv->tv_usec 加上 106us,并相应地将 tv->tv_sec 减 1。直到
tv->tv_usec 不小于 0 为止。
5>用修正后的时间 tv 来更新内核全局时间变量 xtime。
6>最后,重置其它时间状态变量。
参数:struct timeval *tv
返回值:无
(7)Do_slow_gettimeoffset
功能:与 do_fast_gettimeoffset 相对应
参数:无
返回值:延时长度
(8)Do_timer_interrupt
功能:调用 sched.c 的 do_timer()获取机器自启动以来时钟嘀嗒次数
参数:int irq, void *dev_id, struct pt_regs *regs
返回值:无
(9)Get_cmos_time
功能:内核在启动时从 RTC 中读取启动时的时间与日期,仅仅在内核启动时被调用一次
参数:无
返回值:调用 mktime()函数将当前时间与日期转换为相对于 1970-01-01 00:00:00 的
秒数值,并将其作为函数返回值返回。
(10)Set_rtc_mmss
功能:用来更新 RTC 中的时间。
参数:unsigned long nowtime,是以秒数表示的当前时间
返回值:更新后的 RTC 时间(秒数)
(11)Timer_interrupt
功 能 : IRQ0 有 中 断 请 求 时 , 如 果 满 足 响 应 条 件 , 系 统 执 行 此 函 数 以 调 用
8