kernel启动流程第二阶段

来源:互联网 发布:达内java速成班 编辑:程序博客网 时间:2024/05/21 05:41

Kernel启动流程中的Tips:

1、 Kernel一般会存在于存储设备上,比如FLASH\EMMC\SDCARD. 因此,需要先将kernel镜像加载到RAM的位置上,CPU才可以去访问到kernel

但是注意,加载的位置是有要求的,一般是加载到物理RAM偏移0x8000的位置,也就是要在前面预留出32KRAMkernel会从加载的位置上开始解压,而kernel前面的32K空闲RAM中,16K作为boot params,16K作为临时页表 

2、 Arch/arm/kernel/head.S(kernel的入口函数)

3、 bootloader需要通过设置PC指针到kernel的入口代码处(也就是kernel的加载位置)来实现kernel的跳转。 

 

 

 

Start_Kernel():内核启动第二阶段入口  init/main.c


 

来自:https://www.cnblogs.com/yjf512/p/5999532.html

1、lockdep_init();kernel/kernel3.18/include/linux/lockdep.h

do{}while(0)什么都没做,作用暂未知。

2、set_task_stack_end_magic(&init_task);kernel/kernel3.18/kernel/fork.c

init_task:

1)为init_task_union结构体的成员,为系统的第一个进行,PID为0,唯一一个不用fork()创建的进程

2) 由宏’INIT_TASK(tsk)’初始化,方式:为task_struct结构体中的每个成员直接赋值

该函数主要用于指定init_task线程堆栈的结尾,设置标记STACK_END_MAGIC(防溢出操作)

3、smp_setup_processor_id();kernel/kernel3.18/arch/arm/kernel/setup.c

设置smp模型的处理器ID;SMP(多对称处理模型),指多个CPU之间地位平等,共享所有总线、内存、I/O,但也因此会造成抢占资源的问题。

4、boot_init_stack_canary();kernel/kernel3.18/arch/arm/asm/stackprotector.h

该函数也用于防止堆栈溢出,该函数在堆和栈之间设置一个标记位(canaryword)。当该位被修改后,即可探测到溢出。

参考:https://www.ibm.com/developerworks/cn/linux/l-cn-gccstack/

               

           上图2中的EBP(堆栈帧指针,ESP为栈顶指针)和返回地址已经被覆盖,如果能在运行时检测出这种破坏,就有可能对函数栈进行保护。目前的堆栈保护实现大多使用基于 “Canaries”的探测技术来完成对这种破坏的检测。详细见上述http地址。

5、cgroup_init_early();kernel/kernel3.18/kernel/cgoup.c

参考:http://www.cnblogs.com/yjf512/p/6003094.html描述了Linux系统中的cgroup机制,部分截选如下:

        我们把每种资源叫做子系统,比如CPU子系统,内存子系统。为什么叫做子系统呢,因为它是从整个操作系统的资源衍生出来的。然后我们创建一种虚拟的节点,叫做cgroupcontrolgroup,一组进程的行为控制)。

        进程分组css_set,不同层级中的节点cgroup也都有了。那么,就要把节点cgroup和层级进行关联,和数据库中关系表一样。这个事一个多对多的关系。为什么呢?首先,一个节点可以隶属于多个css_set,这就代表这这批css_set中的进程都拥有这个cgroup所代表的资源。其次,一个css_set需要多个cgroup。因为一个层级的cgroup只代表一种或者几种资源,而一般进程是需要多种资源的集合体

       Cgroup机制结构图如下:

结构体css_set中最重要的成员就是cgroup_subsys_statesubsys[]数组以及structcgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];

task_struct->css_set->cgroup_subsys_state->cgroup反映了进程与cgroup之间的连接关系。

即该函数就是初始化这个cgroup机制。

6、local_irq_enable();关闭中断(底层调用汇编指令)

7、boot_cpu_init();初始化第一个CPU,并将当前CPU设置为激活状态

8、page_address_init();

页地址初始化的作用,未定义高端内存,代码为do{}while(0),不需要做任何操作,具体参考上述网址。

9、setup_arch(&command_line);

内核启动最重要的部分

参考:http://blog.csdn.net/qing_ping/article/details/17351541

http://www.it165.net/os/html/201409/9264.html

主要完成四个工作:

                A)取得machine和processor的信息,然后赋值给kernel相应的全局变量

                B)对boot_command_line和tags进行解析

                C)memory和cache的初始化

                D)为kernel后续运行请求资源

1.      /*内核通过machine_desc结构体来控制系统体系架构相关部分的初始化。 

2.      machine_desc结构体通过MACHINE_START宏来初始化,在代码中, 

3.      通过在start_kernel->setup_arch中调用setup_machine_fdt来获取。*/  

每种体系结构都有自己的setup_arch()函数,是体系结构相关的,具体编译哪个 体系结构的setup_arch()函数,由源码树顶层目录下的Makefile中的ARCH变量决定

{

    1)setup_processor();

                首先从寄存器中读取cpuid,之后调用lookup_processor_type来取得proc_info_list

    2)setup_machine_fdt();

                获取machine_desc结构体

    3)

1. 通过连接脚本中得到的Linux代码位置数据来初始化一个mm_struct结构体init_mm中的部分数据 

2.     ps:每一个任务都有一个mm_struct结构以管理内存空间,init_mm是内核自身的mm_struct 

后续暂留

}

10、mm_init_cpumask(&init_mm)

                初始化CPU屏蔽字

                mm.owner = &init_task

11、setup_command_line();

cmdline进行备份和保存:保存未改变的comand_line到字符数组static_command_line[] 中。

保存 boot_command_line到字符数组saved_command_line[]中

 初始化initcall_command_line变量用于后续使用

12、setup_nr_cpu_ids();

                参考:http://blog.csdn.net/yin262/article/details/46778013 

nr_cpu_ids全局变量被声明为__read_mostly属性。 
nr_cpu_ids
保存的是所有可处于联机状态的CPU总数。 
nr_cpu_ids
具有当前系统能具备的CPU数的信息,默认值为NR_CPUS值,NR_CPUS是编译时用户可设置的常量值。NR_CPUS并非当前系统内存在的CPU的数值,而是Linux内核能支持的最大CPU数的最大值。 

三种系统中UP(Uni-Processor)中是132SMP中具有2~32的值,64位内核中具有2~64位的值。

 

其中:

cpu_possible_mask:系统内可安装的最多CPU的位图 
cpu_online_mask
:系统内安装的CPU中,正在使用的CPU的位图 
cpu_present_mask
:系统内安装的CPU的位图 
cpu_active_mask
:处于联机状态且可以动的(migration)CPU的位

 
13
setup_per_cpu_areas()

setup_per_cpu_areas是为了对内核的内存管理(mm)进行初始化而调用的函数之一。只在SMP系统中调用,UP中不执行任何操作。 

setup_per_cpu_areas函数为SMP的每个处理器生成per-cpu数据。 
per-cpu数据按照不同的CPU类别使用,以将性能低下引发的缓存一致性(cachecoherency)问题减小到最小。per-cpu数据由各cpu独立使用,即使不锁也可访问,十分有效。 

系统中维护的per_cpu数据是一个数据结构数组,其中每一个元素即每一个数据结构仅供一个CPU使用,不用担心竞争。

__per_cpu_offset数组中记录了每个CPUpercpu区域的开始地址。我们访问每CPU变量就要依靠__per_cpu_offset中的地址

14、smp_prepare_boot_cpu();donothing

15、build_all_zonelists();

                建立系统内存页区(zone)链表 

是是是

16、page_alloc_init();

内存页初始化 

                Here

17、打印boot_command_line

18、parse_early_param();

 解析早期格式的内核参数 

19、parse_args("Bootingkernel", static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, &unknown_bootoption);

参考:http://blog.csdn.net/funkunho/article/details/51967137

函数对Linux启动命令行参数进行再分析和处理
当不能够识别前面的命令时,所调用的函数。

 

参考:http://blog.csdn.net/tommy_wxie/article/details/8041487

系统的所有参数都是由__setup(str,fn)和early_param(str,fn)宏定义的,两个宏都调用一个宏:

#define __setup(str, fn)                   /
    __setup_param(str, fn, fn
, 0)
#define early_param(str,fn)                   /

    __setup_param(str, fn, fn, 1)

