免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
1234下一页
最近访问板块 发新帖
查看: 29400 | 回复: 39

詹荣开系列文档 [复制链接]

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:45 |显示全部楼层
第五章 Linux内核的中断机制
(By 詹荣开,NUDT)





Copyright © 2003 by 詹荣开
E-mail:zhanrk@sohu.com
Linux-2.4.0
Version 1.0.0,2003-2-14




摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的设备中断流程。本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:Linux、中断、设备驱动程序

申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。

你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA

欢迎各位指出文档中的错误与疑问。

§5.1 I386的中断与异常
中断通常被分为“同步中断”和异步中断两大类。同步中断是指当指令执行时由CPU控制单元产生的中断,之所以称为“同步中断”是因为只有在一条指令中止执行后CPU才会发出这类中断信号。而异步中断则是指由其他硬件设备依照CPU时钟随机产生的中断信号。
在Intel 80x86 CPU手册中,同步中断和异步中断也被分别称为“异常(Exception)”和“中断(Interrupt)”。Intel又详细地把中断和异常细分为以下几类:
(1)中断
1. 可屏蔽中断(Maskable Interrupt):这类中断被送到CPU的INTR引脚,通过清除eflag寄存器的IF标志可以关闭中断。
2. 不可屏蔽中断(Nonmaskable Interrupt):被送到CPU的NMI引脚,通常只有几个危急的事件,如:硬件故障等,才产生不可屏蔽中断信号。寄存器eflag中的IF标志对这类中断不起作用。
(2)异常
1. 处理器探测异常(Processor-detected exception):当CPU执行一条指令时所探测到的一个反常条件所产生的异常。依据CPU控制单元产生异常时保存在内核态堆栈eip寄存器的值,这类异常又可以细分为三种:
n 故障(Fault):保存在eip中的值是引起故障的指令地址,因此但异常处理程序结束后,会重新执行那条指令。“缺页故障”是这类异常的一个常见例子。
n 陷阱(Trap):保存在eip中的值是一个指令地址,但该指令在引起陷阱的指令地址之后。只有当没有必要重新执行已执行过的指令时,才会触发trap,其主要用途是调试程序。
n 异常中止(Abort):当发生了一个严重的错误,致使CPU控制单元除了麻烦而不能在eip寄存器中保存有意义的值。异常中止通常是由硬件故障或系统表中无效的值引起的。由CPU控制单元发生的这个中断是一种紧急信号,用来把CPU的执行路径切换到异常中止的处理程序,而相应的ISR通常除了迫使受到影响的进程中止外,别无选择。
2. 编程异常(Programmed Exception):通常也称为“软中断(software interrupt)”,是由编程者发出中断请求指令时发生的中断,如:int指令和int3指令。当into(检查溢出)和bound(检查地址越界)指令检查的条件不为真时,也引起编程异常。CPU控制单元把编程异常当作Trap来处理,这类异常有两个典型的用途:一、执行系统调用;二、给调试程序通报一个特定条件。
5.1.1 中断向量
每个中断和异常都可以用一个0-255之间的无符号整数来标识,Intel称之为“中断向量(Interrupt Vector)”。通常,不可屏蔽中断和异常的中断向量是固定的,而可屏蔽中断的中断向量则可以对中断控制器进行编程来改变。I386 CPU的256个中断向量是这样分配的:
1. 从0-31这一共32个向量用于异常和不可屏蔽中断。
2. 从32-47这一共16个向量用于可屏蔽中断,分别对应于主、从8259A中断控制器的IRQ输入线。
3. 剩余的48-255用于标识软中断。
Linux全部使用了0-47之间的向量。但对于48-255之间的软中断向量,Linux只使用了其中的一个,即用于实现系统调用的中断向量128(0x80)。当用户态下的进程执行一条int 0x80汇编指令时,CPU切换到内核态,以服务于系统调用。
Linux在头文件include/asm-i386/hw_irq.h中定义了宏FIRST_EXTERNAL_VECTOR来表示第一个外设中断(即8259A的IRQ0)所对应的中断向量,此外还定义了SYSCALL_VECTOR来表示用于系统调用的中断向量。如下所示:
/*
* IDT vectors usable for external interrupt sources start
* at 0x20:
*/
#define FIRST_EXTERNAL_VECTOR 0x20
#define SYSCALL_VECTOR 0x80
5.1.2 I386的IDT
i386 CPU的IDT表一共有256项,分别对应每一个中断向量。每一个表项就是一个中断描述符,用以描述相对应的中断向量,中断向量就是该描述符在IDT中的索引,每一个中断描述符的大小都是8个字节。根据INTEL的术语,中断描述符也称为“门(Gate)”。
中断描述符有下列4种类型:
(1)任务们(Task Gate):包含了一个进程的TSS段选择符。每当中断信号发生时,它被用来取代当前进程的那个TSS段选择符。Linux并没有使用任务们。任务们的格式如下:
(2)中断门(Interrupt Gate):中断门中包含了一个段选择符和一个中断处理程序的段内偏移。注意,当I386 CPU穿越一个中断门进入相应的中断处理程序时,它会清除eflag寄存器中的IF标志,从而屏蔽接下来可能发生的可屏蔽中断。
(3)陷阱门(Trap Gate):与中断门类似,不同之处在于CPU通过陷阱门转入中断处理程序时不会清除IF标志。
(4)调用门(Call Gate):Linux并没有使用调用门。
这三种门的格式如图5-2所示。
5.1.3 中断控制器8259A
我们都知道,PC机中都使用两个级联的8359A PIC(Programmable Interrupt Controller,可编程中断控制器,简称PIC)来管理来自系统外设的中断信号。每个8259A PIC提供8根IRQ(Interrupt ReQuest,中断请求,简称IRQ)输入线。在级联方式中,Master 8259A PIC(第一个PIC)的中断信号输入线IR2用于级联Slave 8259A PIC(第二个PIC)的INT引脚,因此两个8259A一共可以提供15根可用的IRQ输入线。如下图所示:

图5-3 主、从8259A中断控制器的级联
5.1.3.1 8259A PIC的基本原理
8259A PIC芯片的基本逻辑块图如下所示:
“中断屏蔽寄存器”(Interrupt Mask Register,简称IMR)用于屏蔽8259A的中断信号输入,每一位对应一个输入。当IMR中的bit(0≤i≤7)位被置1时,相对应的中断信号输入线IRi上的中断信号将被8259A所屏蔽,也即IRi被禁止。
当外设产生中断信号时(由低到高的跳变信号,80x86系统中的8259A是边缘触发的,Edge Triggered),中断信号被输入到“中断请求寄存器”(Interrupt Request Register,简称IRR),并同时看看IMR中的相应位是否已被设置。如果没有被设置,则IRR中的相应位被设置为1,表示外设产生一个中断请求,等待CPU服务。
然后,8259A的优先级仲裁部分从IRR中选出一个优先级最高中断请求。优先级仲裁之后,8259A就通过其INT引脚向CPU发出中断信号,以通知CPU有外设请求中断服务。CPU在其当前指令执行完后就通过他的INTA引脚给8259A发出中断应答信号,以告诉8259A,CPU已经检测到有中断信号产生。
8259A在收到CPU的INTA信号后,将优先级最高的那个中断请求在ISR寄存器(In-Service Register,简称ISR)中对应的bit置1,表示该中断请求已得到CPU的服务,同时IRR寄存器中的相应位被清零重置。
然后,CPU再向8259A发出一个INTA脉冲信号,8259A在收到CPU的第二个INTA信号后,将中断请求对应的中断向量放到数据总线上,以供CPU读取。CPU读到中断向量后,就可以装入执行相应的中断处理程序。
如果8259A工作在AEOI(Auto End Of Interrupt,简称AEOI)模式下,则当他收到CPU的第二个INTA信号时,它就自动重置ISR寄存器中的相应位。否则,ISR寄存器中的相应位就一直保持为1,直到8259A显示地收到来自于CPU的EOI命令。
5.1.3.2 8259A的I/O端口
Master 8259A的IO端口地址位0x20和0x21,Slave 8259A的IO端口地址为0xA0和0xA1。对这些IO端口进行读写操作时的功能如下表所示:
I/O Port Addrss Read/Write Function
Port A(0x20/0xA0) W Initialization Command Word1(ICW1)
W Operation Command Word2(OCW2)
W Operation Command Word3(OCW3)
R Interrupt Request Register(IRR)
R In-Service Register(ISR)
Port B(0x21/0xA1) W Initialization Command Word2(ICW2)
W Initialization Command Word3(ICW3)
W Initialization Command Word4(ICW4)
W Operation Command Word1(OCW1)
R Interrupt Mask Register(IMR)
表5-1 8259A的I/O端口地址列表
5.1.3.3 初始化8259A
8259A PIC的初始化是通过向其写一系列“初始化命令字”(Initialization Command Word,简称ICW)来实现的。其中,ICW1必须写到Port A(0x20/0xA0)中,而ICW2、ICW3和ICW4则必须写到Port B(0x21/0xA1)中。此外,主、从8259A必须分别进行初始化。
ICW1的格式如下图5-5所示:
ICW2的格式如下图5-6所示:
在MCS-80/85模式下,A15-A8指定中断向量地址;而在80x86模式下,T7-T3用于指定中断向量地址,而bit[2:0]则可被设置为0。
ICW3(Master Device)的格式如下:
Si(0≤i≤7)为1则表示相应的IRi上级联了一个Slave 8259A PIC。
ICW3(Slave Device)的格式如下:

Bit[7:3]总为0,而ID2、ID1、ID0(即bit[2:0])用于标识Slave 8259A连接在Master 8259A的哪一根IRQ线上。例如:010就表示Slave 8259A是连接在Master 8259A的IR2上。
ICW4的格式如下:
5.1.3.4 控制8259A
可以向Port A或Port B写入“控制命令字”(Control Command Word,简称OCW)来控制8259A PIC。有三种类型的OCW,其中OCW1只能被写入到Port B中,OCW2和OCW3只能被写到Port A中。
OCW1(Interrupt Mask Register)的格式如下:
M7…M0就是IRQ7…IRQ0各自对应的屏蔽位。IMR寄存器可以通过向Port B写入OCW1来设置,它的当前值也可以通过读取Port B来得到。
OCW2的格式如下:
OCW3的格式如下:

§5.2 Linux对IDT的初始化
5.2.1 定义IDT的数据结构
Linux在include/asm-i386/Desc.h头文件中定义了数据结构desc_struct,用来描述i386 CPU中的各种描述符(均为8字节),如下:
struct desc_struct {
unsigned long a,b;
};
基于上述结构,Linux在arch/i386/kernel/traps.c文件中定义了数组idt_table[256],来表示中断描述符表IDT,其定义如下:
/*
* The IDT has to be page-aligned to simplify the Pentium
* F0 0F bug workaround.. We have a special link segment
* for this.
*/
struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };
5.2.2 对门的操作函数
Linux在arch/i386/kernel/traps.c文件中定义了宏_set_gate(),用来设置一个描述符(即门)的具体值。由于Linux内核代码均在段选择子__KERNEL_CS所指向的内核段中,因此门中的Segment Selector字段总是等于__KERNEL_CS。宏_set_gate()有四个参数:(1)gate_addr:描述符desc_struct结构类型的指针,指定待操作的描述符,通常指向数组idt_table中的某个项。(2)type:描述符类型,对应于门格式中的Type字段。(3)dpl:该描述符的权限级别;(4)addr:中断处理程序入口地址的段内偏移量,由于内核段的起始地址总是为0,因此中断处理程序在内核段中的段内偏移量也就是中断处理程序的入口地址(即核心虚地址)。
宏_set_gate()的源码如下:
#define _set_gate(gate_addr,type,dpl,addr) \
do { \
int __d0, __d1; \
__asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
"movw %4,%%dx\n\t" \
"movl %%eax,%0\n\t" \
"movl %%edx,%1" \
:"=m" (*((long *) (gate_addr))), \
"=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \
} while (0)

由于不同的门其Type字段和DPL字段是固定的,因此Linux又在宏_set_gate()的基础上为不同类型的门分别封装了专用的操作宏,它们同样也在traps.c文件中:
(1)中断门的操作宏set_intr_gate(),其源码如下:
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr); /* Type=1110(14),dpl=0(ring 0)*/
}
其中,参数n是中断向量(被用作IDT表的索引)。参数addr是中断处理程序的入口地址。
(2)陷阱门的操作宏set_trap_gate()。其源码如下:
static void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr); /* Type=1111(15),dpl=0(ring 0)*/
}
(3)系统门的操作宏set_system_gate()。Linux扩展了INTEL的术语,它将可编程异常所对应的中断描述符称为“系统门”。系统门的DPL字段为3(用户态),因此使得用户进程可以在用户态下(I386运行在ring 3级别)通过int指令或其它指令穿越系统门而进入内核态。
static void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr); /* Type=1111(15),dpl=3(ring 3)*/
}
(4)调用门的操作宏set_call_gate(),其源码如下:
static void __init set_call_gate(void *a, void *addr)
{
_set_gate(a,12,3,addr); /* Type=1100(12),dpl=3(ring 3)*/
}
从上述这四个专用的宏操作实现也可以看出,Linux并没有使用i386 CPU的调用门和任务门,而是仅仅使用了中断门和陷阱门(Linux又将它细分为陷阱门和系统门)两种中断描述符。
5.2.3 对IDT表的初始化设置
Linux内核在初始阶段完成了对也是虚存管理机制的初始化后,便调用trap_init()函数和init_IRQ()函数对i386 CPU中断机制的核心——IDT进行初始化设置。如下:
asmlinkage void __init start_kernel(void)
{
……
trap_init();
init_IRQ();
……
}
其中,函数trap_init()用来对除外设中断(32-47)以外的所有处理器保留的中断向量进行初始化。而init_IRQ()函数则用来初始化对应于主、从8259A中断控制器的可屏蔽外设中断32-47。
函数trap_init()定义在arch/i386/kernel/traps.c文件中,其源码如下:
void __init trap_init(void)
{
#ifdef CONFIG_EISA
if (isa_readl(0x0FFFD9) == 'E'+('I'<<8)+('S'<<16)+('A'<<24))
EISA_bus = 1;
#endif

set_trap_gate(0,&divide_error);
set_trap_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
set_trap_gate(18,&machine_check);
set_trap_gate(19,&simd_coprocessor_error);

set_system_gate(SYSCALL_VECTOR,&system_call);

/*
* default LDT is a single-entry callgate to lcall7 for iBCS
* and a callgate to lcall27 for Solaris/x86 binaries
*/
set_call_gate(&default_ldt[0],lcall7);
set_call_gate(&default_ldt[4],lcall27);

/*
* Should be a barrier for any external CPU state.
*/
cpu_init();

#ifdef CONFIG_X86_VISWS_APIC
superio_init();
lithium_init();
cobalt_init();
#endif
}
从上述代码可以看出,trap_init()函数的核心就是做两件事:(1)设置IDT的前20个表项,这是因为在0-31这32个CPU保留的异常中断向量中,Intel仅定义了前20个(0-19)中断向量,而中断向量20-31这12个中断向量则保留待以后扩展。(2)设置中断向量SYSCALL_VECTOR(0x80),以用于系统调用的实现。
函数init_IRQ()实现在arch/i386/kernel/i8259.c文件中。它负责初始化IDT表中的后224个中断描述符即中断向量32-256所对应的中断描述符(除了用于syscall的中断向量0x80)。其源码如下:
void __init init_IRQ(void)
{
int i;

#ifndef CONFIG_X86_VISWS_APIC
init_ISA_irqs();
#else
init_VISWS_APIC_irqs();
#endif
/*
* Cover the whole vector space, no vector can escape
* us. (some of these will be overridden and become
* 'special' SMP interrupts)
*/
for (i = 0; i < NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt);
}

#ifdef CONFIG_SMP
/*
* IRQ0 must be given a fixed assignment and initialized,
* because it's used before the IO-APIC is set up.
*/
set_intr_gate(FIRST_DEVICE_VECTOR, interrupt[0]);

/*
* The reschedule interrupt is a CPU-to-CPU reschedule-helper
* IPI, driven by wakeup.
*/
set_intr_gate(RESCHEDULE_VECTOR, reschedule_interrupt);

/* IPI for invalidation */
set_intr_gate(INVALIDATE_TLB_VECTOR, invalidate_interrupt);

/* IPI for generic function call */
set_intr_gate(CALL_FUNCTION_VECTOR, call_function_interrupt);
#endif

#ifdef CONFIG_X86_LOCAL_APIC
/* self generated IPI for local APIC timer */
set_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt);

/* IPI vectors for APIC spurious and error interrupts */
set_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt);
set_intr_gate(ERROR_APIC_VECTOR, error_interrupt);
#endif

/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */

#ifndef CONFIG_VISWS
setup_irq(2, &irq2);
#endif

/*
* External FPU? Set up irq13 if so, for
* original braindamaged IBM FERR coupling.
*/
if (boot_cpu_data.hard_math && !cpu_has_fpu)
setup_irq(13, &irq13);
}
该函数主要执行以下几个步骤:
1. 在没有配置80x86 APIC的情况下,调用init_ISA_irqs()函数来初始化中断向量32-256这后224个中断向量所对应的IRQ描述符。否则就调用init_VISWS_APIC_irqs()来完成这一点。PC体系结构中通常都没有配置APIC,因此后面将详细分析init_ISA_irqs()函数。
2. 接下来,用一个简单的for循环来初始化32-256这后224个中断向量(除了用于syscall之外的0x80中断向量)在IDT中对应的描述符。位于32-256之间的中断向量i所对应的中断处理程序入口地址由数组元素interrupt[i-32]。后面将会详细介绍这个数组interrupt[224]——一个被内核用来保存中断处理程序入口地址的数组。
3. 初始化系统时钟。
4. 如果没有定义CONFIG_VISWS配置选项,则调用setup_irq()函数将8259A中断控制器的IRQ2设置为用于级联。
5. 如果使用了FPU,则将IRQ13分配用于数学协处理器的错误报告中断。所以调用setup_irq()函数将IRQ13设置为用于FPU。

接下来将讨论内核对中断处理程序的构建,也即数组interrupt[224]的构建。

§5.3内核对中断服务程序的构建
由上一节对init_IRQ()函数的分析我们知道,函数指针数组interrupt[]中定义了中断向量32-256所对应的中断服务程序的入口地址。本节我们就来分析一下Linux内核是如何巧妙地为这224个中断向量构建ISR的。
函数指针数组interrupt[]定义在arch/i386/kernel/i8259.c文件中:
#define IRQ(x,y) \
IRQ##x##y##_interrupt

#define IRQLIST_16(x) \
IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)

void (*interrupt[NR_IRQS])(void) = {
IRQLIST_16(0x0),

#ifdef CONFIG_X86_IO_APIC
IRQLIST_16(0x1), IRQLIST_16(0x2), IRQLIST_16(0x3),
IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7),
IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb),
IRQLIST_16(0xc), IRQLIST_16(0xd)
#endif
};

#undef IRQ
#undef IRQLIST_16
从上述定义可以看出,在没有定义CONFIG_X86_IO_APIC配置选项的单CPU体系结构中,interrupt[]数组中只有前16个数组元素中包含有有效的指针(也即对应于主、从8259A中断控制器的中断请求)。
先看宏IRQ()的定义。我们知道GCC预编译符号##的作用就是将字符串连接在一起。因此经过GCC的预编译处理后,宏IRQ(x,y)实际上就是符号IRQxy_interrupt。
在来看宏IRQLIST_16()。它的作用主要是为了避免重复的文字输入。因此但在interrupt[]数组的初始化中以参数0x0来调用宏IRQLIST_16()时,我们所得到的就是16个宏定义:IRQ(0x0,0) 、…、IRQ(0x0,f),将IRQ()宏继续展开,我们就可知道interrupt[]函数指针数组的前16项的值为:IRQ0x00_interrupt、…、IRQ0x0f_interrupt。而后208个数组元素则或者都是NULL指针(在没有APIC的情况下),或者分别是IRQ0x10_interrupt…IRQ0xdf_interrupt。
现在我们已经清楚地了解了interrupt[224]数组的定义以及它的初始值。很自然地我们会想到,函数IRQ0x00_interrupt到IRQ0xdf_interrupt这224个函数又是在哪定义的呢?请看i8259.c文件中另外几行宏定义与宏引用:
#define BI(x,y) \
BUILD_IRQ(x##y)

#define BUILD_16_IRQS(x) \
BI(x,0) BI(x,1) BI(x,2) BI(x,3) \
BI(x,4) BI(x,5) BI(x,6) BI(x,7) \
BI(x,8) BI(x,9) BI(x,a) BI(x,b) \
BI(x,c) BI(x,d) BI(x,e) BI(x,f)

/*
* ISA PIC or low IO-APIC triggered (INTA-cycle or APIC) interrupts:
* (these are usually mapped to vectors 0x20-0x2f)
*/
BUILD_16_IRQS(0x0)
显然,以参数0x0来引用BUILD_16_IRQ()宏在经过gcc预处理后,将展开成:BUILD_IRQ(0x00)、BUILD_IRQ(0x01)、…、BUILD_IRQ(0x0f)等共16个宏定义的引用。而宏BUILD_IRQ()则是定义在include/asm-i386/hw_irq.h头文件中定义的:
#define BUILD_IRQ(nr) \
asmlinkage void IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
"pushl $"#nr"-256\n\t" \
"jmp common_interrupt");

上述代码中的IRQ_NAME()宏也是定义在include/asm-i386/hw_irq.h中:
#define IRQ_NAME2(nr) nr##_interrupt(void)
#define IRQ_NAME(nr) IRQ_NAME2(IRQ##nr)
所以,宏引用IRQ_NAME(nr)被展开后也就是一个类似于IRQ0x01_interrupt(void)这样的函数名。
因此,从BUILD_IRQ(0x00)到BUILD_IRQ(0x0f)这一共16个宏引用经过gcc预处理后,将展开成为一系列如下样式的代码:
amslinkage void IRQ0x01_interrupt(void);
__asm__( \
“\n” \
“IRQ0x01_interrupt:\n\t” \
“pushl $0x01-256 \n\t” \
“jmp common_interrupt”);
可以看出,Linux内核正是通过gcc的预处理功能巧妙地定义了从IRQ0x00_interrupt()到IRQ0x0f_interrupt()这16个中断处理函数。

下面在来看看中断处理函数IRQ0x00_interrupt()到IRQ0x0f_interrupt()本身的流程。这16个中断处理函数的执行过程都是一样的,它主要完成两件事:(1)将立即数(IRQ号-256)这样一个负数压入内核堆栈,以供中断处理流程中后面的函数获取中断号。比如对于中断向量0x20,它对应于注8259A中断控制器的IRQ0,因此其中断服务程序的入口地址应该是interrupt[0],也即函数IRQ0x00_interrupt(),该函数所做的第一件事情就是把负数-256压入内核堆栈中。这里之所以用复数来表示中断号,是因为正数已被用于标识0x80中断中的系统调用号。(2)所有的中断服务函数所做的第二件事情都相同——即跳转到一个公共的的程序common_interrupt中,并由该公共程序继续对中断请求进行服务。
5.3.1 公共的中断服务程序——common_interrupt
在源文件arch/i386/kernel/i8259.c中一开始就引用了宏BUILD_COMMON_IRQ(),其作用就是构建面向所有中断的公共服务程序common_interrup,如下所示:
36: BUILD_COMMON_IRQ()

宏BUILD_COMMON_IRQ()定义在头文件include/asm-i386/hw_irq.h中,如下所示:
#define BUILD_COMMON_IRQ() \
asmlinkage void call_do_IRQ(void); \
__asm__( \
"\n" __ALIGN_STR"\n" \
"common_interrupt:\n\t" \
SAVE_ALL \
"pushl $ret_from_intr\n\t" \
SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \
"jmp "SYMBOL_NAME_STR(do_IRQ));

上述代码经过展开后,就成为如下汇编代码:
common_interrupt:
SAVE_ALL
call do_IRQ
jmp ret_from_intr
可以看出,公共服务程序common_interrupt主要做三件事:(1)首先,调用宏SAVE_ALL来保存CPU的执行现场;(2)然后,调用总控函数do_IRQ()对中断请求进行真正的服务;(3)当从do_IRQ()返回后,跳到函数ret_from_intr,进入中断返回操作。

用于保存现场的SAVE_ALL宏定义在arch/i386/kernel/entry.S文件中,如下所示:
#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__KERNEL_DS),%edx; \
movl %edx,%ds; \
movl %edx,%es;
说明几点:(1)用户态堆栈SS寄存器和ESP寄存器是在切换到内核态堆栈被压入的;(2)CPU在进入中断服务程序之前已经把EFLAGS寄存器和返回地址压入堆栈中;(3)段寄存器DS和ES被更改为内核态的段选择符__KERNEL_DS。因此,在执行SAVE_ALL宏之后内核态堆栈的内容应为如下图所示:
而Linux也根据上图中的关系在arch/i386/kernel/entry.S文件中定义了一些常数来表示个寄存器的内容相对于当前内核堆栈指针的偏移:
EBX = 0x00
ECX = 0x04
EDX = 0x08
ESI = 0x0C
EDI = 0x10
EBP = 0x14
EAX = 0x18
DS = 0x1C
ES = 0x20
ORIG_EAX = 0x24
EIP = 0x28
CS = 0x2C
EFLAGS = 0x30
OLDESP = 0x34
OLDSS = 0x38

真正对中断请求进行服务的do_IRQ()函数和中断返回函数ret_from_intr()将在下面介绍。在分析总控函数do_IRQ()之前,先来讨论一下中断请求描述符和中断服务队列。

§5.4 中断请求描述符和中断服务队列
我们都知道,在256个中断向量中,i386 CPU保留了0~31这前32个中断向量用于CPU异常,而剩余的224个中断向量则是可用于外设中断或软中断的可使用中断向量。由于不同体系结构的CPU所保留的中断向量不同,因此剩余的可使用中断向量数目也不同。所以,Linux定义了宏NR_IRQS来表示这个值。对于i386而言,该宏定义在include/asm-i386/irq.h头文件中:
#define TIME_IRQ 0 /* for i386,主8259A的IRQ0用于时钟中断 */
#define NR_IRQS 224

5.4.1 对中断控制器的抽象描述
在剩余的224个可用中断向量中,各中断向量所对应的中断类型也是不同的。比如对于PC机,中断向量0x20~0x2f则来自于主、从8259A PIC,其余中断向量则属于软中断(Linux仅仅使用了其中的0x80)。因此有必要对这些不同类型的中断向量进行区分。
另外,从中断控制器的角度看,尽管不同平台使用不同的PIC,但几乎所有的PIC都由相同的基本功能和工作方式。因此为了获得更好的跨平台兼容性,Linux对中断控制器进行了抽象描述。定义在头文件include/linux/irq.h头文件中的数据结构hw_interrupt_type描述了一个标准的中断控制器,如下所示:
/*
* Interrupt controller descriptor. This is all we need
* to describe about the low-level hardware.
*/
struct hw_interrupt_type {
const char * typename;
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, unsigned long mask);
};
typedef struct hw_interrupt_type hw_irq_controller;

在此基础上,Linux又在arch/i386/kernel/i8259.c文件中定义了全局变量i82559_irq_type,以用于所有来自主、从8259A中断控制器的中断请求(对应的中断向量为0x20~0x2f),如下:
static struct hw_interrupt_type i8259A_irq_type = {
"XT-PIC",
startup_8259A_irq,
shutdown_8259A_irq,
enable_8259A_irq,
disable_8259A_irq,
mask_and_ack_8259A,
end_8259A_irq,
NULL
};

而对于0x30~0xff子间的中断向量,Linux也在arch/i386/kernel/irq.c文件中定义了全局变量no_irq_type——一个虚拟的中断控制器,以表示这些中断向量的中断请求并不是来自于任何硬件PIC的中断,而是由软件编程指令产生的软中断。如下所示:
/* startup is the same as "enable", shutdown is same as "disable" */
#define shutdown_none disable_none
#define end_none enable_none

struct hw_interrupt_type no_irq_type = {
"none",
startup_none,
shutdown_none,
enable_none,
disable_none,
ack_none,
end_none
};

5.4.2 IRQ描述符
由上面的讨论可知,中断向量0x20~0xff所对应的中断请求是不同的,所以Linux在include/linux/irq.h头文件中定义了数据结构irq_desc_t来描述一个中断请求行为的属性。如下所示:
typedef struct {
unsigned int status; /* IRQ status */
hw_irq_controller *handler;
struct irqaction *action; /* IRQ action list */
unsigned int depth; /* nested irq disables */
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;
各成员的含义如下:
(1)status:表示中断请求的状态。它可以是下列值:
/*
* IRQ line status.
*/
#define IRQ_INPROGRESS 1 /* IRQ handler active - do not enter! */
#define IRQ_DISABLED 2 /* IRQ disabled - do not enter! */
#define IRQ_PENDING 4 /* IRQ pending - replay on enable */
#define IRQ_REPLAY 8 /* IRQ has been replayed but not acked yet */
#define IRQ_AUTODETECT 16 /* IRQ is being autodetected */
#define IRQ_WAITING 32 /* IRQ not yet seen - for autodetection */
#define IRQ_LEVEL 64 /* IRQ level triggered */
#define IRQ_MASKED 128 /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU 256 /* IRQ is per CPU */
上述这些状态标志值也是定义在include/linux/irq.h头文件中。
(2)handler指针:指向这个中断请求所来自的中断控制器描述符。对于中断向量0x20~0x2f上的中断请求而言,该成员指针应该指向全局变量i8259_irq_type;而对于其余软中断的中断请求,该指针因该指向no_irq_type。
(3)action指针:指向服务这个中断请求的、由设备驱动程序注册的服务程序(ISR)队列。
(4)depth:表示action指针所指向的中断请求服务队列的长度。
(5)lock:对irq_desc_t结构中其他成员进行访问保护的自旋锁。这是因为内核中irq_desc_t类型的变量(如下面的irq_desc[]数组)都是全局变量,因此对它们的访问都必须是互斥地进行的。

在上述结构的基础上,Linux在arch/i386/kernel/irq.c文件中定义了数组irq_desc[224]来分别描述中断向量0x20~0xff所对应的中断请求:
/*
* Controller mappings for all interrupt sources:
*/
irq_desc_t irq_desc[NR_IRQS] __cacheline_aligned =
{ [0 ... NR_IRQS-1] = { 0, &no_irq_type, NULL, 0, SPIN_LOCK_UNLOCKED}};

5.4.3 中断请求服务队列
现代外设总线(如PCI)通常都允许外设共享一个IRQ线,因此一个中断向量可能会对应有多个由设备驱动程序提供的中断请求服务例程(ISR),所以Linux内核必须有效地将这个多个来自不同的device driver的ISR组织起来。为此,Linux通过数据结构irqaction来描述一个设备驱动程序对中断请求的服务行为,其定义如下所示(include/linux/interrupt.h):
struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};
各成员的含义如下:
(1)handler指针:指向驱动程序的ISR入口地址。
(2)flags:描述中断类型的属性标志,它可以是下列三个值的“或”:
l SA_SHIRQ:表示中断是共享的。
l SA_INTERRUPT:当执行handler函数时,屏蔽同级中断。
l SA_SAPLE_RANDOM。
(3)mask:屏蔽掩码。
(4)name指针:表示提供handler函数的设备名称。
(5)dev_id:一个唯一的设备标识符。注意!当flags属性中设置了SA_SHIRQ属性时,dev_id指针不能为NULL。
(6)next指针:指向同属于该中断请求的下一个服务行为。Linux就是通过next指针把同一个中断向量的中断请求的多个服务行为组织成为一条中断请求服务队列的。
下图5-14清晰地描述了与中断请求相关的几个数据结构之间的关系:

