我们基于RH9 内核从两部分来分析Linux系统动态运行过程
一: 系统初始化开始,Linux进入保护模式,初始内存系统、中断系统、文件系
统等,直到创建第一个用户进程。
二: 用户进程通过系统调用主动进入内核,CPU 接受中断请求被动执行各种中断服务。
第一部分 系统初始化
进入保护模式 Arch/i386/boot/Setup.s
gdt:
.fill GDT_ENTRY_KERNEL_CS,8,0 #空12×(8个字节=2个双字),用0 填充。
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9A00 # code read/exec
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9200 # data read/write
.word 0x00CF # granularity = 4096, 386
gdt_48:
.word 0x8000 # gdt limit=2048,
# 256 GDT entries
我们进入保护模式之后选择子首先应该是0x60 = 1100000b, 右移3 位为1100 =
0xC = 12 ,这就是code在GDT里的位置。当然这只是第一次进入保护模式,以后还会调整。
进入分页模式 arch/i386/kernel/head.S
.org 0x1000
ENTRY(swapper_pg_dir)
.long 0x00102007 #基地址为0x00102000,就是下面pg0,0x00100000是内核加载的地方,1M的位置
.long 0x00103007
.fill BOOT_USER_PGD_PTRS-2,4,0 #768-2 (long) 0
/* default: 766 entries */
.long 0x00102007
.long 0x00103007
/* default: 254 entries */
.fill BOOT_KERNEL_PGD_PTRS-2,4,0 #256-2(long) 0
# 这个说明了内核是1G,用户就3G。
.org 0x2000
ENTRY(pg0)
.org 0x3000
ENTRY(pg1)
说明映射了两个页面,一个页面代表4M, 每个pg0 里的一项代表4K。具体pg0数据是用程序填充的 参考L82。L101 通过短跳转进入分页(但eip 还不是),L104通过绝对地址完全进入分页。比如L105的地址是0xC0100058 =1100 0000 0001 0000 0000 0000 0101 1000B,高10 位为1100 0000 00 = 0x300 = 768。从swapper_pg_dir里找768项就是0x00102007,检查之后基地址为0x00102000;就是pg0,再看0xC0100058 中间10位01 0000 0000B = 0x100 = 256,再找pg0 的256项应该为00100***,那么最后的物理地址为0x00100058,所以内核里的虚拟地址换物理地址很简单,就是0xC0100058 - 0xC0000000 = 0x00100058 就是物理地址。现在系统只是映射了8M的内存空间,之后还会调整。
内存管理
物理内存管理
第一次探测
Arch/i386/boot/setup.S L281
在这里使用了三种方法,通过调用0x15 中断:
E820H 得到memory map。
E801H 得到大小。
88H 0-64M.
第二次探测
start_kernel() -> setup_arch()
这里主要是对物理内存的初始化,建立zone区。并且初始化page。
start_kernel() -> setup_arch() -> setup_memory_region()
把bios 里的memory map 拷贝到 e820 这个全局变量里。
会在屏幕上显示
BIOS-provided physical RAM map:
BIOS-e820: 0000000000000000 – 000000000009FC00 (usable)
BIOS-e820: 0000000000100000-0000000002000000 (usable)
BIOS-e820 说的是通过e820 来读取成功的数据,第一个代表起始地址,后面接
着的是大小。usable 说明内存可用。还有 reserved, ACPI data, ACPI NVS 等。
第二条说明起始地址为0x100000 = 1M,大小是0x2000000 = 32M 内存。
start_kernel() -> setup_arch() -> setup_memory ()
start_kernel() -> setup_arch() -> setup_memory ()->find_max_pfn()
通过e820计算出最大可用页面。
e820.map[1].addr = 0x100000 e820.map[1].size = 0x1f00000,那么start =
PFN_UP(0x100000) = (0x100000+0x1000(4096 = 1>>12)-1)>>12 =
0x100FFF>>12=0x100, end = PFN_DOWN(0x2000000) = 0x2000000 >> 12 =
0x2000 = 8192,那么max_pfn = 0x2000 就是最大的页面
start_kernel() -> setup_arch() -> setup_memory ()->find_max_low_pfn()
主要是高端内存的限制。
max_low_pfn = max_pfn 不能大于896M, PFN_DOWN((-0xC0000000 –
0x8000000(128<<20)) = (0x38000000 = 896M限制)) = 0x38000
start_kernel() -> setup_arch() -> setup_memory ()-> init_bootmem()
引导内存分配只使用在引导过程中,为内核提供保留分配页,并且建立内存的位图。
比如_end 为0xc02e5f18,start_pfn = PFN_UP(__pa(&end)) = 0x2e6,所以
start_pfn 指的是内核结束的下一个页面((_pa(&_end) >> 12 =0x2e5 ,那么
init_bootmen(start_pfn,max_low_pfn) = init_bootmen(0x2e6,0x2000)
start_kernel() -> setup_arch() -> setup_memory ()->
init_bootmem()->init_bootmem_core()
init_bootmen(0x2e6,0x2000) = init_bootmem_core(&contig_page_data, 0x2e6, 0,
0x2000),mapsize就是建立内存位图大小需要多少字节 1024=(8192-0+7)/8,
1个位代表着4K的一个页面。
bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT) =
phys_to_virt(0x2e6<<12) = phys_to_virt(0x2e6000) = 0xc02e6000
bdata->node_boot_start = (start << PAGE_SHIFT) = 0
bdata->node_low_pfn = end = 8192
把0xc02e6000 开始0xff 填充1024 个字节 0xc02e6400,这样就代表着所有内
存不可用。
start_kernel() -> setup_arch() -> setup_memory ()-> register_bootmem_low_pages
e()
根据e820和内存位图来标识位图那些内存可用。
start_kernel() -> setup_arch() -> setup_memory ()-> register_bootmem_low_pages
e()->free_bootmem()->free_bootmem_core()
第一次调用 free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size)) =
free_bootmem(PFN_PHYS( 0), PFN_PHYS(159))= free_bootmem(0, 0x9F000))
free_bootmem_core(contig_page_data.bdata, 0, 0x9f000)
eidx代表这块内存共占用多少页面
eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE = (0+0x9f000-0)/0x1000=0x9f
end = (addr + size)/PAGE_SIZE=(0+0x9f000)/0x1000=0x9f
start = (addr + PAGE_SIZE-1) / PAGE_SIZE = (0+0x1000-1)/0x1000=0
sidx = start - (bdata->node_boot_start/PAGE_SIZE)=0-(0/0x1000)=0
每一次循环清1 位,第一次循环0x9f次清到0x9f /8= 0x13,所以第一次清到
了0xc02e6013的第7位159%8 = 7,所以0xc02e6013的数据应该是0x10000000,
第二次free_bootmem_core(contig_page_data.bdata, 0x100000, 0x 1F00000)
eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE = 0x2000
end = (addr + size)/PAGE_SIZE = 0x2000
start = (addr + PAGE_SIZE-1) / PAGE_SIZE = 0x100
sidx = start - (bdata->node_boot_start/PAGE_SIZE) = 0x100
这次从0xc02e6020 ( 0x100/8 ) 开始清0x2000-0x100 次, 清到
0xc02e6400(0xc02e6020+0x1f00/8)
通过free_bootmem的操作,我们已经把内存的位图标识出来。
start_kernel() -> setup_arch() -> setup_memory ()->reserve_bootmem()
保留内存,说明这部分不能用于动态分配。
reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +
bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY)) =
reserve_bootmem(0x100000, (PFN_PHYS(2e6) +1024 + 4096-1) - (0x100000))=
reserve_bootmem(0x100000, 1E73FF)
保留内核开始1M 到建立的内存位图的结束地方,因为位图是接着内核的下
一个页面存放的,所以一起保留,对于+ PAGE_SIZE-1 操作就是位图的结束
位置也与内存边界对齐。重新改写内存位图之后对于我们的情况是
0xc02e6020(1M)到0xc02e605C 都改成了1(保留)0xc02e605D还是0
reserve_bootmem(0,PAGE_SIZE)= reserve_bootmem(0,4096)
保留物理内存的第0 页内存。0x0-0x4096
start_kernel() -> setup_arch() -> paging_init()
当我们上面的物理内存都完成时,我们就要对所有内存进行管理,需要重新
建立页面映射。
start_kernel() -> setup_arch() -> paging_init()->pagetable_init()
我们根据已有的内存信息,重新修改页面表
end = (unsigned long)__va(max_low_pfn*PAGE_SIZE) = 0xC2000000
pgd_base = swapper_pg_dir = 0xC0101000
i = __pgd_offset(PAGE_OFFSET) = (0xC0000000>>22)&(1024-1)=768
pgd = pgd_base + i= 0xC0101C00
pgd就是内核模式的第一个页目录
第一个for循环就是说从768开始到1024结束,也就是swapper_pg_dir结束,
这部分都是内核的页目录,也就是我们要修改的页目录。
第二个for循环用于中间页表,对我们2 层i386 体系无效。
alloc_bootmem_low_pages(PAGE_SIZE) = __alloc_bootmem(0x1000,0x1000,0)
->__alloc_bootmem_core(pgdat->bdata, 0x1000,0x1000,0)
eidx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT) =
8192-(0>>12) = 8192 代表着这个区域可用的页面,实际上就是整个内存。
我们以前保留过第0 页面的内存,所以我们分配内存就是从第一页面开始,
重新写内存位图将已分配的页面做保留标记。这次运行后我们分配了一个页
面(0x4096),开始地址为0xc0001000,与此同时,0xc02e6000 = 0x3 = 0011。
第三个for循环用于写页表,写满这个我们新分配的页表。
第一次循环的时候pte = 0xc0001000;vaddr = 0xc0000000;
*pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL) =
mk_pte_phys(0, MAKE_GLOBAL(_PAGE_PRESENT | _PAGE_RW |
_PAGE_DIRTY | _PAGE_ACCESSED) =
mk_pte_phys(0, MAKE_GLOBAL(0x001 | 0x002 | 0x040 | 0x020) =
mk_pte_phys(0, ( MAKE_GLOBAL(0x001 | 0x002 | 0x040 | 0x020) = 0x63)) =
mk_pte_phys(0, pgprot_t __ret = __pgprot(0x63)) = mk_pte_phys(0, __ret) =
__mk_pte((0) >> 12, __ret) = __pte(((0)<<12)| pgprot_val(__ret)) = __pte(0x63)
= 0x00000063
第二次运行时 vaddr = 0xc0001000;
*pte = 0x00001063
当pmd = 0xc0101c00 时
set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base))) =
(pmd,__pmd((_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED |
_PAGE_DIRTY) + 0x00001000) = (pmd,__pmd(0x001 | 0x002 | 0x020 | 0x040
+0x00001000) = set_pmd(pmd,__pmd( 0x00001063)
这就是结果0xc0101c00=0x00001063下一次pmd是0xc0101c04=0x00002063
对于我们现在的状态,我们是32M 内存,end = 0xC2000000,所以只映射了8
项,0xC0101C1C=0x00008063
现在要建立专用区的页表了,专用区是位于虚拟地址为0xfffff000 往回的一
个区域。Enum fixed_addresses 描述了专用区的功能
vaddr=__fix_to_virt(__end_of_fixed_addresses-1)&PMD_MASK= 0xFFC00000
这就是专用区起始地址所在的中级页目录边界地址
fixrange_init(vaddr, 0, pgd_base) = fixrange_init(0xffc00000, 0, 0xc0101000)
我们要在里面建立专用区的页面。
0xffc00000 处于页目录的1023 映射的位置(最后一项),所以又申请了一个
页面0xc0009000,(没有作为页表使用)。因为这个页面是接着我们以前的目
录页面申请的,所以这个页面与目录页面在物理上是连续的,但在虚拟地址
却差很大。0xC0101FFC = 0x00009067,这就是目录映射了。
当我们运行完时我们就把所有的页表及页目录都建立好,通过load_cr3 与
__flush_tlb_all重新刷新分页机制。
start_kernel() -> setup_arch() -> paging_init()->zone_sizes_init()
根据内存位图开始建立分区。
max_dma = virt_to_phys((char*) MAX_DMA_ADDRESS) >> PAGE_SHIFT =
virt_to_phys(0xC1000000) >> 12 = 0x1000
对于我们32M内存来说,zones_size = { 4096, 4096, 0 }
start_kernel() -> setup_arch() -> paging_init()->zone_sizes_init()->free_area_init()
建立分区数据结构:标记所有页面保留,标记所有内存队列空,清除内存位
图。(需要详细)
map_size = (totalpages + 1)*sizeof(struct page) = 0xE000 根据page结构看
8192 个页面需要多大空间。
lmem_map =alloc_bootmem_node(pgdat, 0xE000) =
__alloc_bootmem_node(pgdat, 0xE000, 0x10, 0x1000000) = 0xc1000000
lmem_map = (struct page *)(PAGE_OFFSET +
MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET))
= 0xC0000000+MAP_ALIGN(0x1000000) = 0xC0000000+
((((0x1000000) % sizeof(mem_map_t)) == 0) ? (0x1000000) : ((0x1000000) +
sizeof(mem_map_t) - ((0x1000000) % sizeof(mem_map_t))))=
0xC0000000+((8==0)?0x1000000: 0x1000038-8) = 0xC1000030
因为cache需要对齐
接下来循环初始化每个区
zone->zone_mem_map = mem_map + offset; 这个区的mem_map 结构位置
zone->zone_start_paddr = zone_start_paddr; 这个区的开始虚拟地址。
…
for (i = 0; i < size; i++) {
struct page *page = mem_map + offset + i;
…
}
初始化这个区的所有page 结构。在page 里所有的页面都是保留的,不能投
入使用。
…
for (i = 0; ; i++) {
unsigned long bitmap_size;
INIT_LIST_HEAD(&zone->free_area[i].free_list);
…
}
这是伙伴分配系统的初始化
这就说明了每个区有每个区自己的管理系统。
http://www-900.ibm.com/developerWorks/cn/linux/l-numa/index.shtml
start_kernel() -> setup_arch() -> paging_init()->zone_sizes_init()->free_area_init()
->build_zonelists()
start_kernel() -> setup_arch() -> dmi_scan_machine()
dmi_iterate()
isa_memcpy_fromio(buf, fp, 15)= memcpy_fromio(buf,__ISA_IO_base +
0xF0000,15) = __memcpy(buf,__io_virt(0xC00F0000),(15))
buf = 0xC00F0000 里的数据,循环一直到fp = 0xFFFFF为止,每次fp 加0x10
当这些执行完的时候,内核的基础也就大部分已经建立 ,我们在继续看第
三部分的初始化。
第三次探测
start_kernel() ->mem_init()
start_kernel() ->mem_init()->free_pages_init()
start_kernel() ->mem_init()->free_pages_init()->free_all_bootmem_core()
struct page *page = pgdat->node_mem_map; 这就是第一个page.
idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT) =
8192-0>>12 = 8192
第一个循环就是根据我们以前建立的内存位图来初始化Page。
start_kernel() ->mem_init()->free_pages_init()->free_all_bootmem_core()->
__free_page()->__free_pages()->__free_pages_ok()
释放内存页(需要详细)
第二个循环就是释放内存位图本身的内存。
执行完后以后的内存操作就可以使用Page 了,第三阶段的内存初始化完毕,还
有一部分零散的内存操作会在别的部分讨论,至此,物理内存管理结束。
虚拟内存管理
中断系统
描述
X86 微机通过外接两片级连的可编程中断控制器8259A,接收更多的外部中
断。每个8259A控制器可以管理8 条中断线,两个可以控制15 条中断。
由于Intel 公司保留0-31 号中断向量用来处理异常事件,所以硬件中断必须
设在31以后,Linux则在实模式下初始化时把硬件中断设在0x20-0x2F。还有一
个是0x80是系统调用。
中断门与陷阱门区别:通过中断门门进入中断服务程序时CPU 会自动将中断关
闭,而通过陷阱门进入则不会。
原理
Linux的中断实现原理:考虑中断处理的效率,Linux的中断处理程序分为两
个部分:上半部和下半部。上半部的功能是“登记中断”,具体的中断实现会在
下半部。之所以上半部要快,因为上半部是完全屏蔽中断的。下半部有以下几种
实现方式:bottom half, task queue(没看), tasklet, softicq,在我们分析的这个版
本中,我认为bottom half以及softicq 都是基于tasklet实现的,以后会具体介绍。
第一次中断设置arch/i386/kernel/head.S
# L325
ignore_int 这是默认的中断服务程序,实际上什么也不做。
L359
SYMBOL_NAME(idt_descr):
.word IDT_ENTRIES*8-1 # idt contains 256 entries
.long SYMBOL_NAME(idt_table) #idt_table定义在Arch/i386/Kernel/Traps.c
L245
lidt idt_descr
这样如果有中断、异常发生就会去ignore_int空转一次。
第二次中断设置
start_kernel() -> trap_init ()
主要是对一些系统保留的中断向量初始化
struct desc_struct idt_table[256] Arch/i386/kernel/Traps.c L67
extern irq_desc_t irq_desc [NR_IRQS]; include/linux/irq.h L67
idt_table = 0xC028B000
陷阱门 只允许在系统级下调用
set_trap_gate(0,÷_error)=_set_gate(idt_table+0,15,0, 0xC0109060);
0xC028B000 = 0xC0108F0000609060
中断门 只允许在系统级下调用
set_intr_gate(2,&nmi)=_set_gate(idt_table+2,14,0, 0xC0109108);
0xC028B010 = 0xC0108E0000609108
系统门 可以用户级调用,否则无法提供系统调用了。看第三个参数为3
set_system_gate(3,&int3) =_set_gate(idt_table+3,15,3, 0xC0109138);
0xC028B018 = 0xC010EF0000609138
start_kernel() -> trap_init () -> cpu_init()
初始化CPU,并且重新装载GDT 与IDT。
我们先说一下ccurrent 宏。通过这个宏可以得到当前运行进程的struct
task_struct 。
cpu_gdt_table Arch/i386/Kernel/head.S L423
然后通过两行汇编重新装载GDT与IDT。
start_kernel() -> init_IRQ()
主要是对外设的中断初始化
init_ISA_irqs() 中断请求队列的初始化
init_ISA_irqs()->init_8259A() 可编程中断控制器8259A的初始化
继续初始化中断向量,从0x20(32)开始16个,因为之前是保留的。
void (*interrupt[NR_IRQS])(void) = {
IRQLIST_16(0x0),
… }
#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)
#define IRQ(x,y) \
IRQ##x##y##_interrupt
#
define NR_IRQS 224
255 -31 = 224 所以interrupt 定义的Intel保留之后的中断
对于IRQ##x##y##_interrupt函数的定义在 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");
经过处理之后得到:
asmlinkage void IRQ0x01_interrupt(); \
__asm__( \
"\n"__ALIGN_STR"\n" \
IRQ0x01_interrupt:\n\t" \
"pushl $0x01-256\n\t" \ #这就是每个中断的最大区别
"jmp common_interrupt");
#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));
*/
中断发生时都会运行do_IRQ 这个公共的中断处理函数。返回时就会返回到
ret_from_intr继续运行后再返回。
idt_table里存放了具体的中断服务的内存地址,CPU产生中断会通过idt_table
去运行具体的中断服务程序。除了系统保留的中断是运行各自不同的中断服
务,其它都会运行到do_IRQ 这个公共中断服务程序里来。如果有两个设备
使用了同一个中断的话,我们就需要有一种机制来识别具体是那个设备发生
了中断。
中断服务队列的初始化
init_IRQ() 初始化完成后,并没有具体的中断服务程序,真正的中断服务程
序要到具体设备的初始化程序将其中断服务程序通过request_irq()向系统“登
记”,挂入某个中断请求队列以后才会发生。
request_iqrt()->setup_irq()
将具体的中断服务链入相应的中断请求队列。
下面这个图就是有中断服务链入后的数据结构的简图。
中断服务的响应(上半部)
do_IRQ() Arch/i386/kernel/irq.c L563
desc->handler->ack(irq);
中断处理器(如i8259A)在将中断请求“上报”给CPU以后,希望CPU
给一个确认(ACK),表示CPU在处理了。
IRQ_INPROGRESS 主要是为多处理器设置的,IRQ_PENDING 说明再
次有同样的中断发生,可能是SMP系统结构中,一个CPU正在中断服务,
而另一个又进入do_IRQ(),这时IRQ_INPROGRESS标志为1,所以返回了,
也可能是在单处理器中CPU 已经在中断服务程序中,但是因为某种原因又
将中断开启了,而且在同一个中断通道中再次产生了中断,也会因为
IRQ_INPROGRESS标志为1 返回
handle_IRQ_event()是for 循环里的主题,进入for 循环时desc->status的
IRQ_PENDING标志为0,如果执行完handle_IRQ_event()后,发现desc->status
的IRQ_PENDING 标志为1 就说明发生了上述情况。那就在来循环,直到
IRQ_PENDING标志为0为止。
desc->handler->end(irq);
if (softirq_pending(cpu))
do_softirq();
这就是下半部的了,我们在下半部里再分析。
do_IRQ()->handle_IRQ_event()
irq_enter(cpu, irq);说明那个CPU在那个中断服务程序里。
irq_exit(cpu, irq);与上面相反。
循环执行这个中断通道的所有中断程序,因为Linux也不知道是那个设备产
生的中断。(如果中断是共用的话)到了自己的中断服务程序里自己检查。
action是struct irqaction,看上面的图。所以struct irqaction里的handler一般是
在关中断里执行的(可以自己开中断),所以要快速处理,否则中断太久会
丢失别的中断。
第三次中断设置(软中断)
下半部
原始bottom half
原理:
static void (*bh_base[32])(void); Arch/i386/Kernel/Softirq.c L274
可以用来指向一个具体的bh 函数。同时又设置两个32 位bh_active(中断请
求寄存器)和bh_mask(中断屏蔽寄存器)。
使用方式
void init_bh(int nr, void (*routine)(void)) L311
void remove_bh(int nr) L317
static inline void mark_bh(int nr) Include/linux/interrupt.h L228
将bh_active 的nr 位置一,如果bh_mask 中的相应位也是1,那么系统在执
行完do_IRQ()后,以及每次系统调用结束后都会在函数do_buttom_half()中
执行相应的bh函数。
原始bottom half机制有很大局限性,主要是个数限制在32 个以内,所以在
2.4内核里,bottom half使用的是tasklet 机制,只是在接口上保持向下兼容。
task queue
include/linux/tqueue.h L38
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 */
};
typedef struct list_head task_queue; L64
使用方式
#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q) L66
static inline int queue_task(struct tq_struct *bh_pointer, task_queue *bh_list)
L100 – L119
static inline void run_task_queue(task_queue *list)
tasklet
主要是考虑是为了更好的支持SMP,提高SMP 的利用率;不同的tasklet可
以同时运行在不同的CPU上,同时也是Linux推荐使用的方式,我们将重点
解释
struct tasklet_struct Include/Linux/Interrupt.h L104
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
struct tasklet_struct bh_task_vec[32]; L275
使用方式(这中方式是我们普通使用的方式)
void my_tasklet_func(unsigned long);
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data) L113
static inline void tasklet_schedule(&my_tasklet) L158
另外的一些调用接口
DECLARE_TASKLET_DISABLED(name, func, data) L116
static inline void tasklet_disable(struct tasklet_struct *t) L179
static inline void tasklet_enable(struct tasklet_struct *t) L186
extern void tasklet_kill(struct tasklet_struct *t); L198
extern void tasklet_init(struct tasklet_struct *t, L199
我们看一下原始bottom half就怎么用tasklet实现的以及于我们普通的tasklet
的区别。
init_bh(TIMER_BH, timer_bh); 初始化,
mark_bh(TIMER_BH) = tasklet_hi_schedule(bh_task_vec+ TIMER_BH)
提出对timer_bh的执行请求。
可以看到,bh_task_vec就是实现bh的tasklet_struct,看下面的softirq_init()
mark_bh()->tasklet_hi_schedule()
将bh 的tasklet_struct 挂到当前CPU 的软中断执行队列,并通过
__cpu_raise_softirq()正式发出软中断请求。这样子后bh_action就会在将来的
某个适合的时间在do_softirq()得以执行。而bh_action 的主要功能就是实现
了原始bottom half,既在任意时间最多只有一个CPU在执行bh函数。但是
tasklet_struct本身是支持SMP 结构的。
tasklet_hi_schedule 与tasklet_schedule都是将tasklet_struct链入到CPU队列,
不同的是__cpu_raise_softirq() 正式发出软中断请求不一样,
tasklet_hi_schedule 使用的是HI_SOFTIRQ,而tasklet_schedule 使用的是
TASKLET_SOFTIRQ。而这两种都是在softirq_init里我们初始化的,这样我
们就建立起来了softicq。
softicq
这是底层机制,很少直接使用,softicq 基于tasklet实现更复杂更庞大的软中
断子系统。
struct softirq_action Include/Linux/Interrupt.h L69
{
void (*action)(struct softirq_action *);
void *data;
};
extern struct tasklet_head tasklet_vec[NR_CPUS]; L131
start_kernel() -> softirq_init ()
对用于hb 的32 个用于bh的tasklet_struct结构调用tasklet_init()以后,它们
的函数指针func全都指向bh_action()。bh_action就是tasklet实现bh的机制了
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
这是我们普通的tasklet_struct要用到的软中断。
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
这是tasklet_struct实现的bh调用。
软中断的执行
do_softirq()
软中断服务不允许在一个硬中断服务程序内部执行,也不允许在一个软
中断服务程序内部执行,所以通过in_interrupt()加以检查。
h->action 就是串行化执行软中断,当bh 的tasklet_struct 链入的时候,就能
在这里执行,在bh里重新锁了所有CPU,导致一个时间只有一个CPU可以
执行bh 函数,但是do_softirq 是可以在多CPU 上同时执行的。而每个
tasklet_struct在一个时间上是不会出现在两个CPU上的。
只有当Linux初始化完成开启中断后,中断系统才可以开始工作。(开始心跳J)
文件系统
Linux使用了VFS(虚拟文件系统)对上提供统一的接口来提供服务。对下使用
了“总线”来读取各种文件系统及设备。如图所示:
一个块设备的文件及节点在引导之初是不可访问的,只有将它“安装”在文
件系统的某个节点上才可访问。最初只有一个“根”节点“/”。
VFS(虚拟文件系统)
在讲解文件系统时,有很多重要的数据结构需要弄清楚,我们根据需要一个
一个的介绍数据结构之间的关系。
下图是VFS 体系结构,对于具体的文件系统都要融入到这个体系结构里来。
从下图可以看到:每一个转载点都有一个struct vfsmount结构。而每一个节点都
有struct dentry 和 struct inode。而这两个结构侧重面有些不同。 struct dentry主
要是VFS使用,而struct inode主要是由各个具体的文件系统自己解释。通过struct
inode,使各个具体的文件系统融入到VSF体系里来。
在讲解根文件系统的安装之前要先说以下系统堆栈。
我们回过头看
lss stack_start,%esp esp = 0xC0272000 /Arch/i386/Kernel/Head.S
L107
这是在进入分页后执行的第一条语句,它的意义是建立内核堆栈。
ENTRY(stack_start) L317
.long SYMBOL_NAME(init_task_union)+8192
.long __KERNEL_DS
堆栈大小是8K(8192)。并且将init_task_union 写到堆里,这是为系统的进
程准备环境。(可以把内核也看成是一个进程,以后可以看到,不管是内核本身
还是内核创建的进程都是用的这一个堆栈)堆栈底部为0xC0270000,并且
init_task_union就初始化在0xC0270000处。
union task_union init_task_union Arch/i386/Kernel/Init_task.c L22
__attribute__((__section__(".data.init_task"))) =
{ INIT_TASK(init_task_union.task) };
union task_union { include/linux/Sched.h L684
task_t task;
unsigned long stack[INIT_TASK_SIZE/sizeof(long)];
};
typedef struct task_struct task_t; L159
说明task_union既可以是task的结构,也可以是stack数组。
#define INIT_TASK(tsk) Include/linux/Sched.h L636
这个宏是初始化一个task_struct结构。这个结构是一个进程的结构,也就是说,
内核编译时,init_task_union 就已经写了数据,这些数据都是固定的。而我们关
心的是task结构里的fs这一项。&fs = 0xc0270640, fs=0xC0254FE0
fs = static struct fs_struct init_fs = INIT_FS; Arch/i386/Kernel/Init_task.c L9
我们看到,fs这项在编译是写的是&init_fs的地址。而init_fs是通过INIT_FS宏
填充好的。所以我们得到了一个如下的图,这个图是在内核编译是就确定了的。
可以看到,task_struct代表的是一个进程,fs_struct代表着该进程的部分环境。
根文件系统的安装过程
第一次安装:
register_filesystem () fs/Suer.c L99
任何一种文件系统都需要通过调用该函数进行注册,系统才能识别。
这个函数很简单,就是遍历一次注册的文件系统链表,如果没有注册的
话就不将其加入到链表中。file_systems=0xC02BA3E0 就是链表的头,也就
是root_fs_type的位置。
start_kernel () ->vfs_caches_init ()->mnt_init()->init_rootfs()
注册了rootfs_fs_type这种文件系统。
static DECLARE_FSTYPE(rootfs_fs_type, "rootfs", ramfs_read_super,
FS_NOMOUNT|FS_LITTER); fs/ramfs/Inode.c L321
DECLARE_FSTYPE是个宏,定义一个file_system_type的结构,展开为
struct file_system_type rootfs_fs_type = {
name: "rootfs",
read_super: ramfs_read_super,
fs_flags: FS_NOMOUNT|FS_LITTER,
owner: NULL,
}
start_kernel () ->vfs_caches_init ()->mnt_init()->init_mount_tree ()
do_kern_mount()
对文件系统的预安装。
通过alloc_vfsmnt 初始化一个struct vfsmount, mnt = 0xC10EE104,通过flags
来判断应该走那条线路,
do_kern_mount()->get_fs_type(“rootfs”)
可以看到首先通过文件系统的名字遍历文件系统的链表得到文件系统的
file_system_type的结构,fs=0xC025AB0C,try_inc_mod_count是对文件系统
的使用进行计数。request_module 这一行的意义是如果没有找到文件系统则
通过加载的模块方式来看看有没有该文件系统。我们这里的情况是有该文件
系统,所以就返回了该文件系统的指针。
do_kern_mount()->get_sb_nodev()
get_sb_nodev(0xC025AB0C,0,“rootfs”,0)
do_kern_mount()->get_sb_nodev()->get_anon_super()
get_anon_super(0xC025AB0C,0,0)分配一个到超级块,并让这个超
级块与file_system_type建立关联。
通过该文件系统所提供的read_super 函数指针读取该文件系统的超级块
的具体数据(主要是生成struct inode与struct dentry让其与超级块建立关联,
稍侯我们在介绍这两个结构)。实际上我们看到rootfs是一个ramfs系统。超
级块的读取是ramfs_read_super,与ramfs文件系统是一样的。这是一个内存
文件系统。
当get_sb_nodev成功返回后,带回来了一个初始化好的struct super_block。
在将struct vfsmount与struct super_block关联后返回,这样就又带回了一个
初始化好的struct vfsmount。当do_kern_mount()返回时,具体的文件系统那
边就已经准备就绪。
再次初始化一个struct namespace,并且将vsfmount 挂在namespace 下,将
namespace挂到当前进程的mnt_list 里,说明已经安装了,最后将namespace
的根设成当前进程的根。
首先通过do_kern_mount 预安装rootfs这个文件系统得到它得挂接点,初始化后,
通过最后两个set_fs_…来把当前得fs 指向了该文件系统。实际上也就是说,系
统第一个“安装”的文件系统是rootfs,这个时候又多了几个数据结构,它们之
间的关系如图所示:
struct task_struct
init_task_union
fs
namespace
struct fs_struct
struct namespace
namespace
root
struct vfsmount
mnt(0xC10EE104)
mnt_sb
mnt_root
mnt_devname
="root_fs"
mnt_mountpoint
mnt_parent
file_systems struct file_system_type
rootfs
name :"rootfs"
super_block
struct super_block
rootfs_super_block
s_root
s_type
struct dentry
dentry(0xC10E9094)
d_sb
d_inode
INIT_FS
root
pwd
rootmnt
pwdmnt
0xC02BA3E0
0xC025AB0C
0xC10EC000
super_blocks
0xC02590A0
struct inode
inode(0xC10EA044)
i_sb
第一次初始化内存结构
RAMFS 文件系统
http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=
99919&page=164&view=collapsed&sb=5&o=all
ext2 文件系统
http://www-900.ibm.com/developerworks/cn/linux/filesystem/ext2/#1
我们回顾一下ROOTFS 是怎么装载上去的。
开始时init_task_union里是只指向INIT_FS的,可是INIT_FS 里没什么东西,
注册完rootfs文件系统之后,试图装载rootfs文件系统,那么首先要生成一个装
载点struct vfsmount,并且要读取文件系统生成超级块struct super_block,在
ROOTFS 读取超级块的过程中会在生成struct dentry,这个是对于VFS文件系统
用的(VFS 看到一个节点,不用管它是在什么文件系统中,对各种文件系统统一
接口)。而struct inode则是真正的节点(每种文件系统实现自己对节点的具体操
作)。因为这个节点是用于挂接用的,所以要通过struct vfsmount 来标识这个节
点同时也是个挂接点。这样它们就建立了联系,也就是上图表示。
为什么根系统的安装会在系统初始化的尽头呢?J
通过上面的链接我们对Ext2 文件系统应该有了一些了解,我们想要知道的就是
Ext2 的那种物理布局情况是如何融入到VFS 体系结构的。
第二次安装:
init () -> prepare_namespace()
这里就是真正的根文件系统的安装。首先要判断做为根的是什么设备,并且
在root_fs文件系统里创建目录以及创建节点和设备。然后通过mount_root()以只
读方式装载根文件系统。在mount_root()里可以看出,根文件系统是挂接到了
root_fs文件系统的/ root下,最后在mount_block_root通过sys_chdir将当前目录
设置到了/root。出来后卸载/dev 目录,最后在prepare_namespace 的结束处通过
sys_mount重新完成了根目录的挂接(以移动方式)。
在调用mount_root()之前如图所示:
init () -> prepare_namespace()->sys_mkdir()
系统调用,创建目录。
这个地方创建的是 root_fs的目录,实际上就是一个VFS的节点,因为root_fs
是内存文件系统。首先通过path_lookup-> path_init 得到目录,nd->dentry =
0xC10E0904, nd->mnt=0xC10EE104,这两个数据看上面的图。path_lookup->
path_walk 里跳转到last_component:然后再跳转到lookup_parent:设置值后跳到
return_base:返回。lookup_create(),它的作用是查找缓冲,如果没有就创建一个新
的。lookup_hash()->cached_lookup 在hash 里找,因为我们是新创建的,一定不
存在,还有就是root_fs不提供d_hash的操作。所以dentry=0,这是我们就要自己
创建一个新的。new=0xC10E9430,创建了struct dentry还不行,还要创建与之对
应的struct inode,通过lookup 跳转到了ramfs_lookup 函数里,fs/ramfs/inode.c L55
可以看到在这个函数里永远返回NULL,并且通过d_add 函数把dentry->inode
也置成空。这样一来在lookup_hash的dentry=new了,但是现在并没有inode,回
到lookup_create 里,就会跳转到enoent,返回失败。在回到sys_mkdir,发现失
败后会调用vfs_mkdir 来创建。May_create 检查有没有权限,然后通过具体的文
件系统的mkdir 来创建节点。这里的mkdir 是ramfs_mkdir.里面很简单,只是生
成一个inode结构,并且与dentry挂钩。
普通文件系统的装载
假设我们已经启动Linux到了shell里。
执行mount –t iso9600 /dev/cdrom /mnt
sys_mount ()
它也是mount 的系统调用
sys_mount (dev_name=0x8104f78 "/dev/hda",
dir_name=0x8106f98 "/mnt",
type=0xbfffff88 "iso9660",
flags=3236757504,
data=0x8104f68)
可以看到,这是从用户空间进来的。所以首先把数据拷贝到系统空间。
sys_mount ()->do_mount ()
这也是mount 的主体函数,
do_mount (dev_name=0xc1be8000 "/dev/hda",
dir_name=0xc10a9000 "/mnt",
type_page=0xc1be9000 "iso9660",
flags=3236757504,
data_page=0xc1be7000)
经过一些判断之后就要进入path_lookup
sys_mount ()->do_mount ()->path_lookup()->path_init()
可以看到,这个函数的主要目的就是根据name 来确定目录,如果是’/’,那
初始化的目录就是根,否则就是当前目录,对于fs_struct 里rootmnt 就是根
的挂接点,root就是挂接点的目录,而pwdmnt 与pwd 则是当前目录的挂接
点以及当前目录。current 代表了当前进程的task_struct,每一个进程都有一
个task_struct结构,在进程部分我们在继续讨论。通过path_init之后,struct
nameidata *nd 则设置好了初始目录(这个结构只是暂时使用的)。
sys_mount ()->do_mount ()->path_lookup()->path_walk()->link_path_walk()
因为初始目录已经设置好,直接到‘/’子后的字符。这是如果*name为空说
明我们完成任务,返回。link_count 用于防止函数嵌套。
第一个for循环主要处理路径问题,在我们现在的环境里,*name=“mnt”
所以this.name = “mnt”, this.len = 3,并且通过if (!c) 到了 last_component处。
因为我们只是一次目录,如果有多层目录的话往下有几种情况,比如在界面上我
们打的是 “/mnt/”或“/mnt/aaa/” 这样就走 last_with_slashes,说明走的是目录。
另外就是要处理 “.”与“..”的情况了,我们都知道“.”与“..”代表的意义。
所以“..”就是向上走了一层继续,“.”的情况就是不发生变化,继续,这就相
当于在界面上打“mnt/../mnt”或“mnt/./../mnt”,如果不是“.”与“..”,那说明
是正常的目录,就要继续往下走。比如我们在界面上键入的是/mnt/cdrom,那么就
通过cached_lookup 在cache 里查mnt 这个节点有没有,如果没有就通过
real_lookup 从文件系统里读出来并建立struct dentry。对读出来的struct dentry所
指向的目录进行判断,是不是安装点,是不是符号链接,详细的操作过程在具体
的文件系统里讨论,我们现在先看VFS层。
如果是目录就会跳转到last_with_slashes,标记上是目录便继续到
last_component,如果标记中有LOOKUP_PARENT,说明找的是根,便跳
lookup_parent处理,紧接着还是判断“.”与“..”的情况,假如我们在界面上写
的是“/mnt/..”或“/mnt/.”,这种情况就是在这里调整跳到return_reval处理。可
以看出一个目录或文件都是找的struct dentry,那么这个结构也就是整个VFS的
中心结构。我们通过path_init 与path_walk确定了我们要安装的目录在系统的所
在。通过do_add_mount就把文件系统安装到了该目录。
补充
在文件系统里一个很重要的数据结构是file_oprations
进程管理及调度
说明:
在Linux 的实现中,一个进程或一个线程都拥有一个task_struct。都拥有专
用的系统堆栈空间,他们的区别只是在于线程没有用户空间。
下面我们继续看在Linux初始化过程中创建的第一个内核线程
rest_init-> init_idle()
首先初始化idle,也就是CPU无事做的时候运行的函数。
init_idle(0xC0270000, 0);
首先可以看到idle_rq= cpu_rq(0)=runqueues=0xC028BA00, runqueues 是
一个以CPU 个数为下标的数组。代表是每个CPU 的运行队列。然后使用
set_tsk_need_resched 设置current->need_resched = 1.
rest_init-> kernel_thread()
kernel_thread( 0xC010502C <init>, arg=0x0,flags=3584)
rest_init-> kernel_thread()->arch_kernel_thread()
自己做一个struct pt_regs,为什么要做一个?往下看
rest_init-> kernel_thread()->arch_kernel_thread()->do_fork()
do_fork(clone_flags=8392448, stack_start=0, regs=0xc0271f58,
stack_size=0,parent_tidptr=0x0, child_tidptr=0x0)
返回0xC10D0000,也就是新的task_struct
rest_init-> kernel_thread()->arch_kernel_thread()->do_fork()->copy_process()
dup_task_struct 拷贝一个task_struct,还根据参数决定是否拷贝深层的数据结
构,比如:copy_files、copy_fs、等。最后设置需要调度。
拷贝出来的线程开始并没有运行,要等到调度的时候才能运行。这个时候系
统有了一个“进程”和一个线程了
rest_init-> cpu_idle()
CPU 的“初始化”都结束了,CPU 进入idle,其实一进入idle 就是开始了进
程调度。可以看到current->need_resched(init_idle())这个是1.所以不会在while里
循环。
rest_init-> cpu_idle()->schedule()
这是整个系统的进程调度算法(核心),不但这里有调用,很多地方都有调
用。
首先我们看一下runqueues 这个结构的说明,虽然这是2.4 的内核,但是调
度算法却使用的是2.6 的。应该是RH自己修改的吧,我也不清楚,猜的。J
http://www-900.ibm.com/developerWorks/cn/linux/kernel/l-kn26sch/index.shtml
http://www.linuxfans.org/nuke/modules.php?name=News&file=article&op=view&sid
=2368
prev = current; 当前进程,我们这里现在是0xC0270000
rq = this_rq(); 当前运行schedule 的CPU,我们这里现在只有一个CPU,
所以是runqueues,0xC028BA00
prev->last_run = jiffies; 刷新当前进程的运行时间
spin_lock_irq(&rq->lock); 对当前的CPU 的运行队列加锁。并禁用当前
CPU中断。这里我们的进程的运行状态是TASK_RUNNING(init_idle()L1916),
判断nr_running,如果当前CPU上没有进程要运行的话,next=rq->idle.
判断nr_active,本进程组中的进程数如果为0,就把active 与expired对掉。
我们看一下我们这里的rn_running与nr_active是多少。回过头去看
rest_init-> init_idle()->deactivate_task()
deactivate_task(0xC0270000, 0xC028BA00),看上面的数据
那么我们这里nr_running = ?,rq->active->nr_active = ?,再回头看
start_kernel()->sched_init()
我们看到runqueues 是一个全局变量。开始的时候里面是空。这里就是对它
的初始化
首先通过循环按支持的CPU数来初始化runqueues,然将内核看做是一个进
程idle来初始化。
start_kernel()->sched_init()->wake_up_forked_process()
首先调整sleep_avg 等,因为第 一次进来current->array 是空。所以要走
__activate_task。
start_kernel()->sched_init()->wake_up_forked_process()->activate_task()
执行完这里current->array=runqueues->active(0xC028BA24);
并且runqueues->active->nr_active = 1,runqueues->active->nr_running=1;
回到deactivate_task
现在又把nr_nr_running--,nr_active—都变成了0,。并且把array也清了,那
么这个时候又恢复成原来的样子的。
在继续往下看
rest_init-> kernel_thread()->arch_kernel_thread()->do_fork()
再次调用wake_up_forked_process,这样相当于activate_task() 没有执行。J
那么我们这里nr_running=1,rq->active->nr_active=1
回到调度
继续向下sched_find_first_bit,它用于快速定位就绪队列中高优先级任务
next = 0xC10D0000,这就是我们新创建的线程的task_struct 的起始地址,接
着使用clear_tsk_need_resched 清掉need_resched
判断prev 与next 是不是一个进程,是的话可以看到,什么也没有做。不是
的话我们就要开始进行进程切换了。
context_switch 主要就处理进程的用户空间。每个进程都有一个struct
mm_struct 代表着自己的用户空间,但是线程却没有用户空间,怎么办?只好暂
时借用一下上一个进程的用户空间。(会有问题吗?没有,因为现在是在内核空
间,内核访问用户空间都是一样的,就算是有问题,那也是返回到用户空间也会
出现问题。再加上线程并不使用用户空间,所以没有关系)
switch_to这个就是进程切换的关键所在。(主要是切换进程堆栈,已经说过,
不管是进程或是线程,都有自己的堆栈)
prev->thread 里的部分数据
struct thread_struct {
…
unsigned long esp0 = 0
unsigned long eip = 0
unsigned long esp = 0
unsigned long fs = 0
unsigned long gs = 0
…
};
next->thread
struct thread_struct {
…
unsigned long esp0 = C10D2000
unsigned long eip = C0108F60
unsigned long esp = C10D1FC4
unsigned long fs = 0
unsigned long gs = 0
…
};
我们回头看一下我们的线程是什么时候把thread 这个结构初始化的。
rest_init-> kernel_thread()->arch_kernel_thread()->)->do_fork()->copy_process()
->copy_thread()
childregs = ((struct pt_regs *)(THREAD_SIZE+(unsigned long) p))- 1
= ((struct pt_regs *)(8192+0xC10D0000))- 1 = 0xC10D4000 – sizeof( pt_regs)
= 0xC10D4000 – 60 = 0xC10D1FC4
在这里实际上是打造一个堆栈的环境,这个堆栈就是我们生成的线程用的堆
栈。那么这个线程是怎么使用这个堆栈的呢?那就要看调度了。
那么我们运行完这个函数,里面初始化成什么样子呢?
Childregs数据:
ebx = 0xC010502C
eip=0xc01071E4
xes=xds=0x68
xcs=0x60,
orig_eax=0xFFFFFFFF,其它为0,其实childregs 大部分是我们开始在
arch_kernel_thread自己做的那个regs。自己可以对照一下。
(struct task_struct*)(0xC10D0000) ->thread 的数据
esp = 0xC10D1FC4
esp0 = 0xC10D2000
eip = 0xC0108F60 ret_form_fork 的地址(怎么会是它,为什么不是
kernel_thread_helper,回头看copy_thread, p->thread.eip = (unsigned long)
ret_from_fork)
这就完成了next->thread的初始化,现在回过头来看调度
switch_to 反汇编代码
0xc01160c5 <schedule+321>: mov %esi,%eax
0xc01160c7 <schedule+323>: mov %ecx,%edx
0xc01160c9 <schedule+325>: push %esi
0xc01160ca <schedule+326>: push %edi
0xc01160cb <schedule+327>: push %ebp
0xc01160cc <schedule+328>: mov %esp,0x350(%esi)
0xc01160d2 <schedule+334>: mov 0x350(%ecx),%esp
0xc01160d8 <schedule+340>: movl $0xc01160ed,0x34c(%esi)
0xc01160e2 <schedule+350>: pushl 0x34c(%ecx)
0xc01160e8 <schedule+356>: jmp 0xc0107654 <__switch_to>
0xc01160ed <schedule+361>: pop %ebp
0xc01160ee <schedule+362>: pop %edi
0xc01160ef <schedule+363>: pop %esi
执行switch_to 之前寄存器状态 当eip = 0xC01160CC
eax 0xC028BA00 0xC0270000
ecx 0xC10D0000
edx 0x0 0xC010D000
ebx 0xC0255700
esp 0xC0271FB4 0xC0271FA8
ebp 0xC0271FC4
esi 0xC0270000
edi 0xC028BA00
eip 0xC01160C5 0xC01160CC
eflags 0x46
cs 0x60
ss 0x68
ds 0x68
es 0x68
fs 0x0
gs 0x0
当0xC01160CC执行完后,那么当前进程的ESP 就放到了prev->thread.esp 里,也
就是说0xC0270000 ->thread.esp = 0xC0271FA8
当0xC01160D2执行完后ESP = 0xC10D1FC4,这样就切换了堆栈,也就是说,
现在切换了进程。但是,仅仅切换了堆栈进程还不能向下运行。还要做一些工作。
当0xC01160E2 执行完后prev->thread.eip = 0xC01160ED, next->thread.eip =
0xC0108F60 压入了堆栈。这个地址就是返回地址了(如果碰到ret 的话)对于我
们线程来说,通过0xC01160E8 这行执行__switch_to 后就返回到0xC0108F60 去
了,因为我们这里是新创建的线程。
如果不是线程是进程呢,那下面那几句pop 就很有用了,这部分到了第二部分再
讲。
rest_init-> cpu_idle()->schedule()->context_switch()->__switch_to()
arch/i386/kernel/process.c L710
tss->esp0 = next->esp0 = 0xC10D2000
主要是处理TSS,当这个函数返回时,返回到ret_form_fork = 0xC0108F60,最
后要运行到kernel_thread_helper = 0xC01071E4这里。通过call 0xC010502C,就
到init 里去了。
kernel_thread_helper arch/i386/kernel/process.c L494
这是一个宏。做的事情很简单,首先是压参数,接着调用init 函数。执行完
返回来的时候,再调用了do_exit,注消了进程。可以看到在Linux 的线程实现
实际上就是使用一个进程来执行函数,接着注销进程。这种进程的开销还是很小
的。回头看copy_process
网络系统
Linux的网络系统如图所示
用户模式
内核模式
套接字文件系统
TCP UDP
IP
PPP 以太网... ...
ARP
init()->do_basic_setup()-> sock_init()
主要是注册sockfs这个文件系统,也就实现了sock的初始化。
现在我们看PF_INET 这种地址家族的注册
#define module_init(x) __initcall(x) include/linux/init.h L112
init()->do_basic_setup()->do_initcalls()->inet_init() net/ipv4/af_inet.c L1102
init()->do_basic_setup()->do_initcalls()->inet_init()->sock_register()
注册地址家族,这里注册的是inet_family地址家族
在RH9 这个版本中最多支持32 种地址家族
注册完PF_INET 家族之后就要注册PF_INET家族的协议
inet_protocol_base很有趣,使用了C语言技巧
我们看inet_protocol_base的定义
#define IPPROTO_PREVIOUS &udp_protocol
static struct inet_protocol icmp_protocol = {
handler: icmp_rcv,
next: IPPROTO_PREVIOUS,
protocol: IPPROTO_ICMP,
name: "ICMP"
};
#undef IPPROTO_PREVIOUS
#define IPPROTO_PREVIOUS &icmp_protocol
struct inet_protocol *inet_protocol_base = IPPROTO_PREVIOUS; L100
所以它指向的是 icmp_protocol,但是我们再在protocol.c 里向上看,在定义
icmp_protocol 时, IPPROTO_PREVIOUS 指向的是udp_protocol , 所以
icmp_protocol里的next = udp_protocol,同理,向上推,实际上这是在编译时就
完成了一个链表。最后IPPROTO_PREVIOU 指的就是一个头。现在明白之后就
知道循环是怎么回事了。
通过inet_add_protocol把协议添加到inet_protos表中
接着各种注册信息。
整体来说,网络的初始化比较简单。我们会在第二部分从用户角度上来分析网络
具体的流程。
一: 系统初始化开始,Linux进入保护模式,初始内存系统、中断系统、文件系
统等,直到创建第一个用户进程。
二: 用户进程通过系统调用主动进入内核,CPU 接受中断请求被动执行各种中断服务。
第一部分 系统初始化
进入保护模式 Arch/i386/boot/Setup.s
gdt:
.fill GDT_ENTRY_KERNEL_CS,8,0 #空12×(8个字节=2个双字),用0 填充。
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9A00 # code read/exec
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9200 # data read/write
.word 0x00CF # granularity = 4096, 386
gdt_48:
.word 0x8000 # gdt limit=2048,
# 256 GDT entries
我们进入保护模式之后选择子首先应该是0x60 = 1100000b, 右移3 位为1100 =
0xC = 12 ,这就是code在GDT里的位置。当然这只是第一次进入保护模式,以后还会调整。
进入分页模式 arch/i386/kernel/head.S
.org 0x1000
ENTRY(swapper_pg_dir)
.long 0x00102007 #基地址为0x00102000,就是下面pg0,0x00100000是内核加载的地方,1M的位置
.long 0x00103007
.fill BOOT_USER_PGD_PTRS-2,4,0 #768-2 (long) 0
/* default: 766 entries */
.long 0x00102007
.long 0x00103007
/* default: 254 entries */
.fill BOOT_KERNEL_PGD_PTRS-2,4,0 #256-2(long) 0
# 这个说明了内核是1G,用户就3G。
.org 0x2000
ENTRY(pg0)
.org 0x3000
ENTRY(pg1)
说明映射了两个页面,一个页面代表4M, 每个pg0 里的一项代表4K。具体pg0数据是用程序填充的 参考L82。L101 通过短跳转进入分页(但eip 还不是),L104通过绝对地址完全进入分页。比如L105的地址是0xC0100058 =1100 0000 0001 0000 0000 0000 0101 1000B,高10 位为1100 0000 00 = 0x300 = 768。从swapper_pg_dir里找768项就是0x00102007,检查之后基地址为0x00102000;就是pg0,再看0xC0100058 中间10位01 0000 0000B = 0x100 = 256,再找pg0 的256项应该为00100***,那么最后的物理地址为0x00100058,所以内核里的虚拟地址换物理地址很简单,就是0xC0100058 - 0xC0000000 = 0x00100058 就是物理地址。现在系统只是映射了8M的内存空间,之后还会调整。
内存管理
物理内存管理
第一次探测
Arch/i386/boot/setup.S L281
在这里使用了三种方法,通过调用0x15 中断:
E820H 得到memory map。
E801H 得到大小。
88H 0-64M.
第二次探测
start_kernel() -> setup_arch()
这里主要是对物理内存的初始化,建立zone区。并且初始化page。
start_kernel() -> setup_arch() -> setup_memory_region()
把bios 里的memory map 拷贝到 e820 这个全局变量里。
会在屏幕上显示
BIOS-provided physical RAM map:
BIOS-e820: 0000000000000000 – 000000000009FC00 (usable)
BIOS-e820: 0000000000100000-0000000002000000 (usable)
BIOS-e820 说的是通过e820 来读取成功的数据,第一个代表起始地址,后面接
着的是大小。usable 说明内存可用。还有 reserved, ACPI data, ACPI NVS 等。
第二条说明起始地址为0x100000 = 1M,大小是0x2000000 = 32M 内存。
start_kernel() -> setup_arch() -> setup_memory ()
start_kernel() -> setup_arch() -> setup_memory ()->find_max_pfn()
通过e820计算出最大可用页面。
e820.map[1].addr = 0x100000 e820.map[1].size = 0x1f00000,那么start =
PFN_UP(0x100000) = (0x100000+0x1000(4096 = 1>>12)-1)>>12 =
0x100FFF>>12=0x100, end = PFN_DOWN(0x2000000) = 0x2000000 >> 12 =
0x2000 = 8192,那么max_pfn = 0x2000 就是最大的页面
start_kernel() -> setup_arch() -> setup_memory ()->find_max_low_pfn()
主要是高端内存的限制。
max_low_pfn = max_pfn 不能大于896M, PFN_DOWN((-0xC0000000 –
0x8000000(128<<20)) = (0x38000000 = 896M限制)) = 0x38000
start_kernel() -> setup_arch() -> setup_memory ()-> init_bootmem()
引导内存分配只使用在引导过程中,为内核提供保留分配页,并且建立内存的位图。
比如_end 为0xc02e5f18,start_pfn = PFN_UP(__pa(&end)) = 0x2e6,所以
start_pfn 指的是内核结束的下一个页面((_pa(&_end) >> 12 =0x2e5 ,那么
init_bootmen(start_pfn,max_low_pfn) = init_bootmen(0x2e6,0x2000)
start_kernel() -> setup_arch() -> setup_memory ()->
init_bootmem()->init_bootmem_core()
init_bootmen(0x2e6,0x2000) = init_bootmem_core(&contig_page_data, 0x2e6, 0,
0x2000),mapsize就是建立内存位图大小需要多少字节 1024=(8192-0+7)/8,
1个位代表着4K的一个页面。
bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT) =
phys_to_virt(0x2e6<<12) = phys_to_virt(0x2e6000) = 0xc02e6000
bdata->node_boot_start = (start << PAGE_SHIFT) = 0
bdata->node_low_pfn = end = 8192
把0xc02e6000 开始0xff 填充1024 个字节 0xc02e6400,这样就代表着所有内
存不可用。
start_kernel() -> setup_arch() -> setup_memory ()-> register_bootmem_low_pages
e()
根据e820和内存位图来标识位图那些内存可用。
start_kernel() -> setup_arch() -> setup_memory ()-> register_bootmem_low_pages
e()->free_bootmem()->free_bootmem_core()
第一次调用 free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size)) =
free_bootmem(PFN_PHYS( 0), PFN_PHYS(159))= free_bootmem(0, 0x9F000))
free_bootmem_core(contig_page_data.bdata, 0, 0x9f000)
eidx代表这块内存共占用多少页面
eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE = (0+0x9f000-0)/0x1000=0x9f
end = (addr + size)/PAGE_SIZE=(0+0x9f000)/0x1000=0x9f
start = (addr + PAGE_SIZE-1) / PAGE_SIZE = (0+0x1000-1)/0x1000=0
sidx = start - (bdata->node_boot_start/PAGE_SIZE)=0-(0/0x1000)=0
每一次循环清1 位,第一次循环0x9f次清到0x9f /8= 0x13,所以第一次清到
了0xc02e6013的第7位159%8 = 7,所以0xc02e6013的数据应该是0x10000000,
第二次free_bootmem_core(contig_page_data.bdata, 0x100000, 0x 1F00000)
eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE = 0x2000
end = (addr + size)/PAGE_SIZE = 0x2000
start = (addr + PAGE_SIZE-1) / PAGE_SIZE = 0x100
sidx = start - (bdata->node_boot_start/PAGE_SIZE) = 0x100
这次从0xc02e6020 ( 0x100/8 ) 开始清0x2000-0x100 次, 清到
0xc02e6400(0xc02e6020+0x1f00/8)
通过free_bootmem的操作,我们已经把内存的位图标识出来。
start_kernel() -> setup_arch() -> setup_memory ()->reserve_bootmem()
保留内存,说明这部分不能用于动态分配。
reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +
bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY)) =
reserve_bootmem(0x100000, (PFN_PHYS(2e6) +1024 + 4096-1) - (0x100000))=
reserve_bootmem(0x100000, 1E73FF)
保留内核开始1M 到建立的内存位图的结束地方,因为位图是接着内核的下
一个页面存放的,所以一起保留,对于+ PAGE_SIZE-1 操作就是位图的结束
位置也与内存边界对齐。重新改写内存位图之后对于我们的情况是
0xc02e6020(1M)到0xc02e605C 都改成了1(保留)0xc02e605D还是0
reserve_bootmem(0,PAGE_SIZE)= reserve_bootmem(0,4096)
保留物理内存的第0 页内存。0x0-0x4096
start_kernel() -> setup_arch() -> paging_init()
当我们上面的物理内存都完成时,我们就要对所有内存进行管理,需要重新
建立页面映射。
start_kernel() -> setup_arch() -> paging_init()->pagetable_init()
我们根据已有的内存信息,重新修改页面表
end = (unsigned long)__va(max_low_pfn*PAGE_SIZE) = 0xC2000000
pgd_base = swapper_pg_dir = 0xC0101000
i = __pgd_offset(PAGE_OFFSET) = (0xC0000000>>22)&(1024-1)=768
pgd = pgd_base + i= 0xC0101C00
pgd就是内核模式的第一个页目录
第一个for循环就是说从768开始到1024结束,也就是swapper_pg_dir结束,
这部分都是内核的页目录,也就是我们要修改的页目录。
第二个for循环用于中间页表,对我们2 层i386 体系无效。
alloc_bootmem_low_pages(PAGE_SIZE) = __alloc_bootmem(0x1000,0x1000,0)
->__alloc_bootmem_core(pgdat->bdata, 0x1000,0x1000,0)
eidx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT) =
8192-(0>>12) = 8192 代表着这个区域可用的页面,实际上就是整个内存。
我们以前保留过第0 页面的内存,所以我们分配内存就是从第一页面开始,
重新写内存位图将已分配的页面做保留标记。这次运行后我们分配了一个页
面(0x4096),开始地址为0xc0001000,与此同时,0xc02e6000 = 0x3 = 0011。
第三个for循环用于写页表,写满这个我们新分配的页表。
第一次循环的时候pte = 0xc0001000;vaddr = 0xc0000000;
*pte = mk_pte_phys(__pa(vaddr), PAGE_KERNEL) =
mk_pte_phys(0, MAKE_GLOBAL(_PAGE_PRESENT | _PAGE_RW |
_PAGE_DIRTY | _PAGE_ACCESSED) =
mk_pte_phys(0, MAKE_GLOBAL(0x001 | 0x002 | 0x040 | 0x020) =
mk_pte_phys(0, ( MAKE_GLOBAL(0x001 | 0x002 | 0x040 | 0x020) = 0x63)) =
mk_pte_phys(0, pgprot_t __ret = __pgprot(0x63)) = mk_pte_phys(0, __ret) =
__mk_pte((0) >> 12, __ret) = __pte(((0)<<12)| pgprot_val(__ret)) = __pte(0x63)
= 0x00000063
第二次运行时 vaddr = 0xc0001000;
*pte = 0x00001063
当pmd = 0xc0101c00 时
set_pmd(pmd, __pmd(_KERNPG_TABLE + __pa(pte_base))) =
(pmd,__pmd((_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED |
_PAGE_DIRTY) + 0x00001000) = (pmd,__pmd(0x001 | 0x002 | 0x020 | 0x040
+0x00001000) = set_pmd(pmd,__pmd( 0x00001063)
这就是结果0xc0101c00=0x00001063下一次pmd是0xc0101c04=0x00002063
对于我们现在的状态,我们是32M 内存,end = 0xC2000000,所以只映射了8
项,0xC0101C1C=0x00008063
现在要建立专用区的页表了,专用区是位于虚拟地址为0xfffff000 往回的一
个区域。Enum fixed_addresses 描述了专用区的功能
vaddr=__fix_to_virt(__end_of_fixed_addresses-1)&PMD_MASK= 0xFFC00000
这就是专用区起始地址所在的中级页目录边界地址
fixrange_init(vaddr, 0, pgd_base) = fixrange_init(0xffc00000, 0, 0xc0101000)
我们要在里面建立专用区的页面。
0xffc00000 处于页目录的1023 映射的位置(最后一项),所以又申请了一个
页面0xc0009000,(没有作为页表使用)。因为这个页面是接着我们以前的目
录页面申请的,所以这个页面与目录页面在物理上是连续的,但在虚拟地址
却差很大。0xC0101FFC = 0x00009067,这就是目录映射了。
当我们运行完时我们就把所有的页表及页目录都建立好,通过load_cr3 与
__flush_tlb_all重新刷新分页机制。
start_kernel() -> setup_arch() -> paging_init()->zone_sizes_init()
根据内存位图开始建立分区。
max_dma = virt_to_phys((char*) MAX_DMA_ADDRESS) >> PAGE_SHIFT =
virt_to_phys(0xC1000000) >> 12 = 0x1000
对于我们32M内存来说,zones_size = { 4096, 4096, 0 }
start_kernel() -> setup_arch() -> paging_init()->zone_sizes_init()->free_area_init()
建立分区数据结构:标记所有页面保留,标记所有内存队列空,清除内存位
图。(需要详细)
map_size = (totalpages + 1)*sizeof(struct page) = 0xE000 根据page结构看
8192 个页面需要多大空间。
lmem_map =alloc_bootmem_node(pgdat, 0xE000) =
__alloc_bootmem_node(pgdat, 0xE000, 0x10, 0x1000000) = 0xc1000000
lmem_map = (struct page *)(PAGE_OFFSET +
MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET))
= 0xC0000000+MAP_ALIGN(0x1000000) = 0xC0000000+
((((0x1000000) % sizeof(mem_map_t)) == 0) ? (0x1000000) : ((0x1000000) +
sizeof(mem_map_t) - ((0x1000000) % sizeof(mem_map_t))))=
0xC0000000+((8==0)?0x1000000: 0x1000038-8) = 0xC1000030
因为cache需要对齐
接下来循环初始化每个区
zone->zone_mem_map = mem_map + offset; 这个区的mem_map 结构位置
zone->zone_start_paddr = zone_start_paddr; 这个区的开始虚拟地址。
…
for (i = 0; i < size; i++) {
struct page *page = mem_map + offset + i;
…
}
初始化这个区的所有page 结构。在page 里所有的页面都是保留的,不能投
入使用。
…
for (i = 0; ; i++) {
unsigned long bitmap_size;
INIT_LIST_HEAD(&zone->free_area[i].free_list);
…
}
这是伙伴分配系统的初始化
这就说明了每个区有每个区自己的管理系统。
http://www-900.ibm.com/developerWorks/cn/linux/l-numa/index.shtml
start_kernel() -> setup_arch() -> paging_init()->zone_sizes_init()->free_area_init()
->build_zonelists()
start_kernel() -> setup_arch() -> dmi_scan_machine()
dmi_iterate()
isa_memcpy_fromio(buf, fp, 15)= memcpy_fromio(buf,__ISA_IO_base +
0xF0000,15) = __memcpy(buf,__io_virt(0xC00F0000),(15))
buf = 0xC00F0000 里的数据,循环一直到fp = 0xFFFFF为止,每次fp 加0x10
当这些执行完的时候,内核的基础也就大部分已经建立 ,我们在继续看第
三部分的初始化。
第三次探测
start_kernel() ->mem_init()
start_kernel() ->mem_init()->free_pages_init()
start_kernel() ->mem_init()->free_pages_init()->free_all_bootmem_core()
struct page *page = pgdat->node_mem_map; 这就是第一个page.
idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT) =
8192-0>>12 = 8192
第一个循环就是根据我们以前建立的内存位图来初始化Page。
start_kernel() ->mem_init()->free_pages_init()->free_all_bootmem_core()->
__free_page()->__free_pages()->__free_pages_ok()
释放内存页(需要详细)
第二个循环就是释放内存位图本身的内存。
执行完后以后的内存操作就可以使用Page 了,第三阶段的内存初始化完毕,还
有一部分零散的内存操作会在别的部分讨论,至此,物理内存管理结束。
虚拟内存管理
中断系统
描述
X86 微机通过外接两片级连的可编程中断控制器8259A,接收更多的外部中
断。每个8259A控制器可以管理8 条中断线,两个可以控制15 条中断。
由于Intel 公司保留0-31 号中断向量用来处理异常事件,所以硬件中断必须
设在31以后,Linux则在实模式下初始化时把硬件中断设在0x20-0x2F。还有一
个是0x80是系统调用。
中断门与陷阱门区别:通过中断门门进入中断服务程序时CPU 会自动将中断关
闭,而通过陷阱门进入则不会。
原理
Linux的中断实现原理:考虑中断处理的效率,Linux的中断处理程序分为两
个部分:上半部和下半部。上半部的功能是“登记中断”,具体的中断实现会在
下半部。之所以上半部要快,因为上半部是完全屏蔽中断的。下半部有以下几种
实现方式:bottom half, task queue(没看), tasklet, softicq,在我们分析的这个版
本中,我认为bottom half以及softicq 都是基于tasklet实现的,以后会具体介绍。
第一次中断设置arch/i386/kernel/head.S
# L325
ignore_int 这是默认的中断服务程序,实际上什么也不做。
L359
SYMBOL_NAME(idt_descr):
.word IDT_ENTRIES*8-1 # idt contains 256 entries
.long SYMBOL_NAME(idt_table) #idt_table定义在Arch/i386/Kernel/Traps.c
L245
lidt idt_descr
这样如果有中断、异常发生就会去ignore_int空转一次。
第二次中断设置
start_kernel() -> trap_init ()
主要是对一些系统保留的中断向量初始化
struct desc_struct idt_table[256] Arch/i386/kernel/Traps.c L67
extern irq_desc_t irq_desc [NR_IRQS]; include/linux/irq.h L67
idt_table = 0xC028B000
陷阱门 只允许在系统级下调用
set_trap_gate(0,÷_error)=_set_gate(idt_table+0,15,0, 0xC0109060);
0xC028B000 = 0xC0108F0000609060
中断门 只允许在系统级下调用
set_intr_gate(2,&nmi)=_set_gate(idt_table+2,14,0, 0xC0109108);
0xC028B010 = 0xC0108E0000609108
系统门 可以用户级调用,否则无法提供系统调用了。看第三个参数为3
set_system_gate(3,&int3) =_set_gate(idt_table+3,15,3, 0xC0109138);
0xC028B018 = 0xC010EF0000609138
start_kernel() -> trap_init () -> cpu_init()
初始化CPU,并且重新装载GDT 与IDT。
我们先说一下ccurrent 宏。通过这个宏可以得到当前运行进程的struct
task_struct 。
cpu_gdt_table Arch/i386/Kernel/head.S L423
然后通过两行汇编重新装载GDT与IDT。
start_kernel() -> init_IRQ()
主要是对外设的中断初始化
init_ISA_irqs() 中断请求队列的初始化
init_ISA_irqs()->init_8259A() 可编程中断控制器8259A的初始化
继续初始化中断向量,从0x20(32)开始16个,因为之前是保留的。
void (*interrupt[NR_IRQS])(void) = {
IRQLIST_16(0x0),
… }
#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)
#define IRQ(x,y) \
IRQ##x##y##_interrupt
#
define NR_IRQS 224
255 -31 = 224 所以interrupt 定义的Intel保留之后的中断
对于IRQ##x##y##_interrupt函数的定义在 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");
经过处理之后得到:
asmlinkage void IRQ0x01_interrupt(); \
__asm__( \
"\n"__ALIGN_STR"\n" \
IRQ0x01_interrupt:\n\t" \
"pushl $0x01-256\n\t" \ #这就是每个中断的最大区别
"jmp common_interrupt");
#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));
*/
中断发生时都会运行do_IRQ 这个公共的中断处理函数。返回时就会返回到
ret_from_intr继续运行后再返回。
idt_table里存放了具体的中断服务的内存地址,CPU产生中断会通过idt_table
去运行具体的中断服务程序。除了系统保留的中断是运行各自不同的中断服
务,其它都会运行到do_IRQ 这个公共中断服务程序里来。如果有两个设备
使用了同一个中断的话,我们就需要有一种机制来识别具体是那个设备发生
了中断。
中断服务队列的初始化
init_IRQ() 初始化完成后,并没有具体的中断服务程序,真正的中断服务程
序要到具体设备的初始化程序将其中断服务程序通过request_irq()向系统“登
记”,挂入某个中断请求队列以后才会发生。
request_iqrt()->setup_irq()
将具体的中断服务链入相应的中断请求队列。
下面这个图就是有中断服务链入后的数据结构的简图。
中断服务的响应(上半部)
do_IRQ() Arch/i386/kernel/irq.c L563
desc->handler->ack(irq);
中断处理器(如i8259A)在将中断请求“上报”给CPU以后,希望CPU
给一个确认(ACK),表示CPU在处理了。
IRQ_INPROGRESS 主要是为多处理器设置的,IRQ_PENDING 说明再
次有同样的中断发生,可能是SMP系统结构中,一个CPU正在中断服务,
而另一个又进入do_IRQ(),这时IRQ_INPROGRESS标志为1,所以返回了,
也可能是在单处理器中CPU 已经在中断服务程序中,但是因为某种原因又
将中断开启了,而且在同一个中断通道中再次产生了中断,也会因为
IRQ_INPROGRESS标志为1 返回
handle_IRQ_event()是for 循环里的主题,进入for 循环时desc->status的
IRQ_PENDING标志为0,如果执行完handle_IRQ_event()后,发现desc->status
的IRQ_PENDING 标志为1 就说明发生了上述情况。那就在来循环,直到
IRQ_PENDING标志为0为止。
desc->handler->end(irq);
if (softirq_pending(cpu))
do_softirq();
这就是下半部的了,我们在下半部里再分析。
do_IRQ()->handle_IRQ_event()
irq_enter(cpu, irq);说明那个CPU在那个中断服务程序里。
irq_exit(cpu, irq);与上面相反。
循环执行这个中断通道的所有中断程序,因为Linux也不知道是那个设备产
生的中断。(如果中断是共用的话)到了自己的中断服务程序里自己检查。
action是struct irqaction,看上面的图。所以struct irqaction里的handler一般是
在关中断里执行的(可以自己开中断),所以要快速处理,否则中断太久会
丢失别的中断。
第三次中断设置(软中断)
下半部
原始bottom half
原理:
static void (*bh_base[32])(void); Arch/i386/Kernel/Softirq.c L274
可以用来指向一个具体的bh 函数。同时又设置两个32 位bh_active(中断请
求寄存器)和bh_mask(中断屏蔽寄存器)。
使用方式
void init_bh(int nr, void (*routine)(void)) L311
void remove_bh(int nr) L317
static inline void mark_bh(int nr) Include/linux/interrupt.h L228
将bh_active 的nr 位置一,如果bh_mask 中的相应位也是1,那么系统在执
行完do_IRQ()后,以及每次系统调用结束后都会在函数do_buttom_half()中
执行相应的bh函数。
原始bottom half机制有很大局限性,主要是个数限制在32 个以内,所以在
2.4内核里,bottom half使用的是tasklet 机制,只是在接口上保持向下兼容。
task queue
include/linux/tqueue.h L38
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 */
};
typedef struct list_head task_queue; L64
使用方式
#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q) L66
static inline int queue_task(struct tq_struct *bh_pointer, task_queue *bh_list)
L100 – L119
static inline void run_task_queue(task_queue *list)
tasklet
主要是考虑是为了更好的支持SMP,提高SMP 的利用率;不同的tasklet可
以同时运行在不同的CPU上,同时也是Linux推荐使用的方式,我们将重点
解释
struct tasklet_struct Include/Linux/Interrupt.h L104
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
struct tasklet_struct bh_task_vec[32]; L275
使用方式(这中方式是我们普通使用的方式)
void my_tasklet_func(unsigned long);
DECLARE_TASKLET(my_tasklet, my_tasklet_func, data) L113
static inline void tasklet_schedule(&my_tasklet) L158
另外的一些调用接口
DECLARE_TASKLET_DISABLED(name, func, data) L116
static inline void tasklet_disable(struct tasklet_struct *t) L179
static inline void tasklet_enable(struct tasklet_struct *t) L186
extern void tasklet_kill(struct tasklet_struct *t); L198
extern void tasklet_init(struct tasklet_struct *t, L199
我们看一下原始bottom half就怎么用tasklet实现的以及于我们普通的tasklet
的区别。
init_bh(TIMER_BH, timer_bh); 初始化,
mark_bh(TIMER_BH) = tasklet_hi_schedule(bh_task_vec+ TIMER_BH)
提出对timer_bh的执行请求。
可以看到,bh_task_vec就是实现bh的tasklet_struct,看下面的softirq_init()
mark_bh()->tasklet_hi_schedule()
将bh 的tasklet_struct 挂到当前CPU 的软中断执行队列,并通过
__cpu_raise_softirq()正式发出软中断请求。这样子后bh_action就会在将来的
某个适合的时间在do_softirq()得以执行。而bh_action 的主要功能就是实现
了原始bottom half,既在任意时间最多只有一个CPU在执行bh函数。但是
tasklet_struct本身是支持SMP 结构的。
tasklet_hi_schedule 与tasklet_schedule都是将tasklet_struct链入到CPU队列,
不同的是__cpu_raise_softirq() 正式发出软中断请求不一样,
tasklet_hi_schedule 使用的是HI_SOFTIRQ,而tasklet_schedule 使用的是
TASKLET_SOFTIRQ。而这两种都是在softirq_init里我们初始化的,这样我
们就建立起来了softicq。
softicq
这是底层机制,很少直接使用,softicq 基于tasklet实现更复杂更庞大的软中
断子系统。
struct softirq_action Include/Linux/Interrupt.h L69
{
void (*action)(struct softirq_action *);
void *data;
};
extern struct tasklet_head tasklet_vec[NR_CPUS]; L131
start_kernel() -> softirq_init ()
对用于hb 的32 个用于bh的tasklet_struct结构调用tasklet_init()以后,它们
的函数指针func全都指向bh_action()。bh_action就是tasklet实现bh的机制了
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
这是我们普通的tasklet_struct要用到的软中断。
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
这是tasklet_struct实现的bh调用。
软中断的执行
do_softirq()
软中断服务不允许在一个硬中断服务程序内部执行,也不允许在一个软
中断服务程序内部执行,所以通过in_interrupt()加以检查。
h->action 就是串行化执行软中断,当bh 的tasklet_struct 链入的时候,就能
在这里执行,在bh里重新锁了所有CPU,导致一个时间只有一个CPU可以
执行bh 函数,但是do_softirq 是可以在多CPU 上同时执行的。而每个
tasklet_struct在一个时间上是不会出现在两个CPU上的。
只有当Linux初始化完成开启中断后,中断系统才可以开始工作。(开始心跳J)
文件系统
Linux使用了VFS(虚拟文件系统)对上提供统一的接口来提供服务。对下使用
了“总线”来读取各种文件系统及设备。如图所示:
一个块设备的文件及节点在引导之初是不可访问的,只有将它“安装”在文
件系统的某个节点上才可访问。最初只有一个“根”节点“/”。
VFS(虚拟文件系统)
在讲解文件系统时,有很多重要的数据结构需要弄清楚,我们根据需要一个
一个的介绍数据结构之间的关系。
下图是VFS 体系结构,对于具体的文件系统都要融入到这个体系结构里来。
从下图可以看到:每一个转载点都有一个struct vfsmount结构。而每一个节点都
有struct dentry 和 struct inode。而这两个结构侧重面有些不同。 struct dentry主
要是VFS使用,而struct inode主要是由各个具体的文件系统自己解释。通过struct
inode,使各个具体的文件系统融入到VSF体系里来。
在讲解根文件系统的安装之前要先说以下系统堆栈。
我们回过头看
lss stack_start,%esp esp = 0xC0272000 /Arch/i386/Kernel/Head.S
L107
这是在进入分页后执行的第一条语句,它的意义是建立内核堆栈。
ENTRY(stack_start) L317
.long SYMBOL_NAME(init_task_union)+8192
.long __KERNEL_DS
堆栈大小是8K(8192)。并且将init_task_union 写到堆里,这是为系统的进
程准备环境。(可以把内核也看成是一个进程,以后可以看到,不管是内核本身
还是内核创建的进程都是用的这一个堆栈)堆栈底部为0xC0270000,并且
init_task_union就初始化在0xC0270000处。
union task_union init_task_union Arch/i386/Kernel/Init_task.c L22
__attribute__((__section__(".data.init_task"))) =
{ INIT_TASK(init_task_union.task) };
union task_union { include/linux/Sched.h L684
task_t task;
unsigned long stack[INIT_TASK_SIZE/sizeof(long)];
};
typedef struct task_struct task_t; L159
说明task_union既可以是task的结构,也可以是stack数组。
#define INIT_TASK(tsk) Include/linux/Sched.h L636
这个宏是初始化一个task_struct结构。这个结构是一个进程的结构,也就是说,
内核编译时,init_task_union 就已经写了数据,这些数据都是固定的。而我们关
心的是task结构里的fs这一项。&fs = 0xc0270640, fs=0xC0254FE0
fs = static struct fs_struct init_fs = INIT_FS; Arch/i386/Kernel/Init_task.c L9
我们看到,fs这项在编译是写的是&init_fs的地址。而init_fs是通过INIT_FS宏
填充好的。所以我们得到了一个如下的图,这个图是在内核编译是就确定了的。
可以看到,task_struct代表的是一个进程,fs_struct代表着该进程的部分环境。
根文件系统的安装过程
第一次安装:
register_filesystem () fs/Suer.c L99
任何一种文件系统都需要通过调用该函数进行注册,系统才能识别。
这个函数很简单,就是遍历一次注册的文件系统链表,如果没有注册的
话就不将其加入到链表中。file_systems=0xC02BA3E0 就是链表的头,也就
是root_fs_type的位置。
start_kernel () ->vfs_caches_init ()->mnt_init()->init_rootfs()
注册了rootfs_fs_type这种文件系统。
static DECLARE_FSTYPE(rootfs_fs_type, "rootfs", ramfs_read_super,
FS_NOMOUNT|FS_LITTER); fs/ramfs/Inode.c L321
DECLARE_FSTYPE是个宏,定义一个file_system_type的结构,展开为
struct file_system_type rootfs_fs_type = {
name: "rootfs",
read_super: ramfs_read_super,
fs_flags: FS_NOMOUNT|FS_LITTER,
owner: NULL,
}
start_kernel () ->vfs_caches_init ()->mnt_init()->init_mount_tree ()
do_kern_mount()
对文件系统的预安装。
通过alloc_vfsmnt 初始化一个struct vfsmount, mnt = 0xC10EE104,通过flags
来判断应该走那条线路,
do_kern_mount()->get_fs_type(“rootfs”)
可以看到首先通过文件系统的名字遍历文件系统的链表得到文件系统的
file_system_type的结构,fs=0xC025AB0C,try_inc_mod_count是对文件系统
的使用进行计数。request_module 这一行的意义是如果没有找到文件系统则
通过加载的模块方式来看看有没有该文件系统。我们这里的情况是有该文件
系统,所以就返回了该文件系统的指针。
do_kern_mount()->get_sb_nodev()
get_sb_nodev(0xC025AB0C,0,“rootfs”,0)
do_kern_mount()->get_sb_nodev()->get_anon_super()
get_anon_super(0xC025AB0C,0,0)分配一个到超级块,并让这个超
级块与file_system_type建立关联。
通过该文件系统所提供的read_super 函数指针读取该文件系统的超级块
的具体数据(主要是生成struct inode与struct dentry让其与超级块建立关联,
稍侯我们在介绍这两个结构)。实际上我们看到rootfs是一个ramfs系统。超
级块的读取是ramfs_read_super,与ramfs文件系统是一样的。这是一个内存
文件系统。
当get_sb_nodev成功返回后,带回来了一个初始化好的struct super_block。
在将struct vfsmount与struct super_block关联后返回,这样就又带回了一个
初始化好的struct vfsmount。当do_kern_mount()返回时,具体的文件系统那
边就已经准备就绪。
再次初始化一个struct namespace,并且将vsfmount 挂在namespace 下,将
namespace挂到当前进程的mnt_list 里,说明已经安装了,最后将namespace
的根设成当前进程的根。
首先通过do_kern_mount 预安装rootfs这个文件系统得到它得挂接点,初始化后,
通过最后两个set_fs_…来把当前得fs 指向了该文件系统。实际上也就是说,系
统第一个“安装”的文件系统是rootfs,这个时候又多了几个数据结构,它们之
间的关系如图所示:
struct task_struct
init_task_union
fs
namespace
struct fs_struct
struct namespace
namespace
root
struct vfsmount
mnt(0xC10EE104)
mnt_sb
mnt_root
mnt_devname
="root_fs"
mnt_mountpoint
mnt_parent
file_systems struct file_system_type
rootfs
name :"rootfs"
super_block
struct super_block
rootfs_super_block
s_root
s_type
struct dentry
dentry(0xC10E9094)
d_sb
d_inode
INIT_FS
root
pwd
rootmnt
pwdmnt
0xC02BA3E0
0xC025AB0C
0xC10EC000
super_blocks
0xC02590A0
struct inode
inode(0xC10EA044)
i_sb
第一次初始化内存结构
RAMFS 文件系统
http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=
99919&page=164&view=collapsed&sb=5&o=all
ext2 文件系统
http://www-900.ibm.com/developerworks/cn/linux/filesystem/ext2/#1
我们回顾一下ROOTFS 是怎么装载上去的。
开始时init_task_union里是只指向INIT_FS的,可是INIT_FS 里没什么东西,
注册完rootfs文件系统之后,试图装载rootfs文件系统,那么首先要生成一个装
载点struct vfsmount,并且要读取文件系统生成超级块struct super_block,在
ROOTFS 读取超级块的过程中会在生成struct dentry,这个是对于VFS文件系统
用的(VFS 看到一个节点,不用管它是在什么文件系统中,对各种文件系统统一
接口)。而struct inode则是真正的节点(每种文件系统实现自己对节点的具体操
作)。因为这个节点是用于挂接用的,所以要通过struct vfsmount 来标识这个节
点同时也是个挂接点。这样它们就建立了联系,也就是上图表示。
为什么根系统的安装会在系统初始化的尽头呢?J
通过上面的链接我们对Ext2 文件系统应该有了一些了解,我们想要知道的就是
Ext2 的那种物理布局情况是如何融入到VFS 体系结构的。
第二次安装:
init () -> prepare_namespace()
这里就是真正的根文件系统的安装。首先要判断做为根的是什么设备,并且
在root_fs文件系统里创建目录以及创建节点和设备。然后通过mount_root()以只
读方式装载根文件系统。在mount_root()里可以看出,根文件系统是挂接到了
root_fs文件系统的/ root下,最后在mount_block_root通过sys_chdir将当前目录
设置到了/root。出来后卸载/dev 目录,最后在prepare_namespace 的结束处通过
sys_mount重新完成了根目录的挂接(以移动方式)。
在调用mount_root()之前如图所示:
init () -> prepare_namespace()->sys_mkdir()
系统调用,创建目录。
这个地方创建的是 root_fs的目录,实际上就是一个VFS的节点,因为root_fs
是内存文件系统。首先通过path_lookup-> path_init 得到目录,nd->dentry =
0xC10E0904, nd->mnt=0xC10EE104,这两个数据看上面的图。path_lookup->
path_walk 里跳转到last_component:然后再跳转到lookup_parent:设置值后跳到
return_base:返回。lookup_create(),它的作用是查找缓冲,如果没有就创建一个新
的。lookup_hash()->cached_lookup 在hash 里找,因为我们是新创建的,一定不
存在,还有就是root_fs不提供d_hash的操作。所以dentry=0,这是我们就要自己
创建一个新的。new=0xC10E9430,创建了struct dentry还不行,还要创建与之对
应的struct inode,通过lookup 跳转到了ramfs_lookup 函数里,fs/ramfs/inode.c L55
可以看到在这个函数里永远返回NULL,并且通过d_add 函数把dentry->inode
也置成空。这样一来在lookup_hash的dentry=new了,但是现在并没有inode,回
到lookup_create 里,就会跳转到enoent,返回失败。在回到sys_mkdir,发现失
败后会调用vfs_mkdir 来创建。May_create 检查有没有权限,然后通过具体的文
件系统的mkdir 来创建节点。这里的mkdir 是ramfs_mkdir.里面很简单,只是生
成一个inode结构,并且与dentry挂钩。
普通文件系统的装载
假设我们已经启动Linux到了shell里。
执行mount –t iso9600 /dev/cdrom /mnt
sys_mount ()
它也是mount 的系统调用
sys_mount (dev_name=0x8104f78 "/dev/hda",
dir_name=0x8106f98 "/mnt",
type=0xbfffff88 "iso9660",
flags=3236757504,
data=0x8104f68)
可以看到,这是从用户空间进来的。所以首先把数据拷贝到系统空间。
sys_mount ()->do_mount ()
这也是mount 的主体函数,
do_mount (dev_name=0xc1be8000 "/dev/hda",
dir_name=0xc10a9000 "/mnt",
type_page=0xc1be9000 "iso9660",
flags=3236757504,
data_page=0xc1be7000)
经过一些判断之后就要进入path_lookup
sys_mount ()->do_mount ()->path_lookup()->path_init()
可以看到,这个函数的主要目的就是根据name 来确定目录,如果是’/’,那
初始化的目录就是根,否则就是当前目录,对于fs_struct 里rootmnt 就是根
的挂接点,root就是挂接点的目录,而pwdmnt 与pwd 则是当前目录的挂接
点以及当前目录。current 代表了当前进程的task_struct,每一个进程都有一
个task_struct结构,在进程部分我们在继续讨论。通过path_init之后,struct
nameidata *nd 则设置好了初始目录(这个结构只是暂时使用的)。
sys_mount ()->do_mount ()->path_lookup()->path_walk()->link_path_walk()
因为初始目录已经设置好,直接到‘/’子后的字符。这是如果*name为空说
明我们完成任务,返回。link_count 用于防止函数嵌套。
第一个for循环主要处理路径问题,在我们现在的环境里,*name=“mnt”
所以this.name = “mnt”, this.len = 3,并且通过if (!c) 到了 last_component处。
因为我们只是一次目录,如果有多层目录的话往下有几种情况,比如在界面上我
们打的是 “/mnt/”或“/mnt/aaa/” 这样就走 last_with_slashes,说明走的是目录。
另外就是要处理 “.”与“..”的情况了,我们都知道“.”与“..”代表的意义。
所以“..”就是向上走了一层继续,“.”的情况就是不发生变化,继续,这就相
当于在界面上打“mnt/../mnt”或“mnt/./../mnt”,如果不是“.”与“..”,那说明
是正常的目录,就要继续往下走。比如我们在界面上键入的是/mnt/cdrom,那么就
通过cached_lookup 在cache 里查mnt 这个节点有没有,如果没有就通过
real_lookup 从文件系统里读出来并建立struct dentry。对读出来的struct dentry所
指向的目录进行判断,是不是安装点,是不是符号链接,详细的操作过程在具体
的文件系统里讨论,我们现在先看VFS层。
如果是目录就会跳转到last_with_slashes,标记上是目录便继续到
last_component,如果标记中有LOOKUP_PARENT,说明找的是根,便跳
lookup_parent处理,紧接着还是判断“.”与“..”的情况,假如我们在界面上写
的是“/mnt/..”或“/mnt/.”,这种情况就是在这里调整跳到return_reval处理。可
以看出一个目录或文件都是找的struct dentry,那么这个结构也就是整个VFS的
中心结构。我们通过path_init 与path_walk确定了我们要安装的目录在系统的所
在。通过do_add_mount就把文件系统安装到了该目录。
补充
在文件系统里一个很重要的数据结构是file_oprations
进程管理及调度
说明:
在Linux 的实现中,一个进程或一个线程都拥有一个task_struct。都拥有专
用的系统堆栈空间,他们的区别只是在于线程没有用户空间。
下面我们继续看在Linux初始化过程中创建的第一个内核线程
rest_init-> init_idle()
首先初始化idle,也就是CPU无事做的时候运行的函数。
init_idle(0xC0270000, 0);
首先可以看到idle_rq= cpu_rq(0)=runqueues=0xC028BA00, runqueues 是
一个以CPU 个数为下标的数组。代表是每个CPU 的运行队列。然后使用
set_tsk_need_resched 设置current->need_resched = 1.
rest_init-> kernel_thread()
kernel_thread( 0xC010502C <init>, arg=0x0,flags=3584)
rest_init-> kernel_thread()->arch_kernel_thread()
自己做一个struct pt_regs,为什么要做一个?往下看
rest_init-> kernel_thread()->arch_kernel_thread()->do_fork()
do_fork(clone_flags=8392448, stack_start=0, regs=0xc0271f58,
stack_size=0,parent_tidptr=0x0, child_tidptr=0x0)
返回0xC10D0000,也就是新的task_struct
rest_init-> kernel_thread()->arch_kernel_thread()->do_fork()->copy_process()
dup_task_struct 拷贝一个task_struct,还根据参数决定是否拷贝深层的数据结
构,比如:copy_files、copy_fs、等。最后设置需要调度。
拷贝出来的线程开始并没有运行,要等到调度的时候才能运行。这个时候系
统有了一个“进程”和一个线程了
rest_init-> cpu_idle()
CPU 的“初始化”都结束了,CPU 进入idle,其实一进入idle 就是开始了进
程调度。可以看到current->need_resched(init_idle())这个是1.所以不会在while里
循环。
rest_init-> cpu_idle()->schedule()
这是整个系统的进程调度算法(核心),不但这里有调用,很多地方都有调
用。
首先我们看一下runqueues 这个结构的说明,虽然这是2.4 的内核,但是调
度算法却使用的是2.6 的。应该是RH自己修改的吧,我也不清楚,猜的。J
http://www-900.ibm.com/developerWorks/cn/linux/kernel/l-kn26sch/index.shtml
http://www.linuxfans.org/nuke/modules.php?name=News&file=article&op=view&sid
=2368
prev = current; 当前进程,我们这里现在是0xC0270000
rq = this_rq(); 当前运行schedule 的CPU,我们这里现在只有一个CPU,
所以是runqueues,0xC028BA00
prev->last_run = jiffies; 刷新当前进程的运行时间
spin_lock_irq(&rq->lock); 对当前的CPU 的运行队列加锁。并禁用当前
CPU中断。这里我们的进程的运行状态是TASK_RUNNING(init_idle()L1916),
判断nr_running,如果当前CPU上没有进程要运行的话,next=rq->idle.
判断nr_active,本进程组中的进程数如果为0,就把active 与expired对掉。
我们看一下我们这里的rn_running与nr_active是多少。回过头去看
rest_init-> init_idle()->deactivate_task()
deactivate_task(0xC0270000, 0xC028BA00),看上面的数据
那么我们这里nr_running = ?,rq->active->nr_active = ?,再回头看
start_kernel()->sched_init()
我们看到runqueues 是一个全局变量。开始的时候里面是空。这里就是对它
的初始化
首先通过循环按支持的CPU数来初始化runqueues,然将内核看做是一个进
程idle来初始化。
start_kernel()->sched_init()->wake_up_forked_process()
首先调整sleep_avg 等,因为第 一次进来current->array 是空。所以要走
__activate_task。
start_kernel()->sched_init()->wake_up_forked_process()->activate_task()
执行完这里current->array=runqueues->active(0xC028BA24);
并且runqueues->active->nr_active = 1,runqueues->active->nr_running=1;
回到deactivate_task
现在又把nr_nr_running--,nr_active—都变成了0,。并且把array也清了,那
么这个时候又恢复成原来的样子的。
在继续往下看
rest_init-> kernel_thread()->arch_kernel_thread()->do_fork()
再次调用wake_up_forked_process,这样相当于activate_task() 没有执行。J
那么我们这里nr_running=1,rq->active->nr_active=1
回到调度
继续向下sched_find_first_bit,它用于快速定位就绪队列中高优先级任务
next = 0xC10D0000,这就是我们新创建的线程的task_struct 的起始地址,接
着使用clear_tsk_need_resched 清掉need_resched
判断prev 与next 是不是一个进程,是的话可以看到,什么也没有做。不是
的话我们就要开始进行进程切换了。
context_switch 主要就处理进程的用户空间。每个进程都有一个struct
mm_struct 代表着自己的用户空间,但是线程却没有用户空间,怎么办?只好暂
时借用一下上一个进程的用户空间。(会有问题吗?没有,因为现在是在内核空
间,内核访问用户空间都是一样的,就算是有问题,那也是返回到用户空间也会
出现问题。再加上线程并不使用用户空间,所以没有关系)
switch_to这个就是进程切换的关键所在。(主要是切换进程堆栈,已经说过,
不管是进程或是线程,都有自己的堆栈)
prev->thread 里的部分数据
struct thread_struct {
…
unsigned long esp0 = 0
unsigned long eip = 0
unsigned long esp = 0
unsigned long fs = 0
unsigned long gs = 0
…
};
next->thread
struct thread_struct {
…
unsigned long esp0 = C10D2000
unsigned long eip = C0108F60
unsigned long esp = C10D1FC4
unsigned long fs = 0
unsigned long gs = 0
…
};
我们回头看一下我们的线程是什么时候把thread 这个结构初始化的。
rest_init-> kernel_thread()->arch_kernel_thread()->)->do_fork()->copy_process()
->copy_thread()
childregs = ((struct pt_regs *)(THREAD_SIZE+(unsigned long) p))- 1
= ((struct pt_regs *)(8192+0xC10D0000))- 1 = 0xC10D4000 – sizeof( pt_regs)
= 0xC10D4000 – 60 = 0xC10D1FC4
在这里实际上是打造一个堆栈的环境,这个堆栈就是我们生成的线程用的堆
栈。那么这个线程是怎么使用这个堆栈的呢?那就要看调度了。
那么我们运行完这个函数,里面初始化成什么样子呢?
Childregs数据:
ebx = 0xC010502C
eip=0xc01071E4
xes=xds=0x68
xcs=0x60,
orig_eax=0xFFFFFFFF,其它为0,其实childregs 大部分是我们开始在
arch_kernel_thread自己做的那个regs。自己可以对照一下。
(struct task_struct*)(0xC10D0000) ->thread 的数据
esp = 0xC10D1FC4
esp0 = 0xC10D2000
eip = 0xC0108F60 ret_form_fork 的地址(怎么会是它,为什么不是
kernel_thread_helper,回头看copy_thread, p->thread.eip = (unsigned long)
ret_from_fork)
这就完成了next->thread的初始化,现在回过头来看调度
switch_to 反汇编代码
0xc01160c5 <schedule+321>: mov %esi,%eax
0xc01160c7 <schedule+323>: mov %ecx,%edx
0xc01160c9 <schedule+325>: push %esi
0xc01160ca <schedule+326>: push %edi
0xc01160cb <schedule+327>: push %ebp
0xc01160cc <schedule+328>: mov %esp,0x350(%esi)
0xc01160d2 <schedule+334>: mov 0x350(%ecx),%esp
0xc01160d8 <schedule+340>: movl $0xc01160ed,0x34c(%esi)
0xc01160e2 <schedule+350>: pushl 0x34c(%ecx)
0xc01160e8 <schedule+356>: jmp 0xc0107654 <__switch_to>
0xc01160ed <schedule+361>: pop %ebp
0xc01160ee <schedule+362>: pop %edi
0xc01160ef <schedule+363>: pop %esi
执行switch_to 之前寄存器状态 当eip = 0xC01160CC
eax 0xC028BA00 0xC0270000
ecx 0xC10D0000
edx 0x0 0xC010D000
ebx 0xC0255700
esp 0xC0271FB4 0xC0271FA8
ebp 0xC0271FC4
esi 0xC0270000
edi 0xC028BA00
eip 0xC01160C5 0xC01160CC
eflags 0x46
cs 0x60
ss 0x68
ds 0x68
es 0x68
fs 0x0
gs 0x0
当0xC01160CC执行完后,那么当前进程的ESP 就放到了prev->thread.esp 里,也
就是说0xC0270000 ->thread.esp = 0xC0271FA8
当0xC01160D2执行完后ESP = 0xC10D1FC4,这样就切换了堆栈,也就是说,
现在切换了进程。但是,仅仅切换了堆栈进程还不能向下运行。还要做一些工作。
当0xC01160E2 执行完后prev->thread.eip = 0xC01160ED, next->thread.eip =
0xC0108F60 压入了堆栈。这个地址就是返回地址了(如果碰到ret 的话)对于我
们线程来说,通过0xC01160E8 这行执行__switch_to 后就返回到0xC0108F60 去
了,因为我们这里是新创建的线程。
如果不是线程是进程呢,那下面那几句pop 就很有用了,这部分到了第二部分再
讲。
rest_init-> cpu_idle()->schedule()->context_switch()->__switch_to()
arch/i386/kernel/process.c L710
tss->esp0 = next->esp0 = 0xC10D2000
主要是处理TSS,当这个函数返回时,返回到ret_form_fork = 0xC0108F60,最
后要运行到kernel_thread_helper = 0xC01071E4这里。通过call 0xC010502C,就
到init 里去了。
kernel_thread_helper arch/i386/kernel/process.c L494
这是一个宏。做的事情很简单,首先是压参数,接着调用init 函数。执行完
返回来的时候,再调用了do_exit,注消了进程。可以看到在Linux 的线程实现
实际上就是使用一个进程来执行函数,接着注销进程。这种进程的开销还是很小
的。回头看copy_process
网络系统
Linux的网络系统如图所示
用户模式
内核模式
套接字文件系统
TCP UDP
IP
PPP 以太网... ...
ARP
init()->do_basic_setup()-> sock_init()
主要是注册sockfs这个文件系统,也就实现了sock的初始化。
现在我们看PF_INET 这种地址家族的注册
#define module_init(x) __initcall(x) include/linux/init.h L112
init()->do_basic_setup()->do_initcalls()->inet_init() net/ipv4/af_inet.c L1102
init()->do_basic_setup()->do_initcalls()->inet_init()->sock_register()
注册地址家族,这里注册的是inet_family地址家族
在RH9 这个版本中最多支持32 种地址家族
注册完PF_INET 家族之后就要注册PF_INET家族的协议
inet_protocol_base很有趣,使用了C语言技巧
我们看inet_protocol_base的定义
#define IPPROTO_PREVIOUS &udp_protocol
static struct inet_protocol icmp_protocol = {
handler: icmp_rcv,
next: IPPROTO_PREVIOUS,
protocol: IPPROTO_ICMP,
name: "ICMP"
};
#undef IPPROTO_PREVIOUS
#define IPPROTO_PREVIOUS &icmp_protocol
struct inet_protocol *inet_protocol_base = IPPROTO_PREVIOUS; L100
所以它指向的是 icmp_protocol,但是我们再在protocol.c 里向上看,在定义
icmp_protocol 时, IPPROTO_PREVIOUS 指向的是udp_protocol , 所以
icmp_protocol里的next = udp_protocol,同理,向上推,实际上这是在编译时就
完成了一个链表。最后IPPROTO_PREVIOU 指的就是一个头。现在明白之后就
知道循环是怎么回事了。
通过inet_add_protocol把协议添加到inet_protos表中
接着各种注册信息。
整体来说,网络的初始化比较简单。我们会在第二部分从用户角度上来分析网络
具体的流程。