可见唯一区别为最后的early标记是0还是1。为1则为early param,其表示需要在其他内核选项之前被处理(18parse_early_param),若为0则由parse_args处理。

__setup()宏用于定义一个结构体(obs_kernel_param),并指定其放入.init.setup”段,段头段尾分别为(__setup_start和__setup_end),因此解析参数的时候即可以通过从头遍历到尾,将其中的标记提取出来用来判断是否是early参数,之后对符合的参数,提取其结构体中的函数(定义时传入的fn)直接调用。

 

20、jump_label_init();

                处理静态定义在跳转标号

 

21、setup_log_buf(0);

                使用memblock_alloc分配一个启动时log缓冲区

 

 

参考:http://blog.csdn.net/ffmxnjm/article/details/70230958

22pidhash_init(); //初始化pid散列表

参考:http://blog.csdn.net/ongoingcre/article/details/50405356

       设定Pid散列表的原因是:通过遍历进程双向链表来找到指定的进程效率太低,因此再维护一个hash表来提高查找效率

       第一:分配系统支持最大的hash_table,以及长度,在此期间内核动态的为4个散列表分配空间,分别是PID(进程ID),TGID(线程组领头进程的PID),PGID(进程组领头进程的PID),SID(会话领头进程的PID) 
       第二:根据得到的长度初始化pid_hash. 

       第三:hash表可能存在冲突的问题,因此在得到pid_hash后,初始化链表来解决冲突的问题。

 

23vfs_caches_init_early(); //初始化dentryinodehashtable

两个hash表的初始化的方式和pidhash_init()如出一辙,都是先分配空间,初始化hash表,再讲对应的链表初始化。此函数为前期步骤,后续初始化为函数68

24sort_main_extable(); //对内核异常向量表进行排序

 

25trap_init(); //对内核陷阱异常进行初始化,arm没有用到    

 

 

26mm_init(); // 初始化内核内存分配器,过度到伙伴系统,启动slab机制,初始化

                          // 非连续内存区

            参考:http://blog.csdn.net/sunlei0625/article/details/58594542

         Mem_map:描述所有的物理内存采用的struct page结构的数组的基指针。比如说,对于4GB的内存(2^32)来说,如果一个页定义为4KB,即2^12字节。那么可想而知,总共这个mem_map数组大小为2^20个。

          这些页都有一个具体的页帧号与之对应。页帧号一般用pfn来表示,那么由于每个页都有一个页帧号,那最小的页帧号和最大的页帧号为多少呢?需要特别注意的是,页帧号也是与mem_map数组的index相对应。我们一般认为pfn_min为0,而最大pfn_max为mem_map数组下标的最大值,这个最大值也就是max_pfn,这个值跟内核的max_mapnr相对应。

函数set_max_mapnr()就是用于计算max_mapnr。max_mapnr是在setup_arch的paging_init()中调用bootmem_init()来设置的。在成功设置max_mapnr后,我们要把启动过程时所有的空闲内存释放到伙伴系统,这里需要注意三点:

一. bootmem内存管理或者nobootmem管理

二. memblock内存管理

三. 伙伴系统

 

           

27sched_init(); //初始化进程调度器

      根据系统的配置情况,分配空间并初始化root_task_group中的调度实体或调度队列指针。

            root_task_group添加到task_groups

      。。。一系列初始化

      init_idle(current,smp_processor_id()); //将当前进程,即init_task设置为idle

current->sched_class = &fair_sched_class; //设置当前进程,即init_task进程采用CFS调度策

 

28preempt_disable(); //禁止内核抢占

    if (WARN(!irqs_disabled(), "Interrupts wereenabled *very* early, fixing it\n"))

        local_irq_disable(); // 关闭本地中断

29idr_init_cache(); //创建idr(整数id管理机制)高速缓存

     

IDR机制原理:

IDR机制适用在那些需要把某个整数和特定指针关联在一起的地方。例如,在IIC总线中,每个设备都有自己的地址,要想在总线上找到特定的设备,就必须要先发送设备的地址。当适配器要访问总线上的IIC设备时,首先要知道它们的ID号,同时要在内核中建立一个用于描述该设备的结构体,和驱动程序