5.4.4 中断请求描述符数组的初始化
函数init_ISA_irqs()完成对中断请求描述符数组irq_desc[]中各元素的初始化,该函数定义在arch/i386/kernel/i8259.c文件中,如下所示:
void __init init_ISA_irqs (void)
{
int i;

init_8259A(0);

for (i = 0; i < NR_IRQS; i++) {
irq_desc.status = IRQ_DISABLED;
irq_desc.action = 0;
irq_desc.depth = 1;

if (i < 16) {
/*
* 16 old-style INTA-cycle interrupts:
*/
irq_desc.handler = &i8259A_irq_type;
} else {
/*
* 'high' PCI IRQs filled in on demand
*/
irq_desc.handler = &no_irq_type;
}
}
}
初始化的过程很简单:中断向量0x20~0x2f分别与中断描述符irq_desc[0]~irq_desc[15]相对应,因此它们的handler指针指向i8259A_irq_type,因为它们的中断请求来自于主、从8259A中断控制器。而其余软中断描述符的handler指针则指向no_irq_type这个虚拟中断控制器描述符。

5.4.5 注册驱动程序的ISR
Linux在include/linux/sched.h头文件中声明了函数接口request_irq()和free_irq(),以供设备驱动程序向内核注册和注销驱动程序所提供的ISR函数。这两个函数都是实现在文件arch/i386/kernel/irq.c中。
首先,我们来分析request_irq()函数。该函数有5个参数:(1)irq:外设所使用的IRQ线号。(2)handler函数指针:设备驱动程序所实现的ISR函数。(3)irqflags:设备驱动程序指定的中断请求类型标志,它可以是下列三个值的“或”:SA_SHIRQ、SA_INTERRUPT和SA_SAMPLE_RANDOM。(4)devname指针:设备名字字符串。(5)dev_id:指向全局唯一的设备标识ID,这是一个void类型的指针,可供设备驱动程序自行解释。该函数的源码如下:
/**
* request_irq - allocate an interrupt line
* @irq: Interrupt line to allocate
* @handler: Function to be called when the IRQ occurs
* @irqflags: Interrupt type flags
* @devname: An ascii name for the claiming device
* @dev_id: A cookie passed back to the handler function
*
* This call allocates interrupt resources and enables the
* interrupt line and IRQ handling. From the point this
* call is made your handler function may be invoked. Since
* your handler function must clear any interrupt the board
* raises, you must take care both to initialise your hardware
* and to set up the interrupt handler in the right order.
*
* Dev_id must be globally unique. Normally the address of the
* device data structure is used as the cookie. Since the handler
* receives this value it makes sense to use it.
*
* If your interrupt is shared you must pass a non NULL dev_id
* as this is required when freeing the interrupt.
*
* Flags:
*
* SA_SHIRQ Interrupt is shared
*
* SA_INTERRUPT Disable local interrupts while processing
*
* SA_SAMPLE_RANDOM The interrupt can be used for entropy
*
*/

int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char * devname,
void *dev_id)
{
int retval;
struct irqaction * action;

#if 1
/*
* Sanity-check: shared interrupts should REALLY pass in
* a real dev-ID, otherwise we'll have trouble later trying
* to figure out which interrupt is which (messes up the
* interrupt freeing logic etc).
*/
if (irqflags & SA_SHIRQ) {
if (!dev_id)
printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]);
}
#endif

if (irq >= NR_IRQS)
return -EINVAL;
if (!handler)
return -EINVAL;

action = (struct irqaction *)
kmalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;

action->handler = handler;
action->flags = irqflags;
action->mask = 0;
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;

retval = setup_irq(irq, action);
if (retval)
kfree(action);
return retval;
}
对该函数的NOTE如下:
(1)首先进行参数检查。①在指定了中断共享标志IRQ_SHIRQ标志时,参数dev_id必须有效,不能为NULL;②IRQ线号参数irq不能大于NR_IRQS;③handler指针不能为NULL。
(2)调用kmalloc()函数在SLAB分配器缓存中为结构类型irqaction分配内存,以构建一个中断服务描述符。如果分配失败,则返回-ENOMEM,表示系统内存不足。
(3)然后,根据参数相应地初始化刚刚构建的中断服务描述符中的各个成员。
(4)最后调用setup_irq()函数将上述构建号的中断服务描述符插入到参数irq所对应的中断服务队列中去。如果setup_irq()函数返回非0值,表示插入失败,因此调用kfree()函数将前面构建的中断服务描述符释放掉。
(5)最后,返回返回值retval。0表示成功,非0表示失败。

函数setup_irq()用来将一个已经构建好的中断服务描述符插入到相应的中断服务队列中。参数irq指定IRQ输入线号,它指定将中断描述符插到哪一个中断服务队列中去;参数new指针指向待插入的中断服务描述符。该函数的源码如下:

/* this was setup_x86_irq but it seems pretty generic */
int setup_irq(unsigned int irq, struct irqaction * new)
{
int shared = 0;
unsigned long flags;
struct irqaction *old, **p;
irq_desc_t *desc = irq_desc + irq;

/*
* Some drivers like serial.c use request_irq() heavily,
* so we have to be careful not to interfere with a
* running system.
*/
if (new->flags & SA_SAMPLE_RANDOM) {
/*
* This function might sleep, we want to call it first,
* outside of the atomic block.
* Yes, this might clear the entropy pool if the wrong
* driver is attempted to be loaded, without actually
* installing a new handler, but is this really a problem,
* only the sysadmin is able to do this.
*/
rand_initialize_irq(irq);
}

/*
* The following block of code has to be executed atomically
*/
spin_lock_irqsave(&desc->lock,flags);
p = &desc->action;
if ((old = *p) != NULL) {
/* Can't share interrupts unless both agree to */
if (!(old->flags & new->flags & SA_SHIRQ)) {
spin_unlock_irqrestore(&desc->lock,flags);
return -EBUSY;
}

/* add new interrupt at end of irq queue */
do {
p = &old->next;
old = *p;
} while (old);
shared = 1;
}

*p = new;

if (!shared) {
desc->depth = 0;
desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING);
desc->handler->startup(irq);
}
spin_unlock_irqrestore(&desc->lock,flags);

register_irq_proc(irq);
return 0;
}
对该函数的NOTE如下:
(1)首先根据参数irq的值找到相应的中断请求描述符,也即irq_desc + irq,并让指针desc指向它。
(2)如果待插入中断服务描述符中的flags成员中设置了SA_SAMPLE_RANDOM标志,那就调用rand_initialize_irq()函数来引入一些随机性。一般设备驱动程序很少使用这个标志。
(3)因为接下来要对中断请求描述符desc进行访问,所以调用spin_lock_irqsave()函数对自旋锁desc->lock进行加锁,并同时关中断。
(4)将指针p指向desc->action成员。指针desc->action指向将要进行插入操作的中断服务队列的第一个元素。因此*p也就等于desc->action。此外,让指针old等于(*p)。因此old也就指向中断服务队列的第一个元素。
(5)然后判断(*p)所指向的中断服务队列是否为空。如果不为空:①判断中断服务队列的第一个中断服务描述符和new所指向的待插入中断服务描述符是否同时都设置了SA_SHIRQ标志。如果没有,返回错误值-EBUSY表示参数irq所指定的IRQ输入线已经被使用了。注意!必须二者都同意共享IRQ输入线才行。②通过一个do{}while循环来依次遍历整个中断服务队列,直到队列中的最后一个元素。当从do{}while循环退出时,指针p指向队列中最后一个中断服务描述符的next指针成员。③将局部变量shared置1,表示发生中断共享。
(6)让(*p)等于new指针,从而将new所指向的中断服务描述符插到中断服务队列的尾部。注意!如果原来的中断服务队列为空的话,new所指向的中断服务描述符将成为队列中唯一的一个元素。
(7)如果shared变量为0,说明new是中断服务队列中插入的第一个元素,因此对中断请求描述符desc进行相应的初始化设置,包括:将depth置0,清除IRQ_DISABLED、IRQ_AUTODETECT和IRQ_WAITING标志,以及调用中断控制器描述符的startup()函数来使能irq所指定的IRQ输入线。
(8)至此,插入操作结束,因此调用spin_unlock_irqrestore()函数进行解锁和开中断。
(9)最后,调用register_irq_proc()函数注册相应的/proc文件系统。

5.4.6 注销驱动程序的ISR
函数free_irq()用来注销驱动程序先前通过request_irq()函数所注册的ISR函数。其源码如下:
/**
* free_irq - free an interrupt
* @irq: Interrupt line to free
* @dev_id: Device identity to free
*
* Remove an interrupt handler. The handler is removed and if the
* interrupt line is no longer in use by any driver it is disabled.
* On a shared IRQ the caller must ensure the interrupt is disabled
* on the card it drives before calling this function. The function
* does not return until any executing interrupts for this IRQ
* have completed.
*
* This function may be called from interrupt context.
*
* Bugs: Attempting to free an irq in a handler for the same irq hangs
* the machine.
*/

void free_irq(unsigned int irq, void *dev_id)
{
irq_desc_t *desc;
struct irqaction **p;
unsigned long flags;

if (irq >= NR_IRQS)
return;

desc = irq_desc + irq;
spin_lock_irqsave(&desc->lock,flags);
p = &desc->action;
for (;;) {
struct irqaction * action = *p;
if (action) {
struct irqaction **pp = p;
p = &action->next;
if (action->dev_id != dev_id)
continue;

/* Found it - now remove it from the list of entries */
*pp = action->next;
if (!desc->action) {
desc->status |= IRQ_DISABLED;
desc->handler->shutdown(irq);
}
spin_unlock_irqrestore(&desc->lock,flags);

#ifdef CONFIG_SMP
/* Wait to make sure it's not being used on another CPU */
while (desc->status & IRQ_INPROGRESS)
barrier();
#endif
kfree(action);
return;
}
printk("Trying to free free IRQ%d\n",irq);
spin_unlock_irqrestore(&desc->lock,flags);
return;
}
}
对该函数的NOTE如下:
(1)为遍历中断服务队列做准备:首先,根据参数irq所指定的IRQ号找到相应的中断请求描述符desc(=irq_desc+irq)。然后调用spin_lock_irqsave()函数进行加锁和关中断。最后,让指针p指向当前被扫描队列元素的前一个元素的next指针成员,因此(*p)就指向队列中当前正被扫描的中断服务描述符。初始时,让指针p指向中断请求描述符desc的action指针成员,也即(*p)指向中断服务队列的第一个元素。
(2)用一个for死循环来遍历队列。循环体的执行步骤如下:
n 让action指针等于(*p),表示当前正被扫描的队列元素。
n 如果action指针不为空:①让指针pp等于p,因此pp也就指向当前被扫描队列元素的前一个元素的next指针成员。②更新指针p,让它指向当前被扫描的中断服务描述符action的next指针成员,因此(*p)也就指向下一个队列元素。③比较参数dev_id和当前中断服务描述符action的dev_id成员是否不相等,如果不相等,那就执行continue语句继续扫描队列。如果相等,则说明我们已经找到要删除的中断服务描述符,也即是当前被扫描的中断服务描述符action。④找到以后,先将所找到的中断服务描述符action从中断服务队列中摘除。语句*pp=action->next完成这一点,也即让前一个元素的next指针指向当前被扫描元素的下一个元素。⑤判断一下desc->action所指向的中断服务队列是否为空,因为中断服务队列中可能就只有一个元素,因此前一步的摘除操作又可能会使中断服务队列变为空。如果为空,那就在中断请求描述符的status中设置IRQ_DISABLED标志,并调用PIC描述符的shutdown()函数禁止这个IRQ输入线。⑥最后,解除自旋锁并开中断,接着调用kfree()函数释放内存后,函数就可以直接return了。
n 如果action指针为空,说明已经遍历到队列尾部,而此时我们还没有找到参数dev_id所指定的中断服务描述符,因此打印警告信息,然后解锁并开中断。函数以失败结束执行。



我的未来不是梦!

文章选项:           

zhanrk
(journeyman)
03-02-14 12:08
  [精华] Re: Linux内核的中断机制分析(continue...) [re: zhanrk]   



§5.5 Linux对8259A中断控制器的编程实现
Linux对主、从8259A中断控制器的编程实现在源码文件arch/i386/kernel/i8259.c中,主要包含两部分:(1)对主、从8259A PIC的初始化;(2)对控制器描述符i8259A_irq_type中定义的各函数的实现。

5.5.1 对8259A的初始化
函数init_8259A()完成对主、从8259A中断控制器的初始化工作。如前所述,该函数是被init_ISA_irqs()函数所调用的(参见5.4.4节),其源码如下:
void __init init_8259A(int auto_eoi)
{
unsigned long flags;

spin_lock_irqsave(&i8259A_lock, flags);

outb(0xff, 0x21); /* mask all of 8259A-1 */
outb(0xff, 0xA1); /* mask all of 8259A-2 */

/*
* outb_p - this has to work on a wide range of PC hardware.
*/
outb_p(0x11, 0x20); /* ICW1: select 8259A-1 init */
outb_p(0x20 + 0, 0x21); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */
outb_p(0x04, 0x21); /* 8259A-1 (the master) has a slave on IR2 */
if (auto_eoi)
outb_p(0x03, 0x21); /* master does Auto EOI */
else
outb_p(0x01, 0x21); /* master expects normal EOI */

outb_p(0x11, 0xA0); /* ICW1: select 8259A-2 init */
outb_p(0x20 + 8, 0xA1); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */
outb_p(0x02, 0xA1); /* 8259A-2 is a slave on master's IR2 */
outb_p(0x01, 0xA1); /* (slave's support for AEOI in flat mode
is to be investigated) */

if (auto_eoi)
/*
* in AEOI mode we just have to mask the interrupt
* when acking.
*/
i8259A_irq_type.ack = disable_8259A_irq;
else
i8259A_irq_type.ack = mask_and_ack_8259A;

udelay(100); /* wait for 8259A to initialize */

outb(cached_21, 0x21); /* restore master IRQ mask */
outb(cached_A1, 0xA1); /* restore slave IRQ mask */

spin_unlock_irqrestore(&i8259A_lock, flags);
}
参数atuo_eoi决定是否让8259A工作在AEOI模式下。对该函数的注释如下:
(1)首先调用函数spin_lock_irqsave()对自旋锁i8259A_lock进行加锁。所有对主、从8259A进行编程的代码都首先必须进行这一步操作,以保证对主、从8259A的操作是互斥的。
(2)分别向主、从8259A写入屏蔽掩码0xff,以屏蔽所有的IRQ输入。
(3)通过向主8259A发送一些列ICW来初始化主8259A:①ICW1=0x11,也即:Edge Triggered Mode、AOI=0、Cascade Mode(ICW3 Needed)、ICW4 Needed;②ICW2=0x20,也即:IRQ0的中断向量为0x20;③ICW3=0x04,也即:主8259A通过IRQ2级连了一个从8259A;④如果参数auto_eoi非零,则让ICW4=0x03,也即:Auto EOI、80x86 mode;否则如果auto_eoi为0,则让ICW4=0x01,也即:Normal EOI、80x86 mode。
(4)通过向从8259A发送一系列ICW来初始化它:①ICW1与主8259A的相同;②ICW2=0x28,表示从8259A的IRQ0的中断向量为0x28;③ICW3=0x02,表示从8259A是连接在主8259A的IRQ2上;④ICW4=0x01,表示从8259A只工作在Normal EOI模式下。
这里需要注意的是,写端口时最好用outb_p()函数,以等待慢速的8259A中断控制器。
(5)如果参数auto_eoi非零,表示工作在“自动中断结束(AEOI)模式,因此就要将i8259A_irq_type的应答函数ack设置成disable_8259A_irq()函数;否则就将其设置成mask_and_ack_8259A()函数。
(6)至此,写初始化命令字的过程结束。然后调用udelay()函数延时100&micro;s,以等待8259A中断控制器完成其自身的硬件初始化。
(7)最后,从全局变量cached_irq_mask中恢复主、从8259A的IRQ屏蔽掩码。关于cached_irq_mask这个全局屏蔽掩码下面马上会谈到。

5.5.2 对PIC描述符i8259A_irq_type的实现
(1)全局屏蔽掩码cached_irq_mask
Linux在arch/i386/kernel/i8259.c文件中定义了一个全局变量cached_irq_mask来表示8259A中断控制器的当前屏蔽状态。其中,bit[7:0]表示Master 8259A的屏蔽掩码,bit[15:8]表示Slave 8259A的屏蔽掩码。如下所示:
/*
* This contains the irq mask for both 8259A irq controllers,
*/
static unsigned int cached_irq_mask = 0xffff;

#define __byte(x,y) (((unsigned char *)&(y))[x])
#define cached_21 (__byte(0,cached_irq_mask))
#define cached_A1 (__byte(1,cached_irq_mask))

(2)禁止一个特定的IRQ
函数disable_8259A_irq()通过将8259A的IMR中的相应位设置成1来实现禁止某个特定的IRQ,其源码如下:
void disable_8259A_irq(unsigned int irq)
{
unsigned int mask = 1 << irq;
unsigned long flags;

spin_lock_irqsave(&i8259A_lock, flags);
cached_irq_mask |= mask;
if (irq & 8)
outb(cached_A1,0xA1);
else
outb(cached_21,0x21);
spin_unlock_irqrestore(&i8259A_lock, flags);
}
(1)参数irq取值范围为0~15,指定对哪一个IRQ线进行屏蔽。
(2)通过一个“或”操作将cached_irq_mask中的相应位设置为1。
(3)如果irq大于等于8(也即irq&8的结果非0),则说明这个IRQ来自于Slave 8259A,否则就应来自于Master 8259A。

(3)使能一个特定的IRQ
函数enable_8259A_irq()通过将8259A的IMR寄存器中的相应位清零来实现使能一个特定的IRQ。该函数与disable_8259A_irq()刚好相反。如下所示:
void enable_8259A_irq(unsigned int irq)
{
unsigned int mask = ~(1 << irq);
unsigned long flags;

spin_lock_irqsave(&i8259A_lock, flags);
cached_irq_mask &= mask;
if (irq & 8)
outb(cached_A1,0xA1);
else
outb(cached_21,0x21);
spin_unlock_irqrestore(&i8259A_lock, flags);
}

(4)8259A中断控制器的startup()函数和shutdown()函数
8259A中断控制器描述符i8259A_irq_type的shutdown()函数和startup()函数实际上就是其disable函数和enable函数。
#define shutdown_8259A_irq disable_8259A_irq
……
static unsigned int startup_8259A_irq(unsigned int irq)
{
enable_8259A_irq(irq);
return 0; /* never anything pending */
}
(5)end_8259A_irq()函数
8259A中断控制器描述符i8259_irq_type的end函数指针被定义成指向函数end_8259A_irq()。该函数的源码如下:
static void end_8259A_irq (unsigned int irq)
{
if (!(irq_desc[irq].status & (IRQ_DISABLED|IRQ_INPROGRESS)))
enable_8259A_irq(irq);
}
该函数的实现也是基于enable_8259A_irq()函数,它在相应IRQ描述符的status成员没有设置IRQ_DISABLED标志和IRQ_INPROGRESS标志时,通过调用enable_8259A_irq()函数来使能参数irq所指定的IRQ。

(6)应答函数
中断控制器描述符中的应答函数ack用来向PIC发送EOI命令,表示中断服务的结束。具体到8259A来讲,函数mask_and_ack_8259A()用来向主、从8259A发送EOI命令。向8259A应答因该遵循这样的顺序:首先,屏蔽相应的IRQ;然后,发送EOI命令。此外,如果IRQ来自于从8259A,还必须先向Slave 8259A发送EOI命令,再向Master 8259A发送EOI命令。如果IRQ来自于Master 8259A,则仅仅向Master 8259A发送EOI命令就可以了。
函数mask_and_ack_8259A()的源码如下所示:

/*
* Careful! The 8259A is a fragile beast, it pretty
* much _has_ to be done exactly like this (mask it
* first, _then_ send the EOI, and the order of EOI
* to the two 8259s is important!
*/
void mask_and_ack_8259A(unsigned int irq)
{
unsigned int irqmask = 1 << irq;
unsigned long flags;

spin_lock_irqsave(&i8259A_lock, flags);
/*
* Lightweight spurious IRQ detection. We do not want
* to overdo spurious IRQ handling - it's usually a sign
* of hardware problems, so we only do the checks we can
* do without slowing down good hardware unnecesserily.
*
* Note that IRQ7 and IRQ15 (the two spurious IRQs
* usually resulting from the 8259A-1|2 PICs) occur
* even if the IRQ is masked in the 8259A. Thus we
* can check spurious 8259A IRQs without doing the
* quite slow i8259A_irq_real() call for every IRQ.
* This does not cover 100% of spurious interrupts,
* but should be enough to warn the user that there
* is something bad going on ...
*/
if (cached_irq_mask & irqmask)
goto spurious_8259A_irq;
cached_irq_mask |= irqmask;

handle_real_irq:
if (irq & 8) {
inb(0xA1); /* DUMMY - (do we need this?) */
outb(cached_A1,0xA1);
outb(0x60+(irq&7),0xA0);/* 'Specific EOI' to slave */
outb(0x62,0x20); /* 'Specific EOI' to master-IRQ2 */
} else {
inb(0x21); /* DUMMY - (do we need this?) */
outb(cached_21,0x21);
outb(0x60+irq,0x20); /* 'Specific EOI' to master */
}
spin_unlock_irqrestore(&i8259A_lock, flags);
return;

spurious_8259A_irq:
/*
* this is the slow path - should happen rarely.
*/
if (i8259A_irq_real(irq))
/*
* oops, the IRQ _is_ in service according to the
* 8259A - not spurious, go handle it.
*/
goto handle_real_irq;

{
static int spurious_irq_mask;
/*
* At this point we can be sure the IRQ is spurious,
* lets ACK and report it. [once per IRQ]
*/
if (!(spurious_irq_mask & irqmask)) {
printk("spurious 8259A interrupt: IRQ%d.\n", irq);
spurious_irq_mask |= irqmask;
}
irq_err_count++;
/*
* Theoretically we do not have to handle this IRQ,
* but in Linux this does not cause problems and is
* simpler for us.
*/
goto handle_real_irq;
}
}
对该函数的NOTE如下:
①首先,通过全局变量cached_irq_mask判断参数irq所指定的IRQ线是否处于屏蔽状态。如果已经被屏蔽,则说明8259A在IMR寄存器中的相应位被置1的情况下仍然向CPU发出了相应的中断信号,因此这是一个假的中断,所以跳转到spurious_8259A_irq部分进行处理。有关这方面的详细描述请参见源码中的注释。
②如果cached_irq_mask中的相应位为0,也即相应的IRQ位被屏蔽,说明我们收到的是一个真实的中断请求,于是按照上面所述的步骤,先对这个IRQ进行屏蔽,因此将cached_irq_mask中的相应位设置为1。
③接下来进入真实的中断请求处理部分(handle_real_irq)。如果irq&8的结果非0,说明这个中断请求来自于Slave 8259A。否则这个中断请求来自于Master 8259A。
对于来自Slave 8259A的情形:①先将屏蔽字cached_irq_mask中的高字节cache_A1写入端口0xA1,以屏蔽相应的IRQ输入线。②向从8259A的端口0xA0写操作命令字OCW2,其值等于0x60+(irq&7)——Specific EOI、对irq&7这个IR进行操作。③向Master 8259A的端口0x20写入操作命令字OCW2,其值等于0x62——Specific EOI、对IR2进行操作(因为从8259A通过主8259A的IR2进行级连)。
对于来自Master 8259A的情形:①先将屏蔽字cached_irq_mask中的低字节cache_21写入端口0x21,以屏蔽相应的IRQ输入线。②向主8259A的端口0x20写操作命令字OCW2,其值等于0x60+irq——Specific EOI、对irq这个IR进行操作。
④执行完hadle_real_irq程序段后,对真实中断请求的应答过程就宣告结束。因此函数可以立即返回了。
⑤spurious_8259A_irq部分——对假的8259A 中断请求进行处理。首先,调用i8259A_irq_real()函数来根据8259A的ISR寄存器判断这是否是一个真实的由8259A发给CPU的中断请求。如果i8259A_irq_real()函数返回为1,则说明是一个真实的中断请求,于是跳转回handle_real_irq部分对其进行处理。否则,如果i8259A_irq_real()返回0值,说明我们的CPU真的是受到一个虚假的中断请求信号。
⑥对于真的受到一个虚假中断请求这种情形,分三个步骤来进行处理:①视需要打印一条内核信息向用户发出警告。②将全局变量irq_err_count的值加1(该变量定义在irq.c文件中)。③从理论上来讲,对于虚假的中断请求并不需要进行任何处理。但是在Linux中,将它按照真实的中断请求进行应答处理也不会引起任何问题,因此最后又跳转回handl_real_irq部分将它按照真实的中断请求进行应答处理。

函数i8259_real_irq()是定义在i8259.c文件中的一个内部函数。它根据8259A的ISR寄存器中的内容判断参数irq所指定的IRQ输入线上是否真的产生了一个中断请求。其源码如下所示:

/*
* This function assumes to be called rarely. Switching between
* 8259A registers is slow.
* This has to be protected by the irq controller spinlock
* before being called.
*/
static inline int i8259A_irq_real(unsigned int irq)
{
int value;
int irqmask = 1<<irq;

if (irq < 8) {
outb(0x0B,0x20); /* ISR register */
value = inb(0x20) & irqmask;
outb(0x0A,0x20); /* back to the IRR register */
return value;
}
outb(0x0B,0xA0); /* ISR register */
value = inb(0xA0) & (irqmask >> 8);
outb(0x0A,0xA0); /* back to the IRR register */
return value;
}
对该函数的注释如下:
由于端口0x20和0xA0对读指令默认的是IRR寄存器,因此要向端口0x20/0xA0写入操作命令字OCW3=0x0B,以切换到ISR寄存器。然后通过inb()函数读端口0x20/0xA0,以得到ISR寄存器的当前值,如果ISR&irqmask非0,说明指定的IRQ输入线上真的产生了一个中断请求;否则就没有。最后记得要将端口0x20/0xA0切换回IRR寄存器。

(7)函数i8259_irq_pending()
该函数并不是描述符i8259A_irq_type中定义的函数。该函数用来判断某个指定的IRQ是否未得到服务而一直处于pending状态。其源码如下:
int i8259A_irq_pending(unsigned int irq)
{
unsigned int mask = 1<<irq;
unsigned long flags;
int ret;

spin_lock_irqsave(&i8259A_lock, flags);
if (irq < 8)
ret = inb(0x20) & mask;
else
ret = inb(0xA0) & (mask >> 8);
spin_unlock_irqrestore(&i8259A_lock, flags);

return ret;
}
显然,端口0x20/0xA0对于读操作默认的是返回IRR寄存器的值。如果IRR寄存器中的某个位被置1,就表示相应的中断请求正等待CPU的服务(通过一个INTA-cycle),因此该中断请求也就处于pending状态。

