这一周多的时间主要学的是中断相关的内容,对于这部分的很多内容很多还是一知半解的,尤其是时钟和定时器那部分,刚开始的时候看了内核之旅的那篇博文,对中断的原理有了大致的了解,包括中断的定义,中断控制器8259A,以及中断的分类,和中断向量等等概念的了解,在后来的几天里我又看微机原理的中断部分,对中断在汇编级的实现过程有了相应的了解,我觉得在中断向量的引导过程,和中控制器的硬件实现过程对我还有所帮助,而对于用初始化命令字ICW1到ICW4来初始化中断控制器,和用操作命令字OCW1到OCW3修改相关设置的过程较为复杂,且对内核学习的帮助也不明显,所以就不作总结了。
文章接着重点讲了几个重要的结构体,包括irq_desc(对中断源的描述)、irqaction(中断处理所需的信息)、hw_interrupt_type(描述中断控制器)等结构体,通过对这几个结构体关系的分析大致可以看出内核中断处理的流程是怎样的,irq_desc通过chip成员与hw_interrupt_type相联系,通过action成员与irqaction相联系,通过这三个关键的结构体组建出中断的框架来。
其中irq_desc结构体主要包含的成员有handle_irq(指向一个函数)、chip(指向描述中断控制器的结构体)、action(指向描述中断处理信息的结构体)、status(标志位,标志IRQ是否被禁止)、depth(当前用户的个数)等等。想说明的是在irq.h这个头文件中用这个结构体定义了一个irq_desc[]数组,其成员共225项,而中断向量共有256项,少了32(0-31)项是用于异常和非屏蔽中断,所以这个数组的第一项指向中断向量表中的第32项(中断向量号为0x20),接下来依此类推。Irqaction结构体主要的成员有handler(指向中断服务程序,注意与handler_irq相区别,两个类型不同)、flags(标志位,标志着中断是一个快速中断处理程序,还是多个中断服务程序共享一个中断号)、name(产生中断的硬件的名字)、dev_id(在falgs设置为共享中断时,这个字段会告诉内核执行哪个中断服务程序)、next(共享中断时指向下一个结构体)等。hw_interrupt_type结构体的成员主要有typename(给相应的控制器起一个名字),其它都 是一些函数指针,如startup、shutdown等。
接下来讲了中断初始化,定义了init_ISA_irqs()函数,这个过程就是初始化8259A,设置它的相关寄存器,指明它的工作方式,接下来是一个for循环(224次)来初始化irq_desc[],对于前16个设置它的chip字段,调用set_irq_chip_and_handler_name(),也就是完成中断控制器的相关设置(因为前16个是硬件中断),并通过handler_irq字段来完成action与irqaction的关联,并调用handle_IRQ_event函数(这才是真正的执行中断服务程序的函数),而后面的则将其chip字段赋no_irq_chip。定义了函数下面就该调用了,内核中使用了native_init_IRQ()来调用它,并调用了set_intr_gate()函数来设置IDT中中断门描述符,也就是中断向量表中的表项,将中断处理函数入口函数地址设置为interrupt[i],当第i个中断发生后跳转到此地址执行,interrupt[]数组共224项,其中i从0到223,这里就设置完了224个硬件中断和软中断了。Interrupt[]的实现是采用两个宏来实现,定义了14个IRQLIST_16(x),接着调用宏将每个IRQLIST_16(x)又展开为16个IRQ(x,y),并调用#define IRQ(x,y) IRQ##x##y##_interrupt宏,实现IRQ(x,y)连接成IRQxy_interrupt函数的转换,此时便实现了244个中断服务函数的声明。函数的定义部分实在是看不懂了,至此也算是把初始化部分的大概流程弄懂了。
中断实现部分是先跳到common_interrupt处执行,这个是汇编代码,首先用SAVE_ALL保存现场,把中断发生前所有寄存器的状态都保存起来,等中断执行结束后再从栈中取出,恢复现场,然后就调用do_IRQ函数,其中当执行到handle_irq()函数时再调用handle_IRQ_event()函数,这才是真正的中断服务程序,这在上文中也有提到。
在内核代码的中断学习过程中,为了使中断控制不变的复杂,中断处理一般是在关中断的状态下执行的,内核代码中则采用了顶半部和底半部机制,顶半部主要是处理一些尽可能少的比较紧急的功能,其执行过程一般不可以被中断,往往只是简单地读取寄存器中的中断状态并清除中断标志后就进行登记中断的工作。而底半部则几乎做了中断处理程序所有的事情,且还可以被新的中断打断。我们常用的底半部机制有小任务机制和工作队列机制,在实现细节上小任务机制是交由处理函数处理底半部,而工作队列则交由一个处理线程来处理。相比这两种处理机制,工作队列机制运行在进程上下文,因此推迟任务可以进行睡眠,所以一般可以根据任务是否需要睡眠来选择哪种机制,这两种机制的使用方法很相似,包括定义处理函数,声明部分,调试部分,再加上申请IRQ和释放IRQ部分,其结构和模块的基本上是相同的,这就是中断的大体结构。
在随后的几天里,主要学习了定时器和时钟部分,但还是了解的太过肤浅,对于这定时器和时钟中断之间是怎么协同工作的还是缺乏相应认识。在又大致的看了内核的设计与实现后,我觉得很有必要再把相关概念学习一遍,首先是节拍率(HZ)和节拍:节拍率指系统定时器以某种频率自行触发时钟中断,该频率(节拍率)是通过静态预处理定义的,也就是Hz(赫兹),在系统启动时按照Hz值对硬件进行设置,节拍则是系统定时器连续两次时钟中断的间隔时间,在i386体系下HZ被设置成1000,而其它大部分体系则设置成100。全局变量jiffies是用来记录自系统启动以来产生的节拍的总数,其类型采用长整型,在32位系统下其定时器节拍计数最大为4294967295,当超过这个值时再会溢出回绕到0。
计时的设备在PC上有两个,一个是实时时钟,一个是系统定时器,虽然两者实现不同,但作用和设计思路却是相同的。时钟中断处理程序在每次产生时钟中断后都会执行一遍,也就是每秒钟要执行Hz次,中断处理程序又分为与体系结构相关和无关两部分,无关部分则是执行do_timer()函数,执行完后会返回与体系结构相关部分。
实际时间又称为墙上时间,它被存储在struct timespc xtime变量中,这个结构变量中包含了两个成员,一个是秒,一个是纳秒,在读写xtime变量时还需要xtime_lock锁(是seqlock),因此在读写过程中还涉及到加锁和解锁部分。
对于我们编程来说最重要的就是如何来使用定时器,定时器由头文件timer.h来定义,其结构如下:
struct timer_list {
struct list_head entry; /* 定时器链表的入口 */
unsigned long expires; /* 以jiffies为单位的定时值 */
spinlock_t lock; /* 保护定时器的锁 */
void (*function)(unsigned long); /* 定时器处理函数 */
unsigned long data; /* 传给处理函数的长整形参数 */
struct tvec_t_base_s *base; /* 定时器内部值*/
};
创建定时器时首先要定义它,我们采用struct timer_list my_timer来定义,接着采用init_timer()来初始化它,并可以填充结构中需要的值,也可以将自定义的函数赋值给定时器处理函数,我们也可以通过传递data参数给自定义函数,来达到注册多个定时器的目的,只要通过data参数来区别它们,最后是使用add_timer()函数来激活定时器,还可以通过mod_timer()函数来修改expires成员来完成新的超时时间的设定,如果要停止定时器,则可以用del_timer()函数。
这次的实例是秒字符设备,其结构和字符设备驱动结构基本上一样,只是加入了定时器的部分,在second_dev结构体的counter变量定义时引用了原子操作的概念,在操作系统中原子操作指不可分割的操作,在模块加载后运行测试程序则可以看到内核日志文件中的jiffies值以每秒钟100的频率增加,终端里则可以看到输出相应的秒数。在加载模块的时候要确保分配的主设备号没有被别的设备占用,我第一次加载的时候就是因为主设备号被别的设备占用而导致加载不成功。