GDT TSS IDT LDT

这个地方很烦,今天总结一下。这里的代码是2.6.27
     这些都是x86硬件设计的,Linux内核其实并不想依照x86的设计,但是要用人家的硬件就必须应付一下,比如
原来功能实现很复杂,但Linux可能只实现其中的部分功能,其它功能Linux自己通过其它方法去完成。
Linux中的寻址: logical addr --> linear addr --> physical addr
    第一个转换是通过GDT的分段机制,第二个转换是通过分页机制。CPU使用logical addr, CPU中的MMU部件使用
physical addr。比如一个程序编译后,代码段的指令地址是0x08048888,这就是logical addr,CPU就取这个地址。
    GDT是一个表,用来实现logical addr--> linear addr的转化,也就是分段思想的实现。gdtr寄存器指向GDT在内存
中的首地址,用CS,DS中的内容做为index,这个index的学名叫segment selector。
    CS是16位的,前13位作为index,最低2位用来判断CPU是运行在0级还是3级,即系统态还是用户态,剩下那位指明
访问GDT还是LDT。
    GDT的表项里有DPL, CPU访问它时要对比CS最低两位和GDT表项中的DPL,通过了检查才可以继续访问.
    知道这些就够了。经过GDT之后就把logical address转换成了linear address,在Linux中这两个地址是相同的,
可见Linux根本就不想什么分段,它只想分页,没办法而已。
    TSS是一个特殊的段。在Linux中,CPU从系统态切换到用户态时会用到TSS里面的ss0和esp0。每个CPU只维护
一个TSS。TR寄存器指向这个TSS,切换时里面的ss0和esp0会有改变。相应有一个TSSD放在GDT中,是GDT的
一个表项。好了够了。
    IDT是一个表,用在中断处理中,IDT的表项中也含有DPL,CPU在处理若想执行ISR就必须先穿过IDT,
这里也可能有DPL的检验,穿过去了才能继续利用GDT取得ISR所在段的首地址,再根据IDT表项中的偏移就能找到ISR
的地址,然后开始执行。
    穿越IDT就是穿越相应的gate,共四种gate:task gate,interrupt gate,trap gate, call gate,这里只说中间两个。
    IDT有两类表项:
    1. 穿越 trap gate
       div0, page fault等   // gate DPL = 0
       系统调用             // gate DPL = 3
    2. 穿越 interrupt gate
       外设中断             // gate DPL = 0
根据外设中断,page fault, 系统调用分别走一下就知道它们几个的用处了。它们三个进入和返回很类似
一. 外设中断过程:
    假设用户空间的程序被外设中断,CPU根据中断向量从IDT中找到相应表项,该表项就是一个interrupt gate,外中断
在穿越interrupt gate时不检查DPL。由于CPU的CPL=3,GDT中ISR的DPL是0,所以CPU要切换到内核态,切换到
该用户进程的内核栈,这个切换过程要用到TSS: 通过TR从TSS中取出ss0和esp0的值,把它们分别装入ss和esp寄存器
中,这期间还会把一些寄存器内容压入内核栈:
    pushl ss
    pushl esp
    pushl EFLAGS           //关中断 IF = 0
    pushl cs
    pushl eip
前两项是该用户进程用户栈的指针。当然如果是CPU运行在内核态时被中断的,就不用push前两个了.
    接下来就到了IRQ0x03_interrupt的入口了
IRQ0x03_interrupt:
    pushl $0x03 - 256      //变形的中断向量,为了和系统调用号区分开
    jmp common_interrupt
common_interrupt:
    SAVE_ALL               // ss, esp, EFLAGS, cs, eip, $0x03-256, SAVE_ALL
    pushl $ret_from_intr
    jmp do_IRQ
do_IRQ()--> handle_IRQ_event() 这是真正具体的ISR。
该返回了,ret_from_intr中,先根据CS末两位判断进程被中断之前CPU运行运行在用户态还是系统态。如果原来运行在
用户态,那就依据TIF_NEEDRESCHED来判断是否发生调度;如果原来是系统态,那就根据内核是否被配置成
了preemptible,如果是preemptible,那就继续看TIF_NEEDRESCHED决定是否发生调度。如果
non_preemptible那就直接返回了。从返回过程看出,中断和系统调用在返回时是schedule的时机:
    220 ENTRY(ret_from_fork)
    221         CFI_STARTPROC
    222         pushl %eax
    223         CFI_ADJUST_CFA_OFFSET 4
    224         call schedule_tail
    225         GET_THREAD_INFO(%ebp)
    226         popl %eax
    227         CFI_ADJUST_CFA_OFFSET -4
    228         pushl $0x0202                   # Reset kernel eflags
    229         CFI_ADJUST_CFA_OFFSET 4
    230         popfl
    231         CFI_ADJUST_CFA_OFFSET -4
    232         jmp syscall_exit
    233         CFI_ENDPROC
    234 END(ret_from_fork)
    235
    236 /*
    237 * Return to user mode is not as complex as all this looks,
    238 * but we want the default path for a system call return to
    239 * go as quickly as possible which is why some of this is
    240 * less clear than it otherwise should be.
    241 */
    242
    243         # users    247         preempt_stop(CLBR_ANY)
    244         ALIGN
    245         RING0_PTREGS_FRAME
    246 ret_from_exception:
    247         preempt_stop(CLBR_ANY)
    248 ret_from_intr:
    249         GET_THREAD_INFO(%ebp)
    250 check_userspace:
    251         movl PT_EFLAGS(%esp), %eax      # mix EFLAGS and CS
    252         movb PT_CS(%esp), %al
    253         andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax
    254         cmpl $USER_RPL, %eax
    255         jb resume_kernel                # not returning to v8086 or userspace
    256
    257 ENTRY(resume_userspace)
    258         LOCKDEP_SYS_EXIT
    259         DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
    260                                         # setting need_resched or sigpending
    261                                         # between sampling and the iret
    262         TRACE_IRQS_OFF
    263         movl TI_flags(%ebp), %ecx
    264         andl $_TIF_WORK_MASK, %ecx      # is there any work to be done on
    265                                         # int/exception return?
    266         jne work_pending
    267         jmp restore_all
    268 END(ret_from_exception)
    269
    270 #ifdef CONFIG_PREEMPT
    271 ENTRY(resume_kernel)
    272         DISABLE_INTERRUPTS(CLBR_ANY)
    522                                         # setting need_resched or sigpending
    523                                         # between sampling and the iret
    524         TRACE_IRQS_OFF
    525         movl TI_flags(%ebp), %ecx
    526         andl $_TIF_WORK_MASK, %ecx      # is there any work to be done other
    527                                         # than syscall tracing?
    528         jz restore_all
    529         testb $_TIF_NEED_RESCHED, %cl
    530         jnz work_resched
    531
    532 work_notifysig:                         # deal with pending signals and
    533                                         # notify-resume requests
                                                   // 最终会到达resume_userspace
/*
* this is the entry point to schedule() from kernel preemption
* off of irq context.
* Note, that this is called and return with irqs disabled. This will
* protect us against recursive calling from irq.
*/
asmlinkage void __sched preempt_schedule_irq(void)
{
        struct thread_info *ti = current_thread_info();
        /* Catch callers which need to be fixed */
        BUG_ON(ti->preempt_count || !irqs_disabled());
        do {
                add_preempt_count(PREEMPT_ACTIVE);
                local_irq_enable();
                schedule();
                local_irq_disable();
                sub_preempt_count(PREEMPT_ACTIVE);
                /*
                 * Check again in case we missed a preemption opportunity
                 * between schedule and now.
                 */
                barrier();
        } while (unlikely(test_thread_flag(TIF_NEED_RESCHED)));
}
二. page fault 过程:
    这里不象外设中断那样还要有一段公共的处理程序common_interrupt,然后才能到达具体的ISR,page fault穿过
trap gate后,直接就能到达page fault。CPU穿越trap gate时是不自动关中断的,因此handle_mm_fault()是可被
中断的。push的内容和外设中断类似,只是多了一个error_code
    stack: ss, es, EFLAGS,cs, eip, error_code, do_page_fault
error_code:
    ....                            // 类似于SAVE_ALL
    call *edi                    // do_page_fault()
    jmp ret_from_exception        // 最终会到ret_from_intr