§5.6 CPU的中断请求统计信息
Linux在头文件include/asm-i386/hardirq.h和include/linux/irq_cpustat.h中定义了和中断请求相关的CPU统计信息数据结构以及相应的操作宏。
首先,Linux在头文件hardirq.h中定义了数据结构irq_cpustat_t,以描述一个CPU对中断请求进行服务的历史统计信息。如下所示:
/* entry.S is sensitive to the offsets of these fields */
typedef struct {
unsigned int __softirq_active;
unsigned int __softirq_mask;
unsigned int __local_irq_count;
unsigned int __local_bh_count;
unsigned int __syscall_count;
unsigned int __nmi_count; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;
上述结构中各成员的命名已经清楚地表达了其各自的含义,这里就不详述。

在此基础上,Linux又在kernel/softirq.c文件中定义了全局数组irq_stat[NR_CPUS],以分别描述每个CPU各自的中断统计信息。这一点主要是从SMP体系结构来考虑的。该数组的定义如下所示:
/* No separate irq_stat for s390, it is part of PSA */
#if !defined(CONFIG_ARCH_S390)
irq_cpustat_t irq_stat[NR_CPUS];
#endif /* CONFIG_ARCH_S390 */
该数组的原型声明在头文件irq_cpustat.h中,如下:
extern irq_cpustat_t irq_stat[]; /* defined in asm/hardirq.h */

5.6.1 引用数据结构irq_cpustat_t中的成员
首先,Linux在头文件irq_cpustat.h中通过宏__IRQ_STAT来定义了一个引用数据结构irq_cpustat_t中各成员的操作模板,如下所示:
#ifdef CONFIG_SMP
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
#else
#define __IRQ_STAT(cpu, member) ((void)(cpu), irq_stat[0].member)
#endif
显然,在单CPU系统中,我们总是引用irq_stat[0]中的成员,此时参数cpu是一个无用的参数。

基于上述引用模板,Linux分别为irq_cpustat_t中各成员定义了显示的引用操作宏(irq_cpustat.h),如下所示:
/* arch independent irq_stat fields */
#define softirq_active(cpu) __IRQ_STAT((cpu), __softirq_active)
#define softirq_mask(cpu) __IRQ_STAT((cpu), __softirq_mask)
#define local_irq_count(cpu) __IRQ_STAT((cpu), __local_irq_count)
#define local_bh_count(cpu) __IRQ_STAT((cpu), __local_bh_count)
#define syscall_count(cpu) __IRQ_STAT((cpu), __syscall_count)
/* arch dependent irq_stat fields */
#define nmi_count(cpu) __IRQ_STAT((cpu), __nmi_count) /* i386, ia64 */

5.6.2 判断一个CPU是否处于中断上下文中
通过数据结构irq_cpustat_t的__local_irq_count成员和__local_bh_count成员的值,我们可以判断出当前CPU当前是否正处于中断服务上下文中。为此,Linux在hardirq.h头文件中定义了宏in_interrupt()和in_irq(),如下所示:
/*
* Are we in an interrupt context? Either doing bottom half
* or hardware interrupt processing?
*/
#define in_interrupt() ({ int __cpu = smp_processor_id(); \
(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
#define in_irq() (local_irq_count(smp_processor_id()) != 0)
因此,只要当前CPU处于硬件中断处理或执行bottom half时,in_interrupt()宏就返回非零值。而对于in_irq()宏而言,则只有当CPU处于硬件中断处理时,才返回非0值。

5.6.3 增加/减少CPU的本地中断请求计数
当进入中断请求处理时,Linux必须将当前CPU的硬件中断请求计数值__local_irq_count加1(后面我们将会看到这一点)。而在退出硬件中断请求处理时将其减1。如下所示(hardirq.h):
#ifndef CONFIG_SMP

#define hardirq_trylock(cpu) (local_irq_count(cpu) == 0)
#define hardirq_endlock(cpu) do { } while (0)

#define irq_enter(cpu, irq) (local_irq_count(cpu)++)
#define irq_exit(cpu, irq) (local_irq_count(cpu)--)

#define synchronize_irq() barrier()

#else

#include <asm/atomic.h>
#include <asm/smp.h>

extern unsigned char global_irq_holder;
extern unsigned volatile long global_irq_lock; /* long for set_bit -RR */

static inline int irqs_running (void)
{
int i;

for (i = 0; i < smp_num_cpus; i++)
if (local_irq_count(i))
return 1;
return 0;
}

static inline void release_irqlock(int cpu)
{
/* if we didn't own the irq lock, just ignore.. */
if (global_irq_holder == (unsigned char) cpu) {
global_irq_holder = NO_PROC_ID;
clear_bit(0,&global_irq_lock);
}
}

static inline void irq_enter(int cpu, int irq)
{
++local_irq_count(cpu);

while (test_bit(0,&global_irq_lock)) {
/* nothing */;
}
}

static inline void irq_exit(int cpu, int irq)
{
--local_irq_count(cpu);
}

static inline int hardirq_trylock(int cpu)
{
return !local_irq_count(cpu) && !test_bit(0,&global_irq_lock);
}

#define hardirq_endlock(cpu) do { } while (0)

extern void synchronize_irq(void);

#endif /* CONFIG_SMP */

§5.7 中断服务的总控程序do_IRQ
在§5.3节我们谈到,由IDT表项所指向的中断服务程序IRQ0xYY_interrupt()全部都跳转到common_interrupt这个公共的中断服务程序,而common_interrupt函数只作三件事:(1)用SAVE_ALL宏来保存现场;(2)然后调用中断服务程序的总控函数do_IRQ对中断请求进行真正的服务;(3)从do_IRQ返回后,跳转到中断返回函数ret_from_intr以执行中断返回操作。
另外,在§5.4节中我们也谈到,PCI总线允许不同的外设共享同一个IRQ输入线号,每个设备驱动程序都通过request_irq()接口将驱动程序的ISR注册到相对应的中断服务对列中。因此do_IRQ()函数要真正地服务一个中断请求,就必须遍历相应的中断服务队列,以便让相对应正确的ISR得到执行(注意!通常每个设备驱动程序的ISR一进入就判断它所驱动的硬件是否真的产生了中断请求,如果不是它就立即返回,如果是它才继续向下执行,从而真正地服务中断请求)。下面我们就详细地分析一下do_IRQ()函数是如何遍历IRQ描述符中的中断服务队列的。
函数do_IRQ()以及其相关的底层支持函数全部都是现在arch/i386/kernel/irq.c文件中。函数do_IRQ()只有唯一的一个参数regs,它是一个pt_regs结构类型,该数据结构定义在include/asm-i386/ptrace.h中。如下所示:
struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
由上述pt_regs数据结构的内容可见,SAVE_ALL宏的作用不仅再与保存中断现场,它也为函数do_IRQ()模拟出了一个函数调用环境。
函数do_IRQ()的源码如下:

/*
* do_IRQ handles all normal device IRQ's (the special
* SMP cross-CPU interrupts have their own specific
* handlers).
*/
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
/*
* We ack quickly, we don't want the irq controller
* thinking we're snobs just because some other CPU has
* disabled global interrupts (we have already done the
* INT_ACK cycles, it's too late to try to pretend to the
* controller that we aren't taking the interrupt).
*
* 0 return value means that this irq is already being
* handled by some other CPU. (or is disabled)
*/
int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */
int cpu = smp_processor_id();
irq_desc_t *desc = irq_desc + irq;
struct irqaction * action;
unsigned int status;

kstat.irqs[cpu][irq]++;
spin_lock(&desc->lock);
desc->handler->ack(irq);
/*
REPLAY is when Linux resends an IRQ that was dropped earlier
WAITING is used by probe to mark irqs that are being tested
*/
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */

/*
* If the IRQ is disabled for whatever reason, we cannot
* use the action we have.
*/
action = NULL;
if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {
action = desc->action;
status &= ~IRQ_PENDING; /* we commit to handling */
status |= IRQ_INPROGRESS; /* we are handling it */
}
desc->status = status;

/*
* If there is no IRQ handler or it was disabled, exit early.
Since we set PENDING, if another processor is handling
a different instance of this same irq, the other processor
will take care of it.
*/
if (!action)
goto out;

/*
* Edge triggered interrupts need to remember
* pending events.
* This applies to any hw interrupts that allow a second
* instance of the same irq to arrive while we are in do_IRQ
* or in the handler. But the code here only handles the _second_
* instance of the irq, not the third or fourth. So it is mostly
* useful for irq hardware that does not mask cleanly in an
* SMP environment.
*/
for (;;) {
spin_unlock(&desc->lock);
handle_IRQ_event(irq, &regs, action);
spin_lock(&desc->lock);

if (!(desc->status & IRQ_PENDING))
break;
desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;
out:
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
desc->handler->end(irq);
spin_unlock(&desc->lock);

if (softirq_active(cpu) & softirq_mask(cpu))
do_softirq();
return 1;
}
对该函数的NOTE如下:
(1)首先,通过参数regs.orig_eax来获取此次中断请求的IRQ号。然后调用smp_processor_id()函数得到当前CPU的ID。指针desc指向相应于此次中断请求的IRQ描述符。
(2)将内核统计变量kstat.irqs数组中的相应元素kstat.irqs[cpu][irq]加1。并对中断请求描述符desc中的自旋锁成员lock进行加锁,以便使随后对中断服务队列的操作互斥。
(3)向中断控制器发出应答。具体到8259A来说,则是调用mask_and_ack_8259A()函数来向8259A发送EOI命令,表示中断结束,以便让8259A继续接收其它中断请求信号。注意,mask_and_ack_8259A函数同时还将屏蔽变量irq所对应的IRQ输入线。比如,当前服务的是IRQ3,那么在do_IRQ()执行期间,IRQ3是被8259A所屏蔽的。这一点需要特别注意。
(4)接下来,主要是设置中断请求的状态。先将IRQ描述符的status成员去掉IRQ_DISABLED标志位和IRQ_WAITING标志位(如果已经设置了的话)。然后在状态中设置IRQ_PENDING标志,表示这个中断请求的服务还未完成。注意!这里的IRQ_PENDING状态与前面所述的8259A IRQ pending状态是有区别的。8259A的IRQ pending是指在8259A已经向CPU发出了中断请求信号但CPU并没有向8259A发出中断应答(第一个INTA-cycle)时的状态,只要CPU向8259A发出了第一个INTA-cycle,8259A的IRQ pending状态即宣告结束。而这里的IRQ_PENDING标志的真正含义是指已经进入到do_IRQ()函数、但中断服务队列还未开始执行的意思。
(5)根据前面设置好的状态来确定中断服务队列。只有当IRQ状态中没有同时设置IRQ_DISABLED标志和IRQ_INPROGRESS标志时,才让action指针指向IRQ描述符中定义的中断服务队列(由desc->action所指向)。IEQ_DISABLED标志表示该中断请求已经被禁止,因此不需要被服务。而IRQ_INPROGRESS则表示中断请求正在被另一个CPU服务。因此当前CPU就不需要再对这个中断请求进行服务了。
在设置好action指针后,就清除中断请求状态中的IRQ_PENDING标志,同时在状态中设置IRQ_INPROGRESS标志,因为接下来马上就要开始服务这个中断请求了。最后,用刚刚设置好的状态值来更新IRQ描述符中的status值,以便让它对其它CPU可见。
(6)判断action指针是否为空,如果为空,说明没有任何中断服务行为等待执行,因此跳转到out部分,以便退出do_IRQ()函数。
(7)接下来的for死循环真正地遍历中断服务队列并调用其中的ISR来执行。循环体的步骤如下:
1. 首先,解除IRQ描述符中的自旋锁lock。这是因为在执行中断服务队列中的各个ISR时,ISR并不会访问IRQ描述符中的中断服务队列。
2. 调用handle_IRQ_event()函数来依次执行由action所指向的中断服务队列中的所有ISR。
3. 由于接下来又要访问IRQ描述符中的成员值,因此对IRQ描述符中的自旋锁lock进行加锁操作。
4. 最后测试IRQ描述符的status成员是否设置了IRQ_PENDING标志。如果没有,就执行break语句退出for循环。如果设置了IRQ_PENDING标志,则说明有一次中断请求未得到服务,于是首先清除IRQ_PENDING标志,然后继续for循环,以便为该中断请求补上一次中断服务。这个概念就成为“IRQ_REPLAY”。为什么需要这样呢?这是应为在SMP体系结构中,假设CPU1正在为一个IRQ执行do_IRQ()函数,且CPU1已经在相应IRQ描述符的status成员中设置了IRQ_INPROGRESS标志,而此时CPU2上又收到一次同级的中断请求,于是CPU2也执行do_IRQ()函数,但CPU2由于看到相应IRQ描述符的status成员中已经设置了IRQ_INPROGRESS标志,因此CPU2知道另一个CPU(它无需知道是哪个具体的CPU,反正不是他自己:-)已经正在为同级中断请求服务了,于是CPU2就简单地在相应IRQ描述符的status成员中设置一个IRQ_PENDING标志,然后就退出do_IRQ()函数,而把中断服务留给CPU1去执行。当CPU1从handle_IRQ_event()函数返回时,它看到设置了IRQ_PENDING标志,知道其它CPU把中断服务留给他来执行了,于是CPU1先清除IRQ_PENDING标志,然后继续for循环,以便补上一次中断服务。注意!这种机制可能会造成中断服务丢失现象的。
(6)从for循环退出后,CPU对硬件中断请求的服务也就宣告结束,因此,它清除相应IRQ描述符的status成员中的IRQ_INPROGRESS标志。
(7)out部分:接下来就是do_IRQ()函数的退出部分。为此,它首先调用中断控制器描述符中的end()函数。具体到8259A来讲,也就是调用end_8259A_irq()函数,该函数实际上就是通过enable_8259A_irq()函数来重新使能8259A的相应IRQ输入线(因为mask_and_ack_8259A()函数已经将它屏蔽,所以这里必须将其重新开启)。然后解除相应IRQ描述符中自旋锁lock。
(8)最后,在退出do_IRQ()函数之前,看看是否需要执行软中断。如果需要,就调用do_softirq()函数来执行软中断或设备驱动程序的bottom half函数。

5.7.1 函数handle_IRQ_event
函数handle_IRQ_event()用来执行一个中断服务队列中的所有ISR。它的实现相当简单,其核心思想就是用一个do{}while循环来依次执行中断服务队列中的各个ISR,之所以用do{}while循环而不用while循环,是因为:这个函数只被do_IRQ()函数所调用,而do_IRQ()在调用它时已经保证了中断服务队列不为空。该函数的源码如下:
int handle_IRQ_event(unsigned int irq, struct pt_regs * regs, struct irqaction * action)
{
int status;
int cpu = smp_processor_id();

irq_enter(cpu, irq);

status = 1; /* Force the "do bottom halves" bit */

if (!(action->flags & SA_INTERRUPT))
__sti();

do {
status |= action->flags;
action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
__cli();

irq_exit(cpu, irq);

return status;
}
对这个函数请注意三点:
(1)如果中断服务队列中第一个中断服务描述符没有设置SA_INTERRUPT标志,那么整个队列中的ISR都是在CPU开中断的情况下执行的。但在整个队列都行完毕后,handle_IRQ_event()函数又重新用__cli()函数关闭中断。
(2)只要中断服务队列中有一个中断服务描述符中设置了随机标志,那么handle_IRQ_event()函数就将通过add_interrupt_randomness()函数为中断引入一些随机性。但是大多数driver都不使用这个随机标志。
(3)请注意这里对irq_enter()和irq_exit()函数对的使用。

§5.8 中断返回ret_from_intr
从总控程序do_IRQ()返回后,common_interrupt所作的第三件事情就是通过jmp指令跳转到ret_from_intr程序段,以执行中断返回过程。
程序段ret_from_intr定义在arch/i386/kernel/entry.S文件中,如下所示:
ENTRY(ret_from_intr)
GET_CURRENT(%ebx)
movl EFLAGS(%esp),%eax # mix EFLAGS and CS
movb CS(%esp),%al
testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor?
jne ret_with_reschedule
jmp restore_all
(1)首先,通过GET_CURRENT()宏将当前进程的task_struct结构的指针置入寄存器EBX中。
(2)然后,将内核堆栈中的EFLAGS寄存器的值放入EAX寄存器中,EFLAGS(%esp)表示地址为“堆栈指针%esp加上常数EFLAGS”处的内容,也即内核堆栈中的EFLAGS寄存器的值。
(3)将内核堆栈中CS寄存器的值置入al寄存器中。AL寄存器也就是EAX寄存器的低16位。这样,通过这两步也就把EFLAGS寄存器的高16位和CS寄存器的值平凑在一起,放到EAX寄存器中。这样做的目的有两个:
l 检查中断前夕CPU是否运行于VM86模式下。因为EFLAGS寄存器中的bit[16]表示CPU是否运行于VM86模式。
l 检查中断前夕CPU是否运行在用户台下。因为CS寄存器的bit[1:0]表示CPU的运行级别。只要CS的最低两位非0,就说明中断前夕CPU运行在用户台下。
(4)将EAX寄存器与立即数(VM_MASK|3)进行位测试,如果测试结果非0,则说明上述两种情况(VM86模式或用户态)之一发生,于是就跳转到ret_with_reschedule程序段。如果测试结果非0,则说明中断前夕CPU运行在内核态下,于是跳转到restore_all程序段。

5.8.1 返回到用户态
由于Linux不使用VM86模式,因此我们假定中断前夕CPU运行在用户态下。所以就先跳转到ret_with_reschedule程序段去执行。如下所示(entry.S):
ret_with_reschedule:
cmpl $0,need_resched(%ebx)
jne reschedule
cmpl $0,sigpending(%ebx)
jne signal_return
restore_all:
RESTORE_ALL
(1)由于EBX寄存器中的值是当前进程的task_struct结构的地址,因此表达式need_resched(%ebx)也就是当前进程的task_struct结构中的need_resched成员的值。因此第一个语句判断current->need_resched是否为0,如果不为0,表示需要重新调度,于是就跳转到reschedule程序段。该程序段如下所示:
reschedule:
call SYMBOL_NAME(schedule) # test
jmp ret_from_sys_call
(2)判断当前进程的task_struct结构中的sigpending成员是否为0,如果不为0,就表示有信号需要处理,于是跳转到signal_return程序段去处理当前进程的信号。

5.8.2 中断返回的总出口
不管是ret_from_syscall程序段还是signal_return程序段,它们最总都要跳回到restore_all程序段,它是一个总出口。该程序段只有一行代码,也即:执行宏RESTORE_ALL,以恢复内核堆栈到中断前夕的状态。宏RESTORE_ALL的定义如下(entry.S):
#define RESTORE_ALL \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax; \
1: popl %ds; \
2: popl %es; \
addl $4,%esp; \
3: iret;
从上述代码可以看出,它是以和SAVE_ALL相反的顺序从内核堆栈中探出各个寄存器的值,直到ES寄存器,以恢复中断前夕的CPU现场。
由于内核堆栈中ES寄存器之上的是orig_eax,是用来保存IRQ号或syscall号的,而它现在已经没有任何用处,因此通过将内核堆栈指针%esp的加上常数4来简单地将orig_eax的内容从堆栈中丢弃。
最后,CPU执行iret指令,从而使得CPU从中断返回。

至此,整个Linux中断服务流程宣告结束。

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:46 |显示全部楼层
第六章 Linux内核的Softirq机制
(By 詹荣开,NUDT)





Copyright &copy; 2003 by 詹荣开
E-mail:zhanrk@sohu.com
Linux-2.4.0
Version 1.0.0,2003-2-14




摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的Softirq机制。本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:Linux、Softirq、软中断、Bottom half、设备驱动程序

申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。

你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA

欢迎各位指出文档中的错误与疑问。

前言
中断服务程序往往都是在CPU关中断的条件下执行的,以避免中断嵌套而使控制复杂化。但是CPU关中断的时间不能太长,否则容易丢失中断信号。为此,Linux将中断服务程序一分为二,各称作“Top Half”和“Bottom Half”。前者通常对时间要求较为严格,必须在中断请求发生后立即或至少在一定的时间限制内完成。因此为了保证这种处理能原子地完成,Top Half通常是在CPU关中断的条件下执行的。具体地说,Top Half的范围包括:从在IDT中登记的中断入口函数一直到驱动程序注册在中断服务队列中的ISR。而Bottom Half则是Top Half根据需要来调度执行的,这些操作允许延迟到稍后执行,它的时间要求并不严格,因此它通常是在CPU开中断的条件下执行的。
但是,Linux的这种Bottom Half(以下简称BH)机制有两个缺点,也即:(1)在任意一时刻,系统只能有一个CPU可以执行Bottom Half代码,以防止两个或多个CPU同时来执行Bottom Half函数而相互干扰。因此BH代码的执行是严格“串行化”的。(2)BH函数不允许嵌套。
这两个缺点在单CPU系统中是无关紧要的,但在SMP系统中却是非常致命的。因为BH机制的严格串行化执行显然没有充分利用SMP系统的多CPU特点。为此,Linux2.4内核在BH机制的基础上进行了扩展,这就是所谓的“软中断请求”(softirq)机制。

6.1 软中断请求机制
Linux的softirq机制是与SMP紧密不可分的。为此,整个softirq机制的设计与实现中自始自终都贯彻了一个思想:“谁触发,谁执行”(Who marks,Who runs),也即触发软中断的那个CPU负责执行它所触发的软中断,而且每个CPU都由它自己的软中断触发与控制机制。这个设计思想也使得softirq机制充分利用了SMP系统的性能和特点。

6.1.1 软中断描述符
Linux在include/linux/interrupt.h头文件中定义了数据结构softirq_action,来描述一个软中断请求,如下所示:
/* softirq mask and active fields moved to irq_cpustat_t in
* asm/hardirq.h to get better cache usage. KAO
*/
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
其中,函数指针action指向软中断请求的服务函数,而指针data则指向由服务函数自行解释的数据。

基于上述软中断描述符,Linux在kernel/softirq.c文件中定义了一个全局的softirq_vec[32]数组:
static struct softirq_action softirq_vec[32] __cacheline_aligned;
在这里系统一共定义了32个软中断请求描述符。软中断向量i(0≤i≤31)所对应的软中断请求描述符就是softirq_vec[i]。这个数组是个系统全局数组,也即它被所有的CPU所共享。这里需要注意的一点是:每个CPU虽然都由它自己的触发和控制机制,并且只执行他自己所触发的软中断请求,但是各个CPU所执行的软中断服务例程却是相同的,也即都是执行softirq_vec[]数组中定义的软中断服务函数。

6.1.2 软中断触发机制
要实现“谁触发,谁执行”的思想,就必须为每个CPU都定义它自己的触发和控制变量。为此,Linux在include/asm-i386/hardirq.h头文件中定义了数据结构irq_cpustat_t来描述一个CPU的中断统计信息,其中就有用于触发和控制软中断的成员变量。数据结构irq_cpustat_t的定义如下:
/* entry.S is sensitive to the offsets of these fields */
typedef struct {
unsigned int __softirq_active;
unsigned int __softirq_mask;
unsigned int __local_irq_count;
unsigned int __local_bh_count;
unsigned int __syscall_count;
unsigned int __nmi_count; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;
结构中每一个成员都是一个32位的无符号整数。其中__softirq_active和__softirq_mask就是用于触发和控制软中断的成员变量。
①__softirq_active变量:32位的无符号整数,表示软中断向量0~31的状态。如果bit[i](0≤i≤31)为1,则表示软中断向量i在某个CPU上已经被触发而处于active状态;为0表示处于非活跃状态。
②__softirq_mask变量:32位的无符号整数,软中断向量的屏蔽掩码。如果bit[i](0≤i≤31)为1,则表示使能(enable)软中断向量i,为0表示该软中断向量被禁止(disabled)。
根据系统中当前的CPU个数(由宏NR_CPUS表示),Linux在kernel/softirq.c文件中为每个CPU都定义了它自己的中断统计信息结构,如下所示:
/* No separate irq_stat for s390, it is part of PSA */
#if !defined(CONFIG_ARCH_S390)
irq_cpustat_t irq_stat[NR_CPUS];
#endif /* CONFIG_ARCH_S390 */

这样,每个CPU都只操作它自己的中断统计信息结构。假设有一个编号为id的CPU,那么它只能操作它自己的中断统计信息结构irq_stat[id](0≤id≤NR_CPUS-1),从而使各CPU之间互不影响。这个数组在include/linux/irq_cpustat.h头文件中也作了原型声明。

l 触发软中断请求的操作函数
函数__cpu_raise_softirq()用于在编号为cpu的处理器上触发软中断向量nr。它通过将相应的__softirq_active成员变量中的相应位设置为1来实现软中断触发。如下所示(include/linux/interrupt.h):
static inline void __cpu_raise_softirq(int cpu, int nr)
{
softirq_active(cpu) |= (1<<nr);
}
为了保证“原子”性地完成软中断的触发过程,Linux在interrupt.h头文件中对上述内联函数又作了高层封装,也即函数raise_softirq()。该函数向下通过调用__cpu_raise_softirq()函数来实现软中断的触发,但在调用该函数之前,它先通过local_irq_save()函数来关闭当前CPU的中断并保存标志寄存器的内容,如下所示:
/* I do not want to use atomic variables now, so that cli/sti */
static inline void raise_softirq(int nr)
{
unsigned long flags;

local_irq_save(flags);
__cpu_raise_softirq(smp_processor_id(), nr);
local_irq_restore(flags);
}

6.1.3 Linux对软中断的预定义分类
在软中断向量0~31中,Linux内核仅仅使用了软中断向量0~3,其余被留待系统以后扩展。Linux在头文件include/linux/interrupt.h中对软中断向量0~3进行了预定义:
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
frequency threaded job scheduling. For almost all the purposes
tasklets are more than enough. F.e. all serial device BHs et
al. should be converted to tasklets, not to softirqs.
*/
enum
{
HI_SOFTIRQ=0,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
TASKLET_SOFTIRQ
};
其中,软中断向量0(即HI_SOFTIRQ)用于实现高优先级的软中断,如:高优先级的tasklet(将在后面详细描述)。软中断向量1和2则分别用于网络数据的发送与接收。软中断向量3(即TASKLET_SOFTIRQ)则用于实现诸如tasklet这样的一般性软中断。关于tasklet我们将在后面详细描述。NOTE!Linix内核并不鼓励一般用户扩展使用剩余的软中断向量,因为它认为其预定义的软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ已经足够应付绝大多数应用。

6.1.4 软中断机制的初始化
函数softirq_init()完成softirq机制的初始化。该函数由内核启动例程start_kernel()所调用。函数源码如下所示(kernel/softirq.c):
void __init softirq_init()
{
int i;

for (i=0; i<32; i++)
tasklet_init(bh_task_vec+i, bh_action, i);

open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
初始化的过程如下:
(1)先用一个for循环来初始化用于实现BH机制的bh_task_vec[32]数组。这一点我们将在后面详细解释。
(2)调用open_softirq()函数开启使用软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ,并将它们的软中断服务函数指针分别指向tasklet_action()函数和tasklet_hi_action()函数。函数open_softirq()的主要作用是初始化设置软中断请求描述符softirq_vec[nr]。

6.1.5 开启一个指定的软中断向量
函数open_softirq()用于开启一个指定的软中断向量nr,也即适当地初始化软中断向量nr所对应的软中断描述符softirq_vec[nr]。它主要做两件事情:(1)初始化设置软中断向量nr所对应的软中断描述符softirq_vec[nr]。(2)将所有CPU的软中断屏蔽掩码变量__softirq_mask中的对应位设置为1,以使能该软中断向量。该函数的源码如下所示(kernel/softirq.c):
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
unsigned long flags;
int i;

spin_lock_irqsave(&softirq_mask_lock, flags);
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;

for (i=0; i<NR_CPUS; i++)
softirq_mask(i) |= (1<<nr);
spin_unlock_irqrestore(&softirq_mask_lock, flags);
}

6.1.6 软中断服务的执行函数do_softirq()
函数do_softirq()负责执行数组softirq_vec[32]中设置的软中断服务函数。每个CPU都是通过执行这个函数来执行软中断服务的。由于同一个CPU上的软中断服务例程不允许嵌套,因此,do_softirq()函数一开始就检查当前CPU是否已经正出在中断服务中,如果是则do_softirq()函数立即返回。举个例子,假设CPU0正在执行do_softirq()函数,执行过程产生了一个高优先级的硬件中断,于是CPU0转去执行这个高优先级中断所对应的中断服务程序。总所周知,所有的中断服务程序最后都要跳转到do_IRQ()函数并由它来依次执行中断服务队列中的ISR,这里我们假定这个高优先级中断的ISR请求触发了一次软中断,于是do_IRQ()函数在退出之前看到有软中断请求,从而调用do_softirq()函数来服务软中断请求。因此,CPU0再次进入do_softirq()函数(也即do_softirq()函数在CPU0上被重入了)。但是在这一次进入do_softirq()函数时,它马上发现CPU0此前已经处在中断服务状态中了,因此这一次do_softirq()函数立即返回。于是,CPU0回到该开始时的do_softirq()函数继续执行,并为高优先级中断的ISR所触发的软中断请求补上一次服务。从这里可以看出,do_softirq()函数在同一个CPU上的执行是串行的。
函数源码如下(kernel/softirq.c):
asmlinkage void do_softirq()
{
int cpu = smp_processor_id();
__u32 active, mask;

if (in_interrupt())
return;

local_bh_disable();

local_irq_disable();
mask = softirq_mask(cpu);
active = softirq_active(cpu) & mask;

if (active) {
struct softirq_action *h;

restart:
/* Reset active bitmask before enabling irqs */
softirq_active(cpu) &= ~active;

local_irq_enable();

h = softirq_vec;
mask &= ~active;

do {
if (active & 1)
h->action(h);
h++;
active >>= 1;
} while (active);

local_irq_disable();

active = softirq_active(cpu);
if ((active &= mask) != 0)
goto retry;
}

local_bh_enable();

/* Leave with locally disabled hard irqs. It is critical to close
* window for infinite recursion, while we help local bh count,
* it protected us. Now we are defenceless.
*/
return;

retry:
goto restart;
}
结合上述源码,我们可以看出软中断服务的执行过程如下:
(1)调用宏in_interrupt()来检测当前CPU此次是否已经处于中断服务中。该宏定义在hardirq.h,请参见5.7节。
(2)调用local_bh_disable()宏将当前CPU的中断统计信息结构中的__local_bh_count成员变量加1,表示当前CPU已经处在软中断服务状态。
(3)由于接下来要读写当前CPU的中断统计信息结构中的__softirq_active变量和__softirq_mask变量,因此为了保证这一个操作过程的原子性,先用local_irq_disable()宏(实际上就是cli指令)关闭当前CPU的中断。
(4)然后,读当前CPU的__softirq_active变量值和__softirq_mask变量值。当某个软中断向量被触发时(即__softirq_active变量中的相应位被置1),只有__softirq_mask变量中的相应位也为1时,它的软中断服务函数才能得到执行。因此,需要将__softirq_active变量和__softirq_mask变量作一次“与”逻辑操作。
(5)如果active变量非0,说明需要执行软中断服务函数。因此:①先将当前CPU的__softirq_active中的相应位清零,然后用local_irq_enable()宏(实际上就是sti指令)打开当前CPU的中断。②将局部变量mask中的相应位清零,其目的是:让do_softirq()函数的这一次执行不对同一个软中断向量上的再次软中断请求进行服务,而是将它留待下一次do_softirq()执行时去服务,从而使do_sottirq()函数避免陷入无休止的软中断服务中。③用一个do{}while循环来根据active的值去执行相应的软中断服务函数。④由于接下来又要检测当前CPU的__softirq_active变量,因此再一次调用local_irq_disable()宏关闭当前CPU的中断。⑤读取当前CPU的__softirq_active变量的值,并将它与局部变量mask进行与操作,以看看是否又有其他软中断服务被触发了(比如前面所说的那种情形)。如果有的话,那就跳转到entry程序段(实际上是跳转到restart程序段)重新执行软中断服务。如果没有的话,那么此次软中断服务过程就宣告结束。
(6)最后,通过local_bh_enable()宏将当前CPU的__local_bh_count变量值减1,表示当前CPU已经离开软中断服务状态。宏local_bh_enable()也定义在include/asm-i386/softirq.h头文件中。
6.2 tasklet机制
Tasklet机制是一种较为特殊的软中断。Tasklet一词的原意是“小片任务”的意思,这里是指一小段可执行的代码,且通常以函数的形式出现。软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ均是用tasklet机制来实现的。
从某种程度上讲,tasklet机制是Linux内核对BH机制的一种扩展。在2.4内核引入了softirq机制后,原有的BH机制正是通过tasklet机制这个桥梁来纳入softirq机制的整体框架中的。正是由于这种历史的延伸关系,使得tasklet机制与一般意义上的软中断有所不同,而呈现出以下两个显著的特点:
1. 与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,而不像一般的软中断服务函数(即softirq_action结构中的action函数指针)那样——在同一时刻可以被多个CPU并发地执行。
2. 与BH机制不同,不同的tasklet代码在同一时刻可以在多个CPU上并发地执行,而不像BH机制那样必须严格地串行化执行(也即在同一时刻系统中只能有一个CPU执行BH函数)。

6.2.1 tasklet描述符
Linux用数据结构tasklet_struct来描述一个tasklet。该数据结构定义在include/linux/interrupt.h头文件中。如下所示:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
各成员的含义如下:
(1)next指针:指向下一个tasklet的指针。
(2)state:定义了这个tasklet的当前状态。这一个32位的无符号长整数,当前只使用了bit[1]和bit[0]两个状态位。其中,bit[1]=1表示这个tasklet当前正在某个CPU上被执行,它仅对SMP系统才有意义,其作用就是为了防止多个CPU同时执行一个tasklet的情形出现;bit[0]=1表示这个tasklet已经被调度去等待执行了。对这两个状态位的宏定义如下所示(interrupt.h):
enum
{
TASKLET_STATE_SCHED, /* Tasklet is scheduled for execution */
TASKLET_STATE_RUN /* Tasklet is running (SMP only) */
};
(3)原子计数count:对这个tasklet的引用计数值。NOTE!只有当count等于0时,tasklet代码段才能执行,也即此时tasklet是被使能的;如果count非零,则这个tasklet是被禁止的。任何想要执行一个tasklet代码段的人都首先必须先检查其count成员是否为0。
(4)函数指针func:指向以函数形式表现的可执行tasklet代码段。
(5)data:函数func的参数。这是一个32位的无符号整数,其具体含义可供func函数自行解释,比如将其解释成一个指向某个用户自定义数据结构的地址值。

Linux在interrupt.h头文件中又定义了两个用来定义tasklet_struct结构变量的辅助宏:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
显然,从上述源代码可以看出,用DECLARE_TASKLET宏定义的tasklet在初始化时是被使能的(enabled),因为其count成员为0。而用DECLARE_TASKLET_DISABLED宏定义的tasklet在初始时是被禁止的(disabled),因为其count等于1。

6.2.2 改变一个tasklet状态的操作
在这里,tasklet状态指两个方面:(1)state成员所表示的运行状态;(2)count成员决定的使能/禁止状态。
(1)改变一个tasklet的运行状态
state成员中的bit[0]表示一个tasklet是否已被调度去等待执行,bit[1]表示一个tasklet是否正在某个CPU上执行。对于state变量中某位的改变必须是一个原子操作,因此可以用定义在include/asm/bitops.h头文件中的位操作来进行。
由于bit[1]这一位(即TASKLET_STATE_RUN)仅仅对于SMP系统才有意义,因此Linux在Interrupt.h头文件中显示地定义了对TASKLET_STATE_RUN位的操作。如下所示:
#ifdef CONFIG_SMP
#define tasklet_trylock(t) (!test_and_set_bit(TASKLET_STATE_RUN, &(t)->state))
#define tasklet_unlock_wait(t) while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { /* NOTHING */ }
#define tasklet_unlock(t) clear_bit(TASKLET_STATE_RUN, &(t)->state)
#else
#define tasklet_trylock(t) 1
#define tasklet_unlock_wait(t) do { } while (0)
#define tasklet_unlock(t) do { } while (0)
#endif
显然,在SMP系统同,tasklet_trylock()宏将把一个tasklet_struct结构变量中的state成员中的bit[1]位设置成1,同时还返回bit[1]位的非。因此,如果bit[1]位原有值为1(表示另外一个CPU正在执行这个tasklet代码),那么tasklet_trylock()宏将返回值0,也就表示上锁不成功。如果bit[1]位的原有值为0,那么tasklet_trylock()宏将返回值1,表示加锁成功。而在单CPU系统中,tasklet_trylock()宏总是返回为1。
任何想要执行某个tasklet代码的程序都必须首先调用宏tasklet_trylock()来试图对这个tasklet进行上锁(即设置TASKLET_STATE_RUN位),且只能在上锁成功的情况下才能执行这个tasklet。建议!即使你的程序只在CPU系统上运行,你也要在执行tasklet之前调用tasklet_trylock()宏,以便使你的代码获得良好可移植性。
在SMP系统中,tasklet_unlock_wait()宏将一直不停地测试TASKLET_STATE_RUN位的值,直到该位的值变为0(即一直等待到解锁),假如:CPU0正在执行tasklet A的代码,在此期间,CPU1也想执行tasklet A的代码,但CPU1发现tasklet A的TASKLET_STATE_RUN位为1,于是它就可以通过tasklet_unlock_wait()宏等待tasklet A被解锁(也即TASKLET_STATE_RUN位被清零)。在单CPU系统中,这是一个空操作。
宏tasklet_unlock()用来对一个tasklet进行解锁操作,也即将TASKLET_STATE_RUN位清零。在单CPU系统中,这是一个空操作。

(2)使能/禁止一个tasklet
使能与禁止操作往往总是成对地被调用的,tasklet_disable()函数如下(interrupt.h):
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
}
函数tasklet_disable_nosync()也是一个静态inline函数,它简单地通过原子操作将count成员变量的值减1。如下所示(interrupt.h):
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->count);
}
函数tasklet_enable()用于使能一个tasklet,如下所示(interrupt.h):
static inline void tasklet_enable(struct tasklet_struct *t)
{
atomic_dec(&t->count);
}

6.2.3 tasklet描述符的初始化与杀死
函数tasklet_init()用来初始化一个指定的tasklet描述符,其源码如下所示(kernel/softirq.c):
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->func = func;
t->data = data;
t->state = 0;
atomic_set(&t->count, 0);
}

