1.高精度定时器
1.1.论述
要实现高精度定时器,我们得依赖HPET芯片的高精度中断。所以,高精度定时器的实现,属于设备驱动的范畴。我们要驱动的设备是HPET芯片。
所谓设备驱动,就是处理器与设备的寄存器交互。实现设备行为控制,来达到我们预定的目标。
HPET芯片位于主板芯片组。时间精度高达69.84ns。
系统上电后,HPET默认处于禁用状态。
所以,HPET的设备驱动,包含设备的启用,启用后的交互(交互方法,交互效果)。
1.设备启用
HPTC是一个4B寄存器。可以实现HPET设备访问地址开启,选择HPET配置寄存器组物理基地址。
HPTC位于芯片组配置寄存器的0x3404偏移处。
芯片组配置寄存器的物理基地址由RCBA寄存器指定。
这样就相当于告诉了我们HPTC的访问方式。
下面就介绍HPTC的效果:
编号[0,1]区域存储 地址映射范围选择域
编号[7]区域存储 地址映射使能标志位
当HPTC[7]为1时,
地址映射范围选择域与映射地址范围的关系为:
数值 | 范围 |
0b00 | FED0,0000~FED0,03FF |
0b01 | FED0,1000~FED0,13FF |
0b10 | FED0,2000~FED0,23FF |
0b11 | FED0,3000~FED0,33FF |
2.知道如何开启设备,确定设备寄存器映射范围后,介绍设备可编程寄存器
Index | Name | Default-Value |
0x000~0x007 | GCAP_ID | 0x0429,B17F,8086,A701 |
0x010~0x017 | GEN_CONF | 0000,0000,0000,0000 |
0x020~0x027 | GINTR_STA | 0000,0000,0000,0000 |
0x0F0~0x0F7 | MAIN_CNT | |
0x100~0x107 | TIM0_CONF(配置寄存器) | |
0x108~0x10F | TIM0_COMP(对比寄存器) | |
0x120~0x127 | TIM1_CONF | |
0x128~0x12F | TIM1_COMP | |
0x140~0x147 | TIM2_CONF | |
0x148~0x14F | TIM2_COMP | |
0x160~0x167 | TIM3_CONF | |
0x168~0x16F | TIM3_COMP | |
0x180~0x187 | TIM4_CONF | |
0x188~0x18F | TIM4_COMP | |
0x1A0~0x1A7 | TIM5_CONF | |
0x1A8~0x1AF | TIM5_COMP | |
0x1C0~0x1C7 | TIM6_CONF | |
0x1C8~0x1CF | TIM6_COMP | |
0x1E0~0x1E7 | TIM7_CONF | |
0x1E8~0x1EF | TIM7_COMP |
3.有了映射范围及上述的偏移,我们就知道了如何与上述寄存器交互。下面就需要知道寄存器交互的效果了。
3.1.GCAP_ID
编号[0,7]区域存储 修订版本号(默认0x01)
编号[8, 12]区域存储 定时器数
编号[13]区域存储 计数器位宽
编号[15]区域存储 旧设备中断路由兼容功能
编号[16, 31]区域存储 供应商ID
编号[32, 63]区域存储 主计数器时间精度(固定为0x0429,b17f)
主计数器时间精度固定为0x0429,b17f。表示每69.84ns计数一次。
旧设备中断路由兼容:置位表示支持兼容8259A的中断请求链路。
计数器位宽:1,表示64位。
3.2.GEN_CONF
编号[0]区域存储 定时器组使能标志位
编号[1]区域存储 旧设备中断路由兼容标志位
旧设备中断路由兼容:
置位时,
定时器0向8259A的IRQ0&I/O APIC的IRQ2发中断请求
定时器1向8259A的IRQ8&I/O APIC的IRQ8发中断请求
其他定时器按自身配置寄存器决定中断请求的接收引脚。
定时器组使能标志位:
置位时,HPET定时器才能产生中断。
3.3.GINTR_STA
编号[0]区域存储 定时器0的中断触发标志位
编号[1]区域存储 定时器1的中断触发标志位
编号[2]区域存储 定时器2的中断触发标志位
编号[3]区域存储 定时器3的中断触发标志位
编号[4]区域存储 定时器4的中断触发标志位
编号[5]区域存储 定时器5的中断触发标志位
编号[6]区域存储 定时器6的中断触发标志位
编号[7]区域存储 定时器7的中断触发标志位
定时器N的中断触发标志位:
如果定时器中断请求为电平触发,则硬件自动置位对应定时器位。
软件向中断触发标志位写入1才能将其复位。
如果定时器中断请求为边沿触发,触发标志位忽略即可。
3.4.MAIN_CNT
编号[0, 63]区域存储 主计数值
可读,可写
3.5.TIMn_CONF
编号[1]区域存储 中断触发模式
编号[2]区域存储 定时器中断使能标志位
编号[3]区域存储 定时器类型
编号[4]区域存储 周期定时标志位(只读)
编号[5]区域存储 定时器位宽标志位(只读)
编号[6]区域存储 定时器值设置标志位
编号[8]区域存储 计数器位宽模式
编号[9, 13]区域存储 定时器中断路由
编号[14]区域存储 定时器中断消息使能标志位
编号[15]区域存储 定时器中断消息投递标志位(只读)
编号[43, 44]区域存储 定时器中断路由
编号[52, 55]区域存储 定时器中断路由
Timer N | bit 43 | bit 44 | bit 52 | bit 53 | bit 54 | bit 55 |
0/1 | N/A | N/A | IRQ20 | IRQ21 | IRQ22 | IRQ23 |
2 | IRQ11 | N/A | IRQ20 | IRQ21 | IRQ22 | IRQ23 |
3 | N/A | IRQ11 | IRQ20 | IRQ21 | IRQ22 | IRQ23 |
定时器4/5/6/7,使用TIMERn_PROCMSG_ROUT来投递中断消息。
定时器中断消息投递标志位:
1,定时器中断消息不经8259A,I/O APIC直达cpu。
定时器中断消息使能标志位:
1,定时器可以产生中断消息
定时器中断路由:
对0/1/2/3定时器,用于设置使用的中断请求引脚。
定时器4/5/6/7不支持。
计数器位宽模式:
只对定时器0有效。其余定时器固定32位宽。
1,32位宽
0,64位宽。
定时值设置标志位:
只对定时器0在周期定时模式下有效,
置位时,使软件在定时器运行时修改定时值
定时器位宽标志:
1,64位
0,32位
周期定时功能:
1,支持周期定时。只有定时器0支持周期定时。
0,不支持
定时器类型:
1,周期性产生中断。只有定时器0支持周期性产生中断。
0,一次性产生中断
定时器中断使能标志位:
0,禁止中断
1,使能中断
中断触发模式:
0,边沿触发
1,电平触发
3.6.TIMn_COMP
只有当MAIN_CNT计数值与TIMn_COMP保存的定时值相等时,定时器才会产生中断。
1.2.实践
//get RCBA address
io_out32(0xcf8, 0x8000f8f0);
x = io_in32(0xcfc);
类似启用I/O APIC,这里我们要启用HPET。
所以,我们第一步是取得RCBA寄存器内容。
RCBA寄存器位于主板LPC桥控制器组,访问方法的查阅主板使用手册。对于IntelQM67,上述x就是RCBA寄存器值。
芯片组配置寄存器物理基地址是16KB对齐的。
x = x & 0xffffc000;
这样x就是芯片组配置寄存器物理基地址。
因为HPTC位于芯片组配置寄存器的0x3404偏移处。
unsigned int * p = NULL;
//get HPTC address
if(x > 0xfec00000 && x < 0xfee00000)
{
p = (unsigned int *)Phy_To_Virt(x + 0x3404UL);
}
这样p就是HPTC寄存器线性地址。
通过HPTC[7] = 1来允许通过地址映射访问HPET,也就是启用HPET。
//enable HPET
*p = 0x80;
io_mfence();
上述io_mfence()作用是,等待此cpu此前指令全部执行完毕。
// 这是HPET映射区域起始线性地址
unsigned char * HPET_addr = (unsigned char *)Phy_To_Virt(0xfed00000);
// 这样取得8字节是HPET的GCAP_ID寄存器内容
color_printk(RED,BLACK,"HPET - GCAP_ID:<%#018lx>\n", *(unsigned long *)HPET_addr);
// 这样是设置HPET的GEN_CONF寄存器为3
// 1.使得HPET的定时器可以产生中断。
// 2.使得定时器0可向8259A的IRQ 0&I/O APIC的IRQ2发中断请求。
// 3.使得定时器1可向8259A的IRQ 8&I/O APIC的IRQ8发中断请求。
*(unsigned long *)(HPET_addr + 0x10) = 3;
io_mfence();
// 这样是设置HPET的TIM0_CONF寄存器
// 0b0000 0000 0100 1100
// 中断触发模式:边沿触发
// 定时器中断使能标志位:使能中断
// 定时器类型:周期性产生中断--只有定时器0支持
// 定时器值设置标志位:软件在定时器运行时可修改定时值
// 计数器位宽模式:64位宽
// 这样定时器周期性产生中断信号,
// 结合GEN_CONF,定时器0的中断信号会传递到I/O APIC的IRQ2引脚
*(unsigned long *)(HPET_addr + 0x100) = 0x004c;
io_mfence();
// 这样是设置HPET的TIM0_COMP寄存器
// 这样在经过14*69.841279将收到一次来自HPET定时器0的信号中断。
// 这个耗时近似为1毫秒
*(unsigned long *)(HPET_addr + 0x108) = 14000;
io_mfence();
// init MAIN_CNT & get CMOS time
get_cmos_time(&time);
// 这样是设置HPET的MAIN_CNT寄存器
*(unsigned long *)(HPET_addr + 0xf0) = 0;
io_mfence();
color_printk(RED, BLACK,
"year:%#010x,month:%#010x,day:%#010x,hour:%#010x,mintue:%#010x,second:%#010x\n",
time.year, time.month, time.day, time.hour, time.minute, time.second);
启用HPET后,我们通过与其寄存器交互完成设置。达到近似1微妙在I/O APIC的IRQ2收到一次中断信号的效果。
struct IO_APIC_RET_entry entry;
entry.vector = 34;
entry.deliver_mode = APIC_ICR_IOAPIC_Fixed;
entry.dest_mode = ICR_IOAPIC_DELV_PHYSICAL;
entry.deliver_status = APIC_ICR_IOAPIC_Idle;
entry.polarity = APIC_IOAPIC_POLARITY_HIGH;
entry.irr = APIC_IOAPIC_IRR_RESET;
entry.trigger = APIC_ICR_IOAPIC_Edge;
entry.mask = APIC_ICR_IOAPIC_Masked;
entry.reserved = 0;
entry.destination.physical.reserved1 = 0;
entry.destination.physical.phy_dest = 0;
entry.destination.physical.reserved2 = 0;
register_irq(34, &entry , &HPET_handler, NULL, &HPET_int_controller, "HPET");
为了对I/O APIC的IRQ2收到的中断信号进行正确处理,我们需要做上述工作。上述工作的含义解释可以参考中断与异常-I/O APIC实践部分。
上述达到的效果是,I/O APIC的IRQ2引脚收到的中断信号将投递给APIC ID为0的cpu。
处理器收到向量号为34的中断,先是执行do_IRQ,然后进入
// timer.h
extern unsigned long volatile jiffies;
// timer.c
unsigned long volatile jiffies = 0;
// HPET.c
void HPET_handler(unsigned long nr, unsigned long parameter, struct pt_regs * regs)
{
jiffies++;
...
}
这样jiffies每隔1毫秒加1,利用这个特性构成了内核高精度定时器的基础。
2.软中断
2.1.论述
在我们每次处理外部中断时,我们需要执行中断处理完成指定的处理。但是,有些功能,我们虽然依赖中断机制,但是并不需要每次中断处理均触发,而是在中断处理中检测到满足指定条件时候,再触发。
上述这种,中断处理中满足指定条件才触发一次处理的机制称为软中断机制。
2.2.实践
// softirq.h
extern unsigned long softirq_status;
void set_softirq_status(unsigned long status);
unsigned long get_softirq_status();
// softirq.c
unsigned long softirq_status = 0;
void set_softirq_status(unsigned long status)
{
softirq_status |= status;
}
unsigned long get_softirq_status()
{
return softirq_status;
}
为了实现软中断,我们需要有一个上述的实例对象。该实例对象包含64个比特位。这样用每个比特位代表一个软中断类型。我们的系统就可以支持64种软中断类型。
// softirq.h
struct softirq
{
void (*action)(void * data);
void * data;
};
extern struct softirq softirq_vector[64];
void register_softirq(int nr, void (*action)(void * data),void * data);
void unregister_softirq(int nr);
// softirq.c
struct softirq softirq_vector[64] = {0};
void register_softirq(int nr, void (*action)(void * data), void * data)
{
softirq_vector[nr].action = action;
softirq_vector[nr].data = data;
}
void unregister_softirq(int nr)
{
softirq_vector[nr].action = NULL;
softirq_vector[nr].data = NULL;
}
为了知道,指定类型软中断被触发时,如何处理。我们需要softirq_vector[64]。每个索引对应一个软中断类型。索引里面元素存储了对应软中断类型被触发时的处理方法及其参数。
void softirq_init()
{
softirq_status = 0;
memset(softirq_vector, 0, sizeof(struct softirq) * 64);
}
系统启动时,执行上述初始化。
register_softirq(0, &do_timer, NULL);
void do_timer(void * data)
{
...
}
为了使得软中断机制发挥作用,首先的有地方去注册软中断。
上述注册了索引为0的软中断,指定了索引0软中断的处理函数。
#define TIMER_SIRQ (1 << 0)
void HPET_handler(unsigned long nr, unsigned long parameter, struct pt_regs * regs)
{
jiffies++;
if(jiffies % 1000 == 0)
{
// 表示刚好过了1秒
set_softirq_status(TIMER_SIRQ);
}
...
}
有了注册不够。还需要有触发的地方。
HPET_handler我们知道是HPET定时器0的中断处理函数。这个中断处理每隔1微妙被触发一次。这样,如果按上述的写法。我们就实现了每隔1秒设置一次TIMER_SIRQ软中断状态。
这里TIMER_SIQ对应的是索引0。
ret_from_exception:
ENTRY(ret_from_intr)
movq $-1, %rcx
testq softirq_status(%rip), %rcx check softirq
jnz softirq_handler
jmp RESTORE_ALL
softirq_handler:
callq do_softirq
jmp RESTORE_ALL
有了上述设置标志的地方。我们还需一个触发软中断处理的地方。
上述是中断和异常返回时的代码。
我们在中断和异常返回时,去检测softirq_status中是否有比特位被置位。
如果有被置位的,我们转而去执行softirq_handler,进而执行do_softirq。
void do_softirq()
{
int i;
// 软中断处理过程可以开放外部可屏蔽中断(外部可屏蔽中断进入处理时处理器会屏蔽外部可屏蔽中断)
sti();
for(i = 0; i < 64 && softirq_status; i++)
{
// 如果i对应比特位被设置了
if(softirq_status & (1 << i))
{
// 执行i位置的软中断处理函数
softirq_vector[i].action(softirq_vector[i].data);
// 复位i对应比特位
softirq_status &= ~(1 << i);
}
}
// 软中断结束禁止中断(达到和处理器默认执行外部中断一样的效果)
cli();
}
软中断处理中我们对置位的软中断类型,执行注册时提供的处理函数。然后将其复位。
软中断处理执行期间,我们是开放外部可屏蔽中断的。
这样,我们完整的讲述了软中断机制。并实际举了一个利用软中断实现间隔1秒定时器的例子。
值得注意的是,我们HPET定时器0设置为1微妙触发一次中断,虽然可以获得高精度定时。但代价时,cpu会频繁的被中断打断正常任务执行。任务执行切换到中断处理,再恢复。涉及执行现场保存和恢复,会一定程度降低实际用于处理任务的cpu时间。
另一个值得注意的是,我们是在中断处理中设置的软中断标志。在中断返回处检测的标志并转而执行的软中断处理。但软中断处理过程我们是开放可屏蔽外部中断的。
3.定时机制
3.1.论述
有了高精度定时器,有了软中断机制,这样我们可以借此实现内核级的定时任务机制。
3.2.实践
// timer.h
extern unsigned long volatile jiffies;
// timer.c
unsigned long volatile jiffies = 0;
为了实现定时机制,我们首先需要可以自系统启动依赖的时间消耗。就是上述的jiffies,该数值依赖HPET的定时器0的中断每隔1毫秒自增一次。
// timer.h
// 定时任务队列
struct timer_list
{
// 采用双向链表维护
struct List list;
unsigned long expire_jiffies;
void (* func)(void * data);
void *data;
};
extern struct timer_list timer_list_head;
// timer.c
struct timer_list timer_list_head;
// timer_list对象初始化
void init_timer(struct timer_list * timer, void (* func)(void * data), void *data, unsigned long expire_jiffies)
{
list_init(&timer->list);
timer->func = func;
timer->data = data;
timer->expire_jiffies = expire_jiffies + jiffies;// ms单位
}
// 加入链表
void add_timer(struct timer_list * timer)
{
// 链表首元素固定
struct timer_list * tmp = container_of(list_next(&timer_list_head.list), struct timer_list, list);
// 如果链表是空的--只有一个元素
if(list_is_empty(&timer_list_head.list))
{
}
else
{
while(tmp->expire_jiffies < timer->expire_jiffies)
{
// 1.取得链表下个元素--List对象指针
// 2.得到容纳此List对象的timer_list对象指针
tmp = container_of(list_next(&tmp->list), struct timer_list, list);
}
}
// 链表不空时,tmp指向元素此时的expire_jiffies必然大于等于timer指向元素的expire_jiffies
// 因为我们为链表首元素设置了超大expire_jiffies,所以,else中while循环必然经过有限次迭代可以找到这样的tmp
// 在tmp后插入timer
list_add_to_before(&tmp->list, &timer->list);
}
void del_timer(struct timer_list * timer)
{
list_del(&timer->list);
}
为了对系统内定时任务进行管理。我们需要 timer_list来描述定时任务信息,包括定时任务执行时机,处理函数,参数。
我们将所有注册的定时任务通过双向链表链接在一起,且链接时,按expire_jiffies有序存储。
这里的有序存储具体为除了哨兵节点timer_list_head,其余节点按expire_jiffies从小到达顺序链接。
void timer_init()
{
jiffies = 0;
struct timer_list *tmp = NULL;
// 链表首元素初始化-->expire_jiffies设置为超大值
init_timer(&timer_list_head, NULL, NULL, -1UL);
// 软中断注册
register_softirq(0, &do_timer, NULL);
int i = 0;
for(; i < 10; i++)
{
// 分配新的timer_list对象
tmp = (struct timer_list *)kmalloc(sizeof(struct timer_list), 0);
int *lpInt = (int*)kmalloc(sizeof(int), 0);
*lpInt = i + 1;
init_timer(tmp, &test_timer, lpInt, i + 1);
// 加入链表
add_timer(tmp);
}
}
为了对定时任务提供支持,我们利用软中断来实现定时驱动。
上述我们注册了10个定时任务,分别在1~10s后触发。
register_sofrirq注册了一个索引为0的软中断。结合软中断实践部分,这个软中断处理会每间隔1秒被触发一次。触发时,进入do_timer进入软中断处理。
// 软中断类型处理函数
void do_timer(void * data)
{
// 取得首元素下个元素
struct timer_list * tmp = container_of(list_next(&timer_list_head.list), struct timer_list, list);
// 只要链式结构尚有元素 & 当前元素的expire_jiffies小于或等于jiffies
// 一旦遇到首个元素的expire_jiffies大于jiffies,因为链式结构有序。可以认为后续元素必然也大于jiffies,所以可以直接结束
while((!list_is_empty(&timer_list_head.list)) && (tmp->expire_jiffies <= jiffies))
{
// 定时任务一次性
del_timer(tmp);
// 执行定时任务
tmp->func(tmp->data);
kfree(tmp);
kfree(tmp->data);
// 继续取下个元素
tmp = container_of(list_next(&timer_list_head.list), struct timer_list,list);
}
}
在do_timer中我们对注册的定时任务进行检查。如果注册的定时任务执行时机已经满足,则执行其处理函数,执行后将定时任务从链式结构移除,释放关联内存。
这样,我们就完整讲述了定时机制。
讲述了定时任务的注册,触发,销毁完整过程。