三. 系统调用过程: INT 0x80
    CPU穿越trap gate的过程与外设中断穿越interrupt gate的过程基本相同,只是通过INT指令穿越interrupt gate
或者trap gate时要检查DPL。而且此时CPU不自动关中断,因此系统调用也是可被中断的。然后到达system_call,此
时CPU已经是系统态。
system_call:
    pushl %eax                    // save 系统调用号 orig_eax
    SAVE_ALL                      // ss, es, EFLAGS, cs, eip, orig_eax, SAVA_ALL
    ...
    call sys_call_table(, %eax, 4) // 到table中找到对应的程序执行
    系统调用要注意的是:要在用户空间向系统空间传参数,参数是用寄存器来传的。设计copy_from_user,从中可以知道
最好用movsl一次传送4 Byte,不够4 Byte的部分用movsb单独传送,效率高。__copy_user_zeroing宏等价的C代码:
__copy_user_zeroing(void *to, void *from, size_t n) {
    int remain = size &3;
    size /= 4;
    while (size--) {
        *((int *)to) = *((int *)from);
        to++;
        from++;
    }
    while (remain--) {
        *((char *)to) = *((char *))from);
        to++;
        from++;
    }
}
===================================================================
                               do_page_fault()
    既然说到了page fault,现在就来说说。主要是要区分开error 和 page fault。首先明确内存管理中的3种flag,
这里用到了前两种:
    pte: 低12位是flag, Present, Dirty, Accessed, Writable….
    vm_area_struct: vm_flags VM_READ VM_WRITE …
    page struct: PG_dirty…..
如果CPU在用户态时引发了page fault时,如下两点很重要:
1. address在memory region内,例如用户进程访问了kernel space的地址,那就是error了
2. memory region中的vm_flags和access type相符,例如write引发了page fault,则vma->flags & VM_WRITE为true才行
通过了这两个检查之后,处理的时候还要区分 demand paging 和 COW
    无论是read还是write引发了page fault,只要页面不在(也就是pte中Present为0),那就属于demand paging。
需要分配一个新的物理页,里面填上相应内容。
    如果是write引发了page fault,并且页存在,只是pte的flags是readonly,则属于COW,需要分配一个新物理页,填
上刚才那页的内容。
    具体分配物理页的方法:do_page_fault()->handle_mm_fault()->handle_pte_fault():
if (!pte_present(entry)) {       // demand paging
    if (pte_none(entry)) {
        if (vma->vm_ops) {
            if (likely(vma->vm_ops->fault))
                return do_linear_fault(mm, vma, address, pte, pmd, write_access, entry);
        }
        return do_anonymous_page(mm, vma, address, pte, pmd, write_access);
    }
    if (pte_file(entry))
        return do_nonlinear_fault(mm, vma, address, pte, pmd, write_access, entry);
    return do_swap_page(mm, vma, address, pte, pmd, write_access, entry);
}
ptl = pte_lockptr(mm, pmd);
spin_lock(ptl);
if (unlikely(!pte_same(*pte, entry)))
    goto unlock;
if (write_access) {               // COW
    if (!pte_write(entry)
        return do_wp_page(mm, vma, address, pte, pmd, ptl, entry); // COW
    entry = pte_mkdirty(entry);
}
    COW技术在fork()中用到了,那时就没有copy物理页,只是将pte的flag设成了readonly:
copy_mm()->dup_mm()->dup_mmap()->copy_page->range()->copy_pud_range()->copy_pmd_range()->
copy_pte_range()->copy_one_pte():
        if (is_cow_mapping(vm_flags)) {
                ptep_set_wrprotect(src_mm, addr, src_pte);
                pte = pte_wrprotect(pte);
        }
    COW思想在程序设计方面也有应用,比如C++中:
#include <iostream>
using std::cout;
using std::endl;
int main(int argc, char *argv[])
{
    const int a = 10;      // const is readonly
    int *p = (int *) &a;
    *p = 20;
    cout << *p << " " << a << endl;
    return 0;
}
结果为: 20 10
咋一看似乎const的值被修改了,其实是在其它地方开了一块物理内存用来容纳 *p.

posted on 2011-08-02 11:42  不知道  阅读(4207)  评论(1编辑  收藏  举报

导航