函数tasklet_kill()用来将一个已经被调度了的tasklet杀死,即将其恢复到未调度的状态。其源码如下所示(kernel/softirq.c):
void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
printk("Attempt to kill tasklet from interrupt\n");

while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
current->state = TASK_RUNNING;
do {
current->policy |= SCHED_YIELD;
schedule();
} while (test_bit(TASKLET_STATE_SCHED, &t->state));
}
tasklet_unlock_wait(t);
clear_bit(TASKLET_STATE_SCHED, &t->state);
}

6.2.4 tasklet对列
多个tasklet可以通过tasklet描述符中的next成员指针链接成一个单向对列。为此,Linux专门在头文件include/linux/interrupt.h中定义了数据结构tasklet_head来描述一个tasklet对列的头部指针。如下所示:
struct tasklet_head
{
struct tasklet_struct *list;
} __attribute__ ((__aligned__(SMP_CACHE_BYTES)));
尽管tasklet机制是特定于软中断向量HI_SOFTIRQ和TASKLET_SOFTIRQ的一种实现,但是tasklet机制仍然属于softirq机制的整体框架范围内的,因此,它的设计与实现仍然必须坚持“谁触发,谁执行”的思想。为此,Linux为系统中的每一个CPU都定义了一个tasklet对列头部,来表示应该有各个CPU负责执行的tasklet对列。如下所示(kernel/softirq.c):
struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned;
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
其中,tasklet_vec[]数组用于软中断向量TASKLET_SOFTIRQ,而tasklet_hi_vec[]数组则用于软中断向量HI_SOFTIRQ。也即,如果CPUi(0≤i≤NR_CPUS-1)触发了软中断向量TASKLET_SOFTIRQ,那么对列tasklet_vec[i]中的每一个tasklet都将在CPUi服务于软中断向量TASKLET_SOFTIRQ时被CPUi所执行。同样地,如果CPUi(0≤i≤NR_CPUS-1)触发了软中断向量HI_SOFTIRQ,那么队列tasklet_vec[i]中的每一个tasklet都将CPUi在对软中断向量HI_SOFTIRQ进行服务时被CPUi所执行。
队列tasklet_vec[I]和tasklet_hi_vec[I]中的各个tasklet是怎样被所CPUi所执行的呢?其关键就是软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ的软中断服务程序——tasklet_action()函数和tasklet_hi_action()函数。下面我们就来分析这两个函数。

6.2.5 软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ
Linux为软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ实现了专用的触发函数和软中断服务函数。其中,tasklet_schedule()函数和tasklet_hi_schedule()函数分别用来在当前CPU上触发软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ,并把指定的tasklet加入当前CPU所对应的tasklet队列中去等待执行。而tasklet_action()函数和tasklet_hi_action()函数则分别是软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ的软中断服务函数。在初始化函数softirq_init()中,这两个软中断向量对应的描述符softirq_vec[0]和softirq_vec[3]中的action函数指针就被分别初始化成指向函数tasklet_hi_action()和函数tasklet_action()。

(1)软中断向量TASKLET_SOFTIRQ的触发函数tasklet_schedule()
该函数实现在include/linux/interrupt.h头文件中,是一个inline函数。其源码如下所示:
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
int cpu = smp_processor_id();
unsigned long flags;

local_irq_save(flags);
t->next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
}
该函数的参数t指向要在当前CPU上被执行的tasklet。对该函数的NOTE如下:
①调用test_and_set_bit()函数将待调度的tasklet的state成员变量的bit[0]位(也即TASKLET_STATE_SCHED位)设置为1,该函数同时还返回TASKLET_STATE_SCHED位的原有值。因此如果bit[0]为的原有值已经为1,那就说明这个tasklet已经被调度到另一个CPU上去等待执行了。由于一个tasklet在某一个时刻只能由一个CPU来执行,因此tasklet_schedule()函数什么也不做就直接返回了。否则,就继续下面的调度操作。
②首先,调用local_irq_save()函数来关闭当前CPU的中断,以保证下面的步骤在当前CPU上原子地被执行。
③然后,将待调度的tasklet添加到当前CPU对应的tasklet队列的首部。
④接着,调用__cpu_raise_softirq()函数在当前CPU上触发软中断请求TASKLET_SOFTIRQ。
⑤最后,调用local_irq_restore()函数来开当前CPU的中断。

(2)软中断向量TASKLET_SOFTIRQ的服务程序tasklet_action()
函数tasklet_action()是tasklet机制与软中断向量TASKLET_SOFTIRQ的联系纽带。正是该函数将当前CPU的tasklet队列中的各个tasklet放到当前CPU上来执行的。该函数实现在kernel/softirq.c文件中,其源代码如下:
static void tasklet_action(struct softirq_action *a)
{
int cpu = smp_processor_id();
struct tasklet_struct *list;

local_irq_disable();
list = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = NULL;
local_irq_enable();

while (list != NULL) {
struct tasklet_struct *t = list;

list = list->next;

if (tasklet_trylock(t)) {
if (atomic_read(&t->count) == 0) {
clear_bit(TASKLET_STATE_SCHED, &t->state);

t->func(t->data);
/*
* talklet_trylock() uses test_and_set_bit that imply
* an mb when it returns zero, thus we need the explicit
* mb only here: while closing the critical section.
*/
#ifdef CONFIG_SMP
smp_mb__before_clear_bit();
#endif
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = tasklet_vec[cpu].list;
tasklet_vec[cpu].list = t;
__cpu_raise_softirq(cpu, TASKLET_SOFTIRQ);
local_irq_enable();
}
}
注释如下:
①首先,在当前CPU关中断的情况下,“原子”地读取当前CPU的tasklet队列头部指针,将其保存到局部变量list指针中,然后将当前CPU的tasklet队列头部指针设置为NULL,以表示理论上当前CPU将不再有tasklet需要执行(但最后的实际结果却并不一定如此,下面将会看到)。
②然后,用一个while{}循环来遍历由list所指向的tasklet队列,队列中的各个元素就是将在当前CPU上执行的tasklet。循环体的执行步骤如下:
l 用指针t来表示当前队列元素,即当前需要执行的tasklet。
l 更新list指针为list->next,使它指向下一个要执行的tasklet。
l 用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁,如果加锁成功(当前没有任何其他CPU正在执行这个tasklet),则用原子读函数atomic_read()进一步判断count成员的值。如果count为0,说明这个tasklet是允许执行的,于是:(1)先清除TASKLET_STATE_SCHED位;(2)然后,调用这个tasklet的可执行函数func;(3)执行barrier()操作;(4)调用宏tasklet_unlock()来清除TASKLET_STATE_RUN位。(5)最后,执行continue语句跳过下面的步骤,回到while循环继续遍历队列中的下一个元素。如果count不为0,说明这个tasklet是禁止运行的,于是调用tasklet_unlock()清除前面用tasklet_trylock()设置的TASKLET_STATE_RUN位。
l 如果tasklet_trylock()加锁不成功,或者因为当前tasklet的count值非0而不允许执行时,我们必须将这个tasklet重新放回到当前CPU的tasklet队列中,以留待这个CPU下次服务软中断向量TASKLET_SOFTIRQ时再执行。为此进行这样几步操作:(1)先关CPU中断,以保证下面操作的原子性。(2)把这个tasklet重新放回到当前CPU的tasklet队列的首部;(3)调用__cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求TASKLET_SOFTIRQ;(4)开中断。
l 最后,回到while循环继续遍历队列。

(3)软中断向量HI_SOFTIRQ的触发函数tasklet_hi_schedule()
该函数与tasklet_schedule()几乎相同,其源码如下(include/linux/interrupt.h):
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
int cpu = smp_processor_id();
unsigned long flags;

local_irq_save(flags);
t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_restore(flags);
}
}

(4)软中断向量HI_SOFTIRQ的服务函数tasklet_hi_action()
该函数与tasklet_action()函数几乎相同,其源码如下(kernel/softirq.c):
static void tasklet_hi_action(struct softirq_action *a)
{
int cpu = smp_processor_id();
struct tasklet_struct *list;

local_irq_disable();
list = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = NULL;
local_irq_enable();

while (list != NULL) {
struct tasklet_struct *t = list;

list = list->next;

if (tasklet_trylock(t)) {
if (atomic_read(&t->count) == 0) {
clear_bit(TASKLET_STATE_SCHED, &t->state);

t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
__cpu_raise_softirq(cpu, HI_SOFTIRQ);
local_irq_enable();
}
}

.3 Bottom Half机制
Bottom Half机制在新的softirq机制中被保留下来,并作为softirq框架的一部分。其实现也似乎更为复杂些,因为它是通过tasklet机制这个中介桥梁来纳入softirq框架中的。实际上,软中断向量HI_SOFTIRQ是内核专用于执行BH函数的。

6.3.1 数据结构的定义
原有的32个BH函数指针被保留,定义在kernel/softirq.c文件中:
static void (*bh_base[32])(void);

但是,每个BH函数都对应有一个tasklet,并由tasklet的可执行函数func来负责调用相应的bh函数(func函数的参数指定调用哪一个BH函数)。与32个BH函数指针相对应的tasklet的定义如下所示(kernel/softirq.c):
struct tasklet_struct bh_task_vec[32];

上述tasklet数组使系统全局的,它对所有的CPU均可见。由于在某一个时刻只能有一个CPU在执行BH函数,因此定义一个全局的自旋锁来保护BH函数,如下所示(kernel/softirq.c):
spinlock_t global_bh_lock = SPIN_LOCK_UNLOCKED;

6.3.2 初始化
在softirq机制的初始化函数softirq_init()中将bh_task_vec[32]数组中的每一个tasklet中的func函数指针都设置为指向同一个函数bh_action,而data成员(也即func函数的调用参数)则被设置成该tasklet在数组中的索引值,如下所示:
void __init softirq_init()
{
……
for (i=0; i<32; i++)
tasklet_init(bh_task_vec+i, bh_action, i);
……
}
因此,bh_action()函数将负责相应地调用参数所指定的bh函数。该函数是连接tasklet机制与Bottom Half机制的关键所在。

6.2.3 bh_action()函数
该函数的源码如下(kernel/softirq.c):
static void bh_action(unsigned long nr)
{
int cpu = smp_processor_id();

if (!spin_trylock(&global_bh_lock))
goto resched;

if (!hardirq_trylock(cpu))
goto resched_unlock;

if (bh_base[nr])
bh_base[nr]();

hardirq_endlock(cpu);
spin_unlock(&global_bh_lock);
return;

resched_unlock:
spin_unlock(&global_bh_lock);
resched:
mark_bh(nr);
}
对该函数的注释如下:
①首先,调用spin_trylock()函数试图对自旋锁global_bh_lock进行加锁,同时该函数还将返回自旋锁global_bh_lock的原有值的非。因此,如果global_bh_lock已被某个CPU上锁而为非0值(那个CPU肯定在执行某个BH函数),那么spin_trylock()将返回为0表示上锁失败,在这种情况下,当前CPU是不能执行BH函数的,因为另一个CPU正在执行BH函数,于是执行goto语句跳转到resched程序段,以便在当前CPU上再一次调度该BH函数。
②调用hardirq_trylock()函数锁定当前CPU,确保当前CPU不是处于硬件中断请求服务中,如果锁定失败,跳转到resched_unlock程序段,以便先对global_bh_lock解锁,在重新调度一次该BH函数。
③此时,我们已经可以放心地在当前CPU上执行BH函数了。当然,对应的BH函数指针bh_base[nr]必须有效才行。
④从BH函数返回后,先调用hardirq_endlock()函数(实际上它什么也不干,调用它只是为了保此加、解锁的成对关系),然后解除自旋锁global_bh_lock,最后函数就可以返回了。
⑤resched_unlock程序段:先解除自旋锁global_bh_lock,然后执行reched程序段。
⑥resched程序段:当某个CPU正在执行BH函数时,当前CPU就不能通过bh_action()函数来调用执行任何BH函数,所以就通过调用mark_bh()函数在当前CPU上再重新调度一次,以便将这个BH函数留待下次软中断服务时执行。

6.3.4 Bottom Half的原有接口函数
(1)init_bh()函数
该函数用来在bh_base[]数组登记一个指定的bh函数,如下所示(kernel/softirq.c):
void init_bh(int nr, void (*routine)(void))
{
bh_base[nr] = routine;
mb();
}

(2)remove_bh()函数
该函数用来在bh_base[]数组中注销指定的函数指针,同时将相对应的tasklet杀掉。如下所示(kernel/softirq.c):
void remove_bh(int nr)
{
tasklet_kill(bh_task_vec+nr);
bh_base[nr] = NULL;
}

(3)mark_bh()函数
该函数用来向当前CPU标记由一个BH函数等待去执行。它实际上通过调用tasklet_hi_schedule()函数将相应的tasklet加入到当前CPU的tasklet队列tasklet_hi_vec[cpu]中,然后触发软中断请求HI_SOFTIRQ,如下所示(include/linux/interrupt.h):
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}

6.3.5 预定义的BH函数
在32个BH函数指针中,大多数已经固定用于一些常见的外设,比如:第0个BH函数就固定地用于时钟中断。Linux在头文件include/linux/interrupt.h中定义了这些已经被使用的BH函数所引,如下所示:
enum {
TIMER_BH = 0,
TQUEUE_BH,
DIGI_BH,
SERIAL_BH,
RISCOM8_BH,
SPECIALIX_BH,
AURORA_BH,
ESP_BH,
SCSI_BH,
IMMEDIATE_BH,
CYCLADES_BH,
CM206_BH,
JS_BH,
MACSERIAL_BH,
ISICOM_BH
};

6.4 任务队列Task Queue
任务队列是与Bottom Half机制紧密相连的。因为Bottom Half机制只有有限的32个函数指针,而且大部分都已被系统预定义使用,所以早期版本的Linux内核为了扩展Bottom Half机制,就设计了任务队列机制。
所谓任务队列就是指以双向队列形式连接起来的任务链表,每一个链表元数都描述了一个可执行的任务(以函数的形式表现)。如下图所示:

任务队列机制实现在include/linux/tqueue.h头文件中。

6.4.1 数据结构的定义
Linux用数据结构tq_struct来描述任务队列中的每一个链表元数(即一个可执行的任务):
struct tq_struct {
struct list_head list; /* linked list of active bh's */
unsigned long sync; /* must be initialized to zero */
void (*routine)(void *); /* function to call */
void *data; /* argument to function */
};
这个数据结构很简单,在此就不详述。
然后,Linux定义了数据结构task_queue来描述任务队列的头部,其实task_queue就是结构类型list_head,如下:
typedef struct list_head task_queue;

但是Linux又定义了一个宏DECLARE_TASK_QUEUE()来辅助我们更方便地定义任务队列的链表表头:
#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q)

一个任务队列是否处于active状态主要取决于其链表表头(即task_queue结构)是否为空,因此Linux定义宏TQ_ACTIVE()来判断一个任务队列是否有效:
#define TQ_ACTIVE(q) (!list_empty(&q))
显然,只要任务队列表头q不为空,该任务队列就是有效的。

6.4.2 向任务队列中插入一个新任务
(1)保护自旋锁
由于任务队列是系统全局的共享资源,所以面临竞争的问题。为了实现对任务队列链表的互斥访问,Linux在kernel/timer.c文件中定义了一个任务队列保护自旋锁tqueue_lock,如下:
spinlock_t tqueue_lock = SPIN_LOCK_UNLOCKED;
该自旋锁在tqueue.h头文件中也有原型声明:
extern spinlock_t tqueue_lock;
任何想要访问任务队列的代码都首先必须先持有该自旋锁。

(2)queue_task()函数
实现在tqueue.h头文件中的内联函数queue_task()用来将一个指定的任务添加到某指定的任务队列的尾部,如下:
/*
* Queue a task on a tq. Return non-zero if it was successfully
* added.
*/
static inline int queue_task(struct tq_struct *bh_pointer, task_queue *bh_list)
{
int ret = 0;
if (!test_and_set_bit(0,&bh_pointer->sync)) {
unsigned long flags;
spin_lock_irqsave(&tqueue_lock, flags);
list_add_tail(&bh_pointer->list, bh_list);
spin_unlock_irqrestore(&tqueue_lock, flags);
ret = 1;
}
return ret;
}

6.4.3 运行任务队列
函数run_task_queue()用于实现指定的任务队列。它只有一个参数:指针list——指向待运行的任务队列头部task_queue结构变量。该函数实现在tqueue.h头文件中:
static inline void run_task_queue(task_queue *list)
{
if (TQ_ACTIVE(*list))
__run_task_queue(list);
}
显然,函数首先调用宏TQ_ACTIVE()来判断参数list指定的待运行任务队列是否为空。如果不为空,则调用__run_task_queue()函数来实际运行这个有效的任务队列。
函数__run_task_queue()实现在kernel/softirq.c文件中。该函数将依次遍历任务队列中的每一个元数,并调用执行每一个元数的可执行函数。其源码如下:
void __run_task_queue(task_queue *list)
{
struct list_head head, *next;
unsigned long flags;

spin_lock_irqsave(&tqueue_lock, flags);
list_add(&head, list);
list_del_init(list);
spin_unlock_irqrestore(&tqueue_lock, flags);

next = head.next;
while (next != &head) {
void (*f) (void *);
struct tq_struct *p;
void *data;

p = list_entry(next, struct tq_struct, list);
next = next->next;
f = p->routine;
data = p->data;
wmb();
p->sync = 0;
if (f)
f(data);
}
}
对该函数的注释如下:
(1)首先,用一个局部的表头head来代替参数list所指向的表头。这是因为:在__run_task_queue()函数的运行期间可能还会有新的任务加入到list任务队列中来,但是__run_task_queue()函数显然不想陷入无休止的不断增加的任务处理中,因此它用局部的表头head来代替参数list所指向的表头,以使要执行的任务个数固定化。为此:①先对全局的自旋锁tqueue_lock进行加锁,以实现对任务队列的互斥访问;②将局部的表头head加在表头(*list)和第一个元数之间。③将(*list)表头从队列中去除,并将其初始化为空。④解除自旋锁tqueue_lock。
(2)接下来,用一个while循环来遍历整个队列head,并调用执行每一个队列元素中的函数。注意!任务队列是一个双向循环队列。

6.4.4 内核预定义的任务队列
Bottom Half机制与任务队列是紧密相连的。大多数BH函数都是通过调用run_task_queue()函数来执行某个预定义好的任务队列。最常见的内核预定义任务队列有:
l tq_timer:对应于TQUEUE_BH。
l tq_immediate:对应于IMMEDIATE_BH。
l tq_disk:用于块设备任务。

任务队列tq_timer和tq_immediate都定义在kernel/timer.c文件中,如下所示:
DECLARE_TASK_QUEUE(tq_timer);
DECLARE_TASK_QUEUE(tq_immediate);

BH向量TQUEUE_BH和IMMEDIATE_BH的BH函数分别是:queue_bh()函数和immediate_bh()函数,它们都仅仅是简单地调用run_task_queue()函数来分别运行任务队列tq_timer和tq_immediate,如下所示(kernel/timer.c):
void tqueue_bh(void)
{
run_task_queue(&tq_timer);
}

void immediate_bh(void)
{
run_task_queue(&tq_immediate);
}

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:47 |显示全部楼层
The Guide of Upgrading to linux kernel 2.6.x

Author: Rongkai Zhan
date: 2004-3-14

前言: linux-2.6.x内核发布后, 坛子里的兄弟们肯定都迫不及待地要升级原有的2.4.x内核. 但是由于与2.6.x内核配套的系统程序并没有相应的来得及更新, 升级过程会碰到很多问题, 因此觉得有必要把我的经验与大家share一下:-) XD们以后碰到类似问题时, 请先阅读本文, 而不要一上来就盲目发帖.

ps, you are welcome to fix me.

1. 备份重要的数据

这是个好习惯,就不用多说了:-)

2. 下载最新的2.6.x内核源码包

这个当然要到官方网站www.kernel.org或者它的mirror站点去下载了, 最好别到那些unknown的网站去下, 因为那些源码包有可能被恶意做了手脚. 最新的linux-2.6.4.tar.bz2源码包大概有33M左右. 下载的时候可以泡杯coffee, 然后点根烟......

在/usr/src/目录下解开源码包, 得到源代码目录/usr/src/linux-2.6.4/. 在对kernel做任何事情之前, 建议你先看一下它的README文件和Documentation/Changes文件, 从中你会得到不少有用的信息. 这些信息会让你在随后的升级过程避免很多不必要的麻烦.

在文档Documentation/Changes中给出了欲成功地升级到kernel-2.6.x所需的最小系统软件要求, 比如对于kernel-2.6.4是这样的:

o Gnu C 2.95.3 # gcc --version
o Gnu make 3.79.1 # make --version
o binutils 2.12 # ld -v
o util-linux 2.10o # fdformat --version
o module-init-tools 0.9.10 # depmod -V
o e2fsprogs 1.29 # tune2fs
o jfsutils 1.1.3 # fsck.jfs -V
o reiserfsprogs 3.6.3 # reiserfsck -V 2>&1|grep reiserfsprogs
o xfsprogs 2.6.0 # xfs_db -V
o pcmcia-cs 3.1.21 # cardmgr -V
o quota-tools 3.09 # quota -V
o PPP 2.4.0 # pppd --version
o isdn4k-utils 3.1pre1 # isdnctrl 2>&1|grep version
o nfs-utils 1.0.5 # showmount --version
o procps 3.2.0 # ps --version
o oprofile 0.5.3 # oprofiled --version

对于RH8/9的用户而言, 上述要求基本上都能满足. 大概只有两个程序需要更新: modutils工具包和mkinitrd程序.

3. 安装module-init-tools-3.0.tar.gz工具包

linux-2.6.x内核的module处理过程有所改变(很多原先在用户态下由modutils处理的工作都放到内核里去完成了), 因此2.4.x下的modutils工具包已经不在适合新的2.6.x内核, 必需将其升级到module-init-tools工具包. 该工具包的最新版本是3.0(ps, 该工具包的版本从0.9.15一下跳到3.0, 其跨度之大...hoho). 可以下载module-init-tools-3.0.tar.gz源码包的地方有两个:

o Rusty's Linux Kernel Page: http://www.kernel.org/pub/linux/ ... it-tools-3.0.tar.gz
o http://www.kernel.org/pub/linux/ ... it-tools-3.0.tar.gz

接下来, 按照下列步骤安装module-init-tools工具包:

# configure --prefix=/
# make moveold
# make all install
# ./generate-modprobe.conf /etc/modprobe.conf

命令"make moveold"将把系统原来的modutils工具程序改名为"*.old"(比如,lsmod.old等等). NOTE! 这是非常重要的一步, 千万不要省略. 这将使得你可以继续使用原有的linux-2.4.x系统, 因为在2.4.x系统下, 新的module-init-tools工具包实际上是倚赖原来"*.old"程序来加载内核模块. 如果忘记了这一步也不要紧张, 可以先下载并安装原来的modutils程序包, 然后按照上面的步骤重来一遍就可以了.

新的module-init-tools工具包不再使用原来的/etc/modules.conf配置文件了, 而是使用新的配置文件/etc/modprobe.conf. 因此必需用命令"./generate-modprobe.conf /etc/modprobe.conf"来生成新的配置文件/etc/modprobe.conf. 但是令人不爽的是, 这个生成的新配置文件存在BUG, 下面我们将会提到.

4. 更新mkinitrd程序

可以到站点http://people.redhat.com/arjanv/2.6/RPMS.kernel/去下载最新的mkinitrd-3.5.15.1-2.i386.rpm包. 由于mkinitrd程序包依赖于device-mapper包, 而device-mapper包又倚赖于lvm2包, 因此你还要同时下载device-mapper-1.00.07-1.i386.rpm包和lvm2-2.00.08-2.i386.rpm包.

5. 配置,编译和安装linux-2.6.x内核

新内核在Makefile方面的改动很大, "make menuconfig"和"make xconfig"继续存在, 但是"make xconfig"被改写了, 不再使用原来的Tcl/Tk语言, 而是基于QT库了. 新增加了"make gconfig", 它是基于GTK库的. "make xconfig"和"make gconfig"比原来更强大而且更易于使用了. 但不幸的是, 它们好像还不太稳定......因此, 还是使用我最喜欢的"make menuconfig"吧:-)

关于新内核的配置想提几点:

o 关于"Loadable Module support"选项, 一定要选上"Module unloading"和"Automatic kernel module loading"这两个选项.
o 关于"Processor type and features"选项, 一定要选上"Preemptible Kernel"选项, 这是2.6.x内核优于2.4.x内核的重要原因之一.
o 关于"Block Devices"的配置, 一定要选上对loopback和ramdisk的支持.
o 关于"Multi-device support(RAID and LVM)", 要选上"device mapper".
o 关于"Input device support", 记得要选上"Keyboards"和"Mice"的支持.
o 关于"Graphics support", 要选上对frame buffer的支持.
o 关于ALSA, 要选上"OSS API emulation"选项. OSS可以不用了.
o 关于USB, 能选的都选上吧:-)
o 关于文件系统, 选上常用的那几种就可以了. 如果要体验sysfs的话, 要选上对sysfs的支持.

配制好内核后, 就可以直接编译了, "make dep"这一步已经显示地需要了.

# make bzImage
# make modules
# make modules_install
# make install

上面的命令"make install"将: (1)把压缩内核映象拷贝到/boot目录下, 并创建相应的System.map符号链接; (2)修改bootloader的配置文件; (3)调用mkinitrd程序创建内核的initrd映象. 对于GRUB而言, 将在/boot/grub/grub.conf配置文件增加如下类似的配置行:

title Red Hat Linux (2.6.4)
root(hd0, 1)
kernel /boot/vmlinuz-2.6.4 ro root=LABEL=/
initrd /boot/initrd-2.6.4.img

不幸的是, 新内核并不识别"root=LABEL=/"这个启动参数. 如果现在你就重新启动新内核的话, 将发生kernel panic错误. 因此, 必需把这个"root=LABEL=/"启动参数去掉, 就OK了.

到这里, 你已经可以重启机器, 用kernel-2.6.4启动了. 当然, 多半你不会成功, 不是没鼠标就是没键盘:-)

6. 支持Hotplug

当然, 首先要在内核配置时配置了HOTPLUG的支持. 然后, 由于/proc/ksyms改名为/proc/kallsyms, 所以必需修改启动脚本/etc/rc.sysinit. 为了兼容原有的linux-2.4.x系统, 可以加上下面几行:

#
# ----- KERNEL 2.6.x support ------
# This is for compatibility between kernel-2.4.x and kernel-2.6.x
#
UNAME=`uname -r`
KERNELVER=${UNAME:0:3}
if [ "$KERNELVER" = "2.6" ]; then
#
# This is kernel-2.6.x
#
KSYMS=/proc/kallsyms
else
#
# This is kernel-2.4.x
#
KSYMS=/proc/ksyms
fi

然后, 把rc.sysinit文件中所有出现/proc/ksyms的地方都换成"$KSYMS"变量引用. 配置好后, 还必需下载hotplug的用户态辅助工具包:http://www.kernel.org/pub/linux/ ... -2004_03_11.tar.gz. 解开这个包以后, 直接"make install"就可以了. 它将产生/etc/hotplug/配置目录.

7. 体验sysfs

首先, 创建目录/sys:

# mkdir /sys

然后, 按照下列步骤修改启动脚本/etc/rc.sysinit文件:

o 找到行: mount -f /proc", 在下面增加:

if [ "$KERNELVER" = "2.6" ]; then
mount -f /sys
fi

o 找到行: action $"Mounting proc filesystem: " mount -n -t proc /proc /proc, 在其下面增加这样一行:

#
# Mount /sys for kernel-2.6.x
#
if [ "$KERNELVER" = "2.6" ]; then
action $"Mounting sysfs filesystem: " mount -n -t sysfs /sys /sys
fi

接下来, 在/etc/fstab文件中, 加入这样一行:

none /sys sysfs defaults 0 0

最后, 修改/etc/init.d/halt脚本中的halt_get_remaining函数:

将:
awk '$2 ~ /^\/$|^\/proc|^\/dev/{next}
改为:
awk '$2 ~ /^\/$|^\/proc|^\/sys|^\/dev/{next}

8. 支持USB设备

linux-2.6.x的USB驱动模块的名字已经改变了, 所以由此引起的问题多多......, 对应于USB-2.0的host控制器的内核模块名字仍然是ehci-hcd, 对应USB-1.1的host控制器的内核模块名字已经从usb-ohci改为ochi-hcd, 对应于通用USB host控制器的内核模块名字已经从usb-uhci该为uhci-hcd.

然而不幸的是, 在安装module-init-tools程序包时生成的/etc/modprobe.conf配置文件却仍然使用usb-ohci这个名字. 例如:

alias usb-controller usb-ohci
alias usb-controller1 ehci-hcd

因此, 要把它改为:

alias usb-controller ohci-hcd
alias usb-controller1 ehci-hcd

如果不做这样的修改, 那么使用USB-1.1 host控制器的机器在启动内核的时候将着不到相应的驱动模块.

支持USB键盘的模块名字也从keybdev变为usbkbd, 支持USB鼠标的模块名字也从mousedev改为usbmouse. 因此, 我们必需修改启动脚本/etc/rc.sysinit文件. 同时为了兼容原有的2.4.x系统, 我们在脚本的一开始定义两个变量:

#
# ----- KERNEL 2.6.x support ------
# This is for compatibility between kernel-2.4.x and kernel-2.6.x
#
UNAME=`uname -r`
KERNELVER=${UNAME:0:3}
if [ "$KERNELVER" = "2.6" ]; then
#
# This is kernel-2.6.x
#
KSYMS=/proc/kallsyms
KEYBDEV_NAME=usbkbd
MOUSEDEV_NAME=usbmouse
else
#
# This is kernel-2.4.x
#
KSYMS=/proc/ksyms
KEYBDEV_NAME=keybdev
MOUSEDEV_NAME=mousedev
fi

然后, 把/etc/rc.sysinit脚本文件中出现keybdev和mousedev的地方都改为$KEYBDEV_NAME和$MOUSEDEV_NAME. 把脚本文件/etc/rc.sysinit中的needusbstorage部分从:

needusbstorage=
if [ $usb = "1" ]; then
needusbstorage=`LC_ALL=C grep -e "^I.*Cls=08" /proc/bus/usb/devices 2>/dev/null`
LC_ALL=C grep 'hid' /proc/bus/usb/drivers || action $"Initializing USB HID interface: " modprobe hid 2> /dev/null

action $"Initializing USB keyboard: " modprobe $KEYBDEV_NAME 2> /dev/null
action $"Initializing USB mouse: " modprobe $MOUSEDEV_NAME 2> /dev/null
fi

改为:

needusbstorage=
if [ $usb = "1" ]; then
if [ "$KERNELVER" = "2.6" ]; then
needusbstorage=`LC_ALL=C grep -e "^I.*Cls=08" /sys/bus/usb/devices 2>/dev/null`
LC_ALL=C grep 'hid' /sys/bus/usb/drivers || action $"Initializing USB HID interface: " modprobe hid 2> /dev/null
else
needusbstorage=`LC_ALL=C grep -e "^I.*Cls=08" /proc/bus/usb/devices 2>/dev/null`
LC_ALL=C grep 'hid' /proc/bus/usb/drivers || action $"Initializing USB HID interface: " modprobe hid 2> /dev/null
fi
action $"Initializing USB keyboard: " modprobe $KEYBDEV_NAME 2> /dev/null
action $"Initializing USB mouse: " modprobe $MOUSEDEV_NAME 2> /dev/null
fi

