构建64位操作系统-高精度定时器与定时机制

文章详细阐述了如何利用HPET芯片实现高精度定时器,包括启用HPET、配置寄存器交互、定时器中断处理等,并介绍了软中断机制作为辅助的中断处理方式,以实现更灵活的定时任务管理。此外,还讨论了基于这些机制构建的内核定时器系统。
摘要由CSDN通过智能技术生成

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时,

        地址映射范围选择域与映射地址范围的关系为:

数值范围
0b00FED0,0000~FED0,03FF
0b01FED0,1000~FED0,13FF
0b10FED0,2000~FED0,23FF
0b11FED0,3000~FED0,33FF

        2.知道如何开启设备,确定设备寄存器映射范围后,介绍设备可编程寄存器

IndexNameDefault-Value
0x000~0x007GCAP_ID0x0429,B17F,8086,A701
0x010~0x017GEN_CONF0000,0000,0000,0000
0x020~0x027GINTR_STA0000,0000,0000,0000
0x0F0~0x0F7MAIN_CNT
0x100~0x107TIM0_CONF(配置寄存器)
0x108~0x10FTIM0_COMP(对比寄存器)
0x120~0x127TIM1_CONF
0x128~0x12FTIM1_COMP
0x140~0x147TIM2_CONF
0x148~0x14FTIM2_COMP
0x160~0x167TIM3_CONF
0x168~0x16FTIM3_COMP
0x180~0x187TIM4_CONF
0x188~0x18FTIM4_COMP
0x1A0~0x1A7TIM5_CONF
0x1A8~0x1AFTIM5_COMP
0x1C0~0x1C7TIM6_CONF
0x1C8~0x1CFTIM6_COMP
0x1E0~0x1E7TIM7_CONF
0x1E8~0x1EFTIM7_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 Nbit 43bit 44bit 52bit 53bit 54bit 55
0/1N/AN/AIRQ20IRQ21IRQ22IRQ23
2IRQ11N/AIRQ20IRQ21IRQ22IRQ23
3N/AIRQ11IRQ20IRQ21IRQ22IRQ23

        定时器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中我们对注册的定时任务进行检查。如果注册的定时任务执行时机已经满足,则执行其处理函数,执行后将定时任务从链式结构移除,释放关联内存。

        这样,我们就完整讲述了定时机制。

        讲述了定时任务的注册,触发,销毁完整过程。 

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

raindayinrain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值