ID号和设备结构体结合起来,如果使用数组进行索引,一旦ID号很大,则用数组索引会占据大量内存空间。这显然不可能。或者用链表,但是,如果总线中实际存在的设备很多,则链表的查询效率会很低。

此时,IDR机制应运而生。该机制内部采用红黑树实现,可以很方便的将整数和指针关联起来,并且具有很高的搜索效率

 

30rcu_init(); //初始化rcu机制(读--拷贝)

 

31trace_init(); //初始化系统的trace功能

32context_tracking_init();//

 

33radix_tree_init(); //初始化内核基数树

    /* init some links before init_ISA_irqs() */

       Linuxradix树最广泛的用途是用于内存管理,结构address_space通过radix树跟踪绑定到地址映射上的核心页,该radix树允许内存管理代码快速查找标识为dirtywriteback的页。

       Linux基数树(radix tree)是将指针与long整数键值相关联的机制,它存储有效率,并且可快速查询,用于指针与整数值的映射(如:IDR机制)、内存管理等

34early_irq_init(); // arm64没有用到

35init_IRQ(); //初始化中断

    参考:http://blog.chinaunix.net/uid-12567959-id-160975.html

    start_kernel()函数调用trap_init()、early_irq_init()init_IRQ()三个函数来初始化中断管理系统。

    1)trap_init()在arm平台下为空。

    2)early_irq_init()函数在kernel/handle.c文件中根据内核配置时是否选择了CONFIG_SPARSE_IRQ,而可以选择两个不同版本的该函数中的一个进行编译。CONFIG_SPARSE_IRQ配置项,用于支持稀疏irq号,对于发行版的内核很有用,它允许定义一个高CONFIG_NR_CPUS值,但仍然不希望消耗太多内存的情况。

    主要工作即为初始化用于管理中断的irq_desc[NR_IRQS]数组的每个元素,它主要设置数组中每一个成员的中断号,使得数组中每一个元素的kstat_irqs字段(irq stats per cpu),指向定义的二维数组中的对应的行。

    alloc_desc_masks(&desc[i], 0, true)init_desc_masks(&desc[i])函数在非SMP平台上为空函数。arch_early_irq_init()在主要用于x86平台和PPC平台,其他平台上为空函数。

    3)init_IRQ(void)函数是一个特定于体系结构的函数,这个函数将irq_desc[NR_IRQS]结构数组各个元素的状态字段设置为IRQ_NOREQUEST| IRQ_NOPROBE,也就是未请求和未探测状态。然后调用特定机器平台的中断初始化init_arch_irq()函数。

 

36tick_init(); //初始化时钟滴答控制器

      内部调用两个函数:tick_broadcast_init()tick_nohz_init()

      1)Linuxbroadcasttimer机制:防止系统休眠时(CPU深度休眠也会关闭local timer),localtimer也休眠导致无法唤醒本CPU,所以添加了一个broadcast timer机制,用于在cpu休眠时负责唤醒。

详细参考:http://www.wowotech.net/timer_subsystem/tick-broadcast-framework.html

2nohz时钟机制:

参考:http://blog.csdn.net/droidphone/article/details/8112948

Linux中的时钟事件都是由一个周期时钟提供,不管系统中的clock_event_device是工作于周期触发模式,还是工作于单触发模式,也不管定时器系统是工作于低分辨率模式,还是高精度模式,内核都竭尽所能,用不同的方式提供周期时钟,以产生定期的tick事件,tick事件或者用于全局的时间管理(jiffies和时间的更新),或者用于本地cpu的进程统计、时间轮定时器框架等等。周期性时钟虽然简单有效,但是也带来了一些缺点,尤其在系统的功耗上,因为就算系统目前无事可做,也必须定期地发出时钟事件,激活系统。为此,内核的开发者提出了动态时钟这一概念,我们可以通过内核的配置项CONFIG_NO_HZ来激活特性。有时候这一特性也被叫做tickless,不过还是把它称呼为动态时钟比较合适,因为并不是真的没有tick事件了,只是在系统无事所做的idle阶段,我们可以通过停止周期时钟来达到降低系统功耗的目的,只要有进程处于活动状态,时钟事件依然会被周期性地发出。

 

36+ rcu_init_nohz();

 

37init_timers(); //初始化内核定时器

      参考:http://blog.csdn.net/sunnybeike/article/details/7016322