好了, 到此为止, 我们应该可以重新启动机器了, 赶快去体验最新的kernel-2.6.4吧:-)

9. 其它

o 关于在linux-2.6.x系统上不能正常使用RPM命令的问题, 请参阅文档[2]和[6].
o 关于声卡的问题, 可以参阅参考文档[2]和[6].
o 关于在linux-2.6.x系统上运行vmware的问题, 请参阅参考文档[2]和[6].

10. 参考文档

[1] Kernel 2.6.x, the new breed: http://linuxbooks.pananix.com/kernel2.6.html
[2] Upgrading RH9 to 2.6.0-test4: http://www.fearthecow.net/index.pl?section=guest&page=kernel
[3] HowTo Upgrade To The 2.6 Kernel: http://kerneltrap.org/node/view/799
[4] Building Linux Kernel 2.6 on Red Hat 8: http://www.sabalsoftware.com/art ... rnel26OnRedhat8.php
[5] Migrating to Linux kernel 2.6 on recent Redhat systems: http://linux-sxs.org/upgrading/migration26.html
[6] Migrating to Linux Kernel 2.6: http://thomer.com/linux/migrate-to-2.6.html

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:48 |显示全部楼层

嵌入式系统 Boot Loader 技术内幕

嵌入式系统 Boot Loader 技术内幕  

  
级别: 初级

詹荣开 (zhanrk@sohu.com), Linux爱好者


2003 年 12 月 01 日

本文详细地介绍了基于嵌入式系统中的 OS 启动加载程序 ―― Boot Loader 的概念、软件设计的主要任务以及结构框架等内容。
1. 引言

在专用的嵌入式板子运行 GNU/Linux 系统已经变得越来越流行。一个嵌入式 Linux 系统从软件的角度看通常可以分为四个层次:

1. 引导加载程序。包括固化在固件(firmware)中的 boot 代码(可选),和 Boot Loader 两大部分。

2. Linux 内核。特定于嵌入式板子的定制内核以及内核的启动参数。

3. 文件系统。包括根文件系统和建立于 Flash 内存设备之上文件系统。通常用 ram disk 来作为 root fs。

4. 用户应用程序。特定于用户的应用程序。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。常用的嵌入式 GUI 有:MicroWindows 和 MiniGUI 懂。

引导加载程序是系统加电后运行的第一段软件代码。回忆一下 PC 的体系结构我们可以知道,PC 机中的引导加载程序由 BIOS(其本质就是一段固件程序)和位于硬盘 MBR 中的 OS Boot Loader(比如,LILO 和 GRUB 等)一起组成。BIOS 在完成硬件检测和资源分配后,将硬盘 MBR 中的 Boot Loader 读到系统的 RAM 中,然后将控制权交给 OS Boot Loader。Boot Loader 的主要运行任务就是将内核映象从硬盘上读到 RAM 中,然后跳转到内核的入口点去运行,也即开始启动操作系统。

而在嵌入式系统中,通常并没有像 BIOS 那样的固件程序(注,有的嵌入式 CPU 也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由 Boot Loader 来完成。比如在一个基于 ARM7TDMI core 的嵌入式系统中,系统在上电或复位时通常都从地址 0x00000000 处开始执行,而在这个地址处安排的通常就是系统的 Boot Loader 程序。

本文将从 Boot Loader 的概念、Boot Loader 的主要任务、Boot Loader 的框架结构以及 Boot Loader 的安装等四个方面来讨论嵌入式系统的 Boot Loader。






回页首




2. Boot Loader 的概念

简单地说,Boot Loader 就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。

通常,Boot Loader 是严重地依赖于硬件而实现的,特别是在嵌入式世界。因此,在嵌入式世界里建立一个通用的 Boot Loader 几乎是不可能的。尽管如此,我们仍然可以对 Boot Loader 归纳出一些通用的概念来,以指导用户特定的 Boot Loader 设计与实现。

1. Boot Loader 所支持的 CPU 和嵌入式板

每种不同的 CPU 体系结构都有不同的 Boot Loader。有些 Boot Loader 也支持多种体系结构的 CPU,比如 U-Boot 就同时支持 ARM 体系结构和MIPS 体系结构。除了依赖于 CPU 的体系结构外,Boot Loader 实际上也依赖于具体的嵌入式板级设备的配置。这也就是说,对于两块不同的嵌入式板而言,即使它们是基于同一种 CPU 而构建的,要想让运行在一块板子上的 Boot Loader 程序也能运行在另一块板子上,通常也都需要修改 Boot Loader 的源程序。

2. Boot Loader 的安装媒介(Installation Medium)

系统加电或复位后,所有的 CPU 通常都从某个由 CPU 制造商预先安排的地址上取指令。比如,基于 ARM7TDMI core 的 CPU 在复位时通常都从地址 0x00000000 取它的第一条指令。而基于 CPU 构建的嵌入式系统通常都有某种类型的固态存储设备(比如:ROM、EEPROM 或 FLASH 等)被映射到这个预先安排的地址上。因此在系统加电后,CPU 将首先执行 Boot Loader 程序。

下图1就是一个同时装有 Boot Loader、内核的启动参数、内核映像和根文件系统映像的固态存储设备的典型空间分配结构图。


图1 固态存储设备的典型空间分配结构


3. 用来控制 Boot Loader 的设备或机制

主机和目标机之间一般通过串口建立连接,Boot Loader 软件在执行时通常会通过串口来进行 I/O,比如:输出打印信息到串口,从串口读取用户控制字符等。

4. Boot Loader 的启动过程是单阶段(Single Stage)还是多阶段(Multi-Stage)

通常多阶段的 Boot Loader 能提供更为复杂的功能,以及更好的可移植性。从固态存储设备上启动的 Boot Loader 大多都是 2 阶段的启动过程,也即启动过程可以分为 stage 1 和 stage 2 两部分。而至于在 stage 1 和 stage 2 具体完成哪些任务将在下面讨论。

5. Boot Loader 的操作模式 (Operation Mode)

大多数 Boot Loader 都包含两种不同的操作模式:"启动加载"模式和"下载"模式,这种区别仅对于开发人员才有意义。但从最终用户的角度看,Boot Loader 的作用就是用来加载操作系统,而并不存在所谓的启动加载模式与下载工作模式的区别。

启动加载(Boot loading)模式:这种模式也称为"自主"(Autonomous)模式。也即 Boot Loader 从目标机上的某个固态存储设备上将操作系统加载到 RAM 中运行,整个过程并没有用户的介入。这种模式是 Boot Loader 的正常工作模式,因此在嵌入式产品发布的时侯,Boot Loader 显然必须工作在这种模式下。

下载(Downloading)模式:在这种模式下,目标机上的 Boot Loader 将通过串口连接或网络连接等通信手段从主机(Host)下载文件,比如:下载内核映像和根文件系统映像等。从主机下载的文件通常首先被 Boot Loader 保存到目标机的 RAM 中,然后再被 Boot Loader 写到目标机上的FLASH 类固态存储设备中。Boot Loader 的这种模式通常在第一次安装内核与根文件系统时被使用;此外,以后的系统更新也会使用 Boot Loader 的这种工作模式。工作于这种模式下的 Boot Loader 通常都会向它的终端用户提供一个简单的命令行接口。

像 Blob 或 U-Boot 等这样功能强大的 Boot Loader 通常同时支持这两种工作模式,而且允许用户在这两种工作模式之间进行切换。比如,Blob 在启动时处于正常的启动加载模式,但是它会延时 10 秒等待终端用户按下任意键而将 blob 切换到下载模式。如果在 10 秒内没有用户按键,则 blob 继续启动 Linux 内核。

6. BootLoader 与主机之间进行文件传输所用的通信设备及协议

最常见的情况就是,目标机上的 Boot Loader 通过串口与主机之间进行文件传输,传输协议通常是 xmodem/ymodem/zmodem 协议中的一种。但是,串口传输的速度是有限的,因此通过以太网连接并借助 TFTP 协议来下载文件是个更好的选择。

此外,在论及这个话题时,主机方所用的软件也要考虑。比如,在通过以太网连接和 TFTP 协议来下载文件时,主机方必须有一个软件用来的提供 TFTP 服务。

在讨论了 BootLoader 的上述概念后,下面我们来具体看看 BootLoader 的应该完成哪些任务。






回页首




3. Boot Loader 的主要任务与典型结构框架

在继续本节的讨论之前,首先我们做一个假定,那就是:假定内核映像与根文件系统映像都被加载到 RAM 中运行。之所以提出这样一个假设前提是因为,在嵌入式系统中内核映像与根文件系统映像也可以直接在 ROM 或 Flash 这样的固态存储设备中直接运行。但这种做法无疑是以运行速度的牺牲为代价的。

从操作系统的角度看,Boot Loader 的总目标就是正确地调用内核来执行。

另外,由于 Boot Loader 的实现依赖于 CPU 的体系结构,因此大多数 Boot Loader 都分为 stage1 和 stage2 两大部分。依赖于 CPU 体系结构的代码,比如设备初始化代码等,通常都放在 stage1 中,而且通常都用汇编语言来实现,以达到短小精悍的目的。而 stage2 则通常用C语言来实现,这样可以实现给复杂的功能,而且代码会具有更好的可读性和可移植性。

Boot Loader 的 stage1 通常包括以下步骤(以执行的先后顺序):

硬件设备初始化。


为加载 Boot Loader 的 stage2 准备 RAM 空间。


拷贝 Boot Loader 的 stage2 到 RAM 空间中。


设置好堆栈。


跳转到 stage2 的 C 入口点。


Boot Loader 的 stage2 通常包括以下步骤(以执行的先后顺序):

初始化本阶段要使用到的硬件设备。


检测系统内存映射(memory map)。


将 kernel 映像和根文件系统映像从 flash 上读到 RAM 空间中。


为内核设置启动参数。


调用内核。
3.1 Boot Loader 的 stage1

3.1.1 基本的硬件初始化

这是 Boot Loader 一开始就执行的操作,其目的是为 stage2 的执行以及随后的 kernel 的执行准备好一些基本的硬件环境。它通常包括以下步骤(以执行的先后顺序):

1. 屏蔽所有的中断。为中断提供服务通常是 OS 设备驱动程序的责任,因此在 Boot Loader 的执行全过程中可以不必响应任何中断。中断屏蔽可以通过写 CPU 的中断屏蔽寄存器或状态寄存器(比如 ARM 的 CPSR 寄存器)来完成。

2. 设置 CPU 的速度和时钟频率。

3. RAM 初始化。包括正确地设置系统的内存控制器的功能寄存器以及各内存库控制寄存器等。

4. 初始化 LED。典型地,通过 GPIO 来驱动 LED,其目的是表明系统的状态是 OK 还是 Error。如果板子上没有 LED,那么也可以通过初始化 UART 向串口打印 Boot Loader 的 Logo 字符信息来完成这一点。

5. 关闭 CPU 内部指令/数据 cache。

3.1.2 为加载 stage2 准备 RAM 空间

为了获得更快的执行速度,通常把 stage2 加载到 RAM 空间中来执行,因此必须为加载 Boot Loader 的 stage2 准备好一段可用的 RAM 空间范围。

由于 stage2 通常是 C 语言执行代码,因此在考虑空间大小时,除了 stage2 可执行映象的大小外,还必须把堆栈空间也考虑进来。此外,空间大小最好是 memory page 大小(通常是 4KB)的倍数。一般而言,1M 的 RAM 空间已经足够了。具体的地址范围可以任意安排,比如 blob 就将它的 stage2 可执行映像安排到从系统 RAM 起始地址 0xc0200000 开始的 1M 空间内执行。但是,将 stage2 安排到整个 RAM 空间的最顶 1MB(也即(RamEnd-1MB) - RamEnd)是一种值得推荐的方法。

为了后面的叙述方便,这里把所安排的 RAM 空间范围的大小记为:stage2_size(字节),把起始地址和终止地址分别记为:stage2_start 和 stage2_end(这两个地址均以 4 字节边界对齐)。因此:

stage2_end=stage2_start+stage2_size



另外,还必须确保所安排的地址范围的的确确是可读写的 RAM 空间,因此,必须对你所安排的地址范围进行测试。具体的测试方法可以采用类似于 blob 的方法,也即:以 memory page 为被测试单位,测试每个 memory page 开始的两个字是否是可读写的。为了后面叙述的方便,我们记这个检测算法为:test_mempage,其具体步骤如下:

1. 先保存 memory page 一开始两个字的内容。

2. 向这两个字中写入任意的数字。比如:向第一个字写入 0x55,第 2 个字写入 0xaa。

3. 然后,立即将这两个字的内容读回。显然,我们读到的内容应该分别是 0x55 和 0xaa。如果不是,则说明这个 memory page 所占据的地址范围不是一段有效的 RAM 空间。

4. 再向这两个字中写入任意的数字。比如:向第一个字写入 0xaa,第 2 个字中写入 0x55。

5. 然后,立即将这两个字的内容立即读回。显然,我们读到的内容应该分别是 0xaa 和 0x55。如果不是,则说明这个 memory page 所占据的地址范围不是一段有效的 RAM 空间。

6. 恢复这两个字的原始内容。测试完毕。

为了得到一段干净的 RAM 空间范围,我们也可以将所安排的 RAM 空间范围进行清零操作。

3.1.3 拷贝 stage2 到 RAM 中

拷贝时要确定两点:(1) stage2 的可执行映象在固态存储设备的存放起始地址和终止地址;(2) RAM 空间的起始地址。

3.1.4 设置堆栈指针 sp

堆栈指针的设置是为了执行 C 语言代码作好准备。通常我们可以把 sp 的值设置为(stage2_end-4),也即在 3.1.2 节所安排的那个 1MB 的 RAM 空间的最顶端(堆栈向下生长)。

此外,在设置堆栈指针 sp 之前,也可以关闭 led 灯,以提示用户我们准备跳转到 stage2。

经过上述这些执行步骤后,系统的物理内存布局应该如下图2所示。

3.1.5 跳转到 stage2 的 C 入口点

在上述一切都就绪后,就可以跳转到 Boot Loader 的 stage2 去执行了。比如,在 ARM 系统中,这可以通过修改 PC 寄存器为合适的地址来实现。


图2 bootloader 的 stage2 可执行映象刚被拷贝到 RAM 空间时的系统内存布局


3.2 Boot Loader 的 stage2

正如前面所说,stage2 的代码通常用 C 语言来实现,以便于实现更复杂的功能和取得更好的代码可读性和可移植性。但是与普通 C 语言应用程序不同的是,在编译和链接 boot loader 这样的程序时,我们不能使用 glibc 库中的任何支持函数。其原因是显而易见的。这就给我们带来一个问题,那就是从那里跳转进 main() 函数呢?直接把 main() 函数的起始地址作为整个 stage2 执行映像的入口点或许是最直接的想法。但是这样做有两个缺点:1)无法通过main() 函数传递函数参数;2)无法处理 main() 函数返回的情况。一种更为巧妙的方法是利用 trampoline(弹簧床)的概念。也即,用汇编语言写一段trampoline 小程序,并将这段 trampoline 小程序来作为 stage2 可执行映象的执行入口点。然后我们可以在 trampoline 汇编小程序中用 CPU 跳转指令跳入 main() 函数中去执行;而当 main() 函数返回时,CPU 执行路径显然再次回到我们的 trampoline 程序。简而言之,这种方法的思想就是:用这段 trampoline 小程序来作为 main() 函数的外部包裹(external wrapper)。

下面给出一个简单的 trampoline 程序示例(来自blob):

.text
.globl _trampoline
_trampoline:
        bl        main
        /* if main ever returns we just call it again */
        b        _trampoline



可以看出,当 main() 函数返回后,我们又用一条跳转指令重新执行 trampoline 程序――当然也就重新执行 main() 函数,这也就是 trampoline(弹簧床)一词的意思所在。

3.2.1初始化本阶段要使用到的硬件设备

这通常包括:(1)初始化至少一个串口,以便和终端用户进行 I/O 输出信息;(2)初始化计时器等。

在初始化这些设备之前,也可以重新把 LED 灯点亮,以表明我们已经进入 main() 函数执行。

设备初始化完成后,可以输出一些打印信息,程序名字字符串、版本号等。

3.2.2 检测系统的内存映射(memory map)

所谓内存映射就是指在整个 4GB 物理地址空间中有哪些地址范围被分配用来寻址系统的 RAM 单元。比如,在 SA-1100 CPU 中,从 0xC000,0000 开始的 512M 地址空间被用作系统的 RAM 地址空间,而在 Samsung S3C44B0X CPU 中,从 0x0c00,0000 到 0x1000,0000 之间的 64M 地址空间被用作系统的 RAM 地址空间。虽然 CPU 通常预留出一大段足够的地址空间给系统 RAM,但是在搭建具体的嵌入式系统时却不一定会实现 CPU 预留的全部 RAM 地址空间。也就是说,具体的嵌入式系统往往只把 CPU 预留的全部 RAM 地址空间中的一部分映射到 RAM 单元上,而让剩下的那部分预留 RAM 地址空间处于未使用状态。 由于上述这个事实,因此 Boot Loader 的 stage2 必须在它想干点什么 (比如,将存储在 flash 上的内核映像读到 RAM 空间中) 之前检测整个系统的内存映射情况,也即它必须知道 CPU 预留的全部 RAM 地址空间中的哪些被真正映射到 RAM 地址单元,哪些是处于 "unused" 状态的。

(1) 内存映射的描述

可以用如下数据结构来描述 RAM 地址空间中的一段连续(continuous)的地址范围:

typedef struct memory_area_struct {
        u32 start; /* the base address of the memory region */
        u32 size; /* the byte number of the memory region */
        int used;
} memory_area_t;



这段 RAM 地址空间中的连续地址范围可以处于两种状态之一:(1)used=1,则说明这段连续的地址范围已被实现,也即真正地被映射到 RAM 单元上。(2)used=0,则说明这段连续的地址范围并未被系统所实现,而是处于未使用状态。

基于上述 memory_area_t 数据结构,整个 CPU 预留的 RAM 地址空间可以用一个 memory_area_t 类型的数组来表示,如下所示:

memory_area_t memory_map[NUM_MEM_AREAS] = {
        [0 ... (NUM_MEM_AREAS - 1)] = {
                .start = 0,
                .size = 0,
                .used = 0
        },
};



(2) 内存映射的检测

下面我们给出一个可用来检测整个 RAM 地址空间内存映射情况的简单而有效的算法:

/* 数组初始化 */
for(i = 0; i < NUM_MEM_AREAS; i++)
        memory_map.used = 0;
/* first write a 0 to all memory locations */
for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE)
        * (u32 *)addr = 0;
for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) {
     /*
      * 检测从基地址 MEM_START+i*PAGE_SIZE 开始,大小为
* PAGE_SIZE 的地址空间是否是有效的RAM地址空间。
      */
     调用3.1.2节中的算法test_mempage();
     if ( current memory page isnot a valid ram page) {
                /* no RAM here */
                if(memory_map.used )
                        i++;
                continue;
        }
       
        /*
         * 当前页已经是一个被映射到 RAM 的有效地址范围
         * 但是还要看看当前页是否只是 4GB 地址空间中某个地址页的别名?
         */
        if(* (u32 *)addr != 0) { /* alias? */
                /* 这个内存页是 4GB 地址空间中某个地址页的别名 */
                if ( memory_map.used )
                        i++;
                continue;
        }
       
        /*
         * 当前页已经是一个被映射到 RAM 的有效地址范围
         * 而且它也不是 4GB 地址空间中某个地址页的别名。
         */
        if (memory_map.used == 0) {
                memory_map.start = addr;
                memory_map.size = PAGE_SIZE;
                memory_map.used = 1;
        } else {
                memory_map.size += PAGE_SIZE;
        }
} /* end of for (…) */



在用上述算法检测完系统的内存映射情况后,Boot Loader 也可以将内存映射的详细信息打印到串口。

3.2.3 加载内核映像和根文件系统映像

(1) 规划内存占用的布局

这里包括两个方面:(1)内核映像所占用的内存范围;(2)根文件系统所占用的内存范围。在规划内存占用的布局时,主要考虑基地址和映像的大小两个方面。

对于内核映像,一般将其拷贝到从(MEM_START+0x8000) 这个基地址开始的大约1MB大小的内存范围内(嵌入式 Linux 的内核一般都不操过 1MB)。为什么要把从 MEM_START 到 MEM_START+0x8000 这段 32KB 大小的内存空出来呢?这是因为 Linux 内核要在这段内存中放置一些全局数据结构,如:启动参数和内核页表等信息。

而对于根文件系统映像,则一般将其拷贝到 MEM_START+0x0010,0000 开始的地方。如果用 Ramdisk 作为根文件系统映像,则其解压后的大小一般是1MB。

(2)从 Flash 上拷贝

由于像 ARM 这样的嵌入式 CPU 通常都是在统一的内存地址空间中寻址 Flash 等固态存储设备的,因此从 Flash 上读取数据与从 RAM 单元中读取数据并没有什么不同。用一个简单的循环就可以完成从 Flash 设备上拷贝映像的工作:


while(count) {
        *dest++ = *src++; /* they are all aligned with word boundary */
        count -= 4; /* byte number */
};



3.2.4 设置内核的启动参数

应该说,在将内核映像和根文件系统映像拷贝到 RAM 空间中后,就可以准备启动 Linux 内核了。但是在调用内核之前,应该作一步准备工作,即:设置 Linux 内核的启动参数。

Linux 2.4.x 以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。启动参数标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。每个标记由标识被传递参数的 tag_header 结构以及随后的参数值数据结构来组成。数据结构 tag 和 tag_header 定义在 Linux 内核源码的include/asm/setup.h 头文件中:

/* The list ends with an ATAG_NONE node. */
#define ATAG_NONE        0x00000000
struct tag_header {
        u32 size; /* 注意,这里size是字数为单位的 */
        u32 tag;
};
……
struct tag {
        struct tag_header hdr;
        union {
                struct tag_core                core;
                struct tag_mem32        mem;
                struct tag_videotext        videotext;
                struct tag_ramdisk        ramdisk;
                struct tag_initrd        initrd;
                struct tag_serialnr        serialnr;
                struct tag_revision        revision;
                struct tag_videolfb        videolfb;
                struct tag_cmdline        cmdline;
                /*
                 * Acorn specific
                 */
                struct tag_acorn        acorn;
                /*
                 * DC21285 specific
                 */
                struct tag_memclk        memclk;
        } u;
};



在嵌入式 Linux 系统中,通常需要由 Boot Loader 设置的常见启动参数有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。

比如,设置 ATAG_CORE 的代码如下:

params = (struct tag *)BOOT_PARAMS;
        params->hdr.tag = ATAG_CORE;
        params->hdr.size = tag_size(tag_core);
        params->u.core.flags = 0;
        params->u.core.pagesize = 0;
        params->u.core.rootdev = 0;
        params = tag_next(params);



其中,BOOT_PARAMS 表示内核启动参数在内存中的起始基地址,指针 params 是一个 struct tag 类型的指针。宏 tag_next() 将以指向当前标记的指针为参数,计算紧临当前标记的下一个标记的起始地址。注意,内核的根文件系统所在的设备ID就是在这里设置的。

下面是设置内存映射情况的示例代码:

for(i = 0; i < NUM_MEM_AREAS; i++) {
                if(memory_map.used) {
                        params->hdr.tag = ATAG_MEM;
                        params->hdr.size = tag_size(tag_mem32);
                        params->u.mem.start = memory_map.start;
                        params->u.mem.size = memory_map.size;
                       
                        params = tag_next(params);
                }
}



可以看出,在 memory_map[]数组中,每一个有效的内存段都对应一个 ATAG_MEM 参数标记。

Linux 内核在启动时可以以命令行参数的形式来接收信息,利用这一点我们可以向内核提供那些内核不能自己检测的硬件参数信息,或者重载(override)内核自己检测到的信息。比如,我们用这样一个命令行参数字符串"console=ttyS0,115200n8"来通知内核以 ttyS0 作为控制台,且串口采用 "115200bps、无奇偶校验、8位数据位"这样的设置。下面是一段设置调用内核命令行参数字符串的示例代码:

char *p;
        /* eat leading white space */
        for(p = commandline; *p == ' '; p++)
                ;
        /* skip non-existent command lines so the kernel will still
    * use its default command line.
         */
        if(*p == '\0')
                return;
        params->hdr.tag = ATAG_CMDLINE;
        params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;
        strcpy(params->u.cmdline.cmdline, p);
        params = tag_next(params);



请注意在上述代码中,设置 tag_header 的大小时,必须包括字符串的终止符'\0',此外还要将字节数向上圆整4个字节,因为 tag_header 结构中的size 成员表示的是字数。

下面是设置 ATAG_INITRD 的示例代码,它告诉内核在 RAM 中的什么地方可以找到 initrd 映象(压缩格式)以及它的大小:

        params->hdr.tag = ATAG_INITRD2;
        params->hdr.size = tag_size(tag_initrd);
       
        params->u.initrd.start = RAMDISK_RAM_BASE;
        params->u.initrd.size = INITRD_LEN;
       
        params = tag_next(params);



下面是设置 ATAG_RAMDISK 的示例代码,它告诉内核解压后的 Ramdisk 有多大(单位是KB):

params->hdr.tag = ATAG_RAMDISK;
params->hdr.size = tag_size(tag_ramdisk);
       
params->u.ramdisk.start = 0;
params->u.ramdisk.size = RAMDISK_SIZE; /* 请注意,单位是KB */
params->u.ramdisk.flags = 1; /* automatically load ramdisk */
       
params = tag_next(params);



最后,设置 ATAG_NONE 标记,结束整个启动参数列表:

static void setup_end_tag(void)
{
        params->hdr.tag = ATAG_NONE;
        params->hdr.size = 0;
}



3.2.5 调用内核

Boot Loader 调用 Linux 内核的方法是直接跳转到内核的第一条指令处,也即直接跳转到 MEM_START+0x8000 地址处。在跳转时,下列条件要满足:

1. CPU 寄存器的设置:

R0=0;


R1=机器类型 ID;关于 Machine Type Number,可以参见 linux/arch/arm/tools/mach-types。


R2=启动参数标记列表在 RAM 中起始基地址;


2. CPU 模式:

必须禁止中断(IRQs和FIQs);


CPU 必须 SVC 模式;


3. Cache 和 MMU 的设置:

MMU 必须关闭;


指令 Cache 可以打开也可以关闭;


数据 Cache 必须关闭;
如果用 C 语言,可以像下列示例代码这样来调用内核:

void (*theKernel)(int zero, int arch, u32 params_addr) = (void (*)(int, int, u32))KERNEL_RAM_BASE;
……
theKernel(0, ARCH_NUMBER, (u32) kernel_params_start);



注意,theKernel()函数调用应该永远不返回的。如果这个调用返回,则说明出错。






回页首




4. 关于串口终端

在 boot loader 程序的设计与实现中,没有什么能够比从串口终端正确地收到打印信息能更令人激动了。此外,向串口终端打印信息也是一个非常重要而又有效的调试手段。但是,我们经常会碰到串口终端显示乱码或根本没有显示的问题。造成这个问题主要有两种原因:(1) boot loader 对串口的初始化设置不正确。(2) 运行在 host 端的终端仿真程序对串口的设置不正确,这包括:波特率、奇偶校验、数据位和停止位等方面的设置。

此外,有时也会碰到这样的问题,那就是:在 boot loader 的运行过程中我们可以正确地向串口终端输出信息,但当 boot loader 启动内核后却无法看到内核的启动输出信息。对这一问题的原因可以从以下几个方面来考虑:

(1) 首先请确认你的内核在编译时配置了对串口终端的支持,并配置了正确的串口驱动程序。

(2) 你的 boot loader 对串口的初始化设置可能会和内核对串口的初始化设置不一致。此外,对于诸如 s3c44b0x 这样的 CPU,CPU 时钟频率的设置也会影响串口,因此如果 boot loader 和内核对其 CPU 时钟频率的设置不一致,也会使串口终端无法正确显示信息。

(3) 最后,还要确认 boot loader 所用的内核基地址必须和内核映像在编译时所用的运行基地址一致,尤其是对于 uClinux 而言。假设你的内核映像在编译时用的基地址是 0xc0008000,但你的 boot loader 却将它加载到 0xc0010000 处去执行,那么内核映像当然不能正确地执行了。






回页首




5. 结束语

Boot Loader 的设计与实现是一个非常复杂的过程。如果不能从串口收到那激动人心的"uncompressing linux.................. done, booting the kernel……"内核启动信息,恐怕谁也不能说:"嗨,我的 boot loader 已经成功地转起来了!"。




关于作者



  詹荣开,研究兴趣包括:嵌入式 Linux、Linux 内核、驱动程序、文件系统等。您可以通过 zhanrk@sohu.com连系他。

[ 本帖最后由 dreamice 于 2008-11-6 17:50 编辑 ]
image001.gif
image002.gif

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:54 |显示全部楼层

Inside Linux PCI Bus Driver(1)

这个图看不到,不知道为什么。

这两天快回家过年了,有点无心干活,但时间总不能浪费了吧,所以就把以前对Linux内核的PCI总线驱动程序的实现分析笔记给敲出来吧。我的分析笔记很长,所以采用连载的方式贴在这个论坛吧,至于什么时候能完我也不知道:-)


另外,衷心希望各位网友指出我的错误,大家一同进步。建议大家下载附件中的全文来看。ok,言规正转,进入主题。


§1.PCI总线体系结构概述
PCI总线体系结构是一种层次式的(Hierarchical)体系结构。在这种层次式体系结构中,PCI桥设备占据着重要的地位,它将父总线与子总线连接在一起,从而使整个系统看起来像一颗倒置的树型结构。树的顶端是系统的CPU,它通过一个较为特殊的PCI桥设备——Host/PCI桥设备与根PCI总线(root pci bus)连接起来。下图1是一个较为典型的PCI总线体系结构图。
(图1)
从上图也可以看出,作为一种特殊的PCI设备,PCI桥又包括以下几种:
(1)Host/PCI桥:用于连接CPU与PCI根总线(注意,the first root bus总是编号为0)。由于像Memory Controller这样的设备通常也被集成到Host/PCI桥设备芯片中,因此,Host/PCI桥通常也被称为“北桥芯片组(North Bridge Chipset)”。
(2)PCI/ISA桥:用于连接遗留的ISA总线。通常,像i8359A中断控制器这样的设备也会被集成到PCI/ISA桥设备中,因此,PCI/ISA桥通常也被称为“南桥芯片组(South Bridge Chipset)”。
(3)PCI-to-PCI桥:用于连接PCI主总线(primary bus)与次总线(secondary bus)。PCI桥所处的PCI总线称为“主总线”(即次总线的父总线),桥设备所连接的PCI总线称为“次总线”(即主总线的子总线)。
更多类型的PCI桥分类可以参见《PCI Local Bus Specification》Revision2.2的附录D。
本文以下部分假设读者对PCI总线规范有一定的了解,因此本文是为那些想要深入了解Linux内核是如何实现PCI总线驱动程序的Kernel Hacker而写的。

§2 PCI总线驱动程序的核心数据结构
从前面的图1可以看出,在PCI总线体系结构中,有两个核心的概念存在:PCI总线和PCI设备(桥设备是一种特殊的PCI设备)。基于对这两个核心概念的抽象,Linux PCI总线驱动程序定义了两个关键的数据结构:结构类型pci_bus和结构类型pci_dev,以分别描述pci总线和pci设备。在此基础上,Linux PCI总线驱动程序又将系统中当前存在的所有PCI总线的pci_bus结构组织成一张层次式的链表图,显然该链表图的顶层链表是系统中所有根总线的pci_bus结构链表,因此用一个list_head结构类型的全局变量pci_root_buses来描述该链表图中的顶层链表表头,也即根总线链表的表头。系统中当前存在的所有PCI设备的pci_dev结构也被组织成一条链表,称为“全局pci设备链表”,用一个list_head结构类型的变量pci_devices来表示该链表的表头。通过这两条总链表,Linux内核就可以得到一张当前系统的PCI总线体系结构视图。

