Linux内核零碎笔记

来源:岁月联盟 编辑:exp 时间:2011-09-14

#中断下半部.
common_interrupt: 
addl $-0x80,(%esp) /* Adjust vector into the [-256,-1] range */  #调整中断号到0~255的范围.
SAVE_ALL
TRACE_IRQS_OFF
movl %esp,%eax
call do_IRQ  #处理中断例程.
jmp ret_from_intr
ENDPROC(common_interrupt)

unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);

/* high bit used in ret_from_ code */
unsigned vector = ~regs->orig_ax;
unsigned irq;

exit_idle();
irq_enter();

irq = __get_cpu_var(vector_irq)[vector];

if (!handle_irq(irq, regs)) {  #调用对应的中断例程.
ack_APIC_irq();  #如果例程成功返回则回应8259A高级编程中断控制器.

if (printk_ratelimit())
pr_emerg("%s: %d.%d No irq handler for vector (irq %d)/n",
__func__, smp_processor_id(), vector, irq);
}

irq_exit();  #处理完中断上半部后,就调用irq_exit执行中断下半部.

set_irq_regs(old_regs);
return 1;
}

void irq_exit(void)
{
account_system_vtime(current);
trace_hardirq_exit();
sub_preempt_count(IRQ_EXIT_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();  #调用__do_softirq.

rcu_irq_exit();
#ifdef CONFIG_NO_HZ
/* Make sure that timer wheel updates are propagated */
if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
tick_nohz_stop_sched_tick(0);
#endif
preempt_enable_no_resched();
}

# define invoke_softirq() __do_softirq()  #invoke_softirq是替换成__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();  #保存待处理的软中断的32位图到栈变量里.
account_system_vtime(current);

__local_bh_disable((unsigned long)__builtin_return_address(0));
lockdep_softirq_enter();

cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);  #清零待处理的软中断的32位图.

local_irq_enable();  #当前CPU开启中断.

h = softirq_vec;  #获取软中断数组的第一个元素.

do {  #开始轮询软中断数组.好像是32个.
if (pending & 1) {  #判断每一位,为1则说明该位对应的软中断需要处理.
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(h - softirq_vec);

trace_softirq_entry(h, softirq_vec);
h->action(h);  #处理相关软中断.
trace_softirq_exit(h, softirq_vec);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %td %s %p"
"with preempt_count %08x,"
" exited with %08x?/n", h - softirq_vec,
softirq_to_name[h - softirq_vec],
h->action, prev_count, preempt_count());
preempt_count() = prev_count;
}

rcu_bh_qs(cpu);
}
h++;  #下一个.
pending >>= 1;  #下一个.每检查一位就向右移,可见最低位的优先级最高.
} while (pending);

local_irq_disable();  #当前CPU屏蔽中断.

pending = local_softirq_pending();  #再获取一下待处理的软中断的32位图,看处理软中断期间是否又发生了软中断请求.
if (pending && --max_restart)  #如果发生请求并且在单次处理软中断的最大循环检查次数内.
goto restart;  #则跳到restart处,再处理新来的软中断.可见软中真是响应的非常及时呀.

if (pending)  #如果如果软中断的次数确实太频繁了.
wakeup_softirqd();  #则唤醒专门处理软中断的内核线程.

lockdep_softirq_exit();

account_system_vtime(current);
_local_bh_enable();
}

#软中断_32位的位图判断 并行(SMP情况下同时处理) 内核线程辅助检测软中断位图 不可睡眠
#tasklet 串行(加锁) 不可睡眠
#内核线程工作队列 可睡眠
----------------------------------------------------------------------------------------------
#DPL 访问描述符所需的特权等级
#RPL 委托人的真实特权等级
#CPL 当前特权等级(CS.RPL)
----------------------------------------------------------------------------------------------
#内核空间 就是所有进程的独立的页表空间的高地址1G,映射到相同的一块物理空间.
----------------------------------------------------------------------------------------------
#IPC的共享内存 就是把多个进程独立的页表空间的一部分映射到同一块的物理空间.
----------------------------------------------------------------------------------------------
#信号量跟下面的互斥锁实现很类似,都是得不到锁的线程就进入等待队列睡眠.区别在于同时得到锁得线程数是可控的.而互斥锁则只能有一个线程得到锁.

