zoukankan      html  css  js  c++  java
  • 设备树中的中断


    title: 设备树中的中断
    date: 2019/4/29 17:38:38
    toc: true

    设备树中的中断

    原文 100ask

    TODO

    基于设备树的TQ2440的中断(1)
    https://www.cnblogs.com/pengdonglin137/p/6847685.html

    基于设备树的TQ2440的中断(2)
    https://www.cnblogs.com/pengdonglin137/p/6848851.html

    基於tiny4412的Linux內核移植 --- 实例学习中断背后的知识(1)
    http://www.cnblogs.com/pengdonglin137/p/6349209.html

    Linux kernel的中断子系统之(一):综述
    Linux kernel的中断子系统之(二):IRQ Domain介绍
    linux kernel的中断子系统之(三):IRQ number和中断描述符
    linux kernel的中断子系统之(四):High level irq event handler
    Linux kernel中断子系统之(五):驱动申请中断API
    Linux kernel的中断子系统之(六):ARM中断处理过程
    linux kernel的中断子系统之(七):GIC代码分析
    http://www.wowotech.net/irq_subsystem/interrupt_subsystem_architecture.html

    中断概述

    这里就比较复杂了,暂时不去做深入的学习,以后再来挖坑学习,太难了 哈哈.这里放一个框图大概理解下基本流程

    详细流程看窝窝科技

    mark

    以前的内核中断描述符的数组序号与硬件是对应的,后来出了个虚拟中断号,不再强调一定是线性关系.

    中断入口

    2440的中断向量表是这样的(参考uboot)

    .globl _start
    0--->	_start: b	reset
    4--->	ldr	pc, _undefined_instruction
    8--->	ldr	pc, _software_interrupt
    c--->	ldr	pc, _prefetch_abort
    16--> 	ldr	pc, _data_abort
    20-->	ldr	pc, _not_used
    24-->	ldr	pc, _irq //发生中断时,CPU跳到这个地址执行该指令 
    	ldr	pc, _fiq
    

    内核的向量表是这么定义的,在archarmkernelentry-armv.S

    	.section .vectors, "ax", %progbits
    .L__vectors_start:
    	W(b)	vector_rst
    	W(b)	vector_und
    	W(ldr)	pc, .L__vectors_start + 0x1000
    	W(b)	vector_pabt
    	W(b)	vector_dabt
    	W(b)	vector_addrexcptn
    	W(b)	vector_irq
    	W(b)	vector_fiq
    
    

    我们可以看到中断是跳转到vector_irq,这个东西是个宏定义

    /*
     * Interrupt dispatcher
     */
        vector_stub irq, IRQ_MODE, 4   // 相当于 vector_irq: ..., 
                                       // 它会根据SPSR寄存器的值,
                                       // 判断被中断时CPU是处于USR状态还是SVC状态, 
                                       // 然后调用下面的__irq_usr或__irq_svc
    
        .long   __irq_usr               @  0  (USR_26 / USR_32)
        .long   __irq_invalid           @  1  (FIQ_26 / FIQ_32)
        .long   __irq_invalid           @  2  (IRQ_26 / IRQ_32)
        .long   __irq_svc               @  3  (SVC_26 / SVC_32)
        .long   __irq_invalid           @  4
        .long   __irq_invalid           @  5
        .long   __irq_invalid           @  6
        .long   __irq_invalid           @  7
        .long   __irq_invalid           @  8
        .long   __irq_invalid           @  9
        .long   __irq_invalid           @  a
        .long   __irq_invalid           @  b
        .long   __irq_invalid           @  c
        .long   __irq_invalid           @  d
        .long   __irq_invalid           @  e
        .long   __irq_invalid           @  f
    
    @ 下面是宏
    .macro	vector_stub, name, mode, correction=0
    .align	5
    
    vector_
    ame:
    .....
    
    
    • __irq_usr/__irq_svc作用是保存现场,调用 irq_handler,恢复现场

    • irq_handler将会调用C函数 handle_arch_irq

      	.macro	irq_handler
      #ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
      	ldr	r1, =handle_arch_irq
      	mov	r0, sp
      	badr	lr, 9997f
      	ldr	pc, [r1]
      #else
      	arch_irq_handler_default
      #endif
      9997:
      	.endm
      
    • 也就是调用handle_arch_irq来处理中断

    第一个C函数handle_arch_irq

    这个函数是在哪里被设置的?

    int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
    {
    	if (handle_arch_irq)
    		return -EBUSY;
    
    	handle_arch_irq = handle_irq;
    	return 0;
    }
    

    搜索下可以看到在s3c24xx_init_intc中设置了s3c24xx_handle_irq

    s3c24xx_init_intc
    	s3c24xx_clear_intc(intc);
    	intc->domain = irq_domain_add_legacy(np, irq_num, irq_start,
    					     0, &s3c24xx_irq_ops,
    					     intc);
    	set_handle_irq(s3c24xx_handle_irq);
    

    具体的s3c24xx_handle_irq会调用s3c24xx_handle_intc

    /*
     * Array holding pointers to the global controller structs
     * [0] ... main_intc
     * [1] ... sub_intc
     * [2] ... main_intc2 on s3c2416
     */
    static struct s3c_irq_intc *s3c_intc[3];
    
    
    这里有子中断的概念在硬件上,初始化如下
    s3c2410_init_irq
    	s3c_intc[0] = s3c24xx_init_intc(NULL, &init_s3c2410base[0], NULL,0x4a000000);
    	//  s3c24xx_init_intc 的第三个参数是 parent 也就是表示 s3c_intc[1] 是 s3c_intc[0] 的一个子控制器
    	s3c_intc[1] = s3c24xx_init_intc(NULL, &init_s3c2410subint[0],s3c_intc[0], 0x4a000018);
    	// s3c24xx_init_intc 里面会设置如下结构体
    	struct s3c_irq_intc {
    		void __iomem		*reg_pending;
    		void __iomem		*reg_intpnd;
    		void __iomem		*reg_mask;
    		struct irq_domain	*domain;
    		struct s3c_irq_intc	*parent;
    		struct s3c_irq_data	*irqs;
    	};
    
    	
    s3c24xx_handle_intc
    	// 1. 读取 reg_intpnd 这个应该就是顶级中断控制器的 pend 标志
    	readl_relaxed
    	// 2. 计算出硬件中断号
    	irq_domain_get_of_node
    	offset = readl_relaxed(intc->reg_intpnd + 4);
    	offset =  __ffs(pnd);
    	
    	handle_domain_irq(domain,hwirq,regs)
    		__handle_domain_irq
    			// 找到虚拟中断号
    			irq = irq_find_mapping(domain, hwirq);
    			// 根据虚拟中断号,找到描述符所在,执行 irq_flow_handler_t	handle_irq
    			generic_handle_irq
    				// 这里就是找到中断描述符的结构
    				desc = irq_to_desc(irq)
    				// 执行 irq_flow_handler_t	handle_irq
    				generic_handle_irq_desc((desc))
    

    流程小结

    handle_arch_irq:
    a. 读 int controller, 得到hwirq
    b. 根据hwirq得到virq
    c. 调用 irq_desc[virq].handle_irq
    
    如果该中断没有子中断, irq_desc[virq].handle_irq的操作:
    a. 取出irq_desc[virq].action链表中的每一个handler, 执行它
    b. 使用irq_desc[virq].irq_data.chip的函数清中断
    
    如果该中断是由子中断产生, irq_desc[virq].handle_irq的操作:
    a. 读 sub int controller, 得到hwirq'
    b. 根据hwirq'得到virq
    c. 调用 irq_desc[virq].handle_irq
    

    下面老师的这个图很形象具体了

    mark

    中断号的演变

    虚拟中断号与实际的硬件中断源一一对应,以前我们是定死的提前写好的,需要保证所有虚拟中断号不重合,2440定义在这里archarmmach-s3c24xxincludemachirqs.h

    /* main cpu interrupts */
    #define IRQ_EINT0      S3C2410_IRQ(0)	    /* 16 */
    #define IRQ_EINT1      S3C2410_IRQ(1)
    #define IRQ_EINT2      S3C2410_IRQ(2)
    #define IRQ_EINT3      S3C2410_IRQ(3)
    #define IRQ_EINT4t7    S3C2410_IRQ(4)	    /* 20 */
    .....
    // 子中断号
    #define S3C2410_IRQSUB(x)	S3C2410_IRQ((x)+58)
    #define IRQ_S3CUART_RX0		S3C2410_IRQSUB(0)	/* 74 */
    #define IRQ_S3CUART_TX0		S3C2410_IRQSUB(1)
    #define IRQ_S3CUART_ERR0	S3C2410_IRQSUB(2)
    

    那么我们是怎么通过硬件中断号,反算出虚拟中断号?

    • 对于不同中断控制器里面的硬件中断号,它们的转化公式是不同的
    • 根中断控制器和子中断控制器的公式是不一样的,称之为域rq_domain

    缺陷

    • 当中断控制器数量变多时,有成百上千,这种虚拟中断号和硬件中断号一一对应的方式就很麻烦
    • 所以之后虚拟中断号后来只是表示序号,可以自由分配

    查找空余项

    • 内核中使用一个位图数组allocated_irqs标记是否为空闲,1表示占用了
    • 从中断号开始依次查找,直到找到最后空闲项.

    IRQ domain

    Linux使用IRQ domain来描述一个中断控制器(IRQ Controller)所管理的中断源.

    // includelinuxirqdomain.h
    struct irq_domain {
    	linear_revmap[]   //linear_revmap[2] 存储着这个控制器硬件中断2号对应的虚拟中断号
    

    例子

    使用子中断EINT4的过程

    mark

    1. 初始化为注册,首先注册主中断控制器,这里硬件中断号为4,我们去查找第4项空,存进去linear_revmap[4]=4
    2. 为子中断控制器注册虚拟中断号,这里硬件中断号是4,先查找4,发现被占用,则使用5.存到sub/linear_revmap[4]=5
    3. 驱动程序request_irq(5.my_handler),会把my_handler保存在irq_desc[5].action链表中
    4. 中断发生时,cpu先读取顶级的中断控制器,在顶级中断控制器的irq_domain找到linear_revmap[4]找到虚拟中断号是4,去执行中断描述符数组的第4项,这里会调用中断分发函数s3c_irq_demux
    5. s3c_irq_demux又去读取下一级别的sub irq_domain,找到了linear_revmap[4]=5.然后去执行中断描述符数组的第5项
    6. 所以也就是说,在irq_domain.linear_revmap[]大部分数组项都是空闲的

    兼容老的固定中断号

    1. 很明显的要先设置好linear_revmap,也就是一般就是硬件中断号鱼虚拟中断号

    2. 以前写驱动程序直接 request_irq(virq,....),中断号是通过宏方式进行定义的,所以直接使用中断号进行注册

    3. 现在需要先在设备树表明使用哪个中断,内核会把这个中断号和某一个虚拟中断号挂钩,这些信息会转换成(intc,hwirq) ==> virq 这时才可以 request_irq

      .liner_revmap[4] = 5
      .xlate (解析设备树,得到hwirq,irq_type)
      .map(hwirq,virq) (map就是建立联系的作用,若是子中断,去设置父中断)
      

    设备树描述中断

    具体的中断

    • interrupt-parent属于哪个中断控制器
    • interrupts 中断号和触发方式,这个里面的成员具体怎么表述需要去看具体中断控制器的描述

    中断控制器的描述

    • interrupt-controller属性名表示是中断控制器
    • #interrupt-cells,表明下一级的设备要用多少个32位的数据来描述这个中断
    	interrupt-controller@4a000000 {
    		compatible = "samsung,s3c2410-irq";
    		reg = <0x4a000000 0x100>;
    		interrupt-controller;    //---表示是中断控制器
    		#interrupt-cells = <0x4>;
    		phandle = <0x1>;
    	};
    

    2440的表述

    	interrupt-controller@4a000000 {
    		compatible = "samsung,s3c2410-irq";
    		reg = <0x4a000000 0x100>;
    		interrupt-controller;
    		#interrupt-cells = <0x4>;
    		phandle = <0x1>;
    	};
    	
        gpf {
            gpio-controller;
            #gpio-cells = <0x2>;
            interrupt-controller;
            #interrupt-cells = <0x2>;
            phandle = <0x6>;
        };
    
        gpg {
            gpio-controller;
            #gpio-cells = <0x2>;
            interrupt-controller;
            #interrupt-cells = <0x2>;
        };
        
        
    	srom-cs4@20000000 {
    		compatible = "simple-bus";
    		#address-cells = <1>;
    		#size-cells = <1>;
    		reg = <0x20000000 0x8000000>;
    		ranges;
    
    
    		ethernet@20000000 {
    			compatible = "davicom,dm9000";
    			reg = <0x20000000 0x2 0x20000004 0x2>;
    			interrupt-parent = <&gpf>; /*使用gpf中断控制器*/
    			// interrupts 这个数据的解析是由 gpf中断控制器 的cell 表达的
    			interrupts = <7 IRQ_TYPE_EDGE_RISING>;/*使用gpf控制器中的第七号中断,IRQ_TYPE_EDGE_RISING为中断触发方式*/
    			local-mac-address = [00 00 de ad be ef];
    			davicom,no-eeprom;
    		};
    	};
    

    在这里并没有用单独的软件结构去描述硬件中的子中断控制器,而是使用interrupt-controller@4a000000中的一个32位(#interrupt-cells = <0x4>; 总共有4个32位)来表示是否为子中断控制器,看下章节

    2440中使用设备树

    mark

    具体的使用看代码就明白了

    1. 新增一个匹配条件of_match_table

      struct platform_driver buttons_drv = {
          .probe		= buttons_probe,
          .remove		= buttons_remove,
          .driver		= {
              .name	= "mybuttons",
              .of_match_table = of_match_buttons, /* 能支持哪些来自于dts的platform_device */
          }
      };
      
      static const struct of_device_id of_match_buttons[] = {
          { .compatible = "jz2440_button", .data = NULL },
          { /* sentinel */ }
      };
      
    2. probe中获取资源方式,这里获取硬件中断号,以及pin的信息,这里已经转换了虚拟中断

      static int buttons_probe(struct platform_device *pdev)
      {
      	struct device *dev = &pdev->dev;		
      	struct device_node *dp_node = dev->of_node;
      	struct resource		*res;
      	int i;
      
      	for (i = 0; i < sizeof(pins_desc)/sizeof(pins_desc[0]); i++)
      	{
      		/* 根据platform_device的资源进行获得中断号,触发类型 */
      		res = platform_get_resource(pdev, IORESOURCE_IRQ, i);
      		if (res) {
      			pins_desc[i].irq  = res->start;
      			printk("get irq %d
      ", pins_desc[i].irq);
      		}
      		else {
      			printk("can not get irq res for eint0
      ");
      			return -1;
      		}
      
      		pins_desc[i].pin = of_get_named_gpio(dp_node, "eint-pins", i);
      		printk("pins_desc[%d].pin = %d
      ", i, pins_desc[i].pin);
      	}
      	return sixth_drv_init();
      }
      
    3. 打开设备时申请中断,这里的虚拟中断在资源获取时已经转换了

      static int sixth_drv_open(struct inode *inode, struct file *file)
      {
      ret = request_irq(pins_desc[0].irq,  buttons_irq, 0, "S2", &pins_desc[0]);
      ....
      }
      

    内核处理中断

    中断结构

    硬件结构上看, 处理过程分上下两个层面: 中断控制器, 使用中断的设备;

    软件结构上看, 处理过程分左右两个部分: 在设备树中描述信息, 在驱动中处理设备树;

    (1) 中断控制器

    这又分为root irq controller, gpf/gpg irq controller

    a. root irq controller

    a.1 在设备树中的描述

    a.2 在内核中的驱动

    b. 对于S3C2440, 还有: gpf/gpg irq controller

    b.1 在设备树中的描述(在pinctrl节点里)

    b.2 在内核中的驱动 (在pinctrl驱动中)

    (2) 设备的中断

    a.1 在设备节点中描述(表明使用"哪一个中断控制器里的哪一个中断, 及中断触发方式")

    a.2 在内核中的驱动 (在platform_driver.probe中获得IRQ资源, 即中断号)

    irq_domain是核心:

    a. 每一个中断控制器都有一个irq_domain

    b. 对设备中断信息的解析,

    b.1 需要调用 irq_domain->ops->xlate (即从设备树中获得hwirq, type)

    b.2 获取未使用的virq, 保存: irq_domain->linear_revmap[hwirq] = virq;

    b.3 在hwirq和virq之间建立联系

    中断相关代码调用关系

    s3c2440设备树中断相关代码调用关系:

    (1) 上述处理过程如何触发?

    a. 内核启动时初始化中断的入口:

    start_kernel // init/main.c
        init_IRQ();
            if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
                irqchip_init();   // 一般使用它
            else
                machine_desc->init_irq();
    

    b. 设备树中的中断控制器的处理入口:

    irqchip_init // drivers/irqchip/irqchip.c
        of_irq_init(__irqchip_of_table);  // 对设备树文件中每一个中断控制器节点, 调用对应的处理函数
            为每一个符合的"interrupt-controller"节点,
            分配一个of_intc_desc结构体, desc->irq_init_cb = match->data; // = IRQCHIP_DECLARE中传入的函数
            并调用处理函数
            
            (先调用root irq controller对应的函数, 再调用子控制器的函数, 再调用更下一级控制器的函数...)
    

    (2) root irq controller的驱动调用过程

    a. 为root irq controller定义处理函数:

    IRQCHIP_DECLARE(s3c2410_irq, "samsung,s3c2410-irq", s3c2410_init_intc_of);  //drivers/irqchip/irq-s3c24xx.c
    

    其中:

    #define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
    #define OF_DECLARE_2(table, name, compat, fn) 
            _OF_DECLARE(table, name, compat, fn, of_init_fn_2)
    #define _OF_DECLARE(table, name, compat, fn, fn_type)           
        static const struct of_device_id __of_table_##name      
            __used __section(__##table##_of_table)          
             = { .compatible = compat,              
                 .data = (fn == (fn_type)NULL) ? fn : fn  }
    

    展开为:

        static const struct of_device_id __of_table_s3c2410_irq     
            __used __section("__irqchip_of_table")          
             = { .compatible = "samsung,s3c2410-irq",               
                 .data = s3c2410_init_intc_of  }
    

    它定义了一个of_device_id结构体, 段属性为__irqchip_of_table, 在编译内核时这些段被放在__irqchip_of_table地址处。

    __irqchip_of_table起始地址处,放置了一个或多个 of_device_id, 它含有compatible成员;

    设备树中的设备节点含有compatible属性,

    如果双方的compatible相同, 并且设备节点含有interrupt-controller属性,则调用of_device_id中的函数来处理该设备节点。

    所以IRQCHIP_DECLARE是用来声明设备树中的中断控制器的处理函数

    b. root irq controller处理函数的执行过程:

    s3c2410_init_intc_of  // drivers/irqchip/irq-s3c24xx.c
        // 初始化中断控制器: intc, subintc
        s3c_init_intc_of(np, interrupt_parent, s3c2410_ctrl, ARRAY_SIZE(s3c2410_ctrl));
                    
            // 为中断控制器创建irq_domain
            domain = irq_domain_add_linear(np, num_ctrl * 32,
                                     &s3c24xx_irq_ops_of, NULL);
    
            intc->domain = domain;
    
            // 设置handle_arch_irq, 即中断处理的C语言总入口函数
            set_handle_irq(s3c24xx_handle_irq);
    

    (3) pinctrl系统中gpf/gpg irq controller的驱动调用过程

    a. pinctrl系统的驱动程序:

    a.1 源代码: drivers/pinctrl/samsung/pinctrl-samsung.c

    static struct platform_driver samsung_pinctrl_driver = {
        .probe      = samsung_pinctrl_probe,
        .driver = {
            .name   = "samsung-pinctrl",
            .of_match_table = samsung_pinctrl_dt_match, // 含有 { .compatible = "samsung,s3c2440-pinctrl", .data = &s3c2440_of_data },
            .suppress_bind_attrs = true,
            .pm = &samsung_pinctrl_pm_ops,
        },
    };
    

    a.2 设备树中:

    pinctrl@56000000 {
        reg = <0x56000000 0x1000>;
        compatible = "samsung,s3c2440-pinctrl";  // 据此找到驱动
    

    a.3 驱动中的操作:

    samsung_pinctrl_probe  // drivers/pinctrl/samsung/pinctrl-samsung.c
        最终会调用到 s3c24xx_eint_init // drivers/pinctrl/samsung/pinctrl-s3c24xx.c
        
            // eint0,1,2,3的处理函数在处理root irq controller时已经设置; 
            // 设置eint4_7, eint8_23的处理函数(它们是分发函数)
            for (i = 0; i < NUM_EINT_IRQ; ++i) {
                unsigned int irq;
    
                if (handlers[i]) /* add by weidongshan@qq.com, 不再设置eint0,1,2,3的处理函数 */
                {
                    irq = irq_of_parse_and_map(eint_np, i);
                    if (!irq) {
                        dev_err(dev, "failed to get wakeup EINT IRQ %d
    ", i);
                        return -ENXIO;
                    }
    
                    eint_data->parents[i] = irq;
                    irq_set_chained_handler_and_data(irq, handlers[i], eint_data);
                }
            }
    
            // 为GPF、GPG设置irq_domain
            for (i = 0; i < d->nr_banks; ++i, ++bank) {
            
                ops = (bank->eint_offset == 0) ? &s3c24xx_gpf_irq_ops
                                   : &s3c24xx_gpg_irq_ops;
    
                bank->irq_domain = irq_domain_add_linear(bank->of_node, bank->nr_pins, ops, ddata);
            }
    

    (4) 使用中断的驱动调用过程:

    a. 在设备节点中描述(表明使用"哪一个中断控制器里的哪一个中断, 及中断触发方式"),比如:

        buttons {
            compatible = "jz2440_button";
            eint-pins  = <&gpf 0 0>, <&gpf 2 0>, <&gpg 3 0>, <&gpg 11 0>;
            interrupts-extended = <&intc 0 0 0 3>,
                                  <&intc 0 0 2 3>,
                                  <&gpg 3 3>,
                                  <&gpg 11 3>;
        };
    

    b. 设备节点会被转换为 platform_device, "中断的硬件信息" 会转换为"中断号", 保存在platform_device的"中断资源"里

      第3课第05节_device_node转换为platform_device, 讲解了设备树中设备节点转换为 platform_device 的过程;
    

    我们只关心里面对中断信息的处理:

    of_device_alloc (drivers/of/platform.c)
        dev = platform_device_alloc("", PLATFORM_DEVID_NONE);  // 分配 platform_device
        
        num_irq = of_irq_count(np);  // 计算中断数
        
        of_irq_to_resource_table(np, res, num_irq) // drivers/of/irq.c, 根据设备节点中的中断信息, 构造中断资源
            of_irq_to_resource
                int irq = of_irq_get(dev, index);  // 获得virq, 中断号
                                rc = of_irq_parse_one(dev, index, &oirq); // drivers/of/irq.c, 解析设备树中的中断信息, 保存在of_phandle_args结构体中
                                
                                domain = irq_find_host(oirq.np);   // 查找irq_domain, 每一个中断控制器都对应一个irq_domain
                                
                                irq_create_of_mapping(&oirq);             // kernel/irq/irqdomain.c, 创建virq和中断信息的映射
                                    irq_create_fwspec_mapping(&fwspec);
                                        irq_create_fwspec_mapping(&fwspec);
                                            irq_domain_translate(domain, fwspec, &hwirq, &type) // 调用irq_domain->ops->xlate, 把设备节点里的中断信息解析为hwirq, type
                                            
                                            virq = irq_find_mapping(domain, hwirq); // 看看这个hwirq是否已经映射, 如果virq非0就直接返回
                                            
                                            virq = irq_create_mapping(domain, hwirq); // 否则创建映射
                                                        virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node), NULL);  // 返回未占用的virq
                                                        
                                                        irq_domain_associate(domain, virq, hwirq) // 调用irq_domain->ops->map(domain, virq, hwirq), 做必要的硬件设置
    
  • 相关阅读:
    absolute 导致点击事件无效
    windows 下的命令操作
    localStorage 设置本地缓存
    css渐变
    vue-cli webpack全局引入jquery
    #024分段函数。
    #023单词接龙1(字符串)(女友)
    #022 Python 实验课
    #021 Java复习第一天
    #020PAT 没整明白的题L1-009 N个数求和 (20 分)
  • 原文地址:https://www.cnblogs.com/zongzi10010/p/10793087.html
Copyright © 2011-2022 走看看