§2.1 PCI设备描述符——pci_dev结构类型
所有种类的PCI设备都可以用结构类型pci_dev来描述。更为准确地说,应该是每一个PCI功能,即PCI逻辑设备都唯一地对应有一个pci_dev设备描述符。该数据结构的定义如下(include/linux/pci.h):
/*
* The pci_dev structure is used to describe both PCI and ISAPnP devices.
*/
struct pci_dev {
struct list_head global_list; /* node in list of all PCI devices */
struct list_head bus_list; /* node in per-bus list */
struct pci_bus *bus; /* bus this device is on */
struct pci_bus *subordinate; /* bus this device bridges to */

void *sysdata; /* hook for sys-specific extension */
struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */

unsigned int devfn; /* encoded device & function index */
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class; /* 3 bytes: (base,sub,prog-if) */
u8 hdr_type; /* PCI header type (`multi' flag masked out) */
u8 rom_base_reg; /* which config register controls the ROM */

struct pci_driver *driver; /* which driver has allocated this device */
void *driver_data; /* data private to the driver */
dma_addr_t dma_mask; /* Mask of the bits of bus address this
device implements. Normally this is
0xffffffff. You only need to change
this if your device has broken DMA
or supports 64-bit transfers. */

/* device is compatible with these IDs */
unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];
unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];

/*
* Instead of touching interrupt line and base address registers
* directly, use the values stored here. They might be different!
*/
unsigned int irq;
struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */
struct resource dma_resource[DEVICE_COUNT_DMA];
struct resource irq_resource[DEVICE_COUNT_IRQ];

char name[80]; /* device name */
char slot_name[8]; /* slot name */
int active; /* ISAPnP: device is active */
int ro; /* ISAPnP: read only */
unsigned short regs; /* ISAPnP: supported registers */

int (*prepare)(struct pci_dev *dev); /* ISAPnP hooks */
int (*activate)(struct pci_dev *dev);
int (*deactivate)(struct pci_dev *dev);
};
各成员的含义如下:
(1)全局链表元素global_list:每一个pci_dev结构都通过该成员连接到全局pci设备链表pci_devices中。
(2)总线设备链表元素bus_list:每一个pci_dev结构除了链接到全局设备链表中外,还会通过这个成员连接到其所属PCI总线的设备链表中。每一条PCI总线都维护一条它自己的设备链表视图,以便描述所有连接在该PCI总线上的设备,其表头由PCI总线的pci_bus结构中的devices成员所描述。
(3)总线指针bus:指向这个PCI设备所在的PCI总线的pci_bus结构。因此,对于桥设备而言,bus指针将指向桥设备的主总线(primary bus),也即指向桥设备所在的PCI总线。
(4)指针subordinate:指向这个PCI设备所桥接的下级总线。这个指针成员仅对桥设备才有意义,而对于一般的非桥PCI设备而言,该指针成员总是为NULL。
(5)无类型指针sysdata:指向一片特定于系统的扩展数据。
(6)指针procent:指向该PCI设备在/proc文件系统中对应的目录项。
(7)devfn:这个PCI设备的设备功能号,也成为PCI逻辑设备号(0-255)。其中bit[7:3]是物理设备号(取值范围0-31),bit[2:0]是功能号(取值范围0-7)。
(8)vendor:这是一个16无符号整数,表示PCI设备的厂商ID。
(9)device:这是一个16无符号整数,表示PCI设备的设备ID。
(10)subsystem_vendor:这是一个16无符号整数,表示PCI设备的子系统厂商ID。
(11)subsystem_device:这是一个16无符号整数,表示PCI设备的子系统设备ID。
(12)class:32位的无符号整数,表示该PCI设备的类别,其中,bit[7:0]为编程接口,bit[15:8]为子类别代码,bit[23:16]为基类别代码,bit[31:24]无意义。显然,class成员的低3字节刚好对应与PCI配置空间中的类代码。
(13)hdr_type:8位符号整数,表示PCI配置空间头部的类型。其中,bit[7]=1表示这是一个多功能设备,bit[7]=0表示这是一个单功能设备。Bit[6:0]则表示PCI配置空间头部的布局类型,值00h表示这是一个一般PCI设备的配置空间头部,值01h表示这是一个PCI-to-PCI桥的配置空间头部,值02h表示CardBus桥的配置空间头部。
(14)rom_base_reg:8位无符号整数,表示PCI配置空间中的ROM基地址寄存器在PCI配置空间中的位置。ROM基地址寄存器在不同类型的PCI配置空间头部的位置是不一样的,对于type 0的配置空间布局,ROM基地址寄存器的起始位置是30h,而对于PCI-to-PCI桥所用的type 1配置空间布局,ROM基地址寄存器的起始位置是38h。
(15)指针driver:指向这个PCI设备所对应的驱动程序定义的pci_driver结构。每一个pci设备驱动程序都必须定义它自己的pci_driver结构来描述它自己。这个数据结构将在后面分析。
(16)无类型指针driver_data:指向驱动程序为这个PCI设备所分配的一块私有数据区。通常,设备驱动程序会为它所支持的每一种设备类型定义一个特定于设备类型的数据结构,以描述该类型设备的信息。而且,driver会为它所找到的每一个设备实例分配一个特定于该设备类型的数据结构实例。指针driver_data一般就用来指向这个有驱动程序所分配的数据结构实例。
(17)dma_mask:用于DMA的总线地址掩码,一般来说,这个成员的值是0xffffffff。数据类型dma_addr_t定义在include/asm/types.h中,在x86平台上,dma_addr_t类型就是u32类型。
(18)vendor_compatible[DEVICE_COUNT_COMPATIBLE]和device_compatible[DEVICE_COUNT_COMPATIBLE]:定义这个PCI设备与哪些设备相兼容。
(19)无符号的整数irq:表示这个PCI设备通过哪根IRQ输入线产生中断,一般为0-15之间的某个值。
(20)资源数组resource[DEVICE_COUNT_RESOURCE]:表示该设备可能用到的资源,包括:I/O断口区域、设备内存地址区域以及扩展ROM地址区域。宏DEVICE_COUNT_DEVICE在pci.h头文件中被定义为常值12。但是PCI设备通常仅使用这12各资源区域中的一部分。其中,resource[5:0]分别对应于配置空间中的BAR0-BAR5(注意,桥设备只有BAR0和BAR1),resource[6]对应于配置空间中的ROM基地址寄存器所描述的ROM区域,而resource[10:7]分别对应于桥设备的地址过滤窗口。
(21)数组dma_resource[DEVICE_COUNT_DMA]:用于DMA的资源,DEVICE_COUNT_DMA为2。
(22)数组irq_resource[DEVICE_COUNT_IRQ]:用于IRQ的资源,DEVICE_COUNT_IRQ为2。
(23)name[80]数组:定义这个PCI逻辑设备的名字字符串。
(24)slot_name[8]数组:如果这个PCI逻辑设备是通过PCI插槽连接到PCI总线上的,则slot_name数组表示该插槽的名字字符串。
(25)active:被ISAPnP模块用来表示设备是否被激活。
(26)ro:被ISAPnP模块用来表示设备是否为只读设备。
(27)regs:被ISAPnP模块用来表示设备设备所支持的寄存器个数。
(28)函数指针prepare、activate和deactivate:都是仅被ISAPnP模块钩挂的函数指针。

§2.2 PCI总线描述符——pci_bus结构类型
该结构定义在include/linux/pci.h头文件中,如下所示:
struct pci_bus {
struct list_head node; /* node in list of buses */
struct pci_bus *parent; /* parent bus this bridge is on */
struct list_head children; /* list of child buses */
struct list_head devices; /* list of devices on this bus */
struct pci_dev *self; /* bridge device as seen by parent */
struct resource *resource[4]; /* address space routed to this bus */

struct pci_ops *ops; /* configuration access functions */
void *sysdata; /* hook for sys-specific extension */
struct proc_dir_entry *procdir; /* directory entry in /proc/bus/pci */

unsigned char number; /* bus number */
unsigned char primary; /* number of primary bridge */
unsigned char secondary; /* number of secondary bridge */
unsigned char subordinate; /* max number of subordinate buses */

char name[48];
unsigned short vendor;
unsigned short device;
unsigned int serial; /* serial number */
unsigned char pnpver; /* Plug & Play version */
unsigned char productver; /* product version */
unsigned char checksum; /* if zero - checksum passed */
unsigned char pad1;
};
各成员的含义如下:
(1)链表元素node:对于PCI根总线而言,其pci_bus结构通过node成员链接到本节一开始所述的根总线链表中,根总线链表的表头由一个list_head类型的全局变量pci_root_buses所描述。而对于非根pci总线,其pci_bus结构通过node成员链接到其父总线的子总线链表children中(见下面)。
(2)parent指针:指向该pci总线的父总线,即pci桥所在的那条总线。
(3)children指针:描述了这条PCI总线的子总线链表的表头。这条PCI总线的所有子总线都通过上述的node链表元素链接成一条子总线链表,而该链表的表头就由父总线的children指针所描述。
(4)devices链表头:描述了这条PCI总线的逻辑设备链表的表头。除了链接在全局PCI设备链表中之外,每一个PCI逻辑设备也通过其pci_dev结构中的bus_list成员链入其所在PCI总线的局部设备链表中,而这个局部的总线设备链表的表头就由pci_bus结构中的devices成员所描述。
(5)指针self:指向引出这条PCI总线的桥设备的pci_dev结构。
(6)资源指针数组resource[4]:指向应路由到这条pci总线的地址空间资源,通常是指向对应桥设备的pci_dev结构中的资源数组resource[10:7]。
(7)指针ops:指向一个pci_ops结构,表示这条pci总线所使用的配置空间访问函数。下一节将详细讨论这个数据结构。
(8)无类型指针sysdata:指向系统特定的扩展数据。
(9)指针procdir:指向该PCI总线在/proc文件系统中对应的目录项。
(10)number:这条PCI总线的总线编号(bus number),取值范围0-255。
(11)primary:表示引出这条PCI总线的“桥设备的主总线”(也即桥设备所在的PCI总线)编号,取值范围0-255。
(12)secondary:表示引出这条PCI总线的桥设备的次总线号,因此secondary成员总是等于number成员的值。取值范围0-255。
(13)subordinate:这条PCI总线的下属PCI总线(Subordinate pci bus)的总线编号最大值,它应该等于引出这条PCI总线的桥设备的subordinate值。
(13)name[48]:这条PCI总线的名字字符串。
(14)vendor和device:表示引出这条PCI总线的桥设备的厂商ID和设备ID。
(15)serial:系列号。
(16)pnpver:Plug&Play版本号。
(17)productver:产品版本号。
(18)chechsum:pci_bus结构的校验和,因改为0。
(19)pad1:为了使pci_bus结构的大小沿内存边界对齐而设立的成员,无实际意义。
下面对上述结构中的总线编号进行一下说明。假定这样一个PCI总线体系结构:根总线0上有一个PCI桥,它引出子总线bus 1,bus 1上又有一个PCI桥引出bus 2,如下图2所示:
(图2)
PCI桥的配置空间头部中定义桥两侧的主、次总线编号,以及桥后面的下级总线编号的最大可能值。因此在上图中,bus 0总线的pci_bus结构中的number、primary、secondary都应该为0,因为它是通过Host/PCI桥引出的根总线;而bus 1总线的pci_bus结构中的number=secondary=1,而bus 1的primary应该为0;而bus 2总线的pci_bus结构中的number=secondary=2,其primary则应该等于1。这三条总线的subordinate值都应该等于2。

§2.3 PCI设备链表
前面已经讲过,所有的PCI设备都通过其pci_dev结构中的global_list成员链接一条“全局pci设备链表”pci_devices,表头pci_devices定义在drivers/pci/pci.c文件中:
LIST_HEAD(pci_device);
另外,同属一条PCI总线上的所有PCI设备也通过其pci_dev结构中的bus_list成员链接成一条局部这条PCI总线的“总线设备链表”,表头则由该PCI总线的pci_bus结构中的devices成员所定义。
下图3描述可以很清楚地描述可以很清楚地描述上述这两重链表的关系
(图3)
■遍历PCI设备链表的辅助宏
为了更为方便地遍历上述两类PCI设备链表(全局PCI设备链表和局部总线设备链表),Linux在头文件中pci.h定义了几个辅助宏,从而使得遍历链表的代码更为简单、易懂。它们是:
(1)宏pci_for_each_dev(dev):正向遍历全局设备链表pce_devices中的每一个PCI 设备,参数dev是一个pci_dev结构类型的指针。 其定义如下:
#define pci_for_each_dev(dev) \
for(dev=pci_dev_g(pci_devices.next);dev!=pci_dev_g(&pci_devices);dev=pci_dev_g(dev->global_list.next))
上述定义中的宏pci_dev_g()用于将一个list_head类型的指针转换为一个pci_dev类型的指针(下面谈及)。对这个宏的典型使用方法如下所示:
pci_dev *dev;
……
pci_for_each_dev(dev)
{
……/* 对dev所指向的每一个pci设备进行处理 */

(2)宏pci_for_each_dev_reverse(dev):逆向遍历全局设备链表pci_devices中的每一个pci设备。参数dev同样也是一个pci_dev结构类型的指针。其定义如下:
#define pci_for_each_dev_reverse(dev) \
for(dev=pci_dev_g(pci_devices.prev);dev!=pci_dev_g(&pci_devices);dev=pci_dev_g(dev->global_list.prev))
显然,上述宏将从全局设备链表中的最后一个元素(由pci_devices.prev所指向)开始逆向遍历整个链表。该宏的使用与pci_for_each_dev()宏相同,只是遍历方向相反而已。
(3)宏pci_dev_g(n)和pci_dev_b(n):宏pci_dev_g()用来将与全局设备链表对应的list_head类型指针n转换为pci_dev类型的指针,宏pci_dev_b()用来将与总线局部设备链表对应的list_head类型指针n转换为pci_dev类型的指针。实际上,它们都是通过list_entry()宏来实现实际的转换工作,如下所示:
#define pci_dev_g(n) list_entry(n, struct pci_dev, global_list)
#define pci_dev_b(n) list_entry(n, struct pci_dev, bus_list)

§2.4 PCI总线链表
系统中当前存在的所有根总线都通过其pci_bus结构中的node成员链接成一条全局的根总线链表,其表头由list类型的全局变量pci_root_buses来描述。它的定义如下(drivers/pci/pci.c):
LIST_HEAD(pci_root_buses);
而根总线下面的所有下级总线则都通过其pci_bus结构中的node成员链接到其父总线的children链表中。这样,通过这两种PCI总线链表,linux内核就将所有的pci_bus结构以一种倒置树的方式组织起来。假定对于图4所示的PCI总线体系结构,它所对应的总线链表结构如图5所示。
(图4)
(图5)
类似地,宏pci_bus_b(n)则被用来将list_head类型的指针n转换为pci_bus类型的指针。该宏也是定义在include/linux/pci.h头文件中:
#define pci_bus_b(n) list_entry(n, struct pci_bus, node)

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:55 |显示全部楼层
第七章 Linux内核的时钟中断
(By 詹荣开,NUDT)

最下面有本文的PDF文档可以下载



Copyright &copy; 2003 by 詹荣开
E-mail:zhanrk@sohu.com
Linux-2.4.0
Version 1.0.0,2003-2-14




摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的时钟中断、内核对时间的表示等。本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:Linux、时钟、定时器

申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。

你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA

欢迎各位指出文档中的错误与疑问。
前言
时间在一个操作系统内核中占据着重要的地位,它是驱动一个OS内核运行的“起博器”。一般说来,内核主要需要两种类型的时间:
1. 在内核运行期间持续记录当前的时间与日期,以便内核对某些对象和事件作时间标记(timestamp,也称为“时间戳”),或供用户通过时间syscall进行检索。
2. 维持一个固定周期的定时器,以提醒内核或用户一段时间已经过去了。
PC机中的时间是有三种时钟硬件提供的,而这些时钟硬件又都基于固定频率的晶体振荡器来提供时钟方波信号输入。这三种时钟硬件是:(1)实时时钟(Real Time Clock,RTC);(2)可编程间隔定时器(Programmable Interval Timer,PIT);(3)时间戳计数器(Time Stamp Counter,TSC)。

7.1 时钟硬件
7.1.1 实时时钟RTC
自从IBM PC AT起,所有的PC机就都包含了一个叫做实时时钟(RTC)的时钟芯片,以便在PC机断电后仍然能够继续保持时间。显然,RTC是通过主板上的电池来供电的,而不是通过PC机电源来供电的,因此当PC机关掉电源后,RTC仍然会继续工作。通常,CMOS RAM和RTC被集成到一块芯片上,因此RTC也称作“CMOS Timer”。最常见的RTC芯片是MC146818(Motorola)和DS12887(maxim),DS12887完全兼容于MC146818,并有一定的扩展。本节内容主要基于MC146818这一标准的RTC芯片。具体内容可以参考MC146818的Datasheet。

7.1.1.1 RTC寄存器
MC146818 RTC芯片一共有64个寄存器。它们的芯片内部地址编号为0x00~0x3F(不是I/O端口地址),这些寄存器一共可以分为三组:
(1)时钟与日历寄存器组:共有10个(0x00~0x09),表示时间、日历的具体信息。在PC机中,这些寄存器中的值都是以BCD格式来存储的(比如23dec=0x23BCD)。
(2)状态和控制寄存器组:共有4个(0x0A~0x0D),控制RTC芯片的工作方式,并表示当前的状态。
(3)CMOS配置数据:通用的CMOS RAM,它们与时间无关,因此我们不关心它。
时钟与日历寄存器组的详细解释如下:
Address Function
00 Current second for RTC
01 Alarm second
02 Current minute
03 Alarm minute
04 Current hour
05 Alarm hour
06 Current day of week(01=Sunday)
07 Current date of month
08 Current month
09 Current year(final two digits,eg:93)

状态寄存器A(地址0x0A)的格式如下:
其中:
(1)bit[7]——UIP标志(Update in Progress),为1表示RTC正在更新日历寄存器组中的值,此时日历寄存器组是不可访问的(此时访问它们将得到一个无意义的渐变值)。
(2)bit[6:4]——这三位是“除法器控制位”(divider-control bits),用来定义RTC的操作频率。各种可能的值如下:
Divider bits Time-base frequency Divider Reset Operation Mode
DV2 DV1 DV0
0 0 0 4.194304 MHZ NO YES
0 0 1 1.048576 MHZ NO YES
0 1 0 32.769 KHZ NO YES
1 1 0/1 任何 YES NO
PC机通常将Divider bits设置成“010”。
(3)bit[3:0]——速率选择位(Rate Selection bits),用于周期性或方波信号输出。
RS bits 4.194304或1.048578 MHZ 32.768 KHZ
RS3 RS2 RS1 RS0 周期性中断 方波 周期性中断 方波
0 0 0 0 None None None None
0 0 0 1 30.517μs 32.768 KHZ 3.90625ms 256 HZ
0 0 1 0 61.035μs 16.384 KHZ
0 0 1 1 122.070μs 8.192KHZ
0 1 0 0 244.141μs 4.096KHZ
0 1 0 1 488.281μs 2.048KHZ
0 1 1 0 976.562μs 1.024KHZ
0 1 1 1 1.953125ms 512HZ
1 0 0 0 3.90625ms 256HZ
1 0 0 1 7.8125ms 128HZ
1 0 1 0 15.625ms 64HZ
1 0 1 1 31.25ms 32HZ
1 1 0 0 62.5ms 16HZ
1 1 0 1 125ms 8HZ
1 1 1 0 250ms 4HZ
1 1 1 1 500ms 2HZ
PC机BIOS对其默认的设置值是“0110”。

状态寄存器B的格式如下所示:
各位的含义如下:
(1)bit[7]——SET标志。为1表示RTC的所有更新过程都将终止,用户程序随后马上对日历寄存器组中的值进行初始化设置。为0表示将允许更新过程继续。
(2)bit[6]——PIE标志,周期性中断使能标志。
(3)bit[5]——AIE标志,告警中断使能标志。
(4)bit[4]——UIE标志,更新结束中断使能标志。
(5)bit[3]——SQWE标志,方波信号使能标志。
(6)bit[2]——DM标志,用来控制日历寄存器组的数据模式,0=BCD,1=BINARY。BIOS总是将它设置为0。
(7)bit[1]——24/12标志,用来控制hour寄存器,0表示12小时制,1表示24小时制。PC机BIOS总是将它设置为1。
(8)bit[0]——DSE标志。BIOS总是将它设置为0。

状态寄存器C的格式如下:
(1)bit[7]——IRQF标志,中断请求标志,当该位为1时,说明寄存器B中断请求发生。
(2)bit[6]——PF标志,周期性中断标志,为1表示发生周期性中断请求。
(3)bit[5]——AF标志,告警中断标志,为1表示发生告警中断请求。
(4)bit[4]——UF标志,更新结束中断标志,为1表示发生更新结束中断请求。

状态寄存器D的格式如下:
(1)bit[7]——VRT标志(Valid RAM and Time),为1表示OK,为0表示RTC已经掉电。
(2)bit[6:0]——总是为0,未定义。

7.1.1.2 通过I/O端口访问RTC
在PC机中可以通过I/O端口0x70和0x71来读写RTC芯片中的寄存器。其中,端口0x70是RTC的寄存器地址索引端口,0x71是数据端口。
读RTC芯片寄存器的步骤是:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
in al, 71h ;
写RTC寄存器的步骤如下:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
mov al, value
out 71h, al

7.1.2 可编程间隔定时器PIT
每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号。当前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址是0x40~0x43。
Intel 8254 PIT有3个计时通道,每个通道都有其不同的用途:
(1) 通道0用来负责更新系统时钟。每当一个时钟滴答过去时,它就会通过IRQ0向系统产生一次时钟中断。
(2) 通道1通常用于控制DMAC对RAM的刷新。
(3) 通道2被连接到PC机的扬声器,以产生方波信号。
每个通道都有一个向下减小的计数器,8254 PIT的输入时钟信号的频率是1193181HZ,也即一秒钟输入1193181个clock-cycle。每输入一个clock-cycle其时间通道的计数器就向下减1,一直减到0值。因此对于通道0而言,当他的计数器减到0时,PIT就向系统产生一次时钟中断,表示一个时钟滴答已经过去了。当各通道的计数器减到0时,我们就说该通道处于“Terminal count”状态。
通道计数器的最大值是10000h,所对应的时钟中断频率是1193181/(65536)=18.2HZ,也就是说,此时一秒钟之内将产生18.2次时钟中断。

7.1.2.1 PIT的I/O端口
在i386平台上,8254芯片的各寄存器的I/O端口地址如下:
Port Description
40h Channel 0 counter(read/write)
41h Channel 1 counter(read/write)
42h Channel 2 counter(read/write)
43h PIT control word(write only)
其中,由于通道0、1、2的计数器是一个16位寄存器,而相应的端口却都是8位的,因此读写通道计数器必须进行进行两次I/O端口读写操作,分别对应于计数器的高字节和低字节,至于是先读写高字节再读写低字节,还是先读写低字节再读写高字节,则由PIT的控制寄存器来决定。8254 PIT的控制寄存器的格式如下:
(1)bit[7:6]——Select Counter,选择对那个计数器进行操作。“00”表示选择Counter 0,“01”表示选择Counter 1,“10”表示选择Counter 2,“11”表示Read-Back Command(仅对于8254,对于8253无效)。
(2)bit[5:4]——Read/Write/Latch格式位。“00”表示锁存(Latch)当前计数器的值;“01”只读写计数器的高字节(MSB);“10”只读写计数器的低字节(LSB);“11”表示先读写计数器的LSB,再读写MSB。
(3)bit[3:1]——Mode bits,控制各通道的工作模式。“000”对应Mode 0;“001”对应Mode 1;“010”对应Mode 2;“011”对应Mode 3;“100”对应Mode 4;“101”对应Mode 5。
(4)bit[0]——控制计数器的存储模式。0表示以二进制格式存储,1表示计数器中的值以BCD格式存储。

7.1.2.2 PIT通道的工作模式
PIT各通道可以工作在下列6种模式下:
1. Mode 0:当通道处于“Terminal count”状态时产生中断信号。
2. Mode 1:Hardware retriggerable one-shot。
3. Mode 2:Rate Generator。这种模式典型地被用来产生实时时钟中断。此时通道的信号输出管脚OUT初始时被设置为高电平,并以此持续到计数器的值减到1。然后在接下来的这个clock-cycle期间,OUT管脚将变为低电平,直到计数器的值减到0。当计数器的值被自动地重新加载后,OUT管脚又变成高电平,然后重复上述过程。通道0通常工作在这个模式下。
4. Mode 3:方波信号发生器。
5. Mode 4:Software triggered strobe。
6. Mode 5:Hardware triggered strobe。

7.1.2.3 锁存计数器(Latch Counter)
当控制寄存器中的bit[5:4]设置成0时,将把当前通道的计数器值锁存。此时通过I/O端口可以读到一个稳定的计数器值,因为计数器表面上已经停止向下计数(PIT芯片内部并没有停止向下计数)。NOTE!一旦发出了锁存命令,就要马上读计数器的值。

7.1.3 时间戳记数器TSC
从Pentium开始,所有的Intel 80x86 CPU就都又包含一个64位的时间戳记数器(TSC)的寄存器。该寄存器实际上是一个不断增加的计数器,它在CPU的每个时钟信号到来时加1(也即每一个clock-cycle输入CPU时,该计数器的值就加1)。
汇编指令rdtsc可以用于读取TSC的值。利用CPU的TSC,操作系统通常可以得到更为精准的时间度量。假如clock-cycle的频率是400MHZ,那么TSC就将每2.5纳秒增加一次。







第七章 Linux内核的时钟中断
(By 詹荣开,NUDT)





Copyright &copy; 2003 by 詹荣开
E-mail:zhanrk@sohu.com
Linux-2.4.0
Version 1.0.0,2003-2-14




摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的时钟中断、内核对时间的表示等。本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:Linux、时钟、定时器

申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。

你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA

欢迎各位指出文档中的错误与疑问。
前言
时间在一个操作系统内核中占据着重要的地位,它是驱动一个OS内核运行的“起博器”。一般说来,内核主要需要两种类型的时间:
1. 在内核运行期间持续记录当前的时间与日期,以便内核对某些对象和事件作时间标记(timestamp,也称为“时间戳”),或供用户通过时间syscall进行检索。
2. 维持一个固定周期的定时器,以提醒内核或用户一段时间已经过去了。
PC机中的时间是有三种时钟硬件提供的,而这些时钟硬件又都基于固定频率的晶体振荡器来提供时钟方波信号输入。这三种时钟硬件是:(1)实时时钟(Real Time Clock,RTC);(2)可编程间隔定时器(Programmable Interval Timer,PIT);(3)时间戳计数器(Time Stamp Counter,TSC)。

7.1 时钟硬件
7.1.1 实时时钟RTC
自从IBM PC AT起,所有的PC机就都包含了一个叫做实时时钟(RTC)的时钟芯片,以便在PC机断电后仍然能够继续保持时间。显然,RTC是通过主板上的电池来供电的,而不是通过PC机电源来供电的,因此当PC机关掉电源后,RTC仍然会继续工作。通常,CMOS RAM和RTC被集成到一块芯片上,因此RTC也称作“CMOS Timer”。最常见的RTC芯片是MC146818(Motorola)和DS12887(maxim),DS12887完全兼容于MC146818,并有一定的扩展。本节内容主要基于MC146818这一标准的RTC芯片。具体内容可以参考MC146818的Datasheet。

7.1.1.1 RTC寄存器
MC146818 RTC芯片一共有64个寄存器。它们的芯片内部地址编号为0x00~0x3F(不是I/O端口地址),这些寄存器一共可以分为三组:
(1)时钟与日历寄存器组:共有10个(0x00~0x09),表示时间、日历的具体信息。在PC机中,这些寄存器中的值都是以BCD格式来存储的(比如23dec=0x23BCD)。
(2)状态和控制寄存器组:共有4个(0x0A~0x0D),控制RTC芯片的工作方式,并表示当前的状态。
(3)CMOS配置数据:通用的CMOS RAM,它们与时间无关,因此我们不关心它。
时钟与日历寄存器组的详细解释如下:
Address Function
00 Current second for RTC
01 Alarm second
02 Current minute
03 Alarm minute
04 Current hour
05 Alarm hour
06 Current day of week(01=Sunday)
07 Current date of month
08 Current month
09 Current year(final two digits,eg:93)

状态寄存器A(地址0x0A)的格式如下:
其中:
(1)bit[7]——UIP标志(Update in Progress),为1表示RTC正在更新日历寄存器组中的值,此时日历寄存器组是不可访问的(此时访问它们将得到一个无意义的渐变值)。
(2)bit[6:4]——这三位是“除法器控制位”(divider-control bits),用来定义RTC的操作频率。各种可能的值如下:
Divider bits Time-base frequency Divider Reset Operation Mode
DV2 DV1 DV0
0 0 0 4.194304 MHZ NO YES
0 0 1 1.048576 MHZ NO YES
0 1 0 32.769 KHZ NO YES
1 1 0/1 任何 YES NO
PC机通常将Divider bits设置成“010”。
(3)bit[3:0]——速率选择位(Rate Selection bits),用于周期性或方波信号输出。
RS bits 4.194304或1.048578 MHZ 32.768 KHZ
RS3 RS2 RS1 RS0 周期性中断 方波 周期性中断 方波
0 0 0 0 None None None None
0 0 0 1 30.517μs 32.768 KHZ 3.90625ms 256 HZ
0 0 1 0 61.035μs 16.384 KHZ
0 0 1 1 122.070μs 8.192KHZ
0 1 0 0 244.141μs 4.096KHZ
0 1 0 1 488.281μs 2.048KHZ
0 1 1 0 976.562μs 1.024KHZ
0 1 1 1 1.953125ms 512HZ
1 0 0 0 3.90625ms 256HZ
1 0 0 1 7.8125ms 128HZ
1 0 1 0 15.625ms 64HZ
1 0 1 1 31.25ms 32HZ
1 1 0 0 62.5ms 16HZ
1 1 0 1 125ms 8HZ
1 1 1 0 250ms 4HZ
1 1 1 1 500ms 2HZ
PC机BIOS对其默认的设置值是“0110”。

状态寄存器B的格式如下所示:
各位的含义如下:
(1)bit[7]——SET标志。为1表示RTC的所有更新过程都将终止,用户程序随后马上对日历寄存器组中的值进行初始化设置。为0表示将允许更新过程继续。
(2)bit[6]——PIE标志,周期性中断使能标志。
(3)bit[5]——AIE标志,告警中断使能标志。
(4)bit[4]——UIE标志,更新结束中断使能标志。
(5)bit[3]——SQWE标志,方波信号使能标志。
(6)bit[2]——DM标志,用来控制日历寄存器组的数据模式,0=BCD,1=BINARY。BIOS总是将它设置为0。
(7)bit[1]——24/12标志,用来控制hour寄存器,0表示12小时制,1表示24小时制。PC机BIOS总是将它设置为1。
(8)bit[0]——DSE标志。BIOS总是将它设置为0。

状态寄存器C的格式如下:
(1)bit[7]——IRQF标志,中断请求标志,当该位为1时,说明寄存器B中断请求发生。
(2)bit[6]——PF标志,周期性中断标志,为1表示发生周期性中断请求。
(3)bit[5]——AF标志,告警中断标志,为1表示发生告警中断请求。
(4)bit[4]——UF标志,更新结束中断标志,为1表示发生更新结束中断请求。

状态寄存器D的格式如下:
(1)bit[7]——VRT标志(Valid RAM and Time),为1表示OK,为0表示RTC已经掉电。
(2)bit[6:0]——总是为0,未定义。

7.1.1.2 通过I/O端口访问RTC
在PC机中可以通过I/O端口0x70和0x71来读写RTC芯片中的寄存器。其中,端口0x70是RTC的寄存器地址索引端口,0x71是数据端口。
读RTC芯片寄存器的步骤是:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
in al, 71h ;
写RTC寄存器的步骤如下:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
mov al, value
out 71h, al

7.1.2 可编程间隔定时器PIT
每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号。当前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址是0x40~0x43。
Intel 8254 PIT有3个计时通道,每个通道都有其不同的用途:
(1) 通道0用来负责更新系统时钟。每当一个时钟滴答过去时,它就会通过IRQ0向系统产生一次时钟中断。
(2) 通道1通常用于控制DMAC对RAM的刷新。
(3) 通道2被连接到PC机的扬声器,以产生方波信号。
每个通道都有一个向下减小的计数器,8254 PIT的输入时钟信号的频率是1193181HZ,也即一秒钟输入1193181个clock-cycle。每输入一个clock-cycle其时间通道的计数器就向下减1,一直减到0值。因此对于通道0而言,当他的计数器减到0时,PIT就向系统产生一次时钟中断,表示一个时钟滴答已经过去了。当各通道的计数器减到0时,我们就说该通道处于“Terminal count”状态。
通道计数器的最大值是10000h,所对应的时钟中断频率是1193181/(65536)=18.2HZ,也就是说,此时一秒钟之内将产生18.2次时钟中断。

7.1.2.1 PIT的I/O端口
在i386平台上,8254芯片的各寄存器的I/O端口地址如下:
Port Description
40h Channel 0 counter(read/write)
41h Channel 1 counter(read/write)
42h Channel 2 counter(read/write)
43h PIT control word(write only)
其中,由于通道0、1、2的计数器是一个16位寄存器,而相应的端口却都是8位的,因此读写通道计数器必须进行进行两次I/O端口读写操作,分别对应于计数器的高字节和低字节,至于是先读写高字节再读写低字节,还是先读写低字节再读写高字节,则由PIT的控制寄存器来决定。8254 PIT的控制寄存器的格式如下:
(1)bit[7:6]——Select Counter,选择对那个计数器进行操作。“00”表示选择Counter 0,“01”表示选择Counter 1,“10”表示选择Counter 2,“11”表示Read-Back Command(仅对于8254,对于8253无效)。
(2)bit[5:4]——Read/Write/Latch格式位。“00”表示锁存(Latch)当前计数器的值;“01”只读写计数器的高字节(MSB);“10”只读写计数器的低字节(LSB);“11”表示先读写计数器的LSB,再读写MSB。
(3)bit[3:1]——Mode bits,控制各通道的工作模式。“000”对应Mode 0;“001”对应Mode 1;“010”对应Mode 2;“011”对应Mode 3;“100”对应Mode 4;“101”对应Mode 5。
(4)bit[0]——控制计数器的存储模式。0表示以二进制格式存储,1表示计数器中的值以BCD格式存储。

7.1.2.2 PIT通道的工作模式
PIT各通道可以工作在下列6种模式下:
1. Mode 0:当通道处于“Terminal count”状态时产生中断信号。
2. Mode 1:Hardware retriggerable one-shot。
3. Mode 2:Rate Generator。这种模式典型地被用来产生实时时钟中断。此时通道的信号输出管脚OUT初始时被设置为高电平,并以此持续到计数器的值减到1。然后在接下来的这个clock-cycle期间,OUT管脚将变为低电平,直到计数器的值减到0。当计数器的值被自动地重新加载后,OUT管脚又变成高电平,然后重复上述过程。通道0通常工作在这个模式下。
4. Mode 3:方波信号发生器。
5. Mode 4:Software triggered strobe。
6. Mode 5:Hardware triggered strobe。

7.1.2.3 锁存计数器(Latch Counter)
当控制寄存器中的bit[5:4]设置成0时,将把当前通道的计数器值锁存。此时通过I/O端口可以读到一个稳定的计数器值,因为计数器表面上已经停止向下计数(PIT芯片内部并没有停止向下计数)。NOTE!一旦发出了锁存命令,就要马上读计数器的值。

7.1.3 时间戳记数器TSC
从Pentium开始,所有的Intel 80x86 CPU就都又包含一个64位的时间戳记数器(TSC)的寄存器。该寄存器实际上是一个不断增加的计数器,它在CPU的每个时钟信号到来时加1(也即每一个clock-cycle输入CPU时,该计数器的值就加1)。
汇编指令rdtsc可以用于读取TSC的值。利用CPU的TSC,操作系统通常可以得到更为精准的时间度量。假如clock-cycle的频率是400MHZ,那么TSC就将每2.5纳秒增加一次。

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:56 |显示全部楼层
.2 Linux内核对RTC的编程
MC146818 RTC芯片(或其他兼容芯片,如DS12887)可以在IRQ8上产生周期性的中断,中断的频率在2HZ~8192HZ之间。与MC146818 RTC对应的设备驱动程序实现在include/linux/rtc.h和drivers/char/rtc.c文件中,对应的设备文件是/dev/rtc(major=10,minor=135,只读字符设备)。因此用户进程可以通过对她进行编程以使得当RTC到达某个特定的时间值时激活IRQ8线,从而将RTC当作一个闹钟来用。
而Linux内核对RTC的唯一用途就是把RTC用作“离线”或“后台”的时间与日期维护器。当Linux内核启动时,它从RTC中读取时间与日期的基准值。然后再运行期间内核就完全抛开RTC,从而以软件的形式维护系统的当前时间与日期,并在需要时将时间回写到RTC芯片中。
Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h头文件中分别定义了mc146818 RTC芯片各寄存器的含义以及RTC芯片在i386平台上的I/O端口操作。而通用的RTC接口则声明在include/linux/rtc.h头文件中。

7.2.1 RTC芯片的I/O端口操作
Linux在include/asm-i386/mc146818rtc.h头文件中定义了RTC芯片的I/O端口操作。端口0x70被称为“RTC端口0”,端口0x71被称为“RTC端口1”,如下所示:
#ifndef RTC_PORT
#define RTC_PORT(x) (0x70 + (x))
#define RTC_ALWAYS_BCD 1 /* RTC operates in binary mode */
#endif
显然,RTC_PORT(0)就是指端口0x70,RTC_PORT(1)就是指I/O端口0x71。
端口0x70被用作RTC芯片内部寄存器的地址索引端口,而端口0x71则被用作RTC芯片内部寄存器的数据端口。再读写一个RTC寄存器之前,必须先把该寄存器在RTC芯片内部的地址索引值写到端口0x70中。根据这一点,读写一个RTC寄存器的宏定义CMOS_READ()和CMOS_WRITE()如下:
#define CMOS_READ(addr) ({ \
outb_p((addr),RTC_PORT(0)); \
inb_p(RTC_PORT(1)); \
})
#define CMOS_WRITE(val, addr) ({ \
outb_p((addr),RTC_PORT(0)); \
outb_p((val),RTC_PORT(1)); \
})
#define RTC_IRQ 8
在上述宏定义中,参数addr是RTC寄存器在芯片内部的地址值,取值范围是0x00~0x3F,参数val是待写入寄存器的值。宏RTC_IRQ是指RTC芯片所连接的中断请求输入线号,通常是8。