#互斥锁
#define __mutex_fastpath_lock(count, fail_fn)  /
do {  /
unsigned int dummy;  /  #这个作为传进来的函数指针的参数,因为只申请了一个变量,栈顶ESP已经指向他了.
/
typecheck(atomic_t *, count);  /  #|
typecheck_fn(void (*)(atomic_t *), fail_fn);  /  #/宏参数检查
/
asm volatile(LOCK_PREFIX " decl (%%eax)/n"  /  #这里可见,如果先到线程先减去count,SF为不等于1(不等于负数),就往后执行,也就是获得锁.在此后来尝试获取锁的线程执行这个原子操作的话,(在未解锁状态下)SF都等于1,以至于执行void (*)(atomic_t *)类型的函数进入等待队列.所以这种加锁的原子操作用单指令便可完成.在非SMP中,单指令可完成原子操作,SMP的话采用锁存技术完成原子操作.
" jns 1f /n"  /
" call " #fail_fn "/n"  /
"1:/n"  /
: "=a" (dummy)  /  #当互斥锁获取失败时将count原子变量的值赋给dummy,用void (*)(atomic_t *)函数放在等待队列里.以便下一个等待线程顺序地获得互斥锁.
: "a" (count)  /  #参数变量count
: "memory", "ecx", "edx");  /
} while (0)
#fail_fn宏形参用如下的函数作为实参替换
static __used noinline void __sched __mutex_lock_slowpath(atomic_t *lock_count)
{
struct mutex *lock = container_of(lock_count, struct mutex, count);  #按顺序地放进等待队列.

__mutex_lock_common(lock, TASK_UNINTERRUPTIBLE, 0, _RET_IP_);  #然后将本线程置为不可中断的睡眠状态.等待被显式地唤醒(当然是解锁之后).
}

#自旋锁加锁
static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock)  #CPU个数小于256个的情况
{
short inc = 0x0100;

asm volatile (
LOCK_PREFIX "xaddw %w0, %1/n"  #先保存实时锁的高八位,然后再让实时锁高八位加1,结果为线程栈变量(当时锁)高八位等于未加1的实时锁高八位.此操作是原子的,因此绝不对出现两个当时锁相等的线程在自旋.
"1:/t"
"cmpb %h0, %b0/n/t"  #假设实时锁为初始值话(0),那么当时锁的二进制就等于0 = 00000000 00000000.高八跟低八比较.
"je 2f/n/t"  #如上假设的条件成立的话,跳到2f处往下执行.否则开始自旋.
"rep ; nop/n/t"  #空指令
"movb %1, %b0/n/t"  #如果拿不到锁,则一直用线程自己的栈变量inc(当时锁)的高八位跟实时锁(注意:随时会因为解锁的原子操作而发生改变.)的低八位作比较.
#举个例子说明:
#例如有一个A线程进来,尝试获取实时锁,A线程用自己的栈变量inc(当时锁)保存的实时锁,然后让实时锁的高八位加1(也就是加256),接着让当时锁的
#高八位与低八位比较,如果相等则跳过自旋,往下执行.假设他是第一个来获取实时锁的,由于实时锁的初始值为0,所以高低八位都是0,因此能成功获
#取实时锁.接着B,C,D线程同时要来获取实时锁(A线程还没解实时锁),由于原子操作导致每来一个线程,就会让实时锁的高八位加1(也就是加256),所
#以线程自己的栈变量inc(当时锁)高八位分别为1,2,3.现在,实时锁的高八值为4.这3个线程进来后判断他们的栈变量inc(当时锁)的高八位和低八位
#是否相等(废话,肯定不相等),如不相等便进入自旋状态,每一次自旋都获取实时锁的低八位赋值给线程自己的栈变量inc(当时锁)的低八位,然后让当
#时锁的高低八位作比较,如果A线程执行解锁操作(让实时锁低八位加1),这时B线程的的当时锁的高八位为1,跟解锁后的实时锁低八位一致,所以B线程
#获得锁,然后以此类推地实现了有序地自旋等待解锁.因为只用了高低八位,所以同一时刻的请求获取锁的线程数最大为256.
#总结下,自旋锁就是用实时锁的低八位跟当时锁(来尝试获得锁的线程里的栈变量)的高八位不断比较.每来一个线程都让实时锁高八位加1,从而导致了
#来争夺锁的线程里的栈变量当时锁的高八位等于上一次实时锁的高八位(加1之前).结合上一句话,便实现了有序地争夺自旋锁.
/* don't need lfence here, because loads are in-order */
"jmp 1b/n"  #进行下一轮判断,也就是自旋.
"2:"
: "+Q" (inc), "+m" (lock->slock)  #用EA,B,C,DX中的一个装inc的值.而slock直接作为内存变量.
:
: "memory", "cc");
}
#自旋锁解锁
static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock)
{
asm volatile(UNLOCK_LOCK_PREFIX "incb %0"  #解锁操作是原子地让实时锁低八位加1.
: "+m" (lock->slock)
:
: "memory", "cc");
}
----------------------------------------------------------------------------------------------
#当前进程指针current
#define current get_current()