1)初始化本 CPU 上的软件时钟相关的数据结构;

2)向 cpu_chain 通知链注册元素 timers_nb ,该元素的回调函数用于初始化

         指定 CPU 上的软件时钟相关的数据结构;

3)初始化时钟的软中断处理函数。(会注册中断处理函数run_timer_softirq)

 

38hrtimers_init(); //初始化高精度时钟

     

39softirq_init(); //初始化软中断

    软件中断的资源是有限的,内核目前只实现了10种类型的软件中断。定义在   include/linux/interrupt.h

该函数会调用open_softirq()初始化softirq_vet数组中的两个中断类型(每种中断类型对应一个数组元素,故该数组共有10个元素),分别为TASKLET_SOFTIRQ和HI_SOFTIRQ,指定softirq_vet[中断类型]中的action即中断处理函数。

补充:。内核为每个cpu都管理着一个待决软中断变量(pending),它就是irq_cpustat_t,每一位对应一个中断是否触发。疑问:变量为unsigned int类型,如何指定10种中断类型?

     

40timekeeping_init();  // 初始化了大量的时钟相关全局变量

详细见:http://www.wowotech.net/timer_subsystem/timekeeping.html

 

41time_init(); //时钟初始化

            与架构相关,其实就是调用板级初始化文件(arch/arm/mach-*/board-*.c)中定义“设备描述结构体”中的timer成员的初始化函数。

 

41+sched_clock_postinit();

 

41++perf_event_init();//软件性能分析工具初始化

 

42profile_init(); //  初始化内核profile子系统,内核性能调试工具

           

 

43call_function_init(); /smp下跨cpu的函数传递初始化

44WARN(!irqs_disabled(), "Interrupts were enabledearly\n");

45early_boot_irqs_disabled = false;

 

46local_irq_enable(); //使能当前cpu中断

47kmem_cache_init_late();//完善slab分配器的缓存机制,对应于mm_init中的

   //kmem_cache_init

   后续可参考:http://blog.csdn.net/zdy0_2004/article/details/48852147

    /*

     * HACK ALERT! This is early. We're enablingthe console before

     * we've done PCI setups etc, andconsole_init() must be aware of

     * this. But we do want output early, in casesomething goes wrong.

     */

48console_init();//初始化终端

      依次调用__con_initcall_start__con_initcall_end之间的函数,通过vmlinux.lds可以找到宏console_initcall(fn),该宏指定的函数调用register_console()接口实现console的初始化。

    if (panic_later)

        panic(panic_later, panic_param);

 

      Linux内核在发生kernelpanic时会打印出Oops信息(哎呦),会将寄存器状态、堆栈内容和完整的Call trace都打印出来。

      Oops信息的具体含义参考:https://www.cnblogs.com/wwang/archive/2010/11/14/1876735.html



后续均来自:http://www.360doc.com/content/15/0129/20/14530056_444817012.shtml

 

49lockdep_info();//打印当前锁的依赖信息

    /*

     * Need to run this when irqs are enabled,because it wants

     * to self-test [hard/soft]-irqs on/off lockinversion bugs

     * too:

     */

50locking_selftest();



#ifdef CONFIG_BLK_DEV_INITRD

    if (initrd_start && !initrd_below_start_ok&&

        page_to_pfn(virt_to_page((void*)initrd_start)) < min_low_pfn) {

        pr_crit("initrd overwritten(0x%08lx < 0x%08lx) - disabling it.\n",

           page_to_pfn(virt_to_page((void *)initrd_start)),

            min_low_pfn);

        initrd_start = 0;

    }

#endif

      检查initrd的位置是否符合要求。min_low_pfn是系统可用的最小的页帧号,即判断传递进来的initrd_start对应的物理地址是否正常,如果有误就清零。

 

51page_cgroup_init();

 

52debug_objects_mem_init();//Debug_objetcs机制的内存分配初始化

 

53kmemleak_init();//内核内存泄漏检测机制初始化

 

54setup_per_cpu_pageset();//设置每个CPU的页组,并初始化。此前只有启动页

                           //

55numa_policy_init(); //非一致性内存访问(NUMA)初始化

    if (late_time_init)

        late_time_init();

