linux中断处理之初始化
2008-03-12 10:58:16
分类: LINUX
------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
http://blog.chinaunix.net/uid/20543183.html
------------------------------------------
一:引言
在Intel的文档中,把中断分为两种.一种是异常,也叫同步同断.一种称之为中断,也叫异常中断.
同步中断指的是由CPU控制单元产生,之所以称之为同步,是因为只有一条指令执行完毕后才会发出中断.例如除法运算中,除数为零的时候,就会产生一个异常
异步中断是由外部设备按照CPU的时钟随机产生的.例如,网卡检测到一个数据到来就会产生一个中断.
二:x86的中断处理过程
由于中断是开着的,所以当执行完一条指令后,cs和eip这对寄存器中已经包含了下一条将要执行的指令的逻辑地址。在处理那条指令之前,控制单元会检查在运行前一条指令时是否发生了一个中断或异常。如果发生了一个中断和异常,那么控制单元执行下列操作:
1. 确定与中断或异常关联的向量i(0≤ i ≤255)
2. 读由idtr寄存器指向的IDT表中的第i项。
3. 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符标识的段描述符。这个描述符指定中断或异常处理程序所在的段的基地址。
4. 确信中断是由授权的(中断)发生源发出的。首先将当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较。如果CPL小于DPL,就产生一个“通常保护”异常,因为中断处理程序的特权级不能低于引起中断的程序的特权。对于编程异常,则做进一步的安全检查:比较CPL与处于IDT中的门描述符的DPL,如果DPL小于CPL,就产生一个“通常保护”异常,这最后
一个检查可以避免用户应用程序访问特殊的陷阱门和中断门。
5. 检查是否发生了特权级的变化,也就是说,CPL是否不同于所选择的段描述符的DPL。如果是,控制单元必须开始使用与新的特权级相关的栈,通过执行以下步骤来保证这一点:
A. 读tr寄存器,以访问运行进程的TSS段。
B. 用与新特权级相关的栈段和栈指针的正确值装载ss和esp寄存器。这些值可以在TSS中找到。
C. 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
6. 如果故障已发生,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行。
7. 在栈中保存eflag、cs和eip的内容。
8. 如果异常产生了一个硬件出错码,则将它保存在栈中。
9. 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
控制单元所执行的最后一步就是跳转到中断或异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。
上面的处理过程的描述摘自<<深入理解linux内核>>,其中有几点值得注意的地方:
1:通过门后,只能提高运行级别.就像上面所述的 “当前特权级CPL(存放在cs寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较。如果CPL小于DPL,就产生一个“通常保护”异常”.在中断处理中,通常把IDT中的相应段选择符设为__KERNEL_CS.即最高的运行级别
2:上面C所述:“在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址”,那ss,esp以前的值是如何找到的呢?应该是从TSS中.在中断发生的时候,如果检测到运行级别发生了改了,将寄存器SS,ESP中的值保存进TSS的相应级别位置.再加载新的SS,ESP的值,然后从TSS中取出旧的SS,ESP值,再压栈.
3:堆栈的改变,如下图所示:
从上图中我们可以看到
,硬件自动保存的硬件环境是非常少
,要在中断后恢复到以前的环境
,还需要保存更多的寄存器值
,这是由操作系统完成的
.这我们在以后的代码分析中可以看到
中断和异常被处理完毕后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程,这将迫使控制单元:
1. 用保存在栈中的值装载cs、eip和eflag寄存器。如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码。
2. 检查处理程序的CPL是否等于cs中的低两位的值。如果是,iret终止返回;否则,转入下一步。
3. 从栈中转载ss和esp寄存器,因此,返回到与旧特权级相关的栈。
4. 检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么,清相关的段寄存器。控制单元这么做是为了禁止用户态的程序利用内核以前所用的段寄存器。如果不清除这些寄存器的话,恶意的用户程序就会利用他们来访问内核地址空间。
注意到4:举例说明一下.如果通过系统调用进入内核态.然后将DS,ES的值赋为__KERNEL_DS(在2.4的内核里),处理完后(调用iret后),恢复CS,EIP的值,此时CS的CPL是3.因为DS,ES被设为了__KERNEL_DS,所以其DPL是0,所以要将DS,ES中的值清除.在2.6内核中,发生中断或异常后,将DS,ES的值设为了__USER_DS,避免了上述的清除过程,提高了效率.
三:重要的数据结构
在深入源代码之前,先把所用到的数据结构分析如下:
Irq_desc[]定义如下:
extern irq_desc_t irq_desc [NR_IRQS]
typedef struct irq_desc {
unsigned int status; /* IRQ的状态;IRQ 是否被禁止了,有关IRQ 的设备当前是否正被自动检测*/
hw_irq_controller *handler;/*指向一个中断控制器的指针*/
c *action; /* 挂在IRQ上的中断处理程序 */
unsigned int depth; /* 为0:该IRQ被启用,如果为一个正数,表示被禁用 */
unsigned int irq_count; /* 该IRQ发生的中断的次数 */
unsigned int irqs_unhandled; /*该IRQ线上没有被处理的IRQ总数*/
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;
Hw_irq_controller定义如下:
struct hw_interrupt_type {
const char * typename; /*中断控制器的名字*/
unsigned int (*startup)(unsigned int irq); /*允许从IRQ线产生中断*/
void (*shutdown)(unsigned int irq); /*禁止从IRQ线产生中断*/
void (*enable)(unsigned int irq); /*enable与disable函数在8259A中与上述的startup shutdown函数相同*/
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq); /*在IRQ线上产生一个应答*/
void (*end)(unsigned int irq); /*在IRQ处理程序终止时被调用*/
void (*set_affinity)(unsigned int irq, cpumask_t dest); /*在SMP系统中,设置IRQ处理的亲和力*/
}
typedef struct hw_interrupt_type hw_irq_controller;
struct irqaction定义如下:
struct irqaction {
//中断处理例程
irqreturn_t (*handler)(int, void *, struct pt_regs *);
//flags:
//SA_INTERRUPT:中断嵌套
//SA_SAMPLE_RANDOM:这个中断源于物理随机性
//SA_SHIRQ:中断线共享
unsigned long flags;
//在x86平台无用
cpumask_t mask;
//产生中断的硬件名字
const char *name;
//设备ID,一般由厂商指定
void *dev_id;
//下一个irqaction.共享的时候,通常一根中断线对应很多硬件设备的中断处理例程
struct irqaction *next;
}
可以用下图来表示上述数据结构的关系:
四:idt在保护模式下的初始化
有关实模式下的初始化,以后再做专题分析.详情请关注本站更新.
在init/main.c中:
asmlinkage void __init start_kernel(void)
{
……
//设定系统规定的异常/中断
trap_init();
//设置外部IRQ中断
init_IRQ();
……
}
在start_kernel中,调用trap_init()来设置系统规定的异常与中断,调用init_IRQ()来设置外部中断.
void __init trap_init(void)
{
……
set_trap_gate(0,÷_error);
set_intr_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_intr_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_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
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_intr_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
#ifdef CONFIG_X86_MCE
set_trap_gate(18,&machine_check);
#endif
set_trap_gate(19,&simd_coprocessor_error);
set_system_gate(SYSCALL_VECTOR,&system_call); //系统调用
……
}
如上所示,设置了0~19的中断/异常处理程序,这些都是intel所规定的,除些之后设置了系统调用入口(用户空间的 int SYSCALL_VECTOR )
那, set_trap_gate()/set_intr_gate()/set_system_gata()都有一些什么样的区别呢?继续看代码:
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr,__KERNEL_CS);
}
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n, 14, 3, addr, __KERNEL_CS);
}
void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr,__KERNEL_CS);
}
void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr,__KERNEL_CS);
}
都是通过统一的接口_set_gate().在i386中,这段代码是用嵌入式汇编完成的,如下所示:
#define _set_gate(gate_addr,type,dpl,addr,seg) \
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" ((seg) << 16)); \
} while (0)
我们看可以看: _set_gate(gate_addr,type,dpl,addr,seg)中:
Gate_addr:相应IDT项的地址.type:设置IDT项的TYPE字段, 15表示系统门,14表示中断门.dpl:IDT项对应的DPL值,addr:中断处理程序的地址,seg:IDT中对应项的段选择符
由此可以看出,陷阱门与中断门被锁定在内核态(DPL为0),系统门可以从用户态进入.那既然陷阱门与中断门又有什么区别呢?唯一的区别是,通过陷阱门不会改变FLAGES中的中断标志,但是中断门就会改变,即会屏弊中断
接下来看IRQ中断的设置:
void __init init_IRQ(void)
{
int i;
//8259初始化
pre_intr_init_hook();
for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
//FIRST_EXTERNAL_VECTOR:第一个可用号,前面部份均为系统保留
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
//跳过系统调用号
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
}
……
……
}
在深入这段代码之前,我们先看下x86的中断的硬件处理机制.x86中断处理系统一般采用两个8259A芯片级连的方式.每个8259A有8根中断信号线,从片有一根信号线连接至了主片,所以,总共可以处理15个IRQ信号.如下图所示:
来看下具体的代码:
pre_intr_init_hook() -à init_ISA_irqs()
void __init init_ISA_irqs (void)
{
int i;
#ifdef CONFIG_X86_LOCAL_APIC
init_bsp_APIC();
#endif
//初始化8259A芯片
init_8259A(0);
//初始化irq_desc[]数组
for (i = 0; i < NR_IRQS; i++) {
//状态:禁用
irq_desc[i].status = IRQ_DISABLED;
//初始化为NULL.表示无中断处理函数,系统初始化完成之后,可以调用request_irq()注册中断处理函数
irq_desc[i].action = NULL;
//depth值为1,表示当前IRQ线被禁用
irq_desc[i].depth = 1;
// 只使用了15根(两块8259A级联)
if (i < 16) {
//将irq_desc[i].handler:设置为8259A的中断控制器处理
irq_desc[i].handler = &i8259A_irq_type;
} else {
//其它的在x86平台被设为no_irq_type.表示无中断控制器
irq_desc[i].handler = &no_irq_type;
}
}
}
在这个函数里,初始化了irq_desc[]数组.随后,调用了set_intr_gate(vector, interrupt[i])为第n条中断线设置的中断处理函数为interrupt[n-
FIRST_EXTERNAL_VECTOR].
Interrupt[]数组在哪里定义的呢?接下来往下看:
ENTRY(interrupt)
.previous
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
ALIGN
1: pushl $vector-256
jmp common_interrupt
.data
.long 1b
.previous
vector=vector+1
.endr
相当于,interrupt[i]执行下列操作:
Pushl $i-256 //中断号取负再压栈
Jmp common_interrupt //跳转至一段公共的处理函数
至此,保护模式下的中断子系统初始化完成.接下来分析linux如何响应中断
五:中断入口分析
1:IRQ入口分析
如上所述.将中断号取负压栈之后,会跳转一段公共的处理函数,跟进这段函数:
common_interrupt:
SAVE_ALL
call do_IRQ #调用相应的中断处理函数
jmp ret_from_intr #从中断返回
SAVE_ALL定义如下:
#define SAVE_ALL \
__SAVE_ALL; \
__SWITCH_KERNELSPACE; #在没有定义CONFIG_X86_HIGH_ENTRY的情况下,此宏是一个空宏
__SAVE_ALL定义如下:
#define __SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es;
相当于把中断发生时,硬件没有保存的寄存器压栈保存下来.把DS.ES设为了__USER_DS是有一定原因的,参考上节所述.
经过SAVE_ALL后.堆栈内容如下所示:
这个图是从<<中断处理源码情景分析>>一文中摘出来的.事实上图中的用户堆栈指针在IRQ处理中是不可能存在的,因为中断处理对应IDT项的PL值为0,所以,不可能是从用户空间发生的
2:异常处理入口分析
异常处理程序也有很多相同的操作,以16号向量对应的中断处理程序为例:
set_trap_gate(16,&coprocessor_error);
ENTRY(coprocessor_error)
pushl $0 #把0入栈.如果异常没有产生一个硬件出错码,就把0入栈
pushl $do_coprocessor_error #相应的异常处理程序
jmp error_code #跳转到error_code
error_code定义如下:
error_code:
pushl %ds
pushl %eax
xorl %eax, %eax //EAX中的值变为零
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
decl %eax # eax = -1
pushl %ecx
pushl %ebx
cld
movl %es, %ecx
movl ORIG_EAX(%esp), %esi # get the error code
movl ES(%esp), %edi # get the function address
movl %eax, ORIG_EAX(%esp)
movl %ecx, ES(%esp)
pushl %esi # push the error code
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
/* clobbers edx, ebx and ebp */
__SWITCH_KERNELSPACE
leal 4(%esp), %edx # prepare pt_regs
pushl %edx # push pt_regs
call *%edi
addl $8, %esp
jmp ret_from_exception把
在上述代码在cld指令之前的堆栈内容与IRQ处理的堆栈相比,如下示:
其中大部份的数据都是一样的,经过后续处理,堆栈内容会变成如下所示:
经过调整之后变得跟IRQ处理的堆栈一样的了,栈顶指针指向存放EBX的位置,栈顶向上一个存储单位是ERROR_CODE.这样做是因为异常处理程序一般都有两个参数,一个是struct pt_regs.表示所有保存的寄存器的值,一个是unsigned long,表示出错码
这样,就为中断处理跟异常处理构造了一个统一的堆栈,当然这样做只是为了使用统一的参数类型,即struct pt_regs
3:系统调用入口分析
系统调用的处理函数为: system_call,定义如下:
set_system_gate(SYSCALL_VECTOR,&system_call);
ENTRY(system_call)
pushl %eax # save orig_eax(系统调用号).在发生系统调用的时候,系统调用号都是存放在EAX中的
SAVE_ALL #SAVE_ALL在IRQ处理入中已经分析过了
GET_THREAD_INFO(%ebp) #取得当前进程的task描述符
# system call tracing in operation
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry #如果进程被TRACE,跳转至syscall_trace_entry,假设进程没有被trace
cmpl $(nr_syscalls), %eax #在系统调用表中取得相应的处理函数
jae syscall_badsys #系统调用号无效
syscall_call:
call *sys_call_table(,%eax,4) #call相应的处理函数
movl %eax,EAX(%esp) # store the return value
syscall_exit:
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # current->work
jne syscall_exit_work
restore_all:
RESTORE_ALL
六:中断返回分析
1:IRQ中断返回分析
如上节所述,IRQ中断处理如下:
common_interrupt:
SAVE_ALL
call do_IRQ
jmp ret_from_intr
分析一下执行完相应的中断处理函数之后,如何返回,即恢复硬件环境原状
Ret_from_intr定义如下:
ret_from_intr:
GET_THREAD_INFO(%ebp) # 取得当前过程的task描述符
movl EFLAGS(%esp), %eax # 中断前的EFLAGS中的值存进EAX
movb CS(%esp), %al # 将中断前的CS低16移至AL
#至此EFLAGS 的H16和CS的L16构成了EAX的内容
testl $(VM_MASK | 3), %eax # EFLAGS中有一位表示是否运行在vm86模式中
#CS的最低二位表示当前进程的运行级别
jz resume_kernel # 如果中断前不是在用户空间,且不是在VM86模式下,跳转到resume_kernel
ENTRY(resume_userspace) # 用户模式下
cli #开中断,以防中断丢失
movl TI_flags(%ebp), %ecx #将task->flags成员的值存进ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done on 还有事情没做完?
# int/exception return?
jne work_pending #还有事情没有处理完
jmp restore_all #所有事情都处理完了
resume_kernel定义如下:
在没有定义CONFIG_PREEMPT的情况下:
#define resume_kernel restore_all
Restore_all被定义成:
restore_all:
RESTORE_ALL
RESTORE_ALL定义如下:
#define RESTORE_ALL \
__SWITCH_USERSPACE; \ #选择配置项.忽略
__RESTORE_ALL;
转至__RESTORE_ALL:
#define __RESTORE_ALL \
__RESTORE_REGS \ #pop在SAVE_ALL中入栈的寄存器
addl $4, %esp; \ #记否?在SAVE_ALL之前压入了一个中断向量的负值或者是系统调用号
333: iret; #iret中断返回,交给硬件完成中断的返回工作
__RESTORE_REGS被定义成:
#define __RESTORE_REGS \
__RESTORE_INT_REGS; \
#define __RESTORE_INT_REGS \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax
如果编译内核被配置成允许被抢占的情况:
#ifdef CONFIG_PREEMPT
ENTRY(resume_kernel)
cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ?
jnz restore_all #preempt_count为非0,表示此时不能发生抢占,则跳转到restore_all
need_resched:
movl TI_flags(%ebp), %ecx # need_resched set ?
testb $_TIF_NEED_RESCHED, %cl #此时preempt_count=0,如果need_resched标志置位,则发生调度。反之则跳转到restore_all
jz restore_all
#如果是发生了异常,则不会进行抢占调度,此时preempt_count=0,且need_resched置位
testl $IF_MASK,EFLAGS(%esp) # interrupts off (exception path) ?
jz restore_all
#将最大值赋值给preempt_count,表示不允许再次被抢占
movl $PREEMPT_ACTIVE,TI_preempt_count(%ebp)
sti
#调度
call schedule
#preempt_count还原为0
movl $0,TI_preempt_count(%ebp)
cli
#跳转到need_resched,判断是否又需要发生被调度
jmp need_resched
#endif
分析完内核态且不是VM86下的情况,我们接着分析用户态的情况
……
jz resume_kernel # 如果中断前不是在用户空间,且不是在VM86模式下,跳转到resume_kernel
ENTRY(resume_userspace) # 用户模式下
cli #开中断,以防中断丢失
movl TI_flags(%ebp), %ecx #将task->flags成员的值存进ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done on 还有事情没做完?
# int/exception return?
jnev #还有事情没有处理完
jmp restore_all #所有事情都处理完了
restore_all的代码我们在上面已经分析完了.我们继续分析如果还有后续事情没有处理的情况:
work_pending的处理:
work_pending:
#在返回用户空间时,只需要判断need_resched是否置位,不需要判断preempt_count,如果
#置位则发生调度,反之则跳转到work_notifysig
testb $_TIF_NEED_RESCHED, %cl
jz work_notifysig
work_resched:
call schedule
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
#继续测试是否还有其他额外的事情要处理,如果没有,则跳转到restore_all
movl TI_flags(%ebp), %ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
# than syscall tracing?
jz restore_all
#如果need_resched再次被置位,则继续调度,反之,则执行work_notifysig
testb $_TIF_NEED_RESCHED, %cl
jnz work_resched
work_notifysig: # deal with pending signals and
# notify-resume requests
testl $VM_MASK, EFLAGS(%esp) #VM模式
movl %esp, %eax
jne work_notifysig_v86 # returning to kernel-space or
# vm86-space
xorl %edx, %edx
#进行信号处理
call do_notify_resume
# CONFIG_X86_HIGH_ENTRY选择配置项,忽略
#if CONFIG_X86_HIGH_ENTRY
/*
* Reload db7 if necessary:
*/
movl TI_flags(%ebp), %ecx
testb $_TIF_DB7, %cl
jnz work_db7
jmp restore_all
work_db7:
movl TI_task(%ebp), %edx;
movl task_thread_db7(%edx), %edx;
movl %edx, %db7;
#endif
jmp restore_all
由于work_pending涉及到进程的调度与信号处理,详细的处理过程将在后续专题陆续给出.详情请关注本站更新^_^
2:异常返回分析
转回去看下异常处理 error_code的代码:
Error_code:
……
……
call *%edi #调用相应的异常处理函数
addl $8, %esp #esp上移两个存储单元,我们知道,在调整堆栈的时候,把错误码和第一个参数的指针压栈了
jmp ret_from_exception
ret_from_exception如下示:
ret_from_exception:
preempt_stop
ret_from_intr:
GET_THREAD_INFO(%ebp) # 取得当前过程的task描述符
movl EFLAGS(%esp), %eax # 中断前的EFLAGS中的值存进EAX
……
从此我们看到,异常返回的后半部份与IRQ中断返回相比只是多了一个preempt_stop的处理.
Preempt_stop的代码如下:
#ifdef CONFIG_PREEMPT
#define preempt_stop cli
#else
#define preempt_stop
3:系统调用返回分析:
回忆系统调用的处理代码,如下示:
ENTRY(system_call)
……
……
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,EAX(%esp) # 将返回值压入调用前的EAX中
syscall_exit:
cli
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # current->work
jne syscall_exit_work
restore_all:
RESTORE_ALL
Syscall_exit_work代码如下:
syscall_exit_work:
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SINGLESTEP), %cl
jz work_pending
sti # could let do_syscall_trace() call
# schedule() instead
movl %esp, %eax
movl $1, %edx
call do_syscall_trace
jmp resume_userspac
上述很多标号都在前面分析过,这里就不再赘述了.
上面三种情况的返回情况,总结成下图:
七:小结
在这个专题里,主要分析了x86硬件中断处理子系统,异常/中断/系统调用下的入口与返回情况分析,了解了linux中中断的处理流程,在后续的专题里,再对中断响应过程,系统调用过程做更详尽的分析
linux中断处理之IRQ中断
2008-03-12 11:09:58
分类: LINUX
------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------
一:前言
在前一个专题里曾分析过所有IRQ中断处理流程,经过SAVE_ALL保存硬件环境后,都会进入do_IRQ()进行处理,今天接着分析do_IRQ()处理的相关东西.分为两部中断处理程序与软中断两个大的部份进行介绍.
二:中断处理程序
在驱动程序中,通常使用request_irq()来注册中断处理程序.我们先从注册中断处理程序的实现说起.
/*
irq:可断号
handler:中断处理程序
irqflags:中断处理标志.SA_SHIRQ:共享中断线 SA_INTERRUPT:快速处理中断
必须在关中断的情况下运行.SA_SAMPLE_RANDOM:该中断可能用于产生一个随机数
devname dev_id:设备名称与ID
*/
int request_irq(unsigned int irq,
irqreturn_t (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char * devname,
void *dev_id)
{
int retval;
struct irqaction * action;
#if 1
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;
// 分配一个irqaction
action = (struct irqaction *)
kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
if (!action)
return -ENOMEM;
action->handler = handler;
action->flags = irqflags;
cpus_clear(action->mask);
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;
//将创建并初始化完在的action加入irq_desc[NR_IRQS]
retval = setup_irq(irq, action);
if (retval)
kfree(action);
return retval;
}
上面涉及到的irqaction结构与irq_desc[]的关系我们在上一节我们已经详细分析过了,这里不再赘述.
转进setup_irq():
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;
//如果hander == no_irq_type:说明中断控制器不支持该IRQ线
if (desc->handler == &no_irq_type)
return -ENOSYS;
sif (new->flags & SA_SAMPLE_RANDOM) {
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) {
//判断这条中断线上的中断处理程序是否允许SHARE
/* 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;
//如果这一条线还没有被占用,初始化这条中断线
//包含清标志,在8259A上启用这条中断线
if (!shared) {
desc->depth = 0;
desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING | IRQ_INPROGRESS);
desc->handler->startup(irq);
}
spin_unlock_irqrestore(&desc->lock,flags);
//在proc下建立相关的文件
register_irq_proc(irq);
return 0;
}
现在知道怎么打一个中断处理程序挂到irq_desc[NR_IRQS]数组上了,继续分析中断处理中如何调用中断处理函数.从我们开篇时说到的do_IRQ()说起.
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
//屏蔽高位,取得中断号
int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */
//取得中断号对应的desc结构
irq_desc_t *desc = irq_desc + irq;
struct irqaction * action;
unsigned int status;
irq_enter();
// 调试用,忽略
#ifdef CONFIG_DEBUG_STACKOVERFLOW
/* Debugging check for stack overflow: is there less than 1KB free? */
{
long esp;
__asm__ __volatile__("andl %%esp,%0" :
"=r" (esp) : "0" (THREAD_SIZE - 1));
if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) {
printk("do_IRQ: stack overflow: %ld\n",
esp - sizeof(struct thread_info));
dump_stack();
}
}
#endif
//更新统计计数
kstat_this_cpu.irqs[irq]++;
spin_lock(&desc->lock);
//给8259 回一个ack.回ack之后,通常中断控制会屏蔽掉此条IRQ线
desc->handler->ack(irq);
//清除IRQ_REPLAY IRQ_WAITING标志
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
//设置IRQ_PENDING:表示中断被应答,但没有真正被处理
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;
//中断被屏蔽或者正在处理
//IRQ_DIASBLED:中断被禁用
//IRQ_INPROGRESS:这个类型的中断已经在被另一个CPU处理了
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
action = desc->action;
status &= ~IRQ_PENDING; /* we commit to handling */
//置位,表示正在处理中...
status |= c; /* we are handling it */
}
desc->status = status;
//没有挂上相应的中断处理例程或者不满足条件
if (unlikely(!action))
goto out;
for (;;) {
irqreturn_t action_ret;
u32 *isp;
union irq_ctx * curctx;
union irq_ctx * irqctx;
curctx = (union irq_ctx *) current_thread_info();
irqctx = hardirq_ctx[smp_processor_id()];
spin_unlock(&desc->lock);
//通常curctx == irqctx.除非中断程序使用独立的4K堆栈.
if (curctx == irqctx)
action_ret = handle_IRQ_event(irq, ®s, action);
else {
/* build the stack frame on the IRQ stack */
isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
irqctx->tinfo.task = curctx->tinfo.task;
irqctx->tinfo.real_stack = curctx->tinfo.real_stack;
irqctx->tinfo.virtual_stack = curctx->tinfo.virtual_stack;
irqctx->tinfo.previous_esp = current_stack_pointer();
*--isp = (u32) action;
*--isp = (u32) ®s;
*--isp = (u32) irq;
asm volatile(
" xchgl %%ebx,%%esp \n"
" call handle_IRQ_event \n"
" xchgl %%ebx,%%esp \n"
: "=a"(action_ret)
: "b"(isp)
: "memory", "cc", "edx", "ecx"
);
}
spin_lock(&desc->lock);
//调试用,忽略
if (!noirqdebug)
note_interrupt(irq, desc, action_ret, ®s);
if (curctx != irqctx)
irqctx->tinfo.task = NULL;
//如果没有要处理的中断了,退出
if (likely(!(desc->status & IRQ_c)))
break;
//又有中断到来了,继续处理
desc->status &= ~c;
}
//处理完了,清除IRQ_INPROGRESS标志
desc->status &= ~IRQ_INPROGRESS;
out:
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
//处理完了,调用中断控制器的end.通常此函数会使中断控制器恢复IRQ线中断
desc->handler->end(irq);
spin_unlock(&desc->lock);
//irq_exit():理论上中断处理完了,可以处理它的下半部了
irq_exit();
return 1;
}
这段代码比较简单,但里面几个标志让人觉的很迷糊,列举如下:
IRQ_DISABLED:相应的IRQ被禁用.既然中断线被禁用了,也就不会产生中断,进入do_IRQ()了?因为电子器件的各种原因可能会产生 “伪中断”上报给CPU.
IRQ_PENDING:CPU收到这个中断信号了,已经给出了应答,但并末对其进行处理.回顾上面的代码,进入do_IRQ后,发送ack,再设置此标志.
IRQ_ INPROGRESS:表示这条IRQ线的中断正在被处理.为了不弄脏CPU的高速缓存.把相同IRQ线的中断放在一起处理可以提高效率,且使中断处理程序不必重入
举例说明:如果CPU A接收到一个中断信号.回一个ACK,设置c,假设此时末有这个中断线的中断处理程序在处理,继而会将标志位设为IRQ_ INPROGRESS.转去中断处理函数执行.如果此时,CPU B检测到了这条IRQ线的中断信号.它会回一个ACK.设置
IRQ_PENDING.但时此时这条IRQ线的标志为IRQ_ INPROGRESS.所以,它会进经过goto out退出.如果cpu A执行完了中断处理程序,判断它的标志线是否为IRQ_PENDING.因为CPU B已将其设为了IRQ_PENDING.所以继续循环一次.直到循环完后,清除IRQ_INPROGRESS标志
注意上述读写标志都是加锁的.linux采用的这个方法,不能不赞一个 *^_^*
继续看代码:
asmlinkage int handle_IRQ_event(unsigned int irq,
struct pt_regs *regs, struct irqaction *action)
{
int status = 1; /* Force the "do bottom halves" bit */
int ret, retval = 0;
//如果没有设置SA_INTERRUPT.将CPU 中断打开
//应该尽量的避免CPU关中断的情况,因为CPU屏弊本地中断,会使
//中断丢失
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();
//遍历运行中断处理程序
do {
ret = action->handler(irq, action->dev_id, regs);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
//关中断
local_irq_disable();
return retval;
}
可能会有这样的疑问.如果在一根中断线上挂上了很多个中断处理程序,会不会使这一段程序的效率变得很低下呢?事实上,我们在写驱动程序的过程中,都会首先在中断处理程序里判断设备名字与设备ID,只有条件符合的设备中断才会变处理.
三:软中断
为了提高中断的响应速度,很多操作系统都把中断分成了两个部份,上半部份与下半部份.上半部份通常是响应中断,并把中断所得到的数据保存进下半部.耗时的操作一般都会留到下半部去处理.
接下来,我们看一下软中断的处理模型:
Start_kernel() à softirq_init();
在softirq_init()中会注册两个常用类型的软中断,看具体代码:
void __init softirq_init(void)
{
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
//参数含义:nr:软中断类型 action:软中断处理函数 data:软中断处理函数参数
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
在上面的代码中,我们可以看到:open_softirq()中.其实就是对softirq_vec数组的nr项赋值.softirq_vec是一个32元素的数组,实际上linux内核只使用了六项. 如下示:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
SCSI_SOFTIRQ,
TASKLET_SOFTIRQ
}
另外.如果使软中断能被CPU调度,还得要让它激活才可以.激活所使用的函数为__raise_softirq_irqoff()
代码如下:
#define __raise_softirq_irqoff(nr) do { local_softirq_pending() |= 1UL << (nr); } while (0)
这个宏使local_softirq_pending的nr位置1
好了,经过open_softirq()à local_softirq_pending()后,我们来看下软中断怎么被CPU调度.
继续上面中断处理的代码.在处理完硬件中断后,会调用irq_exit().这就是软中断的入口点了,我们来看下
#define irq_exit() \
do { \
preempt_count() -= IRQ_EXIT_OFFSET; \
//注意了,软中断不可以在硬件中断上下文或者是在软中断环境中使用哦 ^_^
//softirq_pending()的判断,注意我们上面分析过的_raise_softirqoff().它判断当前cpu有没有激活软中断
if (!in_interrupt() && softirq_pending(smp_processor_id())) \
do_softirq(); \
preempt_enable_no_resched(); \
} while (0)
跟踪进do_softirq()
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
//在硬件中断环境中,退出
if (in_interrupt())
return;
//禁止本地中断,不要让其受中断的影响
local_irq_save(flags);
pending = local_softirq_pending();
//是否有软中断要处理?
if (pending)
__do_softirq();
//恢复CPU中断
local_irq_restore(flags);
}
转入__do_softirq()
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;
pending = local_softirq_pending();
//禁止软中断,不允许软中断嵌套
local_bh_disable();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
//把挂上去的软中断清除掉,因为我们在这里会全部处理完
local_softirq_pending() = 0;
//开CPU中断
local_irq_enable();
//softirq_vec:32元素数组
h = softirq_vec;
//依次处理挂上去的软中断
do {
if (pending & 1) {
//调用软中断函数
h->action(h);
rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);
//关CPU 中断
local_irq_disable();
pending = local_softirq_pending();
//在规定次数内,如果有新的软中断了,可以继续在这里处理完
if (pending && --max_restart)
goto restart;
//依然有没有处理完的软中断,为了提高系统响应效率,唤醒softirqd进行处理
if (pending)
wakeup_softirqd();
//恢复软中断
__local_bh_enable();
}
从上面的处理流程可以看到,软中断处理就是调用open_ softirq()的action参数.这个函数对应的参数是软中断本身(h->action(h)),采用这样的形式,可以在改变softirq_action结构的时候,不会重写软中断处理函数
在进入了软中断的时候,使用了in_interrupt()来防止软中断嵌套,和抢占硬中断环境。然后软中断以开中断的形式运行,软中断的处理随时都会被硬件中断抢占,由于在软中断运行之前调用了local_bh_disable(),所以in_interrupt()为真,不会执行软中断.
来看下in_interrupt() local_bh_disable() __local_bh_enable()的具体代码:
#define in_interrupt() (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK))
#define local_bh_disable() \
do { preempt_count() += SOFTIRQ_OFFSET; barrier(); } while (0)
#define __local_bh_enable() \
do { barrier(); preempt_count() -= SOFTIRQ_OFFSET; } while (0)
相当于local_bh_disable设置了preempt_count的SOFTIRQ_OFFSET。In_interrupt判断就会返回一个真值
相应的__local_bh_enable()清除了SOFTIRQ_OFFSET标志
还有几个常用的判断,列举如下:
in_softirq():判断是否在一个软中断环境
hardirq_count():判断是否在一个硬中断环境
local_bh_enable()与__local_bh_enable()作用是不相同的:前者不仅会清除SOFTIRQ_OFFSET,还会调用do_softirq(),进行软中断的处理
上述几个判断的代码都很简单,可自行对照分析
四:几种常用的软中断分析
经过上面的分析,看到了linux的软中断处理模式,我们具体分析一下2.6kernel中常用的几种软中断
1:tasklet分析
Tasklet也是俗称的小任务机制,它使用比较方法,另外,还分为了高优先级tasklet与一般tasklet。还记得我们刚开始分析过的softirq_init()这个函数吗
void __init softirq_init(void)
{
//普通优先级
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
//高优先级
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
它们的软中断处理函数其实是tasklet_action与tasklet_hi_action.
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
//禁止本地中断
local_irq_disable();
//per_cpu变量
list = __get_cpu_var(tasklet_vec).list;
//链表置空
__get_cpu_var(tasklet_vec).list = NULL;
//恢复本地中断
local_irq_enable();
//接下来要遍历链表了
while (list) {
struct tasklet_struct *t = list;
list = list->next;
//为了避免竞争,下列操作都是在加锁情况下进行的
if (tasklet_trylock(t)) {
//t->count为零才会调用task_struct里的函数
if (!atomic_read(&t->count)) {
//t->count 为1。但又没有置调度标志。系统BUG
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
//调用tasklet函数
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
//注意 :所有运行过的tasklet全被continue过去了,只有没有运行的tasklet才会重新加入到链表里面
//禁本地中断
local_irq_disable();
//把t放入队列头,准备下一次接收调度
t->next = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
//置软中断调用标志。下次运行到do_softirq的时候,可以继续被调用
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
//启用本地中断
local_irq_enable();
}
}
高优先级tasklet的处理其实与上面分析的函数是一样的,只是per_cpu变量不同而已。
另外,有几个问题值得考虑:
1) cpu怎么计算软中断优先级的
在do_softirq()à__do_softirq()有:
{
pending = local_softirq_pending();
......
do {
if (pending & 1) {
h->action(h);
rcu_bh_qsctr_inc(cpu);
}
h++;
pending >>= 1;
} while (pending);
......
}
从上面看到,从softirq_vec[]中取项是由pending右移位计算的。
另外,在激活软中断的操作中:
#define __raise_softirq_irqoff(nr) do { local_softirq_pending() |= 1UL << (nr); } while (0)
可以看到 nr越小的就会越早被do_softirq遍历到
2) 在什么条件下才会运行tasklet 链表上的任务
我们在上面的代码里看到只有在t->count为零,且设置了TASKLET_STATE_SCHED标志才会被遍历到链表上对应的函数
那在我们自己的代码里该如何使用tasklet呢?举个例子:
#include
#include
#include
#include
static void tasklet_test_handle(unsigned long arg)
{
printk("in tasklet test\n");
}
//声明一个tasklet
DECLARE_TASKLET(tasklet_test,tasklet_test_handle,0);
MODULE_LICENSE("GPL xgr178@163.com");
int kernel_test_init()
{
printk("test_init\n");
//调度这个tasklet
tasklet_schedule(&tasklet_test);
}
int kernel_test_exit()
{
printk("test_exit\n");
//禁用这个tasklet
tasklet_kill(&tasklet_test);
return 0;
}
module_init(kernel_test_init);
module_exit(kernel_test_exit);
示例模块里涉及到tasklet通用的三个API.分别是DECLARE_TASKLET(), tasklet_schedule(),tasklet_kill()
跟踪一下内核代码:
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
实际上,DECLARE_TASKLET就是定义了一个tasklet_struct的变量.相应的tasklet调用函数为func().函数参数为data
static inline void tasklet_schedule(struct tasklet_struct *t)
{
//如果tasklet没有置调度标置,也就是说该tasklet没有被调度
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
void fastcall __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
//把tasklet加到__get_cpu_var(tasklet_vec).list链表头
local_irq_save(flags);
t->next = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
//激活相应的软中断
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}
这个函数比较简单,不详细分析了
void tasklet_kill(struct tasklet_struct *t)
{
//不允许在中断环境中进行此操作
if (in_interrupt())
printk("Attempt to kill tasklet from interrupt\n");
//一直等待tasklet被调度完
while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
do
yield();
while (test_bit(TASKLET_STATE_SCHED, &t->state));
}
//一直等待tasklet被运行完
tasklet_unlock_wait(t);
//清除调度标志
clear_bit(TASKLET_STATE_SCHED, &t->state);
}
该函数会一直等待该tasklet调度并运行完,可能会睡眠,所以不能在中断环境中使用它
2:网络协议栈里专用软中断
在前面分析网络协议协的时候分析过,网卡有两种模式,一种是中断,即数据到来时给CPU上传中断,等到CPU处理中断.第二种是轮询,即在接收到第一个数据包之后,关闭中断,CPU每隔一定时间就去网卡DMA缓冲区取数据.其实,所谓的轮询就是软中断.接下来就来研究一下网络协议栈的软中断
static int __init net_dev_init(void)
{
……
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
……
}
在这里注册了两个软中断,一个用于接收一个用于发送,函数大体差不多,我们以接收为例.从前面的分析可以知道,软中断的处理函数时就是它调用open_softirq的action参数.在这里即是net_rx_action.代码如下:
static void net_rx_action(struct softirq_action *h)
{
//per_cpu链表.所有网卡的轮询处理函数都通过napi_struct结构存放在这链表里面
struct list_head *list = &__get_cpu_var(softnet_data).poll_list;
unsigned long start_time = jiffies;
int budget = netdev_budget;
void *have;
//关中断
local_irq_disable();
//遍历链表
while (!list_empty(list)) {
struct napi_struct *n;
int work, weight;
if (unlikely(budget <= 0 || jiffies != start_time))
goto softnet_break;
local_irq_enable();
// 取链表里的相应数据
n = list_entry(list->next, struct napi_struct, poll_list);
have = netpoll_poll_lock(n);
weight = n->weight;
work = 0;
//如果允许调度,则运行接口的poll函数
if (test_bit(NAPI_STATE_SCHED, &n->state))
work = n->poll(n, weight);
WARN_ON_ONCE(work > weight);
budget -= work;
//关中断
local_irq_disable();
if (unlikely(work == weight)) {
//如果被禁用了,就从链表中删除
if (unlikely(napi_disable_pending(n)))
__napi_complete(n);
Else
//否则加入链表尾,等待下一次调度
list_move_tail(&n->poll_list, list);
}
netpoll_poll_unlock(have);
}
out:
//启用中断
local_irq_enable();
//选择编译部份,忽略
#ifdef CONFIG_NET_DMA
/*
* There may not be any more sk_buffs coming right now, so push
* any pending DMA copies to hardware
*/
if (!cpus_empty(net_dma.channel_mask)) {
int chan_idx;
for_each_cpu_mask(chan_idx, net_dma.channel_mask) {
struct dma_chan *chan = net_dma.channels[chan_idx];
if (chan)
dma_async_memcpy_issue_pending(chan);
}
}
#endif
return;
softnet_break:
__get_cpu_var(netdev_rx_stat).time_squeeze++;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
goto out;
}
一般在接口驱动中,会调用__napi_schedule.将其添加进遍历链表.代码如下示:
void fastcall __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
//加至链表末尾
list_add_tail(&n->poll_list, &__get_cpu_var(softnet_data).poll_list);
//激活软中断
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
local_irq_restore(flags);
}
关于网卡选择哪一种模式最为合适,我们在前面已经讲述过,这里不再赘述.
五:小结
本节主要分析了中断程序的处理过程与软中断的实现.虽然软中断实现有很多种类,究其模型都是一样的,就是把中断的一些费时操作在响应完中断之后再进行.另外,中断与软中断处理中有很多临界区,需要关闭CPU中断和打开CPU中断.其中的奥妙还需要慢慢的体会
Linux中断处理之系统调用
2008-03-20 18:48:08
分类: LINUX
------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------
一:前言
有时候,用户空间为了满足某些要求,要从内核空间去进行操作,比例建立文件,建立socket,查看内核数据等等.因此操作系统必须提供一种方式.供用户态转入内核态.我们在前面分析过tarp_init()函数.只有异常跟系统调用才能从用户空间转入到内核空间(PL值为3).但是异常通常带有很大的随意性,用户程序不好控制异常的发生点.所以,系统调用就成了沟通用户空间与内核空间的一座重要的桥梁.
二:系统调用在用户空间的调用方式.
在前面分析过.系统调用的中断号为0x80.所以,只要在用户空间通过int 0x80软中断方式就可以陷入内核了.为了区分不同的系统调用.必须为每一个调用指定一个序号.即系统调用号.通常,在用int 0x80中断之前,先将中断号放入寄存器eax.
三:系统调用的参数传递方式
系统调用是可以传递参数的.例如:int open(const char *pathname, int flags),那这些参数是如何传递的呢?系统调用采用寄存器来传值,这样,进入内核空间之后,取值非常方便.这几个寄存器依次是:ebx,ecx,edx,esi,edi,ebp.如果参数个数超过了6个,或者参数的大小大于32位,可以用传递参数地址的方法.陷入到内核空间之后,再从地址中去取值.回忆一下:我们在前面分析过的系统调用过程:
ENTRY(system_call)
pushl %eax # save orig_eax(系统调用号)
SAVE_ALL
……
SAVE_ALL:
#define __SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es;
发现了吧,系统调用时,把ebp到ebx压栈,再调用系统调用处理函数.这里其实是模拟了一次函数调用过程.在系统调用处理函数中,会根据处理函数的参数个数,到当前堆栈中去取参数值.
既然在系统调用的时候可以用地址作为参数,那就必须要检查这个地址的合法性了.在以前的内核中.会对地址进行严格的检查.即会查对进程的vma判断此线性地址是否属于进程所拥有.权限是否合法.这个过程是相当耗时的.其实虽然有地址非法的错误,但毕竟是少数.犯不着为少数错误降低整个系统的效率.那还要不要检查呢?当然要了.地址非法访问会产生页面异常,推迟到页面异常程序中再处理
四:系统调用相关代码分析:
在前面我们在<<
linux中断处理之初始化>>一文中分析过系统调用的进入和返回过程.再来看下代码:
ENTRY(system_call)
pushl %eax # save orig_eax(系统调用号)
SAVE_ALL
GET_THREAD_INFO(%ebp) #当前进程的task放入ebp
# system call tracing in operation
#如果定义了系统调用跟踪标志
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry
#判断系统调用号是否合法(是否超过NR_syscalls).在x86中,这个值为285
cmpl $(nr_syscalls), %eax
#如果非法.跳至syscall_badsys:即返回-ENOSYS
jae syscall_badsys
syscall_call:
//调用sys_call_table中寻找第eax项(第项占四字节).
call *sys_call_table(,%eax,4)
movl %eax,EAX(%esp) # store the return value
syscall_exit:
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
testw $_TIF_ALLWORK_MASK, %cx # current->work
jne syscall_exit_work
restore_all:
RESTORE_ALL
Sys_call_table定义如下:
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
……
这个表通常被称为系统调用表.如系统调用号1对应的处理函数为sys_exit.
下面以sys_sethostname为例进行分析:
asmlinkage long sys_sethostname(char __user *name, int len)
{
int errno;
char tmp[__NEW_UTS_LEN];
//检查是否为特权用户?
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
//参数长度检查
if (len < 0 || len > __NEW_UTS_LEN)
return -EINVAL;
down_write(&uts_sem);
errno = -EFAULT;
//将用户空间的值copy到内核空间中
if (!copy_from_user(tmp, name, len)) {
//如果成功的话,设置system_utsname.nodename
memcpy(system_utsname.nodename, tmp, len);
system_utsname.nodename[len] = 0;
errno = 0;
}
up_write(&uts_sem);
return errno;
}
Copy_from_user()是一个通用的api.详细分析一下
unsigned long
copy_from_user(void *to, const void __user *from, unsigned long n)
{
//判断from,n的合法性
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else
//参数非法
memset(to, 0, n);
return n;
}
Access_ok()用来初步检查参数的合法性.定义如下:
#define access_ok(type,addr,size) (likely(__range_ok(addr,size) == 0))
#define __range_ok(addr,size) ({ \
unsigned long flag,roksum; \
__chk_user_ptr(addr); \
asm("addl %3,%1 ; sbbl %0,%0; cmpl %1,%4; sbbl $0,%0" \
:"=&r" (flag), "=r" (roksum) \
:"1" (addr),"g" ((int)(size)),"rm" (current_thread_info()->addr_limit.seg)); \
flag; })
实际上,在此只是检查了add+size 是否大于current_thread_info()->addr_limit.seg(进程所允许的最大数据段)
转入__copy_from_user():
static __always_inline unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
//可能会引起睡眠.例如发生缺页异常
might_sleep();
if (__builtin_constant_p(n)) {
unsigned long ret;
//对特殊情况的优化
switch (n) {
case 1:
__get_user_size(*(u8 *)to, from, 1, ret, 1);
return ret;
case 2:
__get_user_size(*(u16 *)to, from, 2, ret, 2);
return ret;
case 4:
__get_user_size(*(u32 *)to, from, 4, ret, 4);
return ret;
}
}
return __copy_from_user_ll(to, from, n);
}
__get_user_size的代码比较简单,我们转入到__copy_from_user_ll()以便分析更普通的情况
unsigned long __copy_from_user_ll(void *to, const void __user *from,
unsigned long n)
{
//在没有定义CONFIG_X86_INTEL_USERCOPY的情况下,此函数恒为1
if (movsl_is_ok(to, from, n))
__copy_user_zeroing(to, from, n);
else
n = __copy_user_zeroing_intel(to, from, n);
return n;
}
跟踪进__copy_user_zeroing()
#define __copy_user_zeroing(to,from,size) \
do { \
int __d0, __d1, __d2; \
__asm__ __volatile__( \
" cmp $7,%0\n" \
" jbe 1f\n" \
" movl %1,%0\n" \
" negl %0\n" \
" andl $7,%0\n" \
" subl %0,%3\n" \
"4: rep; movsb\n" \
" movl %3,%0\n" \
" shrl $2,%0\n" \
" andl $3,%3\n" \
" .align 2,0x90\n" \
"0: rep; movsl\n" \
" movl %3,%0\n" \
"1: rep; movsb\n" \
"2:\n" \
".section .fixup,\"ax\"\n" \
"5: addl %3,%0\n" \
" jmp 6f\n" \
"3: lea 0(%3,%0,4),%0\n" \
"6: pushl %0\n" \
" pushl %%eax\n" \
" xorl %%eax,%%eax\n" \
" rep; stosb\n" \
" popl %%eax\n" \
" popl %0\n" \
" jmp 2b\n" \
".previous\n" \
".section __ex_table,\"a\"\n" \
" .align 4\n" \
" .long 4b,5b\n" \
" .long 0b,3b\n" \
" .long 1b,6b\n" \
".previous" \
: "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2) \
: "3"(size), "0"(size), "1"(to), "2"(from) \
: "memory"); \
}
首先,我们先思考一个问题.怎么将用户空间的数据拷贝到内核空间?我们知道,32位平台上,1~3G的线性地址属于进程专用.3~4属于内核空间,所有进程共享.在前面进行内存管理的时候,我们分析过内核有内核页目录.那进程的页目录与内核的内目录有什么关系呢?从硬件的角度来看,系统调用从空户空间切换到内核空间的时候,并没有重新装载CR3寄存器,也就是说页目录没有发生改变.事实上,所有进程的高1G映射页目录都是一样的,都为内核页目录.所以在内核空间的寻址与用户空间的寻址也是一样的,所以就可以直接进行数据的拷贝了.
这段代码从开始一直到 ".previous\n"前面是字串的copy操作,相当于我们使用 *(int *)dst = * (int *)src的操作.后半部份涉及到gcc的扩展语法: section 把后述代码加至进程的相应段.
在前面分析过.在进行具体的拷贝之前,只是粗略的检查了一下参数.要是参数异常或者要拷贝的内存数据被交换怎么办呢?这就需要do_page_fault()去处理了.在上面的代码中,引起do_page_fault()只可能是由标号4,0,1引起的.在页面异常的代码分析过,我们说过,如果是一个非法的访问,就会到异常表中找相应的处理函数.回顾一下代码:
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
……
……
no_context:
/* Are we prepared to handle this kernel fault? */
if (fixup_exception(regs))
return;
……
}
Fixup_exception()函数代码如下:
int fixup_exception(struct pt_regs *regs)
{
const struct exception_table_entry *fixup;
#ifdef CONFIG_PNPBIOS
if (unlikely((regs->xcs & ~15) == (GDT_ENTRY_PNPBIOS_BASE << 3)))
{
extern u32 pnp_bios_fault_eip, pnp_bios_fault_esp;
extern u32 pnp_bios_is_utter_crap;
pnp_bios_is_utter_crap = 1;
printk(KERN_CRIT "PNPBIOS fault.. attempting recovery.\n");
__asm__ volatile(
"movl %0, %%esp\n\t"
"jmp *%1\n\t"
: : "g" (pnp_bios_fault_esp), "g" (pnp_bios_fault_eip));
panic("do_trap: can't hit this");
}
#endif
//从异常表中查找相应处理代码的地址.对应的参数是引起异常的代码地址
fixup = search_exception_tables(regs->eip);
if (fixup) {
//将地址存入调用之前的eip寄存器.这样在异常返回后,就会执行对应的代码
regs->eip = fixup->fixup;
return 1;
}
return 0;
}
转入search_exception_tables()
const struct exception_table_entry *search_exception_tables(unsigned long addr)
{
const struct exception_table_entry *e;
//参数:起始地址,结束地址,引起异常的地址
e = search_extable(__start___ex_table, __stop___ex_table-1, addr);
if (!e)
e = search_module_extables(addr);
return e;
}
//利用二分法查找
const struct exception_table_entry *
search_extable(const struct exception_table_entry *first,
const struct exception_table_entry *last,
unsigned long value)
{
while (first <= last) {
const struct exception_table_entry *mid;
mid = (last - first) / 2 + first;
/*
* careful, the distance between entries can be
* larger than 2GB:
*/
if (mid->insn < value)
first = mid + 1;
else if (mid->insn > value)
last = mid - 1;
else
return mid;
}
return NULL;
}
exception_table_entry结构如下示:
struct exception_table_entry
{
//insn:产生异常指令的地址
//fixup:修复地址
unsigned long insn, fixup;
};
返回到我们上面讨论的__copy_user_zeroing()的代码:
……
".section __ex_table,\"a\"\n" \
" .align 4\n" \
" .long 4b,5b\n" \
" .long 0b,3b\n" \
" .long 1b,6b\n" \
…….
对应到异常表就是:
如果异常处理是在标号4处发生的,那么修复地址是标号5
如果异常处理是在标号0处发生的,那么修复地址是标号3
如果异常处理是在标号1处发生的,那么修复地址是标号6
不止是在copy_from_user()有实现中用了这样的方法,在所有会引起异常的代码中,就加入了相应的异常项.也就说谁,谁可能会引起异常,谁就负表在异常表中加入相应项.在链接的时候,ld会按照地址升序将相关项插入到异常表
五:再论异常表
系统常用/异常/IRQ中断处理程序处理完后,都会经过RESTORE_ALL返回,之前为了分析整个过程,没有分析这个过程可能会引起的异常情况.
#define __RESTORE_INT_REGS \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax
#define __RESTORE_REGS \
__RESTORE_INT_REGS; \
111: popl %ds; \
222: popl %es; \
.section .fixup,"ax"; \
444: movl $0,(%esp); \
jmp 111b; \
555: movl $0,(%esp); \
jmp 222b; \
.previous; \
.section __ex_table,"a";\
.align 4; \
.long 111b,444b;\
.long 222b,555b;\
.previous
#define __RESTORE_ALL \
__RESTORE_REGS \
addl $4, %esp; \
333: iret; \
.section .fixup,"ax"; \
666: sti; \
movl $(__USER_DS), %edx; \
movl %edx, %ds; \
movl %edx, %es; \
pushl $11; \
call do_exit; \
.previous; \
.section __ex_table,"a";\
.align 4; \
.long 333b,666b;\
.previous
可以看到,在标号111,222,333处加入了异常处理,相应的指令分别是popl %ds, popl %es和iret.
为什么会这三条指令会引起异常呢?先看popl %ds 和popl %es
每当装载一个段寄存器的时候,CPU都要根据这个段选择码在GDT/LDT中找到相应的描述项.并加以检查.如果描述项和选择项都正常,就会把它装入CPU的”不可见”部份.在映射的时候就不必再去取相应的值.如果选择码或者描述符无效或者不存在时,就会产生一次 “全面”保护异常.当这样的异常发生在系统空间时,都要为它建立相关的异常表项.在代码中,异常处理代码把esp置0.然后再执行pop指令,实际上把ds,es置为零.这样就可以返回到用户空间了.至于用户空间会发生什么,那就由相应的异常处理程序去处理,最多不过把这个进程kill掉.
那iret怎么会引起异常呢?
在<>一文中分析过,中断返回时,会从系统堆栈中取值恢复相关的寄存器.包括CS,EIP,如果运行级别不相同的会,还会恢复SS,ESP.同上面的DS,ES一样,这里也有两个段寄存器CS,SS.不过,CS,SS不接受0值,所以不能像DS,ES那样处理了.那就没有其它的办法了.只能把当前进程kill掉,以保证整个系统的运行..
六:小结
在这一节里,主要分析了系统调用的流程,用户空间与系统空间的数据交互.可能会引起异常指令的修正处理.看完整个流程之后,我们很容易添加自己的系统调用.但是,如果是在实际项目中的话,那就得仔细权衡添加系统调用带来的利与弊了.
Linux中断处理之时钟中断
2008-03-27 09:27:30
分类: LINUX
------------------------------------------
本文系本站原创,欢迎转载!
转载请注明出处:http://ericxiao.cublog.cn/
------------------------------------------
一:前言
时钟是整个操作系统的脉搏,它为进程的时间片调度,定时事件提供了依据.另外,用户空间的很多操作都依赖于时钟,例如select.poll,make.操作系统管理的时间为分两种,一种称为当前时间,也即我们日常生活所用的时间.这个时间一般保存在CMOS中.主板中有特定的芯片为其提供计时依据.另外一种时间称为相对时间.例如系统运行时间.显然对计算机而然,相对时间比当前时间更为重要.
二:与时钟有关的硬件处理.
1):实时时钟(RTC)
该时钟独立于CPU和其它芯片.即使PC断电,该时钟还是继续运行.该计时由一块单独的芯片处理,并把时钟值存放CMOS.该时间可参在IRQ8上周期性的产生时间信号.频率在2Hz ~ 8192Hz之间.但在linux中,只是用RTC来获取当前时间.
2):时间戳计时器(TSC)
CPU附带了一个64位的时间戳寄存器,当时钟信号到来的时候.该寄存器内容自动加1
3):可编程中断定时器(PIC)
该设备可以周期性的发送一个时间中断信号.发送中断信号的间隔可以对其进行编程控制.在linux系统中,该中断时间间隔由HZ表示.这个时间间隔也被称为一个节拍(tick).
4):CPU本地定时器
在处理器的本地APIC还提供了另外的一定定时设备.CPU本地定时器也可以单次或者周期性的产生中断信号.与上次描述的PIC相比.它有以下几点的区别:
APIC本地计时器是32位.而PIC是16位.由此APIC本地计时器可以提供更低频率的中断信号
本地APIC只把中断信号发送给本地CPU.而PIC发送的中断信号任何CPU都可以处理
APIC定时器是基于总线时钟信号的.而PIC有自己的内部时钟振荡器
5):高精度计时器(HPET)
在linux2.6中增加了对HPET的支持.HPET是一种由微软和intel联合开发的新型定时芯片.该设备有一组寄时器,每个寄时器对应有自己的时钟信号,时钟信号到来的时候就会自动加1.
实际上,在intel多理器系统与单处理器系统还有所不同:
在单处理系统中.所有计时活动过由PIC产生的时钟中断信号触发的
在多处理系统中,所有普通活动是由PIC产生的中断触发.所有具体的CPU活动,都由本地APIC触发的.
三:时钟中断相关代码分析
time_init()是时钟初始化函数,他由asmlinkage void __init start_kernel()调用.具体代码如下:
//时钟中断初始化
void __init time_init(void)
{
//如果定义了HPET
#ifdef CONFIG_HPET_TIMER
if (is_hpet_capable()) {
/*
* HPET initialization needs to do memory-mapped io. So, let
* us do a late initialization after mem_init().
*/
late_time_init = hpet_time_init;
return;
}
#endif
//从cmos 中取得实时时间
xtime.tv_sec = get_cmos_time();
//初始化wall_to_monotonic
wall_to_monotonic.tv_sec = -xtime.tv_sec;
xtime.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ);
wall_to_monotonic.tv_nsec = -xtime.tv_nsec;
//选择一个合适的定时器
cur_timer = select_timer();
printk(KERN_INFO "Using %s for high-res timesource\n",cur_timer->name);
//注册时间中断信号处理函数
time_init_hook();
}
该函数从cmos取得了当前时间.并为调整时间精度选择了合适的定时器
转入time_init_hook():
void __init time_init_hook(void)
{
//注册中断处理函数
setup_irq(0, &irq0);
}
Irq0定义如下:
static struct irqaction irq0 = { timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, "timer", NULL, NULL};
对应的中断处理函数为:timer_interrupt():
irqreturn_t timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
//因为该函数会修改xtime的值,为避免多处理器竞争.先加锁
write_seqlock(&xtime_lock);
//记录上一次时间中断的精确时间.做调准时钟用
cur_timer->mark_offset();
do_timer_interrupt(irq, NULL, regs);
//解锁
write_sequnlock(&xtime_lock);
return IRQ_HANDLED;
}
核心处理函数为 do_timer_interrupt():
static inline void do_timer_interrupt(int irq, void *dev_id,
struct pt_regs *regs)
{
#ifdef CONFIG_X86_IO_APIC
if (timer_ack) {
spin_lock(&i8259A_lock);
outb(0x0c, PIC_MASTER_OCW3);
/* Ack the IRQ; AEOI will end it automatically. */
inb(PIC_MASTER_POLL);
spin_unlock(&i8259A_lock);
}
#endif
do_timer_interrupt_hook(regs);
//如果要进行时间同步,那就隔一段时间把当前时间写回coms
if ((time_status & STA_UNSYNC) == 0 &&
xtime.tv_sec > last_rtc_update + 660 &&
(xtime.tv_nsec / 1000)
>= USEC_AFTER - ((unsigned) TICK_SIZE) / 2 &&
(xtime.tv_nsec / 1000)
<= USEC_BEFORE + ((unsigned) TICK_SIZE) / 2) {
/* horrible...FIXME */
if (efi_enabled) {
if (efi_set_rtc_mmss(xtime.tv_sec) == 0)
last_rtc_update = xtime.tv_sec;
else
last_rtc_update = xtime.tv_sec - 600;
} else 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 */
}
#ifdef CONFIG_MCA
if( MCA_bus ) {
/* The PS/2 uses level-triggered interrupts. You can't
turn them off, nor would you want to (any attempt to
enable edge-triggered interrupts usually gets intercepted by a
special hardware circuit). Hence we have to acknowledge
the timer interrupt. Through some incredibly stupid
design idea, the reset for IRQ 0 is done by setting the
high bit of the PPI port B (0x61). Note that some PS/2s,
notably the 55SX, work fine if this is removed. */
irq = inb_p( 0x61 ); /* read the current state */
outb_p( irq|0x80, 0x61 ); /* reset the IRQ */
}
#endif
}
我们忽略选择编译部份,转到do_timer_interrupt_hook()
static inline void do_timer_interrupt_hook(struct pt_regs *regs)
{
do_timer(regs);
/*
* In the SMP case we use the local APIC timer interrupt to do the
* profiling, except when we simulate SMP mode on a uniprocessor
* system, in that case we have to call the local interrupt handler.
*/
#ifndef CONFIG_X86_LOCAL_APIC
//更新内核代码监管器。在每次时钟中断的时候。取得每一次中断前的esp,进而可以得到运行的函//数地址。这样就可以统计运行时间最长的函内核函数区域。以便于内核管理者优化
profile_tick(CPU_PROFILING, regs);
#else
if (!using_apic_timer)
smp_local_timer_interrupt(regs);
#endif
}
这里有几个重要的操作.先看do_timer():
void do_timer(struct pt_regs *regs)
{
// 更新jiffies计数.jiffies_64与jiffies在链接的时候,实际是指向同一个区域
jiffies_64++;
#ifndef CONFIG_SMP
/* SMP process accounting uses the local APIC timer */
//更新当前运行进程的与时钟相关的信息
update_process_times(user_mode(regs));
#endif
//更新当前时间.xtime的更新
update_times();
}
Update_process_times()代码如下:
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);
//激活时间软中断
run_local_timers();
//减少时间片。这个函数涉及到的东西过多,等到进程调度的时候再来分析。请关注本站更新*^_^*
scheduler_tick(user_tick, system);
}
先看update_one_process():
static void update_one_process(struct task_struct *p, unsigned long user,
unsigned long system, int cpu)
{
do_process_times(p, user, system);
//检查进程的定时器
do_it_virt(p, user);
do_it_prof(p);
}
在这里简单介绍一下do_it_virt()与do_it_prof():
这两个函数主要检查用户空间的进程定时器是否到期.在进程的内存描述符有相关的字段.如下:
struct task_struct{
⋯⋯
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
⋯⋯
}
(1)真实间隔定时器(ITIMER_REAL):这种间隔定时器在启动后,不管进程是否运行,每个时钟滴答都将其间隔计数器减1。当减到0值时,内核向进程发送SIGALRM信号。结构类型task_struct中的成员it_real_incr则表示真实间隔定时器的间隔计数器的初始值,而成员it_real_value则表示真实间隔定时器的间隔计数器的当前值。由于这种间隔定时器本质上与上一节的内核定时器时一样的,因此Linux实际上是通过real_timer这个内嵌在task_struct结构中的内核动态定时器来实现真实间隔定时器ITIMER_REAL的。
(2)虚拟间隔定时器ITIMER_VIRT:也称为进程的用户态间隔定时器。结构类型task_struct中成员it_virt_incr和it_virt_value分别表示虚拟间隔定时器的间隔计数器的初始值和当前值,二者均以时钟滴答次数位计数单位。当虚拟间隔定时器启动后,只有当进程在用户态下运行时,一次时钟滴答才能使间隔计数器当前值it_virt_value减1。当减到0值时,内核向进程发送SIGVTALRM信号(虚拟闹钟信号),并将it_virt_value重置为初值it_virt_incr。具体请见7.4.3节中的do_it_virt()函数的实现。
(3)PROF间隔定时器ITIMER_PROF:进程的task_struct结构中的it_prof_value和it_prof_incr成员分别表示PROF间隔定时器的间隔计数器的当前值和初始值(均以时钟滴答为单位)。当一个进程的PROF间隔定时器启动后,则只要该进程处于运行中,而不管是在用户态或核心态下执行,每个时钟滴答都使间隔计数器it_prof_value值减1。当减到0值时,内核向进程发送SIGPROF信号,并将it_prof_value重置为初值it_prof_incr.
Do_process_times():
static inline void do_process_times(struct task_struct *p,
static inline void do_process_times(struct task_struct *p,
unsigned long user, unsigned long system)
{
unsigned long psecs;
//p->utime:在用户空间所花的时间
psecs = (p->utime += user);
//p->stime:在系统空间所花的时间
psecs += (p->stime += system);
//如果运行的时间片到达
if (psecs / HZ >= p->rlim[RLIMIT_CPU].rlim_cur) {
/* Send SIGXCPU every second.. */
//每秒发送一个SIGXCPU
if (!(psecs % HZ))
send_sig(SIGXCPU, p, 1);
/* and SIGKILL when we go over max.. */
//发送SIGKILL
if (psecs / HZ >= p->rlim[RLIMIT_CPU].rlim_max)
send_sig(SIGKILL, p, 1);
}
}
该函数检查当前进程的时间片是否到达,如果到达就给当前进程发送SIGKILL和SIGXCPU
do_it_virt()/do_it_prof()检查过程的定时器是否到期.如果到期就给进程发送相应的信号:
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;
//发送SIGVTALRM
send_sig(SIGVTALRM, p, 1);
}
p->it_virt_value = it_virt;
}
}
返回到update_process_times()的其它函数:
run_local_timers()
void run_local_timers(void)
{
raise_softirq(TIMER_SOFTIRQ);
}
激活时间软中断.这个函数我们在IRQ中断中已经分析过了,不再赘述
我们在
do_timer()还漏掉了一个函数:
static inline void update_times(void)
{
unsigned long ticks;
//wall_jiffies:上一次更新的值
ticks = jiffies - wall_jiffies;
if (ticks) {
wall_jiffies += ticks;
//更新
xtime
update_wall_time(ticks);
}
//统计
TASK_RUNNING TASK_UNINTERRUPTIBLE进程数量
calc_load(ticks);
}
四:定时器
在模块的编写过程中,我们经常使用定时器来等待一段时间之后再来执行某一个操作。为方便分析,写了下列一段测试程序:
#include
#include
#include
#include
#include
#include
MODULE_LICENSE("GPL");
void test_timerfuc(unsigned long x)
{
printk("Eric xiao test ......\n");
}
//声明一个定个器
struct timer_list test_timer = TIMER_INITIALIZER(test_timerfuc, 0, 0);
int kernel_test_init()
{
printk("test_init\n");
//修改定时器到期时间。为
3个
HZ。一个
HZ产生一个时钟中断
mod_timer(&test_timer,jiffies+3*HZ);
//把定时器加入时钟软中断处理链表
add_timer(&test_timer);
}
int kernel_test_exit()
{
printk("test_exit\n");
return 0;
}
module_init(kernel_test_init);
module_exit(kernel_test_exit);
上面的例子程序比较简单,我们从这个例子开始研究
linux下的定时器实现。
TIMER_INITIALIZER():
1):TIMER_INITIALIZER()用来声明一个定时器,它的定义如下:
#define TIMER_INITIALIZER(_function, _expires, _data) { \
.function = (_function), \
.expires = (_expires), \
.data = (_data), \
.base = NULL, \
.magic = TIMER_MAGIC, \
.lock = SPIN_LOCK_UNLOCKED, \
}
Struct timer_list定义如下:
struct timer_list {
//用来形成链表
struct list_head entry;
//定始器到达时间
unsigned long expires;
spinlock_t lock;
unsigned long magic;
//定时器时间到达后,所要运行的函数
void (*function)(unsigned long);
//定时器函数对应的参数
unsigned long data;
//挂载这个定时器的
tvec_t_base_s.这个结构我们等会会看到
struct tvec_t_base_s *base;
};
从上面的过程中我们可以看到
TIMER_INITIALIZER()只是根据传入的参数初始化了
struct timer_list结构
.并把
magic 成员初始化成
TIMER_MAGIC
2): mod_timer():修改定时器的到时时间
int mod_timer(struct timer_list *timer, unsigned long expires)
{
//如果该定时器没有定义
fuction
BUG_ON(!timer->function);
//判断
timer的
magic是否为
TIMER_MAGIC.如果不是
,则将其修正为
TIMER_MAGIC
check_timer(timer);
//如果要调整的时间就是定时器的定时时间而且已经被激活
,则直接返回
if (timer->expires == expires && timer_pending(timer))
return 1;
//调用
_mod_timer().呆会再给出分析
return __mod_timer(timer, expires);
}
3): add_timer()用来将定时器挂载到定时软中断队列
,激活该定时器
static inline void add_timer(struct timer_list * timer)
{
__mod_timer(timer, timer->expires);
}
可以看到
mod_timer与
add_timer 最后都会调用
__mod_timer().为了分析这个函数
,我们先来了解一下定时系统相关的数据结构
.
tvec_bases: per cpu变量
,它的定义如下
:
static DEFINE_PER_CPU(tvec_base_t, tvec_bases) = { SPIN_LOCK_UNLOCKED };
由此可以看到
tves_bases的数型数据为
teves_base_t.数据结构的定义如下
:
typedef struct tvec_t_base_s tvec_base_t;
struct tvec_t_base_s的定义
:
struct tvec_t_base_s {
spinlock_t lock;
//上一次运行计时器的
jiffies 值
unsigned long timer_jiffies;
struct timer_list *running_timer;
//tv1 tv2 tv3 tv4 tv5是五个链表数组
tvec_root_t tv1;
tvec_t tv2;
tvec_t tv3;
tvec_t tv4;
tvec_t tv5;
} ____cacheline_aligned_in_smp;
Tves_root_t与
tvec_t的定义如下
:
#define TVN_BITS 6
#define TVR_BITS 8
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
typedef struct tvec_s {
struct list_head vec[TVN_SIZE];
} tvec_t;
typedef struct tvec_root_s {
struct list_head vec[TVR_SIZE];
} tvec_root_t;
系统规定定时器最大超时时间间隔为
0xFFFFFFFF.即为一个
32位数
.即使在
64位系统上
.如果超过此值也会将其强制设这
oxFFFFFFFF(这在后面的代码分析中可以看到
).内核最关心的就是间隔在
0~255个
HZ之间的定时器
.次重要的是间隔在
255~1<<(8+6)之间的定时器
.第三重要的是间隔在
1<<(8+6) ~ 1<<(8+6+6)之间的定器
.依次往下推
.也就是把
32位的定时间隔为份了五个部份
.1个
8位
.4个
6位
.所以内核定义了五个链表数组
.第一个链表数组大小为
8位大小
,也即上面定义的
#define TVR_SIZE (1 << TVR_BITS).其它的四个数组大小为
6位大小
.即上面定义的
#define TVN_SIZE (1 << TVN_BITS)
在加入定时器的时候
,按照时间间隔把定时器加入到相应的数组即可
.了解这点之后
,就可以来看
__mod_timer()的代码了
:
//修改
timer或者新增一个
timer都会调用此接口
int __mod_timer(struct timer_list *timer, unsigned long expires)
{
tvec_base_t *old_base, *new_base;
unsigned long flags;
int ret = 0;
//入口参数检测
BUG_ON(!timer->function);
check_timer(timer);
spin_lock_irqsave(&timer->lock, flags);
//取得当前
CPU对应的
tvec_bases
new_base = &__get_cpu_var(tvec_bases);
repeat:
//该定时器所在的
tvec_bases.对于新增的
timer.它的
base字段为
NULL
old_base = timer->base;
/*
* Prevent deadlocks via ordering by old_base < new_base.
*/
//在把
timer从当前
tvec_bases摘下来之前
,要充分考虑好竞争的情况
if (old_base && (new_base != old_base)) {
//按次序获得锁
if (old_base < new_base) {
spin_lock(&new_base->lock);
spin_lock(&old_base->lock);
} else {
spin_lock(&old_base->lock);
spin_lock(&new_base->lock);
}
/*
* The timer base might have been cancelled while we were
* trying to take the lock(s):
*/
//如果
timer->base != old_base.那就是说在
Lock的时候
.其它
CPU更改它的值
//那就解锁
.重新判断
if (timer->base != old_base) {
spin_unlock(&new_base->lock);
spin_unlock(&old_base->lock);
goto repeat;
}
} else {
//old_base == NULl 或者是
new_base==old_base的情况
//获得锁
spin_lock(&new_base->lock);
//同理
,在
Lock的时候
timer会生了改变
if (timer->base != old_base) {
spin_unlock(&new_base->lock);
goto repeat;
}
}
/*
* Delete the previous timeout (if there was any), and install
* the new one:
*/
//将其从其它的
tvec_bases上删除
.注意运行到这里的话
,说话已经被
Lock了
if (old_base) {
list_del(&timer->entry);
ret = 1;
}
//修改它的定时器到达时间
timer->expires = expires;
//将其添加到
new_base中
internal_add_timer(new_base, timer);
//修改
base字段
timer->base = new_base;
//操作完了
,解锁
if (old_base && (new_base != old_base))
spin_unlock(&old_base->lock);
spin_unlock(&new_base->lock);
spin_unlock_irqrestore(&timer->lock, flags);
return ret;
}
internal_add_timer()的代码如下
:
static void internal_add_timer(tvec_base_t *base, struct timer_list *timer)
{
//定时器到达的时间
unsigned long expires = timer->expires;
//计算时间间间隔
unsigned long idx = expires - base->timer_jiffies;
struct list_head *vec;
//根据时间间隔
,将
timer放入相应数组的相应位置
if (idx < TVR_SIZE) {
int i = expires & TVR_MASK;
vec = base->tv1.vec + i;
} else if (idx < 1 << (TVR_BITS + TVN_BITS)) {
int i = (expires >> TVR_BITS) & TVN_MASK;
vec = base->tv2.vec + i;
} else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) {
int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK;
vec = base->tv3.vec + i;
} else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) {
int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK;
vec = base->tv4.vec + i;
} else if ((signed long) idx < 0) {
/*
* Can happen if you add a timer with expires == jiffies,
* or you set a timer to go off in the past
*/
//如果间隔小于
0
vec = base->tv1.vec + (base->timer_jiffies & TVR_MASK);
} else {
int i;
/* If the timeout is larger than 0xffffffff on 64-bit
* architectures then we use the maximum timeout:
*/
//时间间隔超长
,将其设为
oxFFFFFFFF
if (idx > 0xffffffffUL) {
idx = 0xffffffffUL;
expires = idx + base->timer_jiffies;
}
i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
vec = base->tv5.vec + i;
}
/*
* Timers are FIFO:
*/
//加入到链表末尾
list_add_tail(&timer->entry, vec);
}
计算时间间隔即可知道要加入到哪一个数组
.哪又怎么计算加入到该数组的那一项呢
?
对于间隔时间在
0~255的定时器
: 它的计算方式是将定时器到达时间的低八位与低八位为
1的数相与而成
对于第
1个六位
,它是先将到达时间右移
8位
.然后与低六位全为
1的数相与而成
对于第
2个六位
, 它是先将到达时间右移
8+6位
.然后与低六位全为
1的数相与而成
依次为下推
…
在后面结合超时时间到达的情况再来分析相关部份
4):定时器更新
每过一个
HZ,就会检查当前是否有定时器的定时器时间到达
.如果有
,运行它所注册的函数
,再将其删除
.为了分析这一过程
,我们先从定时器系统的初始化看起
.
asmlinkage void __init start_kernel(void)
{
……
init_timers();
……
}
Init_timers()的定义如下
:
void __init init_timers(void)
{
timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
(void *)(long)smp_processor_id());
register_cpu_notifier(&timers_nb);
//注册
TIMER_SOFTIRQ软中断
open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);
}
timer_cpu_notify()à
init_timers_cpu():
代码如下
:
static void /* __devinit */ init_timers_cpu(int cpu)
{
int j;
tvec_base_t *base;
//初始化各个数组中的链表
base = &per_cpu(tvec_bases, cpu);
spin_lock_init(&base->lock);
for (j = 0; j < TVN_SIZE; j++) {
INIT_LIST_HEAD(base->tv5.vec + j);
INIT_LIST_HEAD(base->tv4.vec + j);
INIT_LIST_HEAD(base->tv3.vec + j);
INIT_LIST_HEAD(base->tv2.vec + j);
}
for (j = 0; j < TVR_SIZE; j++)
INIT_LIST_HEAD(base->tv1.vec + j);
//将最近到达时间设为当前
jiffies
base->timer_jiffies = jiffies;
}
我们在前面分析过
,每当时钟当断函数到来的时候
,就会打开定时器的软中断
.运行其软中断函数
.run_timer_softirq()
代码如下
:
static void run_timer_softirq(struct softirq_action *h)
{
//取得当于
CPU的
tvec_base_t结构
tvec_base_t *base = &__get_cpu_var(tvec_bases);
//如果
jiffies > base->timer_jiffies
if (time_after_eq(jiffies, base->timer_jiffies))
__run_timers(base);
}
__run_timers()代码如下
:
static inline void __run_timers(tvec_base_t *base)
{
struct timer_list *timer;
unsigned long flags;
spin_lock_irqsave(&base->lock, flags);
//因为
CPU可能关闭中断
,引起时钟中断信号丢失
.可能
jiffies要大
base->timer_jiffies 好几个
//HZ
while (time_after_eq(jiffies, base->timer_jiffies)) {
//定义并初始化一个链表
struct list_head work_list = LIST_HEAD_INIT(work_list);
struct list_head *head = &work_list;
int index = base->timer_jiffies & TVR_MASK;
/*
* Cascade timers:
*/
//当
index == 0时
,说明已经循环了一个周期
//则将
tv2填充
tv1.如果
tv2为空
,则用
tv3填充
tv2.依次类推
......
if (!index &&
(!cascade(base, &base->tv2, INDEX(0))) &&
(!cascade(base, &base->tv3, INDEX(1))) &&
!cascade(base, &base->tv4, INDEX(2)))
cascade(base, &base->tv5, INDEX(3));
//更新
base->timer_jiffies
++base->timer_jiffies;
//将
base->tv1.vec项移至
work_list.并将
base->tv1.vec置空
list_splice_init(base->tv1.vec + index, &work_list);
repeat:
//work_List中的定时器是已经到时的定时器
if (!list_empty(head)) {
void (*fn)(unsigned long);
unsigned long data;
//遍历链表中的每一项
.运行它所对应的函数
,并将定时器从链表上脱落
timer = list_entry(head->next,struct timer_list,entry);
fn = timer->function;
data = timer->data;
list_del(&timer->entry);
set_running_timer(base, timer);
smp_wmb();
timer->base = NULL;
spin_unlock_irqrestore(&base->lock, flags);
fn(data);
spin_lock_irq(&base->lock);
goto repeat;
}
}
set_running_timer(base, NULL);
spin_unlock_irqrestore(&base->lock, flags);
}
如果
base->timer_jiffies低八位为零
.说明它向第九位有进位
.所以把第九位到十五位对应的定时器搬到前八位对应的数组
.如果第九位到十五位为空的话
.就到它的上个六位去搬数据
.上面的代码也说明
.要经过
1<<8个
HZ才会更新全部数组中的定时器
.这样做的效率是很高的
.
分析下里面的两个重要的子函数
:
static int cascade(tvec_base_t *base, tvec_t *tv, int index)
{
/* cascade all the timers from tv up one level */
struct list_head *head, *curr;
//取数组中序号对应的链表
head = tv->vec + index;
curr = head->next;
/*
* We are removing _all_ timers from the list, so we don't have to
* detach them individually, just clear the list afterwards.
*/
//遍历这个链表
,将定时器重新插入到
base中
while (curr != head) {
struct timer_list *tmp;
tmp = list_entry(curr, struct timer_list, entry);
BUG_ON(tmp->base != base);
curr = curr->next;
internal_add_timer(base, tmp);
}
//将链表设为初始化状态
INIT_LIST_HEAD(head);
return index;
}
//将
list中的数据放入
head中
,并将
list置为空
static inline void list_splice_init(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list)) {
__list_splice(list, head);
INIT_LIST_HEAD(list);
}
}
//将
list中的数据放入
head
static inline void __list_splice(struct list_head *list,
struct list_head *head)
{
//list的第一个元素
struct list_head *first = list->next;
//list的最后一个元素
struct list_head *last = list->prev;
//head的第一个元素
struct list_head *at = head->next;
将
first对应的链表链接至
head
first->prev = head;
head->next = first;
//将
head 原有的数据加入到链表末尾
last->next = at;
at->prev = last;
}
5):del_timer()删除定时器
//删除一个
timer
int del_timer(struct timer_list *timer)
{
unsigned long flags;
tvec_base_t *base;
check_timer(timer);
repeat:
base = timer->base;
//该定时器没有被激活
if (!base)
return 0;
//加锁
spin_lock_irqsave(&base->lock, flags);
//如果在加锁的过程中
,有其它操作改变了
timer
if (base != timer->base) {
spin_unlock_irqrestore(&base->lock, flags);
goto repeat;
}
//将
timer从链表中删除
list_del(&timer->entry);
timer->base = NULL;
spin_unlock_irqrestore(&base->lock, flags);
return 1;
}
6): del_timer_sync()有竞争情况下的定时器删除
在
SMP系统中
,可能要删除的定时器正在某一个
CPU上运行
.为了防止这种在情况
.在删除定时器的时候
,应该优先使用
del_timer_synsc().它会一直等待所有
CPU上的定时器执行完成
.
int del_timer_sync(struct timer_list *timer)
{
tvec_base_t *base;
int i, ret = 0;
check_timer(timer);
del_again:
//删除些定时器
ret += del_timer(timer);
//遍历
CPU
for_each_online_cpu(i) {
base = &per_cpu(tvec_bases, i);
//如果此
CPU正在运行这个
timer
if (base->running_timer == timer) {
//一直等待
,直到这个
CPU执行完
while (base->running_timer == timer) {
cpu_relax();
preempt_check_resched();
}
break;
}
}
smp_rmb();
//如果这个
timer又被调用
.再删除
if (timer_pending(timer))
goto del_again;
return ret;
}
定时器部份到这里就介绍完了
.为了管理定时器
.内核用了一个很巧妙的数据结构
.值得好好的体会
.
五
:小结
2.6内核在时钟管理子系统的修改比较大
.因为在
2.6完全摒弃掉了下半部机制
.2.4中下半部处理的大部份都放在了中断处理程序里
,只有定时器控制被移到了时钟软中断
.另外时钟中断初始化涉及到了很多硬件的操作
.需要查阅相关资料才能完全理解
.