static __always_inline struct task_struct *get_current(void)
{
return percpu_read_stable(current_task);
}

DEFINE_PER_CPU(struct task_struct *, current_task) ____cacheline_aligned = &init_task;  #当前进程PCB指针,是一个每CPU变量(属于每个CPU自己的变量),默认初始化为init_task,也就是swapper或idle.

#define DEFINE_PER_CPU(type, name) __attribute__((__section__(".data.percpu"))) __typeof__(type) per_cpu__##name #定义每CPU变量的宏定义.

struct thread_info {
struct task_struct *task;  /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32  flags;  /* low level flags */
__u32  status;  /* thread synchronous flags */
__u32  cpu;  /* current CPU */
int  preempt_count; /* 0 => preemptable,
<0 => BUG */
mm_segment_t  addr_limit;
struct restart_block restart_block;
void __user  *sysenter_return;
#ifdef CONFIG_X86_32
unsigned long previous_esp; /* ESP of the previous stack in
case of nested (IRQ) stacks
*/
__u8  supervisor_stack[0];
#endif
int  uaccess_err;
};

union thread_union {  #线程内核态堆栈.
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

#每CPU变量
#为每一个cpu分配一段专有数据区,然后把.data.percpu段中的数据拷贝到其中,每个cpu各一份.
#由于复制给了每个cpu各一份,所以存取其中的值就不能再直接用per_cpu__name做偏移量了,而
#要加上__per_cpu_offset[i],i表示cpuid.
#例如:__get_cpu_var(name)等价于__per_cpu_offset[smp_processor_id()] + per_cpu__name.
#或者这样看也行: sizeof(.data.percpu) * cpuid + per_cpu__name.
----------------------------------------------------------------------------------------------
#检查信号的时机 从系统调用,中断(当然,也包调用进程调度程序的时钟中断返回的时候)或异常处理返回用户空间之前检查.
----------------------------------------------------------------------------------------------
#fork 复制本进程PCB,数据段,代码段,栈段和已写入的堆数据.
#exec 替换掉本进程的数据段,代码段,并重置各个寄存器,例如EIP,ESP...
#pthread_create 复制本进程PCB,但共享同一块虚拟空间,不同点在于线程都拥有自己的线程栈段.
----------------------------------------------------------------------------------------------
#看了下设置中断向量表的代码,加深下记性...
setup_idt:  #设置中断描述符表
lea ignore_int,%edx  #把ignore_int的32位偏移地址放到EDX中.
movl $(__KERNEL_CS << 16),%eax  #把内核代码段选择子放在EAX的高16位,此版本的的__KERNEL_CS被设置为((12 + 0) * 8),可见,内核代码段在的描述符表项被放在GTD中,DPL是0,在GDT表中的索引是12.
movw %dx,%ax  /* selector = 0x0010 = cs */  #把ignore_int的地址的低16位放到EAX的低16位AX中
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */  #再设置ignore_int中断例程在内存中的标志为真,DPL是0,标志位为32位,并且门类型为中断门(关中断)
#所以上面的内容为: "嗨" (16位内核代码段选择子)|(32位中断例程地址偏移)|(1位在内存的标志)|(2位的特权等级)|(2位的位数标志)|(3位的门类型标志)|(8位不知道...)
lea idt_table,%edi  #获取中断向量表首地址
mov $256,%ecx  #设置循环计数,因为最大中断的个数是256.
rp_sidt:
movl %eax,(%edi)  #|
movl %edx,4(%edi)  #/把256个中断例程都设置为上面那个"嗨"!
addl $8,%edi  #386的加8...所以,下一个.
dec %ecx
jne rp_sidt

.macro set_early_handler handler,trapno  #定义个设置中断的宏,参数是中断例程的地址偏移和中断号,此宏设置的中断例程均为内核代码段,DPL为0,门类型为中断门.
lea /handler,%edx  #这里的跟上面的一致,就不多说了...
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea idt_table,%edi
movl %eax,8*/trapno(%edi)
movl %edx,8*/trapno+4(%edi)
.endm

set_early_handler handler=early_divide_err,trapno=0  #调用上面的宏依次把下面的中断例程都设置...
set_early_handler handler=early_illegal_opcode,trapno=6
set_early_handler handler=early_protection_fault,trapno=13
set_early_handler handler=early_page_fault,trapno=14

ret
#总结下,先把所有的中断例程都设置为一个通用的例程,然后再依次设置目的例程.
#例外,也贴一下Traps设置中断的代码说明:
static inline void _set_gate(int gate, unsigned type, void *addr,  #这个内联函数比较灵活地设置相应段选择子,DPL,门类型的中断例程.
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s;
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);  #这里也就等同上面那些设置内核代码段选择子,DPL...用位操作设置后会放在desc_struct(32位)的结构里.
/*
* does not need to be atomic because it is only done once at
* setup time
*/
write_idt_entry(idt_table, gate, &s);  #连同中断号和desc_struct结构,中断向量表首地址传给write_idt_entry,write_idt_entry用memcpy设置相应的中断例程.
}
----------------------------------------------------------------------------------------------
#内核初始化时的几个重要文件:
#Traps.c  #初始化各种中断例程
#entry_32.S  #系统调用定义,系统跟踪定义,中断通用例程定义...
#Irqinit.c #
#head_32.S  #初始化内存分配,设置中断向量表... 
#Segment.h  #各种模式下的段选择子定义
#Processor.h #任务环境上下文的一些结构定义
#Irq_32.c  #
#Softirq.c  #中断下半部
----------------------------------------------------------------------------------------------
#cr0 主要的两个功能_控制开启关闭分页功能_控制开启关闭虚拟模式
----------------------------------------------------------------------------------------------
# define SYSCALL_VECTOR  0x80  #宏定义0x80为系统调用中断号
#set_system_trap_gate(SYSCALL_VECTOR, &system_call); #设置把系统调用的中断例程的地址放到idt+128x8的位置
----------------------------------------------------------------------------------------------
#系统调用,先看下大概的过程:

#int 0x80的微指令分解类似于下面的操作
-------------------------------------
if(CS->RPL <= (IDTR+128*8)->DPL){
SS = TR->Index->GDT->Base->TSS->SS0
ESP = TR->Index->GDT->Base->TSS->SP0
PUSH SS(Old)
PUSH ESP(Old)
PUSHF
PUSH CS
PUSH EIP
TF = 0
if((IDTR+128*8)->gateType == 1110b)
IF = 0 => CLI
CS = (IDTR+128*8)->segmentSelector
EIP = (IDTR+128*8)->handlerOffset
}else
Segmentation fault
------------------------------------
#硬中断没有CS->RPL <= (IDTR+128*8)->DPL判断,而是直接往下执行,所以硬中断的代码总是在内核模式下运行(取决于IDT相应的中断表项).
#(IDTR+128*8)->segmentSelector初始化为__KERNEL_CS,也就是说当用户模式下成功执行int 0x80这条指令的时候,当前模式已经改为内核模式.

#成功执行int 0x80之后:
ENTRY(system_call)
----------
pushl %eax  #保存EAX
cld  #改变方向
pushl %gs
pushl %fs
pushl %es
pushl %ds
pushl %eax  #将用户空间传进来的参数和中断号压进线程内核栈.
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
pushl %ecx
pushl %ebx
----------
movl $(15 * 8 + 3), %edx  #__USER_DS
movl %edx, %ds
movl %edx, %es
movl $(__KERNEL_PERCPU), %edx
movl %edx, %fs
movl $(__KERNEL_STACK_CANARY), %edx
movl %edx, %gs
movl $-THREAD_SIZE, %ebp; #define THREAD_SIZE (PAGE_SIZE << THREAD_ORDER) #取得当前进程的task描述符
andl %esp, %ebp  #
testl $_TIF_WORK_SYSCALL_ENTRY,8(%ebp) #system call tracing in operation / emulation 是否开启跟踪,如果开启了就进入syscall_trace_entry
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax  #判断系统调用的调用号,如果超过最大系统调用号则跳到返回代码处,并设置错误号.
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)  #这里是调用以sys_call_table为基址加传进来的系统调用号乘以4字节(32位)的长度处的内核函数,也就是真正的系统调用.
movl %eax,24(%esp)  #保存系统调用的返回值到EAX对应的栈中
syscall_exit:
LOCKDEP_SYS_EXIT  #貌似是debug的时候要做点无用功?请高手指点.
cli  #屏蔽中断
TRACE_IRQS_OFF
movl 8(%ebp), %ecx  #获取当前线程的跟踪标志状态值
testl $_TIF_ALLWORK_MASK, %ecx  #如果有开启就跳进syscall_exit_work做关闭处理
jne syscall_exit_work