7.2.2 对RTC寄存器的定义
Linux在include/linux/mc146818rtc.h这个头文件中定义了RTC各寄存器的含义。

(1)寄存器内部地址索引的定义
Linux内核仅使用RTC芯片的时间与日期寄存器组和控制寄存器组,地址为0x00~0x09之间的10个时间与日期寄存器的定义如下:
#define RTC_SECONDS 0
#define RTC_SECONDS_ALARM 1
#define RTC_MINUTES 2
#define RTC_MINUTES_ALARM 3
#define RTC_HOURS 4
#define RTC_HOURS_ALARM 5
/* RTC_*_alarm is always true if 2 MSBs are set */
# define RTC_ALARM_DONT_CARE 0xC0

#define RTC_DAY_OF_WEEK 6
#define RTC_DAY_OF_MONTH 7
#define RTC_MONTH 8
#define RTC_YEAR 9

四个控制寄存器的地址定义如下:
#define RTC_REG_A 10
#define RTC_REG_B 11
#define RTC_REG_C 12
#define RTC_REG_D 13

(2)各控制寄存器的状态位的详细定义
控制寄存器A(0x0A)主要用于选择RTC芯片的工作频率,因此也称为RTC频率选择寄存器。因此Linux用一个宏别名RTC_FREQ_SELECT来表示控制寄存器A,如下:
#define RTC_FREQ_SELECT RTC_REG_A
RTC频率寄存器中的位被分为三组:①bit[7]表示UIP标志;②bit[6:4]用于除法器的频率选择;③bit[3:0]用于速率选择。它们的定义如下:
# define RTC_UIP 0x80
# define RTC_DIV_CTL 0x70
/* Periodic intr. / Square wave rate select. 0=none, 1=32.8kHz,... 15=2Hz */
# define RTC_RATE_SELECT 0x0F
正如7.1.1.1节所介绍的那样,bit[6:4]有5中可能的取值,分别为除法器选择不同的工作频率或用于重置除法器,各种可能的取值如下定义所示:
/* divider control: refclock values 4.194 / 1.049 MHz / 32.768 kHz */
# define RTC_REF_CLCK_4MHZ 0x00
# define RTC_REF_CLCK_1MHZ 0x10
# define RTC_REF_CLCK_32KHZ 0x20
/* 2 values for divider stage reset, others for "testing purposes only" */
# define RTC_DIV_RESET1 0x60
# define RTC_DIV_RESET2 0x70

寄存器B中的各位用于使能/禁止RTC的各种特性,因此控制寄存器B(0x0B)也称为“控制寄存器”,Linux用宏别名RTC_CONTROL来表示控制寄存器B,它与其中的各标志位的定义如下所示:
#define RTC_CONTROL RTC_REG_B
# define RTC_SET 0x80 /* disable updates for clock setting */
# define RTC_PIE 0x40 /* periodic interrupt enable */
# define RTC_AIE 0x20 /* alarm interrupt enable */
# define RTC_UIE 0x10 /* update-finished interrupt enable */
# define RTC_SQWE 0x08 /* enable square-wave output */
# define RTC_DM_BINARY 0x04 /* all time/date values are BCD if clear */
# define RTC_24H 0x02 /* 24 hour mode - else hours bit 7 means pm */
# define RTC_DST_EN 0x01 /* auto switch DST - works f. USA only */

寄存器C是RTC芯片的中断请求状态寄存器,Linux用宏别名RTC_INTR_FLAGS来表示寄存器C,它与其中的各标志位的定义如下所示:
#define RTC_INTR_FLAGS RTC_REG_C
/* caution - cleared by read */
# define RTC_IRQF 0x80 /* any of the following 3 is active */
# define RTC_PF 0x40
# define RTC_AF 0x20
# define RTC_UF 0x10

寄存器D仅定义了其最高位bit[7],以表示RTC芯片是否有效。因此寄存器D也称为RTC的有效寄存器。Linux用宏别名RTC_VALID来表示寄存器D,如下:
#define RTC_VALID RTC_REG_D
# define RTC_VRT 0x80 /* valid RAM and time */

(3)二进制格式与BCD格式的相互转换
由于时间与日期寄存器中的值可能以BCD格式存储,也可能以二进制格式存储,因此需要定义二进制格式与BCD格式之间的相互转换宏,以方便编程。如下:
#ifndef BCD_TO_BIN
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
#endif

#ifndef BIN_TO_BCD
#define BIN_TO_BCD(val) ((val)=(((val)/10)<<4) + (val)%10)
#endif

7.2.3 内核对RTC的操作
如前所述,Linux内核与RTC进行互操作的时机只有两个:(1)内核在启动时从RTC中读取启动时的时间与日期;(2)内核在需要时将时间与日期回写到RTC中。为此,Linux内核在arch/i386/kernel/time.c文件中实现了函数get_cmos_time()来进行对RTC的第一种操作。显然,get_cmos_time()函数仅仅在内核启动时被调用一次。而对于第二种操作,Linux则同样在arch/i386/kernel/time.c文件中实现了函数set_rtc_mmss(),以支持向RTC中回写当前时间与日期。下面我们将来分析这二个函数的实现。
在分析get_cmos_time()函数之前,我们先来看看RTC芯片对其时间与日期寄存器组的更新原理。

(1)Update In Progress
当控制寄存器B中的SET标志位为0时,MC146818芯片每秒都会在芯片内部执行一个“更新周期”(Update Cycle),其作用是增加秒寄存器的值,并检查秒寄存器是否溢出。如果溢出,则增加分钟寄存器的值,如此一致下去直到年寄存器。在“更新周期”期间,时间与日期寄存器组(0x00~0x09)是不可用的,此时如果读取它们的值将得到未定义的值,因为MC146818在整个更新周期期间会把时间与日期寄存器组从CPU总线上脱离,从而防止软件程序读到一个渐变的数据。
在MC146818的输入时钟频率(也即晶体增荡器的频率)为4.194304MHZ或1.048576MHZ的情况下,“更新周期”需要花费248us,而对于输入时钟频率为32.768KHZ的情况,“更新周期”需要花费1984us=1.984ms。控制寄存器A中的UIP标志位用来表示MC146818是否正处于更新周期中,当UIP从0变为1的那个时刻,就表示MC146818将在稍后马上就开更新周期。在UIP从0变到1的那个时刻与MC146818真正开始Update Cycle的那个时刻之间时有一段时间间隔的,通常是244us。也就是说,在UIP从0变到1的244us之后,时间与日期寄存器组中的值才会真正开始改变,而在这之间的244us间隔内,它们的值并不会真正改变。如下图所示:

(2)get_cmos_time()函数
该函数只被内核的初始化例程time_init()和内核的APM模块所调用。其源码如下:
/* not static: needed by APM */
unsigned long get_cmos_time(void)
{
unsigned int year, mon, day, hour, min, sec;
int i;

/* The Linux interpretation of the CMOS clock register contents:
* When the Update-In-Progress (UIP) flag goes from 1 to 0, the
* RTC registers show the second which has precisely just started.
* Let's hope other operating systems interpret the RTC the same way.
*/
/* read RTC exactly on falling edge of update flag */
for (i = 0 ; i < 1000000 ; i++) /* may take up to 1 second... */
if (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)
break;
for (i = 0 ; i < 1000000 ; i++) /* must try at least 2.228 ms */
if (!(CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))
break;
do { /* Isn't this overkill ? UIP above should guarantee consistency */
sec = CMOS_READ(RTC_SECONDS);
min = CMOS_READ(RTC_MINUTES);
hour = CMOS_READ(RTC_HOURS);
day = CMOS_READ(RTC_DAY_OF_MONTH);
mon = CMOS_READ(RTC_MONTH);
year = CMOS_READ(RTC_YEAR);
} while (sec != CMOS_READ(RTC_SECONDS));
if (!(CMOS_READ(RTC_CONTROL) & RTC_DM_BINARY) || RTC_ALWAYS_BCD)
{
BCD_TO_BIN(sec);
BCD_TO_BIN(min);
BCD_TO_BIN(hour);
BCD_TO_BIN(day);
BCD_TO_BIN(mon);
BCD_TO_BIN(year);
}
if ((year += 1900) < 1970)
year += 100;
return mktime(year, mon, day, hour, min, sec);
}
对该函数的注释如下:
(1)在从RTC中读取时间时,由于RTC存在Update Cycle,因此软件发出读操作的时机是很重要的。对此,get_cmos_time()函数通过UIP标志位来解决这个问题:第一个for循环不停地读取RTC频率选择寄存器中的UIP标志位,并且只要读到UIP的值为1就马上退出这个for循环。第二个for循环同样不停地读取UIP标志位,但他只要一读到UIP的值为0就马上退出这个for循环。这两个for循环的目的就是要在软件逻辑上同步RTC的Update Cycle,显然第二个for循环最大可能需要2.228ms(TBUC+max(TUC)=244us+1984us=2.228ms)
(2)从第二个for循环退出后,RTC的Update Cycle已经结束。此时我们就已经把当前时间逻辑定准在RTC的当前一秒时间间隔内。也就是说,这是我们就可以开始从RTC寄存器中读取当前时间值。但是要注意,读操作应该保证在244us内完成(准确地说,读操作要在RTC的下一个更新周期开始之前完成,244us的限制是过分偏执的:-)。所以,get_cmos_time()函数接下来通过CMOS_READ()宏从RTC中依次读取秒、分钟、小时、日期、月份和年分。这里的do{}while(sec!=CMOS_READ(RTC_SECOND))循环就是用来确保上述6个读操作必须在下一个Update Cycle开始之前完成。
(3)接下来判定时间的数据格式,PC机中一般总是使用BCD格式的时间,因此需要通过BCD_TO_BIN()宏把BCD格式转换为二进制格式。
(4)接下来对年分进行修正,以将年份转换为“19XX”的格式,如果是1970以前的年份,则将其加上100。
(5)最后调用mktime()函数将当前时间与日期转换为相对于1970-01-01 00:00:00的秒数值,并将其作为函数返回值返回。

函数mktime()定义在include/linux/time.h头文件中,它用来根据Gauss算法将以year/mon/day/hour/min/sec(如1980-12-31 23:59:59)格式表示的时间转换为相对于1970-01-01 00:00:00这个UNIX时间基准以来的相对秒数。其源码如下:
static inline unsigned long
mktime (unsigned int year, unsigned int mon,
unsigned int day, unsigned int hour,
unsigned int min, unsigned int sec)
{
if (0 >= (int) (mon -= 2)) { /* 1..12 -> 11,12,1..10 */
mon += 12; /* Puts Feb last since it has leap day */
year -= 1;
}

return (((
(unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) +
year*365 - 719499
)*24 + hour /* now have hours */
)*60 + min /* now have minutes */
)*60 + sec; /* finally seconds */
}

(3)set_rtc_mmss()函数
该函数用来更新RTC中的时间,它仅有一个参数nowtime,是以秒数表示的当前时间,其源码如下:
static int set_rtc_mmss(unsigned long nowtime)
{
int retval = 0;
int real_seconds, real_minutes, cmos_minutes;
unsigned char save_control, save_freq_select;

/* gets recalled with irq locally disabled */
spin_lock(&rtc_lock);
save_control = CMOS_READ(RTC_CONTROL); /* tell the clock it's being set */
CMOS_WRITE((save_control|RTC_SET), RTC_CONTROL);

save_freq_select = CMOS_READ(RTC_FREQ_SELECT); /* stop and reset prescaler */
CMOS_WRITE((save_freq_select|RTC_DIV_RESET2), RTC_FREQ_SELECT);

cmos_minutes = CMOS_READ(RTC_MINUTES);
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD)
BCD_TO_BIN(cmos_minutes);

/*
* since we're only adjusting minutes and seconds,
* don't interfere with hour overflow. This avoids
* messing with unknown time zones but requires your
* RTC not to be off by more than 15 minutes
*/
real_seconds = nowtime % 60;
real_minutes = nowtime / 60;
if (((abs(real_minutes - cmos_minutes) + 15)/30) & 1)
real_minutes += 30; /* correct for half hour time zone */
real_minutes %= 60;

if (abs(real_minutes - cmos_minutes) < 30) {
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD) {
BIN_TO_BCD(real_seconds);
BIN_TO_BCD(real_minutes);
}
CMOS_WRITE(real_seconds,RTC_SECONDS);
CMOS_WRITE(real_minutes,RTC_MINUTES);
} else {
printk(KERN_WARNING
"set_rtc_mmss: can't update from %d to %d\n",
cmos_minutes, real_minutes);
retval = -1;
}

/* The following flags have to be released exactly in this order,
* otherwise the DS12887 (popular MC146818A clone with integrated
* battery and quartz) will not reset the oscillator and will not
* update precisely 500 ms later. You won't find this mentioned in
* the Dallas Semiconductor data sheets, but who believes data
* sheets anyway ... -- Markus Kuhn
*/
CMOS_WRITE(save_control, RTC_CONTROL);
CMOS_WRITE(save_freq_select, RTC_FREQ_SELECT);
spin_unlock(&rtc_lock);

return retval;
}
对该函数的注释如下:
(1)首先对自旋锁rtc_lock进行加锁。定义在arch/i386/kernel/time.c文件中的全局自旋锁rtc_lock用来串行化所有CPU对RTC的操作。
(2)接下来,在RTC控制寄存器中设置SET标志位,以便通知RTC软件程序随后马上将要更新它的时间与日期。为此先把RTC_CONTROL寄存器的当前值读到变量save_control中,然后再把值(save_control | RTC_SET)回写到寄存器RTC_CONTROL中。
(3)然后,通过RTC_FREQ_SELECT寄存器中bit[6:4]重启RTC芯片内部的除法器。为此,类似地先把RTC_FREQ_SELECT寄存器的当前值读到变量save_freq_select中,然后再把值(save_freq_select | RTC_DIV_RESET2)回写到RTC_FREQ_SELECT寄存器中。
(4)接着将RTC_MINUTES寄存器的当前值读到变量cmos_minutes中,并根据需要将它从BCD格式转化为二进制格式。
(5)从nowtime参数中得到当前时间的秒数和分钟数。分别保存到real_seconds和real_minutes变量。注意,这里对于半小时区的情况要修正分钟数real_minutes的值。
(6)然后,在real_minutes与RTC_MINUTES寄存器的原值cmos_minutes二者相差不超过30分钟的情况下,将real_seconds和real_minutes所表示的时间值写到RTC的秒寄存器和分钟寄存器中。当然,在回写之前要记得把二进制转换为BCD格式。
(7)最后,恢复RTC_CONTROL寄存器和RTC_FREQ_SELECT寄存器原来的值。这二者的先后次序是:先恢复RTC_CONTROL寄存器,再恢复RTC_FREQ_SELECT寄存器。然后在解除自旋锁rtc_lock后就可以返回了。

最后,需要说明的一点是,set_rtc_mmss()函数尽可能在靠近一秒时间间隔的中间位置(也即500ms处)左右被调用。此外,Linux内核对每一次成功的更新RTC时间都留下时间轨迹,它用一个系统全局变量last_rtc_update来表示内核最近一次成功地对RTC进行更新的时间(单位是秒数)。该变量定义在arch/i386/kernel/time.c文件中:
/* last time the cmos clock got updated */
static long last_rtc_update;
每一次成功地调用set_rtc_mmss()函数后,内核都会马上将last_rtc_update更新为当前时间(具体请见7.4.3节)。

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:56 |显示全部楼层
7.3 Linux对时间的表示
通常,操作系统可以使用三种方法来表示系统的当前时间与日期:①最简单的一种方法就是直接用一个64位的计数器来对时钟滴答进行计数。②第二种方法就是用一个32位计数器来对秒进行计数,同时还用一个32位的辅助计数器对时钟滴答计数,之子累积到一秒为止。因为232超过136年,因此这种方法直至22世纪都可以让系统工作得很好。③第三种方法也是按时钟滴答进行计数,但是是相对于系统启动以来的滴答次数,而不是相对于相对于某个确定的外部时刻;当读外部后备时钟(如RTC)或用户输入实际时间时,根据当前的滴答次数计算系统当前时间。
UNIX类操作系统通常都采用第三种方法来维护系统的时间与日期。

7.3.1 基本概念
首先,有必要明确一些Linux内核时钟驱动中的基本概念。
(1)时钟周期(clock cycle)的频率:8253/8254 PIT的本质就是对由晶体振荡器产生的时钟周期进行计数,晶体振荡器在1秒时间内产生的时钟脉冲个数就是时钟周期的频率。Linux用宏CLOCK_TICK_RATE来表示8254 PIT的输入时钟脉冲的频率(在PC机中这个值通常是1193180HZ),该宏定义在include/asm-i386/timex.h头文件中:
#define CLOCK_TICK_RATE 1193180 /* Underlying HZ */
(2)时钟滴答(clock tick):我们知道,当PIT通道0的计数器减到0值时,它就在IRQ0上产生一次时钟中断,也即一次时钟滴答。PIT通道0的计数器的初始值决定了要过多少时钟周期才产生一次时钟中断,因此也就决定了一次时钟滴答的时间间隔长度。
(3)时钟滴答的频率(HZ):也即1秒时间内PIT所产生的时钟滴答次数。类似地,这个值也是由PIT通道0的计数器初值决定的(反过来说,确定了时钟滴答的频率值后也就可以确定8254 PIT通道0的计数器初值)。Linux内核用宏HZ来表示时钟滴答的频率,而且在不同的平台上HZ有不同的定义值。对于ALPHA和IA62平台HZ的值是1024,对于SPARC、MIPS、ARM和i386等平台HZ的值都是100。该宏在i386平台上的定义如下(include/asm-i386/param.h):
#ifndef HZ
#define HZ 100
#endif
根据HZ的值,我们也可以知道一次时钟滴答的具体时间间隔应该是(1000ms/HZ)=10ms。
(4)时钟滴答的时间间隔:Linux用全局变量tick来表示时钟滴答的时间间隔长度,该变量定义在kernel/timer.c文件中,如下:
long tick = (1000000 + HZ/2) / HZ; /* timer interrupt period */
tick变量的单位是微妙(μs),由于在不同平台上宏HZ的值会有所不同,因此方程式tick=1000000÷HZ的结果可能会是个小数,因此将其进行四舍五入成一个整数,所以Linux将tick定义成(1000000+HZ/2)/HZ,其中被除数表达式中的HZ/2的作用就是用来将tick值向上圆整成一个整型数。
另外,Linux还用宏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):
/* LATCH is used in the interval timer and ftape setup. */
#define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */
类似地,被除数表达式中的HZ/2也是用来将LATCH向上圆整成一个整数。

7.3.2 表示系统当前时间的内核数据结构
作为一种UNIX类操作系统,Linux内核显然采用本节一开始所述的第三种方法来表示系统的当前时间。Linux内核在表示系统当前时间时用到了三个重要的数据结构:
①全局变量jiffies:这是一个32位的无符号整数,用来表示自内核上一次启动以来的时钟滴答次数。每发生一次时钟滴答,内核的时钟中断处理函数timer_interrupt()都要将该全局变量jiffies加1。该变量定义在kernel/timer.c源文件中,如下所示:
unsigned long volatile jiffies;
C语言限定符volatile表示jiffies是一个易该变的变量,因此编译器将使对该变量的访问从不通过CPU内部cache来进行。
②全局变量xtime:它是一个timeval结构类型的变量,用来表示当前时间距UNIX时间基准1970-01-01 00:00:00的相对秒数值。结构timeval是Linux内核表示时间的一种格式(Linux内核对时间的表示有多种格式,每种格式都有不同的时间精度),其时间精度是微秒。该结构是内核表示时间时最常用的一种格式,它定义在头文件include/linux/time.h中,如下所示:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
其中,成员tv_sec表示当前时间距UNIX时间基准的秒数值,而成员tv_usec则表示一秒之内的微秒值,且1000000>tv_usec>=0。
Linux内核通过timeval结构类型的全局变量xtime来维持当前时间,该变量定义在kernel/timer.c文件中,如下所示:
/* The current time */
volatile struct timeval xtime __attribute__ ((aligned (16)));
但是,全局变量xtime所维持的当前时间通常是供用户来检索和设置的,而其他内核模块通常很少使用它(其他内核模块用得最多的是jiffies),因此对xtime的更新并不是一项紧迫的任务,所以这一工作通常被延迟到时钟中断的底半部分(bottom half)中来进行。由于bottom half的执行时间带有不确定性,因此为了记住内核上一次更新xtime是什么时候,Linux内核定义了一个类似于jiffies的全局变量wall_jiffies,来保存内核上一次更新xtime时的jiffies值。时钟中断的底半部分每一次更新xtime的时侯都会将wall_jiffies更新为当时的jiffies值。全局变量wall_jiffies定义在kernel/timer.c文件中:
/* jiffies at the most recent update of wall time */
unsigned long wall_jiffies;
③全局变量sys_tz:它是一个timezone结构类型的全局变量,表示系统当前的时区信息。结构类型timezone定义在include/linux/time.h头文件中,如下所示:
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of dst correction */
};
基于上述结构,Linux在kernel/time.c文件中定义了全局变量sys_tz表示系统当前所处的时区信息,如下所示:
struct timezone sys_tz;

7.3.3 Linux对TSC的编程实现
Linux用定义在arch/i386/kernel/time.c文件中的全局变量use_tsc来表示内核是否使用CPU的TSC寄存器,use_tsc=1表示使用TSC,use_tsc=0表示不使用TSC。该变量的值是在time_init()初始化函数中被初始化的(详见下一节)。该变量的定义如下:
static int use_tsc;
宏cpu_has_tsc可以确定当前系统的CPU是否配置有TSC寄存器。此外,宏CONFIG_X86_TSC也表示是否存在TSC寄存器。

7.3.3.1 读TSC寄存器的宏操作
x86 CPU的rdtsc指令将TSC寄存器的高32位值读到EDX寄存器中、低32位读到EAX寄存器中。Linux根据不同的需要,在rdtsc指令的基础上封装几个高层宏操作,以读取TSC寄存器的值。它们均定义在include/asm-i386/msr.h头文件中,如下:
#define rdtsc(low,high) \
__asm__ __volatile__("rdtsc" : "=a" (low), "=d" (high))

#define rdtscl(low) \
__asm__ __volatile__ ("rdtsc" : "=a" (low) : : "edx")

#define rdtscll(val) \
__asm__ __volatile__ ("rdtsc" : "=A" (val))
宏rdtsc()同时读取TSC的LSB与MSB,并分别保存到宏参数low和high中。宏rdtscl则只读取TSC寄存器的LSB,并保存到宏参数low中。宏rdtscll读取TSC的当前64位值,并将其保存到宏参数val这个64位变量中。

7.3.3.2 校准TSC
与可编程定时器PIT相比,用TSC寄存器可以获得更精确的时间度量。但是在可以使用TSC之前,它必须精确地确定1个TSC计数值到底代表多长的时间间隔,也即到底要过多长时间间隔TSC寄存器才会加1。Linux内核用全局变量fast_gettimeoffset_quotient来表示这个值,其定义如下(arch/i386/kernel/time.c):
/* Cached *multiplier* to convert TSC counts to microseconds.
* (see the equation below).
* Equal to 2^32 * (1 / (clocks per usec) ).
* Initialized in time_init.
*/
unsigned long fast_gettimeoffset_quotient;
根据上述定义的注释我们可以看出,这个变量的值是通过下述公式来计算的:
fast_gettimeoffset_quotient = (2^32) / (每微秒内的时钟周期个数)
定义在arch/i386/kernel/time.c文件中的函数calibrate_tsc()就是根据上述公式来计算fast_gettimeoffset_quotient的值的。显然这个计算过程必须在内核启动时完成,因此,函数calibrate_tsc()只被初始化函数time_init()所调用。

用TSC实现高精度的时间服务
在拥有TSC(TimeStamp Counter)的x86 CPU上,Linux内核可以实现微秒级的高精度定时服务,也即可以确定两次时钟中断之间的某个时刻的微秒级时间值。如下图所示:
图7-7 TSC时间关系

从上图中可以看出,要确定时刻x的微秒级时间值,就必须确定时刻x距上一次时钟中断产生时刻的时间间隔偏移offset_usec的值(以微秒为单位)。为此,内核定义了以下两个变量:
(1)中断服务执行延迟delay_at_last_interrupt:由于从产生时钟中断的那个时刻到内核时钟中断服务函数timer_interrupt真正在CPU上执行的那个时刻之间是有一段延迟间隔的,因此,Linux内核用变量delay_at_last_interrupt来表示这一段时间延迟间隔,其定义如下(arch/i386/kernel/time.c):
/* Number of usecs that the last interrupt was delayed */
static int delay_at_last_interrupt;
关于delay_at_last_interrupt的计算步骤我们将在分析timer_interrupt()函数时讨论。
(2)全局变量last_tsc_low:它表示中断服务timer_interrupt真正在CPU上执行时刻的TSC寄存器值的低32位(LSB)。
显然,通过delay_at_last_interrupt、last_tsc_low和时刻x处的TSC寄存器值,我们就可以完全确定时刻x距上一次时钟中断产生时刻的时间间隔偏移offset_usec的值。实现在arch/i386/kernel/time.c中的函数do_fast_gettimeoffset()就是这样计算时间间隔偏移的,当然它仅在CPU配置有TSC寄存器时才被使用,后面我们会详细分析这个函数。

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:56 |显示全部楼层
7.4 时钟中断的驱动
如前所述,8253/8254 PIT的通道0通常被用来在IRQ0上产生周期性的时钟中断。对时钟中断的驱动是绝大数操作系统内核实现time-keeping的关键所在。不同的OS对时钟驱动的要求也不同,但是一般都包含下列要求内容:
1. 维护系统的当前时间与日期。
2. 防止进程运行时间超出其允许的时间。
3. 对CPU的使用情况进行记帐统计。
4. 处理用户进程发出的时间系统调用。
5. 对系统某些部分提供监视定时器。
其中,第一项功能是所有OS都必须实现的基础功能,它是OS内核的运行基础。通常有三种方法可用来维护系统的时间与日期:(1)最简单的一种方法就是用一个64位的计数器来对时钟滴答进行计数。(2)第二种方法就是用一个32位计数器来对秒进行计数。用一个32位的辅助计数器来对时钟滴答计数直至累计一秒为止。因为232超过136年,因此这种方法直至22世纪都可以工作得很好。(3)第三种方法也是按滴答进行计数,但却是相对于系统启动以来的滴答次数,而不是相对于一个确定的外部时刻。当读后备时钟(如RTC)或用户输入实际时间时,根据当前的滴答次数计算系统当前时间。
UNIX类的OS通常都采用第三种方法来维护系统的时间与日期。

7.4.1 Linux对时钟中断的初始化
Linux对时钟中断的初始化是分为几个步骤来进行的:(1)首先,由init_IRQ()函数通过调用init_ISA_IRQ()函数对中断向量32~256所对应的中断向量描述符进行初始化设置。显然,这其中也就把IRQ0(也即中断向量32)的中断向量描述符初始化了。(2)然后,init_IRQ()函数设置中断向量32~256相对应的中断门。(3)init_IRQ()函数对PIT进行初始化编程;(4)sched_init()函数对计数器、时间中断的Bottom Half进行初始化。(5)最后,由time_init()函数对Linux内核的时钟中断机制进行初始化。这三个初始化函数都是由init/main.c文件中的start_kernel()函数调用的,如下:
asmlinkage void __init start_kernel()
{

trap_init();
init_IRQ();
sched_init();
time_init();
softirq_init();

}

(1)init_IRQ()函数对8254 PIT的初始化编程
函数init_IRQ()函数在完成中断门的初始化后,就对8254 PIT进行初始化编程设置,设置的步骤如下:(1)设置8254 PIT的控制寄存器(端口0x43)的值为“01100100”,也即选择通道0、先读写LSB再读写MSB、工作模式2、二进制存储格式。(2)将宏LATCH的值写入通道0的计数器中(端口0x40),注意要先写LATCH的LSB,再写LATCH的高字节。其源码如下所示(arch/i386/kernel/i8259.c):
void __init init_IRQ(void)
{
……
/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
……
}

(2)sched_init()对定时器机制和时钟中断的Bottom Half的初始化
函数sched_init()中与时间相关的初始化过程主要有两步:(1)调用init_timervecs()函数初始化内核定时器机制;(2)调用init_bh()函数将BH向量TIMER_BH、TQUEUE_BH和IMMEDIATE_BH所对应的BH函数分别设置成timer_bh()、tqueue_bh()和immediate_bh()函数。如下所示(kernel/sched.c):
void __init sched_init(void)
{
……
init_timervecs();

init_bh(TIMER_BH, timer_bh);
init_bh(TQUEUE_BH, tqueue_bh);
init_bh(IMMEDIATE_BH, immediate_bh);
……
}

(3)time_init()函数对内核时钟中断机制的初始化
前面两个函数所进行的初始化步骤都是为时间中断机制做好准备而已。在执行完init_IRQ()函数和sched_init()函数后,CPU已经可以为IRQ0上的时钟中断进行服务了,因为IRQ0所对应的中断门已经被设置好指向中断服务函数IRQ0x20_interrupt()。但是由于此时中断向量0x20的中断向量描述符irq_desc[0]还是处于初始状态(其status成员的值为IRQ_DISABLED),并未挂接任何具体的中断服务描述符,因此这时CPU对IRQ0的中断服务并没有任何具体意义,而只是按照规定的流程空跑一趟。但是当CPU执行完time_init()函数后,情形就大不一样了。
函数time_init()主要做三件事:(1)从RTC中获取内核启动时的时间与日期;(2)在CPU有TSC的情况下校准TSC,以便为后面使用TSC做好准备;(3)在IRQ0的中断请求描述符中挂接具体的中断服务描述符。其源码如下所示(arch/i386/kernel/time.c):
void __init time_init(void)
{
extern int x86_udelay_tsc;

xtime.tv_sec = get_cmos_time();
xtime.tv_usec = 0;

/*
* If we have APM enabled or the CPU clock speed is variable
* (CPU stops clock on HLT or slows clock to save power)
* then the TSC timestamps may diverge by up to 1 jiffy from
* 'real time' but nothing will break.
* The most frequent case is that the CPU is "woken" from a halt
* state by the timer interrupt itself, so we get 0 error. In the
* rare cases where a driver would "wake" the CPU and request a
* timestamp, the maximum error is < 1 jiffy. But timestamps are
* still perfectly ordered.
* Note that the TSC counter will be reset if APM suspends
* to disk; this won't break the kernel, though, 'cuz we're
* smart. See arch/i386/kernel/apm.c.
*/
/*
* Firstly we have to do a CPU check for chips with
* a potentially buggy TSC. At this point we haven't run
* the ident/bugs checks so we must run this hook as it
* may turn off the TSC flag.
*
* NOTE: this doesnt yet handle SMP 486 machines where only
* some CPU's have a TSC. Thats never worked and nobody has
* moaned if you have the only one in the world - you fix it!
*/

dodgy_tsc();

if (cpu_has_tsc) {
unsigned long tsc_quotient = calibrate_tsc();
if (tsc_quotient) {
fast_gettimeoffset_quotient = tsc_quotient;
use_tsc = 1;
/*
* We could be more selective here I suspect
* and just enable this for the next intel chips ?
*/
x86_udelay_tsc = 1;
#ifndef do_gettimeoffset
do_gettimeoffset = do_fast_gettimeoffset;
#endif
do_get_fast_time = do_gettimeofday;

/* report CPU clock rate in Hz.
* The formula is (10^6 * 2^32) / (2^32 * 1 / (clocks/us)) =
* clock/second. Our precision is about 100 ppm.
*/
{ unsigned long eax=0, edx=1000;
__asm__("divl %2"
:"=a" (cpu_khz), "=d" (edx)
:"r" (tsc_quotient),
"0" (eax), "1" (edx));
printk("Detected %lu.%03lu MHz processor.\n", cpu_khz / 1000, cpu_khz % 1000);
}
}
}

#ifdef CONFIG_VISWS
printk("Starting Cobalt Timer system clock\n");

/* Set the countdown value */
co_cpu_write(CO_CPU_TIMEVAL, CO_TIME_HZ/HZ);

/* Start the timer */
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) | CO_CTRL_TIMERUN);

/* Enable (unmask) the timer interrupt */
co_cpu_write(CO_CPU_CTRL, co_cpu_read(CO_CPU_CTRL) & ~CO_CTRL_TIMEMASK);

/* Wire cpu IDT entry to s/w handler (and Cobalt APIC to IDT) */
setup_irq(CO_IRQ_TIMER, &irq0);
#else
setup_irq(0, &irq0);
#endif
}
对该函数的注解如下:
(1)调用函数get_cmos_time()从RTC中得到系统启动时的时间与日期,它返回的是当前时间相对于1970-01-01 00:00:00这个UNIX时间基准的秒数值。因此这个秒数值就被保存在系统全局变量xtime的tv_sec成员中。而xtime的另一个成员tv_usec则被初始化为0。
(2)通过dodgy_tsc()函数检测CPU是否存在时间戳记数器BUG(I know nothing about it:-)
(3)通过宏cpu_has_tsc来确定系统中CPU是否存在TSC计数器。如果存在TSC,那么内核就可以用TSC来获得更为精确的时间。为了能够用TSC来修正内核时间。这里必须作一些初始化工作:①调用calibrate_tsc()来确定TSC的每一次计数真正代表多长的时间间隔(单位为us),也即一个时钟周期的真正时间间隔长度。②将calibrate_tsc()函数所返回的值保存在全局变量fast_gettimeoffset_quotient中,该变量被用来快速地计算时间偏差;同时还将另一个全局变量use_tsc设置为1,表示内核可以使用TSC。这两个变量都定义在arch/i386/kernel/time.c文件中,如下:
/* Cached *multiplier* to convert TSC counts to microseconds.
* (see the equation below).
* Equal to 2^32 * (1 / (clocks per usec) ).
* Initialized in time_init.
*/
unsigned long fast_gettimeoffset_quotient;
……
static int use_tsc;
③接下来,将系统全局变量x86_udelay_tsc设置为1,表示可以通过TSC来实现微妙级的精确延时。该变量定义在arch/i386/lib/delay.c文件中。④将函数指针do_gettimeoffset强制性地指向函数do_fast_gettimeoffset()(与之对应的是do_slow_gettimeoffset()函数),从而使内核在计算时间偏差时可以用TSC这种快速的方法来进行。⑤将函数指针do_get_fast_time指向函数do_gettimeofday(),从而可以让其他内核模块通过do_gettimeofday()函数来获得更精准的当前时间。⑥计算并报告根据TSC所算得的CPU时钟频率。
(4)不考虑CONFIG_VISWS的情况,因此time_init()的最后一个步骤就是调用setup_irq()函数来为IRQ0挂接具体的中断服务描述符irq0。全局变量irq0是时钟中断请求的中断服务描述符,其定义如下(arch/i386/kernel/time.c):
static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};
显然,函数timer_interrupt()将成为时钟中断的服务程序(ISR),而SA_INTERRUPT标志也指定了timer_interrupt()函数将是在CPU关中断的条件下执行的。结构irq0中的next指针被设置为NULL,因此IRQ0所对应的中断服务队列中只有irq0这唯一的一个元素,且IRQ0不允许中断共享。

7.4.2 时钟中断服务例程timer_interrupt()
中断服务描述符irq0一旦被钩挂到IRQ0的中断服务队列中去后,Linux内核就可以通过irq0->handler函数指针所指向的timer_interrupt()函数对时钟中断请求进行真正的服务,而不是向前面所说的那样只是让CPU“空跑”一趟。此时,Linux内核可以说是真正的“跳动”起来了。
在本节一开始所述的对时钟中断驱动的5项要求中,通常只有第一项(即timekeeping)是最为迫切的,因此必须在时钟中断服务例程中完成。而其余的几个要求可以稍缓,因此可以放在时钟中断的Bottom Half中去执行。这样,Linux内核就是timer_interrupt()函数的执行时间尽可能的短,因为它是在CPU关中断的条件下执行的。
函数timer_interrupt()的源码如下(arch/i386/kernel/time.c):
/*
* This is the same as the above, except we _also_ save the current
* Time Stamp Counter value at the time of the timer interrupt, so that
* we later on can estimate the time of day more exactly.
*/
static void timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
int count;

/*
* Here we are in the timer irq handler. We just have irqs locally
* disabled but we don't know if the timer_bh is running on the other
* CPU. We need to avoid to SMP race with it. NOTE: we don' t need
* the irq version of write_lock because as just said we have irq
* locally disabled. -arca
*/
write_lock(&xtime_lock);

if (use_tsc)
{
/*
* It is important that these two operations happen almost at
* the same time. We do the RDTSC stuff first, since it's
* faster. To avoid any inconsistencies, we need interrupts
* disabled locally.
*/

/*
* Interrupts are just disabled locally since the timer irq
* has the SA_INTERRUPT flag set. -arca
*/

/* read Pentium cycle counter */

rdtscl(last_tsc_low);

spin_lock(&i8253_lock);
outb_p(0x00, 0x43); /* latch the count ASAP */

count = inb_p(0x40); /* read the latched count */
count |= inb(0x40) << 8;
spin_unlock(&i8253_lock);

count = ((LATCH-1) - count) * TICK_SIZE;
delay_at_last_interrupt = (count + LATCH/2) / LATCH;
}

do_timer_interrupt(irq, NULL, regs);

write_unlock(&xtime_lock);

}
对该函数的注释如下:
(1)由于函数执行期间要访问全局时间变量xtime,因此一开就对自旋锁xtime_lock进行加锁。
(2)如果内核使用CPU的TSC寄存器(use_tsc变量非0),那么通过TSC寄存器来计算从时间中断的产生到timer_interrupt()函数真正在CPU上执行这之间的时间延迟:
l 调用宏rdtscl()将64位的TSC寄存器值中的低32位(LSB)读到变量last_tsc_low中,以供do_fast_gettimeoffset()函数计算时间偏差之用。这一步的实质就是将CPU TSC寄存器的值更新到内核对TSC的缓存变量last_tsc_low中。
l 通过读8254 PIT的通道0的计数器的当前值来计算时间延迟,为此:首先,对自旋锁i8253_lock进行加锁。自旋锁i8253_lock的作用就是用来串行化对8254 PIT的读写访问。其次,向8254的控制寄存器(端口0x43)中写入值0x00,以便对通道0的计数器进行锁存。最后,通过端口0x40将通道0的计数器的当前值读到局部变量count中,并解锁i8253_lock。
l 显然,从时间中断的产生到timer_interrupt()函数真正执行这段时间内,以一共流逝了((LATCH-1)-count)个时钟周期,因此这个延时长度可以用如下公式计算:
delay_at_last_interrupt=(((LATCH-1)-count)÷LATCH)﹡TICK_SIZE
显然,上述公式的结果是个小数,应对其进行四舍五入,为此,Linux用下述表达式来计算delay_at_last_interrupt变量的值:
(((LATCH-1)-count)*TICK_SIZE+LATCH/2)/LATCH
上述被除数表达式中的LATCH/2就是用来将结果向上圆整成整数的。
(3)在计算出时间延迟后,最后调用函数do_timer_interrupt()执行真正的时钟服务。

函数do_timer_interrupt()的源码如下(arch/i386/kernel/time.c):
/*
* timer_interrupt() needs to keep up the real-time clock,
* as well as call the "do_timer()" routine every clocktick
*/
static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
。。。。。。
do_timer(regs);
。。。。。。。
/*
* If we have an externally synchronized Linux clock, then update
* CMOS clock accordingly every ~11 minutes. Set_rtc_mmss() has to be
* called as close as possible to 500 ms before the new second starts.
*/
if ((time_status & STA_UNSYNC) == 0 &&
xtime.tv_sec > last_rtc_update + 660 &&
xtime.tv_usec >= 500000 - ((unsigned) tick) / 2 &&
xtime.tv_usec <= 500000 + ((unsigned) tick) / 2) {
if (set_rtc_mmss(xtime.tv_sec) == 0)
last_rtc_update = xtime.tv_sec;
else
last_rtc_update = xtime.tv_sec - 600; /* do it again in 60 s */
}
……
}
上述代码中省略了许多与SMP相关的代码,因为我们不关心SMP。从上述代码我们可以看出,do_timer_interrupt()函数主要作两件事:
(1)调用do_timer()函数。
(2)判断是否需要更新CMOS时钟(即RTC)中的时间。Linux仅在下列三个条件同时成立时才更新CMOS时钟:①系统全局时间状态变量time_status中没有设置STA_UNSYNC标志,也即说明Linux有一个外部同步时钟。实际上全局时间状态变量time_status仅在一种情况下会被清除STA_SYNC标志,那就是执行adjtimex()系统调用时(这个syscall与NTP有关)。②自从上次CMOS时钟更新已经过去了11分钟。全局变量last_rtc_update保存着上次更新CMOS时钟的时间。③由于RTC存在Update Cycle,因此最好在一秒时间间隔的中间位置500ms左右调用set_rtc_mmss()函数来更新CMOS时钟。因此Linux规定仅当全局变量xtime的微秒数tv_usec在500000±(tick/2)微秒范围范围之内时,才调用set_rtc_mmss()函数。如果上述条件均成立,那就调用set_rtc_mmss()将当前时间xtime.tv_sec更新回写到RTC中。
如果上面是的set_rtc_mmss()函数返回0值,则表明更新成功。于是就将“最近一次RTC更新时间”变量last_rtc_update更新为当前时间xtime.tv_sec。如果返回非0值,说明更新失败,于是就让last_rtc_update=xtime.tv_sec-600(相当于last_rtc_update+=60),以便在在60秒之后再次对RTC进行更新。

函数do_timer()实现在kernel/timer.c文件中,其源码如下:
void do_timer(struct pt_regs *regs)
{
(*(unsigned long *)&jiffies)++;
#ifndef CONFIG_SMP
/* SMP process accounting uses the local APIC timer */

update_process_times(user_mode(regs));
#endif
mark_bh(TIMER_BH);
if (TQ_ACTIVE(tq_timer))
mark_bh(TQUEUE_BH);
}
该函数的核心是完成三个任务:
(1)将表示自系统启动以来的时钟滴答计数变量jiffies加1。
(2)调用update_process_times()函数更新当前进程的时间统计信息。注意,该函数的参数原型是“int user_tick”,如果本次时钟中断(即时钟滴答)发生时CPU正处于用户态下执行,则user_tick参数应该为1;否则如果本次时钟中断发生时CPU正处于核心态下执行时,则user_tick参数应改为0。所以这里我们以宏user_mode(regs)来作为update_process_times()函数的调用参数。该宏定义在include/asm-i386/ptrace.h头文件中,它根据regs指针所指向的核心堆栈寄存器结构来判断CPU进入中断服务之前是处于用户态下还是处于核心态下。如下所示:
#ifdef __KERNEL__
#define user_mode(regs) ((VM_MASK & (regs)->eflags) || (3 & (regs)->xcs))
……
#endif
(3)调用mark_bh()函数激活时钟中断的Bottom Half向量TIMER_BH和TQUEUE_BH(注意,TQUEUE_BH仅在任务队列tq_timer不为空的情况下才会被激活)。

至此,内核对时钟中断的服务流程宣告结束,下面我们详细分析一下update_process_times()函数的实现。

7.4.3 更新时间记帐信息——CPU分时的实现
函数update_process_times()被用来在发生时钟中断时更新当前进程以及内核中与时间相关的统计信息,并根据这些信息作出相应的动作,比如:重新进行调度,向当前进程发出信号等。该函数仅有一个参数user_tick,取值为1或0,其含义在前面已经叙述过。
该函数的源代码如下(kernel/timer.c):
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id(), system = user_tick ^ 1;

update_one_process(p, user_tick, system, cpu);
if (p->pid) {
if (--p->counter <= 0) {
p->counter = 0;
p->need_resched = 1;
}
if (p->nice > 0)
kstat.per_cpu_nice[cpu] += user_tick;
else
kstat.per_cpu_user[cpu] += user_tick;
kstat.per_cpu_system[cpu] += system;
} else if (local_bh_count(cpu) || local_irq_count(cpu) > 1)
kstat.per_cpu_system[cpu] += system;
}
(1)首先,用smp_processor_id()宏得到当前进程的CPU ID。
(2)然后,让局部变量system=user_tick^1,表示当发生时钟中断时CPU是否正处于核心态下。因此,如果user_tick=1,则system=0;如果user_tick=0,则system=1。
(3)调用update_one_process()函数来更新当前进程的task_struct结构中的所有与时间相关的统计信息以及成员变量。该函数还会视需要向当前进程发送相应的信号(signal)。
(4)如果当前进程的PID非0,则执行下列步骤来决定是否重新进行调度,并更新内核时间统计信息:
l 将当前进程的可运行时间片长度(由task_struct结构中的counter成员表示,其单位是时钟滴答次数)减1。如果减到0值,则说明当前进程已经用完了系统分配给它的的运行时间片,因此必须重新进行调度。于是将当前进程的task_struct结构中的need_resched成员变量设置为1,表示需要重新执行调度。
l 如果当前进程的task_struct结构中的nice成员值大于0,那么将内核全局统计信息变量kstat中的per_cpu_nice[cpu]值将上user_tick。否则就将user_tick值加到内核全局统计信息变量kstat中的per_cpu_user[cpu]成员上。
l 将system变量值加到内核全局统计信息kstat.per_cpu_system[cpu]上。
(5)否则,就判断当前CPU在服务时钟中断前是否处于softirq软中断服务的执行中,或则正在服务一次低优先级别的硬件中断中。如果是这样的话,则将system变量的值加到内核全局统计信息kstat.per_cpu.system[cpu]上。

l update_one_process()函数
实现在kernel/timer.c文件中的update_one_process()函数用来在时钟中断发生时更新一个进程的task_struc结构中的时间统计信息。其源码如下(kernel/timer.c):

void update_one_process(struct task_struct *p, unsigned long user,
unsigned long system, int cpu)
{
p->per_cpu_utime[cpu] += user;
p->per_cpu_stime[cpu] += system;
do_process_times(p, user, system);
do_it_virt(p, user);
do_it_prof(p);
}
注释如下:
(1)由于在一个进程的整个生命期(Lifetime)中,它可能会在不同的CPU上执行,也即一个进程可能一开始在CPU1上执行,当它用完在CPU1上的运行时间片后,它可能又会被调度到CPU2上去执行。另外,当进程在某个CPU上执行时,它可能又会在用户态和内核态下分别各执行一段时间。所以为了统计这些事件信息,进程task_struct结构中的per_cpu_utime[NR_CPUS]数组就表示该进程在各CPU的用户台下执行的累计时间长度,per_cpu_stime[NR_CPUS]数组就表示该进程在各CPU的核心态下执行的累计时间长度;它们都以时钟滴答次数为单位。
所以,update_one_process()函数的第一个步骤就是更新进程在当前CPU上的用户态执行时间统计per_cpu_utime[cpu]和核心态执行时间统计per_cpu_stime[cpu]。
(2)调用do_process_times()函数更新当前进程的总时间统计信息。
(3)调用do_it_virt()函数为当前进程的ITIMER_VIRTUAL软件定时器更新时间间隔。
(4)调用do_it_prof()函数为当前进程的ITIMER_PROF软件定时器更新时间间隔。

l do_process_times()函数
函数do_process_times()将更新指定进程的总时间统计信息。每个进程task_struct结构中都有一个成员times,它是一个tms结构类型(include/linux/times.h):
struct tms {
clock_t tms_utime; /* 本进程在用户台下的执行时间总和 */
clock_t tms_stime; /* 本进程在核心态下的执行时间总和 */
clock_t tms_cutime; /* 所有子进程在用户态下的执行时间总和 */
clock_t tms_cstime; /* 所有子进程在核心态下的执行时间总和 */
};
上述结构的所有成员都以时钟滴答次数为单位。
函数do_process_times()的源码如下(kernel/timer.c):
static inline void do_process_times(struct task_struct *p,
unsigned long user, unsigned long system)
{
unsigned long psecs;

psecs = (p->times.tms_utime += user);
psecs += (p->times.tms_stime += system);
if (psecs / HZ > p->rlim[RLIMIT_CPU].rlim_cur) {
/* Send SIGXCPU every second.. */
if (!(psecs % HZ))
send_sig(SIGXCPU, p, 1);
/* and SIGKILL when we go over max.. */
if (psecs / HZ > p->rlim[RLIMIT_CPU].rlim_max)
send_sig(SIGKILL, p, 1);
}
}
注释如下:
(1)根据参数user更新指定进程task_struct结构中的times.tms_utime值。根据参数system更新指定进程task_struct结构中的times.tms_stime值。
(2)将更新后的times.tms_utime值与times.tms_stime值的和保存到局部变量psecs中,因此psecs就表示了指定进程p到目前为止已经运行的总时间长度(以时钟滴答次数计)。如果这一总运行时间长超过进程P的资源限额,那就每隔1秒给进程发送一个信号SIGXCPU;如果运行时间长度超过了进程资源限额的最大值,那就发送一个SIGKILL信号杀死该进程。

l do_it_virt()函数
每个进程都有一个用户态执行时间的itimer软件定时器。进程任务结构task_struct中的it_virt_value成员是这个软件定时器的时间计数器。当进程在用户态下执行时,每一次时钟滴答都使计数器it_virt_value减1,当减到0时内核向进程发送SIGVTALRM信号,并重置初值。初值保存在进程的task_struct结构的it_virt_incr成员中。
函数do_it_virt()的源码如下(kernel/timer.c):
static inline void do_it_virt(struct task_struct * p, unsigned long ticks)
{
unsigned long it_virt = p->it_virt_value;

if (it_virt) {
it_virt -= ticks;
if (!it_virt) {
it_virt = p->it_virt_incr;
send_sig(SIGVTALRM, p, 1);
}
p->it_virt_value = it_virt;
}
}

l do_it_prof()函数
类似地,每个进程也都有一个itimer软件定时器ITIMER_PROF。进程task_struct中的it_prof_value成员就是这个定时器的时间计数器。不管进程是在用户态下还是在内核态下运行,每个时钟滴答都使it_prof_value减1。当减到0时内核就向进程发送SIGPROF信号,并重置初值。初值保存在进程task_struct结构中的it_prof_incr成员中。
函数do_it_prof()就是用来完成上述功能的,其源码如下(kernel/timer.c):
static inline void do_it_prof(struct task_struct *p)
{
unsigned long it_prof = p->it_prof_value;

if (it_prof) {
if (--it_prof == 0) {
it_prof = p->it_prof_incr;
send_sig(SIGPROF, p, 1);
}
p->it_prof_value = it_prof;
}
}

论坛徽章:
3
金牛座
日期:2014-06-14 22:04:062015年辞旧岁徽章
日期:2015-03-03 16:54:152015年迎新春徽章
日期:2015-03-04 09:49:45
发表于 2008-11-06 17:57 |显示全部楼层
7.5 时钟中断的Bottom Half
与时钟中断相关的Bottom Half向两主要有两个:TIMER_BH和TQUEUE_BH。与TIMER_BH相对应的BH函数是timer_bh(),与TQUEUE_BH对应的函数是tqueue_bh()。它们均实现在kernel/timer.c文件中。

7.5.1 TQUEUE_BH向量
TQUEUE_BH的作用是用来运行tq_timer这个任务队列中的任务。因此do_timer()函数仅仅在tq_timer任务队列不为空的情况才激活TQUEUE_BH向量。函数tqueue_bh()的实现非常简单,它只是简单地调用run_task_queue()函数来运行任务队列tq_timer。如下所示:
void tqueue_bh(void)
{
run_task_queue(&tq_timer);
}
任务对列tq_timer也是定义在kernel/timer.c文件中,如下所示:
DECLARE_TASK_QUEUE(tq_timer);

7.5.2 TIMER_BH向量
TIMER_BH这个Bottom Half向量是Linux内核时钟中断驱动的一个重要辅助部分。内核在每一次对时钟中断的服务快要结束时,都会无条件地激活一个TIMER_BH向量,以使得内核在稍后一段延迟后执行相应的BH函数——timer_bh()。该任务的源码如下:
void timer_bh(void)
{
update_times();
run_timer_list();
}
从上述源码可以看出,内核在时钟中断驱动的底半部分主要有两个任务:(1)调用update_times()函数来更新系统全局时间xtime;(2)调用run_timer_list()函数来执行定时器。关于定时器我们将在下一节讨论。本节我们主要讨论TIMER_BH的第一个任务——对内核时间xtime的更新。
我们都知道,内核局部时间xtime是用来供用户程序通过时间syscall来检索或设置当前系统时间的,而内核代码在大多数情况下都引用jiffies变量,而很少使用xtime(偶尔也会有引用xtime的情况,比如更新inode的时间标记)。因此,对于时钟中断服务程序timer_interrupt()而言,jiffies变量的更新是最紧迫的,而xtime的更新则可以延迟到中断服务的底半部分来进行。
由于Bottom Half机制在执行时间具有某些不确定性,因此在timer_bh()函数得到真正执行之前,期间可能又会有几次时钟中断发生。这样就会造成时钟滴答的丢失现象。为了处理这种情况,Linux内核使用了一个辅助全局变量wall_jiffies,来表示上一次更新xtime时的jiffies值。其定义如下(kernel/timer.c):
/* jiffies at the most recent update of wall time */
unsigned long wall_jiffies;
而timer_bh()函数真正执行时的jiffies值与wall_jiffies的差就是在timer_bh()真正执行之前所发生的时钟中断次数。
函数update_times()的源码如下(kernel/timer.c):
static inline void update_times(void)
{
unsigned long ticks;

/*
* update_times() is run from the raw timer_bh handler so we
* just know that the irqs are locally enabled and so we don't
* need to save/restore the flags of the local CPU here. -arca
*/
write_lock_irq(&xtime_lock);

ticks = jiffies - wall_jiffies;
if (ticks) {
wall_jiffies += ticks;
update_wall_time(ticks);
}
write_unlock_irq(&xtime_lock);
calc_load(ticks);
}
(1)首先,根据jiffies和wall_jiffies的差值计算在此之前一共发生了几次时钟滴答,并将这个值保存到局部变量ticks中。并在ticks值大于0的情况下(ticks大于等于1,一般情况下为1):①更新wall_jiffies为jiffies变量的当前值(wall_jiffies+=ticks等价于wall_jiffies=jiffies)。②以参数ticks调用update_wall_time()函数去真正地更新全局时间xtime。
(2)调用calc_load()函数去计算系统负载情况。这里我们不去深究它。

函数update_wall_time()函数根据参数ticks所指定的时钟滴答次数相应地更新内核全局时间变量xtime。其源码如下(kernel/timer.c):
/*
* Using a loop looks inefficient, but "ticks" is
* usually just one (we shouldn't be losing ticks,
* we're doing this this way mainly for interrupt
* latency reasons, not because we think we'll
* have lots of lost timer ticks
*/
static void update_wall_time(unsigned long ticks)
{
do {
ticks--;
update_wall_time_one_tick();
} while (ticks);

if (xtime.tv_usec >= 1000000) {
xtime.tv_usec -= 1000000;
xtime.tv_sec++;
second_overflow();
}
}
对该函数的注释如下:
(1)首先,用一个do{}循环来根据参数ticks的值一次一次调用update_wall_time_one_tick()函数来为一次时钟滴答更新xtime中的tv_usec成员。
(2)根据需要调整xtime中的秒数成员tv_usec和微秒数成员tv_usec。如果微秒数成员tv_usec的值超过106,则说明已经过了一秒钟。因此将tv_usec的值减去1000000,并将秒数成员tv_sec的值加1,然后调用second_overflow()函数来处理微秒数成员溢出的情况。

函数update_wall_time_one_tick()用来更新一次时钟滴答对系统全局时间xtime的影响。由于tick全局变量表示了一次时钟滴答的时间间隔长度(以us为单位),因此该函数的实现中最核心的代码就是将xtime的tv_usec成员增加tick微秒。这里我们不去关心函数实现中与NTP(Network Time Protocol)和系统调用adjtimex()的相关部分。其源码如下(kernel/timer.c):
/* in the NTP reference this is called "hardclock()" */
static void update_wall_time_one_tick(void)
{
if ( (time_adjust_step = time_adjust) != 0 ) {
/* We are doing an adjtime thing.
*
* Prepare time_adjust_step to be within bounds.
* Note that a positive time_adjust means we want the clock
* to run faster.
*
* Limit the amount of the step to be in the range
* -tickadj .. +tickadj
*/
if (time_adjust > tickadj)
time_adjust_step = tickadj;
else if (time_adjust < -tickadj)
time_adjust_step = -tickadj;

/* Reduce by this step the amount of time left */
time_adjust -= time_adjust_step;
}
xtime.tv_usec += tick + time_adjust_step;
/*
* Advance the phase, once it gets to one microsecond, then
* advance the tick more.
*/
time_phase += time_adj;
if (time_phase <= -FINEUSEC) {
long ltemp = -time_phase >> SHIFT_SCALE;
time_phase += ltemp << SHIFT_SCALE;
xtime.tv_usec -= ltemp;
}
else if (time_phase >= FINEUSEC) {
long ltemp = time_phase >> SHIFT_SCALE;
time_phase -= ltemp << SHIFT_SCALE;
xtime.tv_usec += ltemp;
}
}
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP