新浪微博 登陆  注册   设为首页 加入收藏

学PHP >> Linux >> Linux内核启动过程分析

Linux内核启动过程分析

查看次数8440 发表时间2012-09-10 23:18:18

http://www.arm9home.net/simple/index.php?t5645.html

http://www.linuxfordevices.com/c/a/Linux-For-Devices-Articles/Introducing-initramfs-a-new-model-for-initial-RAM-disks/

 

http://womendu.iteye.com/blog/1069948

http://blog.csdn.net/zhoudaxia/article/details/6666683

1、Linux内核启动协议
    阅读文档\linux-2.6.35\Documentation\x86\boot.txt
    传统支持Image和zImage内核的启动装载内存布局(2.4以前的内核装载就是这样的布局):
    |             |
0A0000    +------------------------+
    |  Reserved for BIOS     |    Do not use.  Reserved for BIOS EBDA.
09A000    +------------------------+
    |  Command line         |
    |  Stack/heap         |    For use by the kernel real-mode code.
098000    +------------------------+    
    |  Kernel setup         |    The kernel real-mode code.
090200    +------------------------+
    |  Kernel boot sector     |    The kernel legacy boot sector.
090000    +------------------------+
    |  Protected-mode kernel |    The bulk of the kernel image.
010000    +------------------------+
    |  Boot loader         |    <- Boot sector entry point 0000:7C00
001000    +------------------------+
    |  Reserved for MBR/BIOS |
000800    +------------------------+
    |  Typically used by MBR |
000600    +------------------------+
    |  BIOS use only     |
000000    +------------------------+
    当使用bzImage时,保护模式的内核会被重定位到0x1000000(高端内存),内核实模式的代码(boot sector,setup和stack/heap)会被编译成可重定位到0x100000与低端内存底端之间的任何地址处。不幸的是,在2.00和2.01版的引导协议中,0x90000+的内存区域仍然被使用在内核的内部。2.02版的引导协议解决了这个问题。boot loader应该使BIOS的12h中断调用来检查低端内存中还有多少内存可用。
    人们都希望“内存上限”,即boot loader触及的低端内存最高处的指针,尽可能地低,因为一些新的BIOS开始分配一些相当大的内存,所谓的扩展BIOS数据域,几乎快接近低端内存的最高处了。
    不幸的是,如果BIOS 12h中断报告说内存的数量太小了,则boot loader除了报告一个错误给用户外,什么也不会做。因此,boot loader应该被设计成占用尽可能少的低端内存。对zImage和以前的bzImage,这要求数据能被写到x090000段,boot loader应该确保不会使用0x9A000指针以上的内存;很多BIOS在这个指针以上会终止。
    对一个引导协议>=2.02的现代bzImage内核,其内存布局使用以下格式:
        |  Protected-mode kernel |
100000  +------------------------+
    |  I/O memory hole     |
0A0000    +------------------------+
    |  Reserved for BIOS     |    Leave as much as possible unused
    ~                        ~
    |  Command line         |    (Can also be below the X+10000 mark)
X+10000    +------------------------+
    |  Stack/heap         |    For use by the kernel real-mode code.
X+08000    +------------------------+    
    |  Kernel setup         |    The kernel real-mode code.
    |  Kernel boot sector     |    The kernel legacy boot sector.
X       +------------------------+
    |  Boot loader         |    <- Boot sector entry point 0000:7C00
001000    +------------------------+
    |  Reserved for MBR/BIOS |
000800    +------------------------+
    |  Typically used by MBR |
000600    +------------------------+
    |  BIOS use only     |