restore_all:
#ifdef CONFIG_TRACE_IRQFLAGS  #如果定义跟踪功能则运行以下代码 
testl $515,56(%esp) #判断中断是否关闭了,如果关闭
jz 1f
TRAC QS_ON
1:
#endif
restore_all_notrace:
movl PT_EFLAGS(%esp), %eax # mix EFLAGS, SS and CS
# Warning: PT_OLDSS(%esp) contains the wrong/random values if we
# are returning to the kernel.
# See comments in process.c:copy_thread() for details.
movb PT_OLDSS(%esp), %ah
movb PT_CS(%esp), %al
andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
CFI_REMEMBER_STATE
je ldt_ss  # returning to user-space with LDT SS
restore_nocheck:
RESTORE_REGS 4  # skip orig_eax/error_code
CFI_ADJUST_CFA_OFFSET -4
irq_return:
INTERRUPT_RETURN
.section .fixup,"ax"
ENTRY(iret_exc)
pushl $0  # no error code
pushl $do_iret_error
jmp error_code
.previous
.section __ex_table,"a"
.align 4
.long irq_return,iret_exc
.previous

CFI_RESTORE_STATE
ldt_ss:
larl PT_OLDSS(%esp), %eax
jnz restore_nocheck
testl $0x00400000, %eax  # returning to 32bit stack?
jnz restore_nocheck  # allright, normal return

#ifdef CONFIG_PARAVIRT
/*
* The kernel can't run on a non-flat stack if paravirt mode
* is active. Rather than try to fixup the high bits of
* ESP, bypass this code entirely. This may break DOSemu
* and/or Wine support in a paravirt VM, although the option
* is still available to implement the setting of the high
* 16-bits in the INTERRUPT_RETURN paravirt-op.
*/
cmpl $0, pv_info+PARAVIRT_enabled
jne restore_nocheck
#endif

/*
* Setup and switch to ESPFIX stack
*
* We're returning to userspace with a 16 bit stack. The CPU will not
* restore the high word of ESP for us on executing iret... This is an
* "official" bug of all the x86-compatible CPUs, which we can work
* around to make dosemu and wine happy. We do this by preloading the
* high word of ESP with the high word of the userspace ESP while
* compensating for the offset by changing to the ESPFIX segment with
* a base address that matches for the difference.
*/
mov %esp, %edx  /* load kernel esp */
mov PT_OLDESP(%esp), %eax /* load userspace esp */
mov %dx, %ax  /* eax: new kernel esp */
sub %eax, %edx  /* offset (low word is 0) */
PER_CPU(gdt_page, %ebx)
shr $16, %edx
mov %dl, GDT_ENTRY_ESPFIX_SS * 8 + 4(%ebx) /* bits 16..23 */
mov %dh, GDT_ENTRY_ESPFIX_SS * 8 + 7(%ebx) /* bits 24..31 */
pushl $__ESPFIX_SS
CFI_ADJUST_CFA_OFFSET 4
push %eax  /* new kernel esp */
CFI_ADJUST_CFA_OFFSET 4
/* Disable interrupts, but do not irqtrace this section: we
* will soon execute iret and the tracer was already set to
* the irqstate after the iret */
DISABLE_INTERRUPTS(CLBR_EAX)
lss (%esp), %esp  /* switch to espfix segment */
CFI_ADJUST_CFA_OFFSET -8
jmp restore_nocheck
CFI_ENDPROC
ENDPROC(system_call)
----------------------------------------------------------------------------------------------
有空继续研究...

作者“逆水行舟不进则退-敏少”