zoukankan      html  css  js  c++  java
  • Linux内核之电源开和关时都发生了什么

    原文转自:http://www.cnblogs.com/lihuidashen/p/4250095.html

      说实话感觉自己快写不下去了,其一是有些勉强跟不上来,其二是感觉自己越写越差,刚开始可能是新鲜感以及很多读者的鼓励,现在就是想快点完成自己制定的任务,不过总有几个读者给自己鼓励,很欣慰的事情,不多感慨了,加紧时间多多去探索吧,今天要去描述的是电源开和关时都发生了什么,一起去看看吧~~

      bootloader引导装入程序将内核映像加载到内存并处理控制权传送到内核后在内核引导时每个子系统都必须要初始化,我们根据实际执行的线性顺序跟踪内核的初始化过程,下图说明了从系统加电到断电这一过程中所有事情发生的顺序,这个图不多加解释了,看图就知道其线性执行顺序,中间过程也是很简单的步骤啦。

      我们首先讨论的是BIOS和Open和Fireware,它们分别是x86和PPC系统加电后在只读内存的某一地址(一般是Flash ROM)最先运行的代码,这些代码负责激活系统中相应的部分,以便处理内核的加载,对于x86而言,这就是系统BIOS的驻留之处,基本的输入输出是一块引导系统并与硬件相关的系统初始化代码,这里不多说了,对于PowerPC而言,初始化代码的类型与PowerPC体系结构的出现时间有关,详情就参见Open Fireware 的主页www.openfireware.org

      引导装入程序Bootloaders大家应该早就有所了解了,Boot Loaders是驻留在计算机引导设备的程序,第一个引导设备往往是系统中的第一个硬盘,完成足够的系统初始化工作后,BIOS或固件调用引导装入程序,一旦成功加载进来,内核就初始化并配置操作系统,对x86系统而言,BIOS允许用户为其系统设置引导设备的顺序,这里提一下GRUB,Grand Unified Bootloader 是基于x86的引导装入程序,用来加载Linux。GRUB2在设计之初就考虑了移植到PPC系统的问题,哎具体的就www.gnu.org/software/grub上有丰富的文档,并且百度上也有相关的例程之类的,我觉得这里我就不再阐释了。

      Linux装入程序即LILO这个我还是得说说,和GRUB相类似但是LILO仅仅使用配置文件,并且没有命令行接口。LILO运行时的第一阶段步骤如下:

    第一阶段:

    开始执行并且显示“L.";
    检测磁盘几何信息并且显示“I.";
    加载第二阶段的代码。
    第二阶段:

    开始执行并且显示“L.";
    确定引导数据和操作系统的位置,并且显示”O.";
    确定启动哪个操作系统并且跳转到该操作系统,LILO配置文件中的一段代码如下(代码在etc/lilo.conf上可以查看):

    1 image = /boot/bzimage-2.6.7-mytestkernel  //image指明内核所在地
    2 label = Kernel 2.6.7, my test kernel  //label描述配置的字符串
    3 root = /dev/hda6          //root指明根文件系统驻留的分区
    4 read-only              //表明根分区在引导时候不可被修改
      GRUB和LILO的主要区别

    LILO将配置信息存储在主引导记录中,若有任何改动,必须运行/sbin/lilo来更新主引导记录
    LILO没有交互式的命令行接口
    LILO不能读取不同的文件系统
      最后说一下Yaboot,yaboot是另一个引导程序(比如说,grub和lilo是比较出名的引导程序),用于Macintosh。主页:http://yaboot.ozlabs.org/ Yaboot引导时的步骤如下:

    OF调用Yaboot
    找到引导设备和引导路径并打开引导分区
    打开/etc/yaboot.conf或命令解释器
    加载映像或内核以及initrd
    执行映像
    这里需要在Ubuntu上自己操作,当然对Ubuntu的基本命令操作是首先要了解的,然后才能根据步骤一步一步执行下去。

      x86和PowerPC体系结构的硬件初始化,由于内存管理的初始化与硬件息息相关,要理解其初始化过程就必须了解硬件的规格,那么都只能去看看资料才能了解了,这里我多讲不了。现在的PowerPC和x86的代码都集中在init/main.c的start_kernel()中,该例程位于体系结构无关的代码段,它调用特定体系结构的例程来完成内存初始化。下面我们来探究一下start_kernel()函数。

      跳转到start_kernel()时候,执行进程0,也就是平时说的超级用户进程,进程0孕育了进程1,也就是init进程,然后进程0就变成CPU的空闲进程,调/sbin/init时,仅有这两个进程在运行:

    复制代码
    1 asmlinkage void __init start_kernel(void)
    2 {
    3 char * command_line;
    4 extern struct kernel_param __start___param[], __stop___param[];
    5 //来设置smp process id,当然目前看到的代码里面这里是空的
    6 smp_setup_processor_id();
    7 //lockdep是linux内核的一个调试模块,用来检查内核互斥机制尤其是自旋锁潜在的死锁问题。
    8 //自旋锁由于是查询方式等待,不释放处理器,比一般的互斥机制更容易死锁,
    9 //故引入lockdep检查以下几种情况可能的死锁(lockdep将有专门的文章详细介绍,在此只是简单列举):
    10 //
    11 //·同一个进程递归地加锁同一把锁;
    12 //
    13 //·一把锁既在中断(或中断下半部)使能的情况下执行过加锁操作,
    14 // 又在中断(或中断下半部)里执行过加锁操作。这样该锁有可能在锁定时由于中断发生又试图在同一处理器上加锁;
    15 //
    16 //·加锁后导致依赖图产生成闭环,这是典型的死锁现象。
    17 lockdep_init();
    18 debug_objects_early_init();
    19 //初始化stack_canary栈3
    20 //stack_canary的是带防止栈溢出攻击保护的堆栈。
    21 // 当user space的程序通过int 0x80进入内核空间的时候,CPU自动完成一次堆栈切换,
    22 //从user space的stack切换到kernel space的stack。
    23 // 在这个进程exit之前所发生的所有系统调用所使用的kernel stack都是同一个。
    24 //kernel stack的大小一般为4096/8192,
    25 //内核堆栈示意图帮助大家理解:
    26 //
    27 // 内存低址 内存高址
    28 // | |<-----------------------------esp|
    29 // +-----------------------------------4096-------------------------------+
    30 // | 72 | 4 | x < 4016 | 4 |
    31 // +------------------+-----------------+---------------------------------+
    32 // |thread_info | | STACK_END_MAGIC | var/call chain |stack_canary |
    33 // +------------------+-----------------+---------------------------------+
    34 // | 28 | 44 | | |
    35 // V | |
    36 // restart_block V
    37 //
    38 //esp+0x0 +0x40
    39 // +---------------------------------------------------------------------------+
    40 // |ebx|ecx|edx|esi|edi|ebp|eax|ds|es|fs|gs|orig_eax|eip|cs|eflags|oldesp|oldss|
    41 // +---------------------------------------------------------------------------+
    42 // | kernel完成 | cpu自动完成 |
    43 boot_init_stack_canary();
    44 // cgroup: 它的全称为control group.即一组进程的行为控制.
    45 // 比如,我们限制进程/bin/sh的CPU使用为20%.我们就可以建一个cpu占用为20%的cgroup.
    46 // 然后将/bin/sh进程添加到这个cgroup中.当然,一个cgroup可以有多个进程.
    47
    48 cgroup_init_early();
    49 //更新kernel中的所有的立即数值,但是包括哪些需要再看?
    50 core_imv_update();
    51 //关闭当前CUP中断
    52 local_irq_disable();
    53 //修改标记early_boot_irqs_enabled;
    54 //通过一个静态全局变量 early_boot_irqs_enabled来帮助我们调试代码,
    55 //通过这个标记可以帮助我们知道是否在”early bootup code”,也可以通过这个标志警告是有无效的终端打开
    56 early_boot_irqs_off();
    57 //每一个中断都有一个IRQ描述符(struct irq_desc)来进行描述。
    58 //这个函数的主要作用是设置所有的 IRQ描述符(struct irq_desc)的锁是统一的锁,
    59 //还是每一个IRQ描述符(struct irq_desc)都有一个小锁。
    60 early_init_irq_lock_class();
    61
    62 // 大内核锁(BKL--Big Kernel Lock)
    63 //大内核锁本质上也是自旋锁,但是它又不同于自旋锁,自旋锁是不可以递归获得锁的,因为那样会导致死锁。
    64 //但大内核锁可以递归获得锁。大内核锁用于保护整个内核,而自旋锁用于保护非常特定的某一共享资源。
    65 //进程保持大内核锁时可以发生调度,具体实现是:
    66 //在执行schedule时,schedule将检查进程是否拥有大内核锁,如果有,它将被释放,以致于其它的进程能够获得该锁,
    67 //而当轮到该进程运行时,再让它重新获得大内核锁。注意在保持自旋锁期间是不运行发生调度的。
    68 //需要特别指出,整个内核只有一个大内核锁,其实不难理解,内核只有一个,而大内核锁是保护整个内核的,当然有且只有一个就足够了。
    69 //还需要特别指出的是,大内核锁是历史遗留,内核中用的非常少,一般保持该锁的时间较长,因此不提倡使用它。
    70 //从2.6.11内核起,大内核锁可以通过配置内核使其变得可抢占(自旋锁是不可抢占的),这时它实质上是一个互斥锁,使用信号量实现。
    71 //大内核锁的API包括:
    72 //
    73 //void lock_kernel(void);
    74 //
    75 //该函数用于得到大内核锁。它可以递归调用而不会导致死锁。
    76 //
    77 //void unlock_kernel(void);
    78 //
    79 //该函数用于释放大内核锁。当然必须与lock_kernel配对使用,调用了多少次lock_kernel,就需要调用多少次unlock_kernel。
    80 //大内核锁的API使用非常简单,按照以下方式使用就可以了:
    81 //lock_kernel(); //对被保护的共享资源的访问 … unlock_kernel();
    82 lock_kernel();
    83 //初始化time ticket,时钟
    84 tick_init();
    85 //函数 tick_init() 很简单,调用 clockevents_register_notifier 函数向 clockevents_chain 通知链注册元素:
    86 // tick_notifier。这个元素的回调函数指明了当时钟事件设备信息发生变化(例如新加入一个时钟事件设备等等)时,
    87 //应该执行的操作,该回调函数为 tick_notify
    88 boot_cpu_init();
    89 //初始化页地址,当然对于arm这里是个空函数
    90 page_address_init();
    91 printk(KERN_NOTICE "%s", linux_banner);
    92 //系结构相关的内核初始化过程
    93 setup_arch(&command_line);
    94 //初始化内存管理
    95 mm_init_owner(&init_mm, &init_task);
    96 //处理启动命令,这里就是设置的cmd_line
    97 setup_command_line(command_line);
    98 //这个在定义了SMP的时候有作用,现在这里为空函数;对于smp的使用,后面在看。。。
    99 setup_nr_cpu_ids();
    100 //如果没有定义CONFIG_SMP宏,则这个函数为空函数。
    101 //如果定义了CONFIG_SMP宏,则这个setup_per_cpu_areas()函数给每个CPU分配内存,
    102 //并拷贝.data.percpu段的数据。为系统中的每个CPU的per_cpu变量申请空间。
    103 setup_per_cpu_areas();
    104 //定义在include/asm-x86/smp.h。
    105 //如果是SMP环境,则设置boot CPU的一些数据。在引导过程中使用的CPU称为boot CPU
    106 smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks /
    107 //设置node 和 zone 数据结构
    108 //内存管理的讲解:
    109 build_all_zonelists(NULL);
    110 //初始化page allocation相关结构
    111 page_alloc_init();
    112 printk(KERN_NOTICE "Kernel command line: %s/n", boot_command_line);
    113 //解析内核参数
    114 parse_early_param();
    115 parse_args("Booting kernel", static_command_line, __start___param,
    116 __stop___param - __start___param,
    117 &unknown_bootoption);
    118
    119 //初始化hash表,以便于从进程的PID获得对应的进程描述指针,按照实际的物理内存初始化pid hash表
    120 //这里涉及到进程管理
    121 pidhash_init();
    122 //初始化VFS的两个重要数据结构dcache和inode的缓存。
    123 vfs_caches_init_early();
    124 //把编译期间,kbuild设置的异常表,也就是__start___ex_table和__stop___ex_table之中的所有元素进行排序
    125 sort_main_extable();
    126 //初始化中断向量表
    127 trap_init();
    128 //memory map初始化
    129 mm_init();
    130 //核心进程调度器初始化,调度器的初始化的优先级要高于任何中断的建立,
    131 //并且初始化进程0,即idle进程,但是并没有设置idle进程的NEED_RESCHED标志,
    132 //所以还会继续完成内核初始化剩下的事情。
    133 //这里仅仅为进程调度程序的执行做准备。
    134 //它所做的具体工作是调用init_bh函数(kernel/softirq.c)把timer,tqueue,immediate三个人物队列加入下半部分的数组
    135 sched_init();
    136 //抢占计数器加1
    137 preempt_disable();
    138 //检查中断是否打开
    139 if (!irqs_disabled()) {
    140 printk(KERN_WARNING "start_kernel(): bug: interrupts were "
    141 "enabled very early, fixing it/n");
    142 local_irq_disable();
    143 }
    144 //Read-Copy-Update的初始化
    145 //RCU机制是Linux2.6之后提供的一种数据一致性访问的机制
    146 //从RCU(read-copy-update)的名称上看,我们就能对他的实现机制有一个大概的了解,
    147 //在修改数据的时候,首先需要读取数据,然后生成一个副本,对副本进行修改,
    148 //修改完成之后再将老数据update成新的数据,此所谓RCU。
    149 rcu_init();
    150 //定义在lib/radix-tree.c。
    151 //Linux使用radix树来管理位于文件系统缓冲区中的磁盘块,
    152 //radix树是trie树的一种
    153 radix_tree_init();
    154 /
    init some links before init_ISA_irqs() */
    155 //early_irq_init 则对数组中每个成员结构进行初始化,
    156 //例如, 初始每个中断源的中断号.其他的函数基本为空.
    157 early_irq_init();
    158 //初始化IRQ中断和终端描述符。
    159 //初始化系统中支持的最大可能的中断描述结构struct irqdesc变量数组irq_desc[NR_IRQS],
    160 //把每个结构变量irq_desc[n]都初始化为预先定义好的坏中断描述结构变量bad_irq_desc,
    161 //并初始化该中断的链表表头成员结构变量pend
    162 init_IRQ();
    163 //prio-tree是一棵查找树,管理的是什么?
    164 //http://blog.csdn.net/dog250/archive/2010/06/28/5700317.aspx
    165 prio_tree_init();
    166 //初始化定时器Timer相关的数据结构
    167 init_timers();
    168 //对高精度时钟进行初始化
    169 hrtimers_init();
    170 //软中断初始化
    171 softirq_init();
    172 //初始化时钟源
    173 timekeeping_init();
    174 //初始化系统时间,
    175 //检查系统定时器描述结构struct sys_timer全局变量system_timer是否为空,
    176 //如果为空将其指向dummy_gettimeoffset()函数。
    177 time_init();
    178 //profile只是内核的一个调试性能的工具,
    179 //这个可以通过menuconfig中的Instrumentation Support->profile打开。
    180 profile_init();
    181 if (!irqs_disabled())
    182 printk(KERN_CRIT "start_kernel(): bug: interrupts were "
    183 "enabled early/n");
    184 //与开始的early_boot_irqs_off相对应
    185 early_boot_irqs_on();
    186 //与local_irq_disbale相对应,开中断
    187 local_irq_enable();
    188 gfp_allowed_mask = __GFP_BITS_MASK;
    189 //memory cache的初始化
    190 kmem_cache_init_late();
    191 //初始化控制台以显示printk的内容,在此之前调用的printk,只是把数据存到缓冲区里,
    192 //只有在这个函数调用后,才会在控制台打印出内容
    193 //该函数执行后可调用printk()函数将log_buf中符合打印级别要求的系统信息打印到控制台上。
    194 console_init();
    195 if (panic_later)
    196 panic(panic_later, panic_param);
    197 //如果定义了CONFIG_LOCKDEP宏,那么就打印锁依赖信息,否则什么也不做
    198 lockdep_info();
    199
    200 //如果定义CONFIG_DEBUG_LOCKING_API_SELFTESTS宏
    201 //则locking_selftest()是一个空函数,否则执行锁自测
    202 locking_selftest();
    203 #ifdef CONFIG_BLK_DEV_INITRD
    204 if (initrd_start && !initrd_below_start_ok &&
    205 page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
    206 printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
    207 "disabling it./n",
    208 page_to_pfn(virt_to_page((void )initrd_start)),
    209 min_low_pfn);
    210 initrd_start = 0;
    211 }
    212 #endif
    213 //页面初始化,可以参考上面的cgroup机制
    214 page_cgroup_init();
    215 //页面分配debug启用
    216 enable_debug_pagealloc();
    217 //此处函数为空
    218 kmemtrace_init();
    219 //memory lead侦测初始化,如何侦测???
    220 kmemleak_init();
    221
    222 //在kmem_caches之后表示建立一个高速缓冲池,建立起SLAB_DEBUG_OBJECTS标志。???
    223 debug_objects_mem_init();
    224 //idr在linux内核中指的就是整数ID管理机制,
    225 //从本质上来说,这就是一种将整数ID号和特定指针关联在一起的机制
    226 //idr机制适用在那些需要把某个整数和特定指针关联在一起的地方。
    227 idr_init_cache();
    228 //是否是对SMP的支持,单核是否需要??这个要分析
    229 setup_per_cpu_pageset();
    230 //NUMA (Non Uniform Memory Access) policy
    231 //具体是什么不懂
    232 numa_policy_init();
    233 if (late_time_init)
    234 late_time_init();
    235 //初始化调度时钟
    236 sched_clock_init();
    237 //calibrate_delay()函数可以计算出cpu在一秒钟内执行了多少次一个极短的循环,
    238 //计算出来的值经过处理后得到BogoMIPS 值,
    239 //Bogo是Bogus(伪)的意思,MIPS是millions of instructions per second(百万条指令每秒)的缩写。
    240 //这样我们就知道了其实这个函数是linux内核中一个cpu性能测试函数。
    241 calibrate_delay();
    242 //PID是process id的缩写
    243 pidmap_init();
    244 //来自mm/rmap.c
    245 //分配一个anon_vma_cachep作为anon_vma的slab缓存。
    246 //这个技术是PFRA(页框回收算法)技术中的组成部分。
    247 //这个技术为定位而生——快速的定位指向同一页框的所有页表项。
    248 anon_vma_init();
    249 #ifdef CONFIG_X86
    250 if (efi_enabled)
    251 efi_enter_virtual_mode();
    252 #endif
    253 //创建thread_info缓存
    254 thread_info_cache_init();
    255 //申请了一个slab来存放credentials??????如何理解?
    256 cred_init();
    257 //根据物理内存大小计算允许创建进程的数量
    258 fork_init(totalram_pages);
    259 //给进程的各种资源管理结构分配了相应的对象缓存区
    260 proc_caches_init();
    261 //创建 buffer_head SLAB 缓存
    262 buffer_init();
    263 //初始化key的management stuff
    264 key_init();
    265 //关于系统安全的初始化,主要是访问控制
    266 security_init();
    267 //与debug kernel相关
    268 dbg_late_init();
    269 //调用kmem_cache_create()函数来为VFS创建各种SLAB分配器缓存
    270 //包括:names_cachep、filp_cachep、dquot_cachep和bh_cachep等四个SLAB分配器缓存
    271 vfs_caches_init(totalram_pages);
    272 //创建信号队列
    273 signals_init();
    274 //回写相关的初始化
    275 page_writeback_init();
    276 #ifdef CONFIG_PROC_FS
    277 proc_root_init();
    278 #endif
    279 //它将剩余的subsys初始化.然后将init_css_set添加进哈希数组css_set_table[ ]中.
    280 //在上面的代码中css_set_hash()是css_set_table的哈希函数.
    281 //它是css_set->subsys为哈希键值,到css_set_table[ ]中找到对应项.然后调用hlist_add_head()将init_css_set添加到冲突项中.
    282 //然后,注册了cgroup文件系统.这个文件系统也是我们在用户空间使用cgroup时必须挂载的.
    283 //最后,在proc的根目录下创建了一个名为cgroups的文件.用来从用户空间观察cgroup的状态.
    284 cgroup_init();
    285 cpuset_init();
    286 ////进程状态初始化,实际上就是分配了一个存储线程状态的高速缓存
    287 taskstats_init_early();
    288 delayacct_init();
    289 //此处为一空函数
    290 imv_init_complete();
    291 //测试CPU的各种缺陷,记录检测到的缺陷,以便于内核的其他部分以后可以使用他们工作。
    292 check_bugs();
    293 //电源相关的初始化
    294 acpi_early_init(); /
    before LAPIC and SMP init */
    295 //
    296 sfi_init_late();
    297 ftrace_init();
    298 //创建1号进程,详细分析之
    299 rest_init();
    300 }
    复制代码
      下面将一些内核引导的函数归类,以后查询就可以使这个样子的,,,这些都是我百度到的,总结的如下,如果想了解每个函数的含义,可以参考一个文档,我把链接附在下面,http://wenku.baidu.com/link?url=BW4g5AS9KHtvkHorS90lbFOAac2HZFgErLaqeiQr-fejBuRWu8bF28LxKKKxGwGuaQI0ALSUsJeY_dj6m_jgkxX9ozZiE17U0i__sZk_YYa
    CPU初始化
    smp_setup_processor_id()
    boot_cpu_init()
    setup_arch(&command_line);
    setup_nr_cpu_ids()
    setup_per_cpu_areas()
    smp_prepare_boot_cpu()
    setup_per_cpu_pageset();
    calibrate_delay();
    cpuset_init();
    内存管理初始化
    boot_init_stack_canary()
    page_address_init();
    mm_init_owner();
    page_alloc_init();
    mm_init();
    rcu_init();
    kmem_cache_init_late();
    page_cgroup_init();
    kmemleak_init();
    numa_policy_init();
    anon_vma_init();
    page_writeback_init();
    进程管理
    pidhash_init();
    sched_init();
    sched_clock_init()
    pidmap_init();
    fork_init(totalram_pages);
    taskstats_init_early();
    文件系统
    vfs_caches_init_early();
    thread_info_cache_init();
    vfs_caches_init(totalram_pages);
    中断
    early_irq_init();
    init_IRQ();
    softirq_init();
    同步互斥
    lockdep_init();
    lockdep_info();
    locking_selftest();
    时钟
    tick_init();
    init_timers();
    hrtimers_init();
    timekeeping_init();
    time_init();
    调试
    debug_objects_early_init();
    console_init();
    enable_debug_pagealloc();
    debug_objects_mem_init();
    dbg_late_init();
    其他
    sort_main_extable();
    trap_init();
    efi_enter_virtual_mode();
    cred_init();
    proc_caches_init();
    buffer_init();
    key_init();
    security_init();
    signals_init();
    proc_root_init();
    delayacct_init();
    check_bugs();
    acpi_early_init();
    sfi_init_late();
    未知
    1
    2
    3
    4
    5
    6
    7
    8
    9
    cgroup_init_early();
    build_all_zonelists(NULL);
    preempt_disable();
    radix_tree_init();
    prio_tree_init();
    profile_init();
    idr_init_cache();
    cgroup_init();
    ftrace_init();
      最后来总结一下Linux内核的初始化过程:

    启动和锁住内核
    为Linux的内存管理初始化页高速缓存和页面地址
    为多CPU做好准备
    显示内核标志
    初始化Linux调度程序
    分析传到Linux内核的参数
    初始化中断处理程序,定时器处理程序和信号处理程序
    挂载初始文件系统
    完成系统的初始化,并且将控制权从init交回系统
     

     小结

      今天主要是描述了系统加电和断电时候的内核引导期间发生了什么事情,先讨论了BIOS和Firmware以及它们是如何与内核引导装入程序交互的,也讨论了装入程序LILO,GRUB和Yaboot,,最后着重分析了start_kernel()函数的代码,这个是我看网上代码的,主要是别人分析的太好了,所以借鉴了一下,只要能懂就行,最后列出了一系列的函数,只要看了链接上的 都能懂的,,我也努力在看,,共同进步吧大家~

  • 相关阅读:
    网络编程
    正则表达式
    对空气质量历史数据的爬取
    通过移动设备行为数据预测性别年龄
    电影口碑与海报图像的相关性分析
    微博情感分析
    《python3网络爬虫开发实战》--验证码的识别
    python编程快速上手
    Echarts树图定制详解
    Servlet学习笔记【2】---Http数据包
  • 原文地址:https://www.cnblogs.com/wangdac/p/13183301.html
Copyright © 2011-2022 走看看