56sched_clock_init();//对每个CPU,初始化调度时钟

57calibrate_delay(); //计算BogoMIPS值,他是衡量一个CPU性能的标识

58pidmap_init();            //PID分配映射初始化

59anon_vma_init();    //匿名虚拟内存域(anonymousVMA)初始化

60acpi_early_init();

#ifdef CONFIG_X86

    if (efi_enabled(EFI_RUNTIME_SERVICES))

        efi_enter_virtual_mode();

#endif

61thread_info_cache_init();//获取thread_info缓存空间,arm为空

62cred_init();//任务信用系统初始化。详见:Documentation/credentials.txt

63fork_init(totalram_pages);//进程创建初始化。为内核“task_struct”分配

                             //空间,计算最大任务数

64proc_caches_init();//初始化进程创建机制所需的其他数据结构,为其申请空间。

 

65buffer_init();//缓存系统初始化,创建缓存头空间,并检查其大小限时。

66key_init(); //内核密钥管理系统初始化

67security_init();//内核安全框架初始化

68dbg_late_init();//内核调试系统后期初始化

69vfs_caches_init(totalram_pages);//虚拟文件系统缓存初始化

70signals_init();//信号管理系统初始化

 

    /* rootfs populating might need page-writeback */

    page_writeback_init();//页回写机制初始化

 

#ifdef CONFIG_PROC_FS

    proc_root_init();//proc文件系统初始化

#endif

 

71cgroup_init();//control group的正式初始化

72cpuset_init();//cpuset初始化

73taskstats_init_early();//任务状态早起初始化函数:为结构体获取高速缓存,

                          //并初始化互斥机制

74delayacct_init();//任务延迟机制初始化



75check_bugs();//检查CPU BUG的函数,通过软件规避BUG


76
acpi_subsystem_init();//ACPI(高级配置和电源接口)

77sfi_init_late();



    if (efi_enabled(EFI_RUNTIME_SERVICES)) {

        efi_late_init();

        efi_free_boot_services();

    }



78ftrace_init();



    /* Do the rest non-__init'ed, we're now alive */

79rest_init();

{

   1)start_sheduler_starting();打开RCU,从该函数后所有RCU事件都讲被相应

    2)调用kernel_thread()创建kernel_init线程(线程号为1),kernel线程中会调用wait接口,等待一个条件触发,条件就是kthreadd_done

    3numa_default_policy();设定NUMA系统的内存访问策略:意图未知

    4)再次调用kernel_thread()创建kthreadd线程,其作用是管理和调度其他的内核线程。

      Kthreadd:调度其他线程步骤如下

               设置内核环境;

  for循环内,查看kthread_create_list全局链表(维护内核线程)是否为空,若不为空则先将链表中的下一个元素摘除,然后创建一个kthread线程,传入的参数为这个链表元素指针。然后调用schedule函数

进行调度(猜测:使链表中的内核线程得以运行)。最后退出kthread线程。

      5)主线程在创建了kernel_initkthreadd进程后会将kernel_init线程等待的条件kthreadd_done置位。

      6)执行一次schedule函数

 

再:init线程等待的条件满足后,接下来的做如下操作:

参考:http://blog.chinaunix.net/uid-20543672-id-3172321.html

      1)执行kernel_init_freeable()

            初始化可在任何node分配到内存页

            初始化可在任何CPU运行

            设置init线程可被任何CPU运行,除非在位掩码中删除该CPU

            smp系统做准备,激活所有的CPU

            Do_basic_setup()初始化设备驱动程序

            打开根文件系统中的/dev/console

      2)释放内存前完成所有的异步__init代码

      3)释放所有init.*段中的内存

      4)设置系统为运行态

      5)设置当前进程状态为不可杀(SIGNAL_UNKILLABLE

      6)查看ramdisk_execute_commandexecute_command是否有指定的init进程,若有则执行,没有就执行后续系统的默认程序。

      7)执行默认的程序:/sbin/init  /etc/init /bin/init  /bin/sh

}

 

 

重要的点:后续接着学习

1、内核启动参数的获取和处理;

2setup_arch(&command_line)函数;

3、内存管理的初化(从bootmemslab);

4rest_init()函数

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

原创粉丝点击