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.