000000    +------------------------+
    这里程序段地址是由grub的大小来决定的。地址X应该在bootloader所允许的范围内尽可能地低。
    2、BIOS POST过程
    传统意义上,由于CPU加电之后,CPU只能访问ROM或者RAM里的数据,而这个时候是没有计算机操作系统的,所以需要有一段程序能够完成加载存储在非易失性存储介质(比如硬盘)上的操作系统到RAM中的功能。这段程序存储在ROM里,BIOS就是这类程序中的一种。对于BIOS,主要由两家制造商制造,驻留在主板的ROM里。有了BIOS,硬件制造商可以只需要关注硬件而不需要关注软件。BIOS的服务程序,是通过调用中断服务程序来实现的。BIOS加载bootloader程序,Bootloader也可以通过BIOS提供的中断,向BIOS获取系统的信息。整个过程如下:
    (1)电源启动时钟发生器并在总线上产生一个#POWERGOOD的中断。
    (2)产生CPU的RESET中断(此时CPU处于8086工作模式)。
    (3)进入BIOS POST代码处:%ds=%es=%fs=%gs=%ss=0,%cs=0xFFFF0000,%eip = 0x0000FFF0 (ROM BIOS POST code,指令指针eip,数据段寄存器ds,代码段寄存器cs)。
    (4)在中断无效状态下执行所有POST检查。
    (5)在地址0初始化中断向量表IVT。
    (6)0x19中断:以启动设备号为参数调用BIOS启动装载程序。这个程序从启动设备(硬盘)的0扇面1扇区读取数据到内存物理地址0x7C00处开始装载。这个0扇面1扇区称为Boot sector(引导扇区),共512字节,也称为MBR。
    就是说,CPU 在  BIOS 的入口(CS:IP=FFFF:0000)处执行BIOS的汇编程序,BIOS程序功能有系统硬件的检测,提供中断访问接口以访问硬件。而后被BIOS程序通过中断0x19调用磁盘MBR上的bootloader程序,将bootloader程序加载到ox7c00处,而后跳转到0x7c00,这样,位于 0x7c00处的bootloader程序,就可以执行了。
    从BIOS执行MBR中的bootloader程序开始,就是linux的代码在做的事情了。
    3、Bootloader过程
    bootloader程序是为计算机加载(load)计算机操作系统的。boot(引导)是bootstrap的简写,bootstrap是引导指令的意思。bootloader程序通常位于硬盘上,被BIOS调用,用于加载内核。在PC机上常见的bootloader主要有grub、lilo、syslinux等。
    GRUB(GRand Unified Bootloader)是当前linux诸多发行版本默认的引导程序。嵌入式系统上,最常见的bootloader是U-BOOT。这样的bootloader一般位于MBR的最前部。在linux系统中,bootloader也可以写入文件系统所在分区中。比如,grub程序就非常强大。Gurb运行后,将初始化设置内核运行所需的环境。然后加载内核镜像。
    grub磁盘引导全过程:
    (1)stage1: grub读取磁盘第一个512字节(硬盘的0道0面1扇区,被称为MBR(主引导记录),也称为bootsect)。MBR由一部分bootloader的引导代码、分区表和魔数三部分组成。
    (2)stage1_5: 识别各种不同的文件系统格式。这使得grub识别到文件系统。
    (3)stage2: 加载系统引导菜单(/boot/grub/menu.lst或grub.lst),加载内核vmlinuz和RAM磁盘initrd。
    4、内核启动过程    
    内核映像文件vmlinuz:包含有linux内核的静态链接的可执行文件,传统上,vmlinux被称为可引导的内核镜像。vmlinuz是vmlinux的压缩文件。其构成如下:
    (1)第一个512字节(以前是在arch/i386/boot/bootsect.S);
    (2)第二个,一段代码,若干个不多于512字节的段(以前是在arch/i386/boot/setup.S);
    (3)保护模式下的内核代码(在arch/x86/boot/main.c)。
    bzImage文件:使用make bzImage命令编译内核源代码,可以得到采用zlib算法压缩的zImage文件,即big zImage文件。老的zImage解压缩内核到低端内存,bzImage则解压缩内核到高端内存(1M(0x100000)以上),在保护模式下执行。bzImage文件一般包含有vmlinuz、bootsect.o、setup.o、解压缩程序misc.o、以及其他一些相关文件(如piggy.o)。注意,在Linux 2.6内核中,bootsect.S和setup.S被整合为header.S。
    initramfs(或initrd)文件:initrd是initialized ram disk的意思。主要用于加载硬件驱动模块,辅助内核的启动,挂载真正的根文件系统。

    例如,我电脑上的grub启动项如下(在/boot/grub/grub.lst中):

  1. title Fedora (2.6.35.10-74.fc14.i686)  
  2.     root (hd0,0)  
  3.     kernel /vmlinuz-2.6.35.10-74.fc14.i686 ro root=/dev/mapper/VolGroup-lv_root rd_LVM_LV=VolGroup/lv_root rd_LVM_LV=VolGroup/lv_swap rd_NO_LUKS rd_NO_MD rd_NO_DM LANG=zh_CN.UTF-8 KEYBOARDTYPE=pc KEYTABLE=us rhgb quiet  
  4.     initrd /initramfs-2.6.35.10-74.fc14.i686.img  

    内核的执行参数可以控制内核的行为,比如ro参数告诉内核,以只读方式挂载根分区,而quiet则告诉内核,启动的时候不要打印任何信息。这些参数不光影响内核的执行,大多数的发行版也使用这些参数控制启动完毕以后后续的动作。这些参数可以在任何时候从/proc/cmdline 这个文件中获得。现在,grub找到了内核(hd0,0)/boot/vmlinuz-2.6.35.10-74.fc14.i686,它将整个电脑的控制权交给了这个程序,内核开始进行各种初始化的动作,你可以将quiet参数去掉,以便看看内核都做了哪些事情,也可以在系统启动成功以后,使用dmesg这个命令查看内核启动的时候,都打印了哪些东西。
    启动过程是和体系结构相关的,对于2.6内核,x86体系结构,CPU在上电初始化时,指令寄存器CS:EIP总是被初始化为固定值,这就是CPU复位后的第一条指令的地址。对于32位地址总线的系统来说,4GB的物理空间至少被划分为两个部分,一部分是内存的地址空间,另外一部分地址空间用于对BIOS芯片存储单元进行寻址。x86复位后工作在实模式下,该模式下CPU的寻址空间为1MB。CS:IP的复位值是FFFF:0000,物理地址为FFFF0。主板设计者必须保证把这个物理地址映射到BIOS芯片上,而不是RAM上。
    装载Linux内核的第一步应该是加载实模式代码(boot sector和setup代码),然后检查偏移0x01f1处的头部(header)中的各个参数值。实模式的代码总共有32K,但是boot loader可以选择只装载前面的两个扇区(1K),然后检查bootup扇区的大小。
    header中各个域的格式如下:

  1. Offset/Size     Proto       Name                    Meaning  
  2.   
  3. 01F1/1          ALL(1       setup_sects         The size of the setup in sectors  
  4. 01F2/2          ALL     root_flags          If set, the root is mounted readonly  
  5. 01F4/4          2.04+       syssize             The size of the 32-bit code in 16-byte paras  
  6. 01F8/2          ALL     ram_size            DO NOT USE - for bootsect.S use only  
  7. 01FA/2          ALL     vid_mode            Video mode control  
  8. 01FC/2          ALL     root_dev            Default root device number  
  9. 01FE/2          ALL     boot_flag           0xAA55 magic number  
  10. 0200/2          2.00+       jump                Jump instruction  
  11. 0202/4          2.00+       header              Magic signature "HdrS"  
  12. 0206/2          2.00+       version             Boot protocol version supported  
  13. 0208/4          2.00+       realmode_swtch      Boot loader hook (see below)  
  14. 020C/2          2.00+       start_sys_seg       The load-low segment (0x1000) (obsolete)  
  15. 020E/2          2.00+       kernel_version      Pointer to kernel version string  
  16. 0210/1          2.00+       type_of_loader      Boot loader identifier  
  17. 0211/1          2.00+       loadflags           Boot protocol option flags  
  18. 0212/2          2.00+       setup_move_size     Move to high memory size (used with hooks)  
  19. 0214/4          2.00+       code32_start        Boot loader hook (see below)  
  20. 0218/4          2.00+       ramdisk_image       initrd load address (set by boot loader)  
  21. 021C/4          2.00+       ramdisk_size        initrd size (set by boot loader)  
  22. 0220/4          2.00+       bootsect_kludge     DO NOT USE - for bootsect.S use only  
  23. 0224/2          2.01+       heap_end_ptr        Free memory after setup end  
  24. 0226/1          2.02+       ext_loader_ver      Extended boot loader version  
  25. 0227/1          2.02+       ext_loader_type     Extended boot loader ID  
  26. 0228/4          2.02+       cmd_line_ptr        32-bit pointer to the kernel command line  
  27. 022C/4          2.03+       ramdisk_max         Highest legal initrd address  
  28. 0230/4          2.05+       kernel_alignment        Physical addr alignment required for kernel  
  29. 0234/1          2.05+       relocatable_kernel      Whether kernel is relocatable or not  
  30. 0235/1          2.10+       min_alignment           Minimum alignment, as a power of two  
  31. 0236/2          N/A     pad3                    Unused  
  32. 0238/4          2.06+       cmdline_size            Maximum size of the kernel command line  
  33. 023C/4          2.07+       hardware_subarch        Hardware subarchitecture  
  34. 0240/8          2.07+       hardware_subarch_data       Subarchitecture-specific data  
  35. 0248/4          2.08+       payload_offset          Offset of kernel payload  
  36. 024C/4          2.08+       payload_length          Length of kernel payload  
  37. 0250/8          2.09+       setup_data          64-bit physical pointer to linked list of struct setup_data  
  38. 0258/8          2.10+       pref_address            Preferred loading address  
  39. 0260/4          2.10+       init_size           Linear memory required during initialization  

    每个域的具体细节可参考boot.txt文档。
    BIOS把Boot Loader加载到0x7C00的地方并跳转到这里继续执行之后,BootLoader就会把实模式代码setup加载到0x07C00之上的某个地址上,其中setup的前512个字节是boot sector(引导扇区),现在这个引导扇区的作用并不是用来引导系统,而是为了兼容及传递一些参数。之后Boot Loader跳转到setup的入口点,入口点为_start例程(根据arch/x86/boot/setup.ld可知)。
    注意,bzImage由setup和vmlinux两部分组成,setup是实模式下的代码,vmlinux是保护模式下的代码。
    实模式设置(setup)阶段用于体系结构相关的硬件初始化工作,涉及的文件有arch/x86/boot/header.S、链接脚本setup.ld、arch/x86/boot/main.c。header.S第一部分定义了bstext、.bsdata、.header这3个节,共同构成了vmlinuz的第一个512字节(即引导扇区的内容)。常量BOOTSEG和SYSSEG定义了引导扇区和内核的载入地址。下面是header.S的代码:

  1. BOOTSEG     = 0x07C0        /* 引导扇区的原始地址 */  
  2. SYSSEG      = 0x1000        /* 历史的载入地址>>4 */  
  3.   
  4. #ifndef SVGA_MODE   
  5. #define SVGA_MODE ASK_VGA  
  6. #endif   
  7.   
  8. #ifndef RAMDISK   
  9. #define RAMDISK 0   
  10. #endif   
  11.   
  12. #ifndef ROOT_RDONLY   
  13. #define ROOT_RDONLY 1   
  14. #endif   
  15.   
  16.     .code16  
  17.     .section ".bstext""ax"  
  18.   
  19.     .global bootsect_start  
  20. bootsect_start:  
  21.   
  22.     # 使开始地址正常化   
  23.     ljmp    $BOOTSEG, $start2  
  24.   
  25. start2:  
  26.     movw    %cs, %ax  
  27.     movw    %ax, %ds  
  28.     movw    %ax, %es  
  29.     movw    %ax, %ss  
  30.     xorw    %sp, %sp  
  31.     sti  
  32.     cld  
  33.   
  34.     movw    $bugger_off_msg, %si  
  35.   
  36. msg_loop:  
  37.     lodsb  
  38.     andb    %al, %al  
  39.     jz  bs_die  
  40.     movb    $0xe, %ah  
  41.     movw    $7, %bx  
  42.     int $0x10  
  43.     jmp msg_loop  
  44.   
  45. bs_die:  
  46.     # 允许用户按一个键,然后重启   
  47.     xorw    %ax, %ax  
  48.     int $0x16  
  49.     int $0x19  
  50.   
  51.     # 0x19中断绝不会返回,无论它做什么   
  52.     # 调用BIOS复位代码,便CPU工作在实模式下   
  53.     ljmp    $0xf000,$0xfff0  
  54.   
  55.     .section ".bsdata""a"  
  56. bugger_off_msg:  
  57.     .ascii  "Direct booting from floppy is no longer supported.\r\n"  
  58.     .ascii  "Please use a boot loader program instead.\r\n"  
  59.     .ascii  "\n"  
  60.     .ascii  "Remove disk and press any key to reboot . . .\r\n"  
  61.     .byte   0  
  62.   
  63.   
  64.     # 下面设置内核的一些属性,setup需要。这是header的第一部分,来自以前的boot sector  
  65.   
  66.     .section ".header""a"  
  67.     .globl  hdr  
  68. hdr:  
  69. setup_sects:    .byte 0         /* 被build.c填充 */  
  70. root_flags: .word ROOT_RDONLY  
  71. syssize:    .long 0         /* 被build.c填充 */  
  72. ram_size:   .word 0         /* 已过时 */  
  73. vid_mode:   .word SVGA_MODE  
  74. root_dev:   .word 0         /* 被build.c填充 */  
  75. boot_flag:  .word 0xAA55  
  76.   
  77.     # 偏移512处,setup的入口点   
  78.   
  79.     .globl  _start  
  80. _start:  
  81.         # Explicitly enter this as bytes, or the assembler  
  82.         # tries to generate a 3-byte jump here, which causes  
  83.         # everything else to push off to the wrong offset.  
  84.         .byte   0xeb        # short (2-byte) jump  
  85.         .byte   start_of_setup-1f  
  86. 1:  
  87.   
  88.     # header的第二部分,来自以前的setup.S:设置头部header,包括大量的bootloader参数,如header版本、内核版本字符串指针、bootloader类型、  
  89.     # 内核装载时的很多标志、堆栈尾部地址指针、内核命令行地址指针和大小、32位保护模式入口地址、ramdisk地址和大小等  
  90.       
  91. code32_start:               # 这里对32位的代码,装载器可以设置可设置一个不同的入口地址  
  92.         .long   0x100000    # 0x100000 = 为大内核的默认入口地址(保护模式)  
  93.       
  94.     # ............ (省略)   
  95.   
  96. # End of setup header #####################################################  
  97.   
  98.     .section ".entrytext""ax"  
  99. start_of_setup:  
  100. #ifdef SAFE_RESET_DISK_CONTROLLER   
  101. # 重置磁盘控制器   
  102.     movw    $0x0000, %ax        # 重置磁盘控制器  
  103.     movb    $0x80, %dl      # 所有的的磁盘控制器All disks  
  104.     int $0x13  
  105. #endif   
  106.   
  107. # ............(省略)   
  108.   
  109.     # 让%ss无效,创建一个新的栈   
  110.     movw    $_end, %dx  
  111.     testb   $CAN_USE_HEAP, loadflags  
  112.     jz  1f  
  113.     movw    heap_end_ptr, %dx  
  114. 1:  addw    $STACK_SIZE, %dx  
  115.     jnc 2f  
  116.     xorw    %dx, %dx    # Prevent wraparound  
  117.   
  118. 2:  # 现在%dx应该指向我们栈空间的尾部  
  119.     andw    $~3, %dx    # dword对齐  
  120.     jnz 3f  
  121.     movw    $0xfffc, %dx    # 确保不是0  
  122. 3:  movw    %ax, %ss  
  123.     movzwl  %dx, %esp   # 清除%esp的上半部分  
  124.     sti         # 现在我们应该有一个工作空间  
  125.   
  126. # 我们将进入%cs=%ds+0x20,设置好%cs   
  127.     pushw   %ds  
  128.     pushw   $6f  
  129.     lretw  
  130. 6:  
  131.   
  132. # 在setup终止时检查签名   
  133.     cmpl    $0x5a5aaa55, setup_sig  
  134.     jne setup_bad  
  135.   
  136. # 对BSS(Block Started by Symbol)清零   
  137.     movw    $__bss_start, %di  
  138.     movw    $_end+3, %cx  
  139.     xorl    %eax, %eax  
  140.     subw    %di, %cx  
  141.     shrw    $2, %cx  
  142.     rep; stosl  
  143.   
  144. # 跳转到C代码(不会返回)   
  145.     calll   main  
  146.   
  147. # ............(省略)  

    由setup.ld中的ENTRY(_start)可知,_start汇编例程是bzImage内核映像开始执行的入口点,即引导扇区之后的开始处(偏移512字节处),它会准备大量的bootloader参数。最后的call main跳转到arch/x86/boot/main.c:main()函数处执行,这就是众所周知的main函数,它们都工作在实模式下。main函数先调用copy_boot_params函数把位于第一个扇区的参数复制到boot_params变量中,boot_params位于setup的数据段,然后调用链为arch/x86/boot/pm.c:go_to_protected_mode(void) --->arch/x86/boot/pmjump.S:protected_mode_jump()。
    实模式的protected_mode_jump执行后,跳出了bzImage的第一部分,BootLoader默认把第二部分放在0x100000处,这个入口处是startup_32,先执行arch/x86/boot/compressed/head_32.S中的startup_32(保护模式下的入口函数),然后执行arch/x86/kernel/head_32.S中的startup_32(32位内核的入口函数),这里会拷贝boot_params以及boot_command_line, 初始化页表,开启分页机制。
    startup_32()函数会调用head32.c:i386_start_kernel()函数,它会调用init/main.c:start_kernel()函数,这是Linux内核的启动函数。init/main.c文件是整个Linux内核的中央联结点。每种体系结构都会执行一些底层设置函数,然后执行名为start_kernel的函数(在init/main.c中可以找到这个函数)。可以认为main.c是内核的“粘合剂(glue)”,之前执行的代码都是各种体系结构相关的代码,一旦到达start_kernel(),就与体系结构无关了。
    start_kernel()会调用一系列初始化函数来设置中断,执行进一步的内存配置,解析内核命令行参数。然后调用fs/dcache.c:vfs_caches_init()--->fs/namespace.c:mnt_init()创建基于内存的rootfs文件系统(是一个虚拟的内存文件系统,称为VFS),这是系统初始化时的根结点,即"/"结点,后面VFS会指向真实的文件系统。注意在Linux系统中,目录结构与Windows上有较大的不同。系统中只有一个根目录,路径是“/”,而其它的分区只是挂载在根目录中的一个文件夹内,如“/proc”和“/sys”等,这里的“/”就是Linux中的根目录。
    下面是mnt_init()的代码:

  1. void __init mnt_init(void)  
  2. {  
  3.     unsigned u;  
  4.     int err;  
  5.   
  6.     init_rwsem(&namespace_sem);  
  7.   
  8.     mnt_cache = kmem_cache_create("mnt_cache"sizeof(struct vfsmount),  
  9.             0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);  
  10.   
  11.     mount_hashtable = (struct list_head *)__get_free_page(GFP_ATOMIC);  
  12.   
  13.     if (!mount_hashtable)  
  14.         panic("Failed to allocate mount hash table\n");  
  15.   
  16.     printk("Mount-cache hash table entries: %lu\n", HASH_SIZE);  
  17.   
  18.     for (u = 0; u < HASH_SIZE; u++)  
  19.         INIT_LIST_HEAD(&mount_hashtable[u]);  
  20.   
  21.     err = sysfs_init();  
  22.     if (err)  
  23.         printk(KERN_WARNING "%s: sysfs_init error: %d\n",  
  24.             __func__, err);  
  25.     fs_kobj = kobject_create_and_add("fs", NULL);  
  26.     if (!fs_kobj)  
  27.         printk(KERN_WARNING "%s: kobj create error\n", __func__);  
  28.     init_rootfs();  
  29.     init_mount_tree();  
  30. }  

    这里fs/ramfs/inode.c:init_rootfs()会调用fs/filesystems.c:register_filesystem()注册rootfs。然后fs/namespace.c:init_mount_tree()调用fs/super.c:do_kern_mount()在内核中挂载rootfs,调用fs/fs_struct.c:set_fs_root()将当前的rootfs文件系统配置为根文件系统。
    为什么不直接把真实的文件系统配置为根文件系统?答案很简单,内核中没有真实根文件系统设备(如硬盘,USB)的驱动,而且即便你将根文件系统的设备驱动编译到内核中,此时它们还尚未加载,实际上所有内核中的驱动是由后面的kernel_init线程进行加载。另外,我们的root设备都是以设备文件的方式指定的,如果没有根文件系统,设备文件怎么可能存在呢?
    注意根据调用链do_kern_mount()--->vfs_kern_mount(type)--->type->get_sb()--->fs/ramfs/inode.c:rootfs_get_sb()--->ramfs_fill_super()--->fs/dcache.c:d_alloc_root(),函数d_alloc_root分配最终的根结点,代码如下:

  1. struct dentry * d_alloc_root(struct inode * root_inode)  
  2. {  
  3.     struct dentry *res = NULL;  
  4.   
  5.     if (root_inode) {  
  6.         static const struct qstr name = { .name = "/", .len = 1 };  
  7.  
    (转发请注明转自:学PHP)    


      相关推荐



    1楼 -1'说: 2015-11-10 15:22:40
    1

      发表评论
    昵称:
    (不超过20个字符或10个汉字)
    内容: