linux中断编程、中断基础介绍 - Go语言中文社区

linux中断编程、中断基础介绍


中断基础介绍

中断就是CPU正常运行期间,由于内、外部事件引起的CPU暂时停止正在运行的程序,去执行该内部事件或外部事件的引起的服务中去,服务执行完毕后再返回断点处继续执行的情形。

中断的意义

极大提高CPU运行效率

中断服务程序

中断处理程序:在中断发生时被调用的函数称为中断服务函数。
中断服务函数的原则:linux是多进程操作系统

中断不属于任何一个进程,因此不能在中断程序中休眠和调用schedule函数放弃CPU
实现终端处理函数有一个原则,就是尽可能的处理并返回

linux中断顶部和中断底部

裸机中并没有此概念,他们是什么?

linux中断顶部、底部概念

为保证系统实时性,中断服务程序必须足够简短,但实际应用中某些时候发生中断时必须处理大量的事物,这时候如果都在中断服务程序中完成,则会严重降低中断的实时性,基于这个原因,linux系统提出了一个概念:把中断服务程序分为两部分-顶半部-底半部
顶半部
完成尽可能少的比较急的功能,它往往只是简单的读取寄存器的中断状态,并清除中断标志后就进行“中断标记”(也就是把底半部处理程序挂到设备的底半部执行队列中)的工作。
特点:响应速度快
底半部:
中断处理的大部分工作都在底半部,它几乎做了中断处理程序的所有事情。
特点:处理相对来说不是非常紧急的事件
底半部机制主要有:tasklet、工作队列和软中断

裸机中断设计形式

第一种写法:把发生中断所需要做的所有的事情全部写到中断服务函数体中

void isr()
{
  //中断程序
}

第二种写法:中断只记录,标志,要做的事情在主循环中编写。

int flag=0;
void main(void)
{
  while(1)
  {
    if(flag)
    {
     flag=0;
      //中断需要做的事情
    }
  }
}

void isr()
{
//只做登记
  flag=1;
}

第二种写法,不会影响到其他中断响应,保证系统的实时性。
补充:是否所有的中断都需要分为两部分来实现呢?
不一定!如果发生中断要做的事情很少不会影响系统的实时性,则就不必分成两部分实现。


linux中断系统简要框架图

中断简要框图

linux中断API

linux中断有专门的中断子系统,其实现原理很复杂,但是驱动开发者不需要知道其实现的具体细节,只需要知道如何应用该子系统提供的API函数来编写中断相关驱动代码即可。
内核使用一个struct irqaction结构描述一个中断,编写中断终极目标就是实现这个结构,当然结构不是我们自己去定义,但是结构中的材料是我们提供的。

struct irq_desc {
    struct irq_data     irq_data;
    ·········//省略
    irq_flow_handler_t  handle_irq;
    struct irqaction    *action;    /* IRQ action list */
    }

最关注的几个成员
interrupt.h linux-3.5includelinux

struct irq_data {
    unsigned int        irq;
    unsigned long       hwirq;
    unsigned int        node;
    unsigned int        state_use_accessors;
    struct irq_chip     *chip;
    struct irq_domain   *domain;
    void            *handler_data;
    void            *chip_data;
    struct msi_desc     *msi_desc;
#ifdef CONFIG_SMP
    cpumask_var_t       affinity;
#endif
};

interrupt.h linux-3.5includelinux

/**
 * struct irqaction - per interrupt action descriptor
 * @handler:    interrupt handler function
 * @name:   name of the device
 * @dev_id: cookie to identify the device
 * @percpu_dev_id:  cookie to identify the device
 * @next:   pointer to the next irqaction for shared interrupts
 * @irq:    interrupt number
 * @flags:  flags (see IRQF_* above)
 * @thread_fn:  interrupt handler function for threaded interrupts
 * @thread: thread pointer for threaded interrupts
 * @thread_flags:   flags related to @thread
 * @thread_mask:    bitmask for keeping track of @thread activity
 * @dir:    pointer to the proc/irq/NN/name entry
 */
struct irqaction {
    irq_handler_t       handler; /*我们要提供的*/
    void            *dev_id;     /*我们要提供的*/
    void __percpu       *percpu_dev_id;
    struct irqaction    *next;
    irq_handler_t       thread_fn;
    struct task_struct  *thread;
    unsigned int        irq;     /*我们要提供的*/
    unsigned int        flags;   /*我们要提供的*/
    unsigned long       thread_flags;
    unsigned long       thread_mask;
    const char      *name;      /*我们要提供的*/
    struct proc_dir_entry   *dir;
} ____cacheline_internodealigned_in_smp;

//有五个成员您需要我们提供

request_irq()

原型
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev)
可以跟struct irqaction 结构体中的成员对应。
功能:向内核注册一个中断服务函数,发生中断号为irq的中断的时候,会执行handle指针函数。
参数:
irq:中断编号(每个中断源有唯一编号),这里的中断编号不是看硬件手册,与裸机不同。由内核分配。
handler:中断服务函数指针,原型typedef irqreturn_t (*irq_handler_t)(int, void *);
flag:中断属性,如快速中断,共享中断,如果是外部中断还有:上升沿,下降沿触发这类标志。
name:中断名字,注册后会在/proc/irq/``irq号name文件夹出现。
dev_id:这个参数时传递给中断服务函数。对共享中断来说,这个参数一定要有,当注销共享中断其中一个时,用这个指定标识注销哪一个。对于有唯一入口的中断,可以传递NULL,但一般来说都会传递一个有意义的指针,在中断程序中使用,以方便编程。
返回值:0表示成功,返回-EINVAL表示中断号无效 ,返回-EBUSY表示中断被占用。
头文件:include/linux/interrupt.h

linux共享中断

共享中断是指多个中断共享一个中断线的情况,在中断到来时,会遍历共次中断的所有中断处理函数,直到某一个中断服务函数时返回IRQ_HANDLED
在中断处理程序顶半部中,应根据中断相关硬件寄存器中的信息,判断是否为本设备的中断,若不是立即返回IRQ_NONE

request_irq函数参数补充说明

中断编号

在linux系统中总段编号可能和裸机的中断编号数值不相同。如何确定一个中断源的中断编号?
一般是由芯片厂商写好所有可中断号宏定义。在内核源码中可以找到,如果是外部中断,内核还提供了通用API函数,可以通过IO管脚编号转换成它对应的中断编号
中断服务函数类型是typedef irqreturn_t (*irq_handler_t)(int, void *);是一个指向有一个整形参数,一个void类型指针,返回值是irqreturn_t的函数指针。这个数据结构规定了中断服务函数的原型。返回值类型在内核中定义为:

/**
 * enum irqreturn
 * @IRQ_NONE        interrupt was not from this device
 * @IRQ_HANDLED     interrupt was handled by this device
 * @IRQ_WAKE_THREAD handler requests to wake the handler thread
 */
enum irqreturn {
    IRQ_NONE        = (0 << 0),
    IRQ_HANDLED     = (1 << 0),
    IRQ_WAKE_THREAD     = (1 << 1),
};

第一个参数int类型的参数是中断号,第二个void*类型指针是共享中断的标识id,
这两个参数在中断服务被调用的时候传递进来的内容实际上就是reauest_irq()在注册s时候的irq,dev参数。这一点在源码中有体现,编程的时候要注意,善于利用这个特性编写程序。

中断属性flag

include/linux/interrupt.h中定义可用的数值,以IRQF开头的宏,若设置了IRQF_DISABLED,则表示中断类型属于独占类型的中断,不是共享中断,若设置了 IRQF_SHARED,则表示镀铬设备共享中断,若设置了 IRQF_SAMPLE_RANDOM,表示对系统获取随机数有好处,(这几个宏可以通过或的方式组合使用),若是外部中断还要设置IRQF_TRIGGER_XXX标志,表示选择外部中断的触发电平,内核中相关定义如下:

/*
 * These correspond to the IORESOURCE_IRQ_* defines in
 * linux/ioport.h to select the interrupt line behaviour.  When
 * requesting an interrupt without specifying a IRQF_TRIGGER, the
 * setting should be assumed to be "as already configured", which
 * may be as per machine or firmware initialisation.
 */
#define IRQF_TRIGGER_NONE   0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING    0x00000002
#define IRQF_TRIGGER_HIGH   0x00000004
#define IRQF_TRIGGER_LOW    0x00000008
#define IRQF_TRIGGER_MASK   (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | 
                 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE  0x00000010

/*
 * These flags used only by the kernel as part of the
 * irq handling routines.
 *
 * IRQF_DISABLED - keep irqs disabled when calling the action handler.
 *                 DEPRECATED. This flag is a NOOP and scheduled to be removed
 * IRQF_SAMPLE_RANDOM - irq is used to feed the random generator
 * IRQF_SHARED - allow sharing the irq among several devices
 * IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
 * IRQF_TIMER - Flag to mark this interrupt as timer interrupt
 * IRQF_PERCPU - Interrupt is per cpu
 * IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
 * IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
 *                registered first in an shared interrupt is considered for
 *                performance reasons)
 * IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
 *                Used by threaded interrupts which need to keep the
 *                irq line disabled until the threaded handler has been run.
 * IRQF_NO_SUSPEND - Do not disable this IRQ during suspend
 * IRQF_FORCE_RESUME - Force enable it on resume even if IRQF_NO_SUSPEND is set
 * IRQF_NO_THREAD - Interrupt cannot be threaded
 * IRQF_EARLY_RESUME - Resume IRQ early during syscore instead of at device
 *                resume time.
 */
#define IRQF_DISABLED       0x00000020
#define IRQF_SAMPLE_RANDOM  0x00000040
#define IRQF_SHARED     0x00000080
#define IRQF_PROBE_SHARED   0x00000100
#define __IRQF_TIMER        0x00000200
#define IRQF_PERCPU     0x00000400
#define IRQF_NOBALANCING    0x00000800
#define IRQF_IRQPOLL        0x00001000
#define IRQF_ONESHOT        0x00002000
#define IRQF_NO_SUSPEND     0x00004000
#define IRQF_FORCE_RESUME   0x00008000
#define IRQF_NO_THREAD      0x00010000
#define IRQF_EARLY_RESUME   0x00020000

#define IRQF_TIMER      (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)

常用值:IRQF_DISABLED,IRQF_SHARED 这两个可以用来设置任何中断,但是不能同时使用
IRQF_TRIGGER_RISING IRQF_TRIGGER_FALLING 只对外部中断有用,可以和上面的IRQF_DISABLED IRQF_SHARED之一组合使用
IRQF_SHARED:添加上这个标志之后,此中断号还可以注册
IRQF_DISABLED:添加上这个标志之后,此中断号只能执行一次

中断名name

中断名字,在cat /proc/interrupts中可以看到此名称,同时会出现/proc/irq/
irq号/name
文件夹出现

中断设备id识别标志dev

这个参数会在发生中断,执行服务函数时,作为实参传递给中断服务函数的第二个参数。传递什么内容,完全由开发者决定,只要有利于更好的编写中断服务函数的都可以传递。
在飞共享中断时候可以是NULL,也可以传递任何用户自定的结构地址,在共享中断时必须传递有效参数,用于发生中断时,用于在注销中断时识别具体要注销中断服务函数中的具体哪一个子项共享中断。
很多书,很多人认为dev这个参数能用于识别在发生中断时候,是否是本设备产生的中断,但是实际并不是这样,要看开发者给这个参数传递什么内容,如果传递的是一个和中断状态有关的硬件寄存器地址,中断服务程序中可以读取这个寄存器的内容,从而判断是否是本设备产生的中断,如果只是传递一个普通的变量地址,并不能通过这个地址区分到底是那个硬件设备产生的中断

handled

irqreturn_t (*irq_handler_t)(int, void *);
返回值:有下面三种情况

/**
 * enum irqreturn
 * @IRQ_NONE        interrupt was not from this device
 * @IRQ_HANDLED     interrupt was handled by this device
 * @IRQ_WAKE_THREAD handler requests to wake the handler thread
 */
enum irqreturn {
    IRQ_NONE        = (0 << 0),
    IRQ_HANDLED     = (1 << 0),
    IRQ_WAKE_THREAD     = (1 << 1),
};

参数:
第一个参数:中断函数注册时的中断号irq
第二个参数:注册的时候最后一个参数dev_id
中断服务函数原型:
irqreturn_t isr_function(int irq,void* dev_id)

中断服务函数模板:

irqreturn_t isr_function(int irq,void* dev_id)
{


return IRQ_HANDLED;
}

对于共享中断:

irqreturn_t isr_function(int irq,void* dev_id)
{
//根据dev_id判断是否是本设备产生的中断
if(readreg(dev_id)!=本设备产生中断)
  return IRQ_NONE;
//以下是本设备产生中断的代码
return IRQ_HANDLED;
}

free_irq()

描述 说明
函数原型 void free_irq(int irq,void *dev_id)
函数功能 从内核中断服务函数链表中删除一个中断结构
函数参数 同上
函数返回值
函数头文件 include/linux/interrupt.h
函数定义文件 kernelirqmanage.c (用EXPORT_SYMBOL(free_irq)导出给内核模块使用)

这个函数跟request_irq函数功能相反,当设备不使用中断时,使用这个函数把相关中断在中断链表中的节点释放掉释放掉。

disable_irq,disable_irq_nosync

描述 说明
函数原型 void disabled_irq(unsigned int irq)
函数功能 关闭指定的中断,如果中断没有执行完,等待执行完在关闭,不能再中断中使用,否则自己关闭自己,引起内核崩溃
函数原型 void disabled_irq_nosync(unsigned int irq)
函数功能 关闭中断,不等待中断执行完毕,可以在中断函数中执行

enable_irq

描述 说明
函数原型 void enabled_irq(unsigned int irq)
函数功能 使能指定中断

local_save_flags(flags)

描述 说明
宏原型 local_save_flags(flags)
宏功能 禁止本CPU全部中断,并保存CPU状态信息,(现在很多芯片是多核CPU)这个函数需要和local_irq_restore函数配合使用
宏参数 flags:unsigned long类型用于保存当前CPU状态信息
宏返回值

使用示例:
unsigned long flags;
local_save_flags(flags);
······
local_irq_restore(flags);

local_irq_restore(flags)

描述 说明
宏原型 local_irq_restore(flags)
宏功能 与local_save_flags(flags)功能函数相反,配对使用,用来使能由与local_save_flags禁止的中断
宏参数 flags:unsigned long类型用于恢复当前CPU状态信息
宏返回值

local_irq_disable()

禁止本CPU中断,不能保存当前CPU信息

local_irq_enable()

使能由local_irq_disable禁止的中断,不能还原CPU信息

linux3.5内核关于CPU中断号

linux中断注册函数中的irq中断号并不是芯片物理上的编号,而是芯片厂商在移植linux系统时定在相关架构的头文件中定义好的,在内核源码中,名字一般是irqs.h
irqs.h linux-3.5archarmmach-exynosincludemach
下面是其中关于外部中断的一部分内容,还有其他内容这里没有列举

/* For EXYNOS4 SoCs */

#define EXYNOS4_IRQ_EINT0       IRQ_SPI(16)
#define EXYNOS4_IRQ_EINT1       IRQ_SPI(17)
#define EXYNOS4_IRQ_EINT2       IRQ_SPI(18)
#define EXYNOS4_IRQ_EINT3       IRQ_SPI(19)
#define EXYNOS4_IRQ_EINT4       IRQ_SPI(20)
#define EXYNOS4_IRQ_EINT5       IRQ_SPI(21)
#define EXYNOS4_IRQ_EINT6       IRQ_SPI(22)
#define EXYNOS4_IRQ_EINT7       IRQ_SPI(23)
#define EXYNOS4_IRQ_EINT8       IRQ_SPI(24)
#define EXYNOS4_IRQ_EINT9       IRQ_SPI(25)
#define EXYNOS4_IRQ_EINT10      IRQ_SPI(26)
#define EXYNOS4_IRQ_EINT11      IRQ_SPI(27)
#define EXYNOS4_IRQ_EINT12      IRQ_SPI(28)
#define EXYNOS4_IRQ_EINT13      IRQ_SPI(29)
#define EXYNOS4_IRQ_EINT14      IRQ_SPI(30)
#define EXYNOS4_IRQ_EINT15      IRQ_SPI(31)
······

可以看出这个文件是存放在和芯片体系架构相关的文件夹中
对于外部中断,可以通过IO口编号转换成对应的中断编号。关于IO口在下小节中说明
static inline int gpio_to_irq(unsigned int gpio)
为什么IO外部中断不直接给出?还要通过一个函数转换对应呢?

linux3.5内核关于CPU的IO编号

IO口编号并不是我们芯片物理上的编号,而是linux内核定义好的宏获得的数值,不同芯片gpio口的数量、定义方式不一样,芯片相关宏存放在gpio.h文件中。此文件和芯片架构相关的文件夹中。
gpio.h linux-3.5archarmmach-exynosincludemach
列举一些内容:

/* EXYNOS4 GPIO number definitions */

#define EXYNOS4_GPA0(_nr)   (EXYNOS4_GPIO_A0_START + (_nr))
#define EXYNOS4_GPA1(_nr)   (EXYNOS4_GPIO_A1_START + (_nr))
#define EXYNOS4_GPB(_nr)    (EXYNOS4_GPIO_B_START + (_nr))
#define EXYNOS4_GPC0(_nr)   (EXYNOS4_GPIO_C0_START + (_nr))
#define EXYNOS4_GPC1(_nr)   (EXYNOS4_GPIO_C1_START + (_nr))
#define EXYNOS4_GPD0(_nr)   (EXYNOS4_GPIO_D0_START + (_nr))
#define EXYNOS4_GPD1(_nr)   (EXYNOS4_GPIO_D1_START + (_nr))

#define EXYNOS4210_GPE0(_nr)    (EXYNOS4210_GPIO_E0_START + (_nr))
#define EXYNOS4210_GPE1(_nr)    (EXYNOS4210_GPIO_E1_START + (_nr))
#define EXYNOS4210_GPE2(_nr)    (EXYNOS4210_GPIO_E2_START + (_nr))
#define EXYNOS4210_GPE3(_nr)    (EXYNOS4210_GPIO_E3_START + (_nr))
#define EXYNOS4210_GPE4(_nr)    (EXYNOS4210_GPIO_E4_START + (_nr))

#define EXYNOS4_GPF0(_nr)   (EXYNOS4_GPIO_F0_START + (_nr))
#define EXYNOS4_GPF1(_nr)   (EXYNOS4_GPIO_F1_START + (_nr))
#define EXYNOS4_GPF2(_nr)   (EXYNOS4_GPIO_F2_START + (_nr))
#define EXYNOS4_GPF3(_nr)   (EXYNOS4_GPIO_F3_START + (_nr))
#define EXYNOS4_GPJ0(_nr)   (EXYNOS4_GPIO_J0_START + (_nr))
#define EXYNOS4_GPJ1(_nr)   (EXYNOS4_GPIO_J1_START + (_nr))
#define EXYNOS4_GPK0(_nr)   (EXYNOS4_GPIO_K0_START + (_nr))
#define EXYNOS4_GPK1(_nr)   (EXYNOS4_GPIO_K1_START + (_nr))
#define EXYNOS4_GPK2(_nr)   (EXYNOS4_GPIO_K2_START + (_nr))
#define EXYNOS4_GPK3(_nr)   (EXYNOS4_GPIO_K3_START + (_nr))
#define EXYNOS4_GPL0(_nr)   (EXYNOS4_GPIO_L0_START + (_nr))
#define EXYNOS4_GPL1(_nr)   (EXYNOS4_GPIO_L1_START + (_nr))
#define EXYNOS4_GPL2(_nr)   (EXYNOS4_GPIO_L2_START + (_nr))

#define EXYNOS4X12_GPM0(_nr)    (EXYNOS4X12_GPIO_M0_START + (_nr))
#define EXYNOS4X12_GPM1(_nr)    (EXYNOS4X12_GPIO_M1_START + (_nr))
#define EXYNOS4X12_GPM2(_nr)    (EXYNOS4X12_GPIO_M2_START + (_nr))
#define EXYNOS4X12_GPM3(_nr)    (EXYNOS4X12_GPIO_M3_START + (_nr))
#define EXYNOS4X12_GPM4(_nr)    (EXYNOS4X12_GPIO_M4_START + (_nr))

#define EXYNOS4_GPX0(_nr)   (EXYNOS4_GPIO_X0_START + (_nr))
#define EXYNOS4_GPX1(_nr)   (EXYNOS4_GPIO_X1_START + (_nr))
#define EXYNOS4_GPX2(_nr)   (EXYNOS4_GPIO_X2_START + (_nr))
#define EXYNOS4_GPX3(_nr)   (EXYNOS4_GPIO_X3_START + (_nr))
#define EXYNOS4_GPY0(_nr)   (EXYNOS4_GPIO_Y0_START + (_nr))
#define EXYNOS4_GPY1(_nr)   (EXYNOS4_GPIO_Y1_START + (_nr))
#define EXYNOS4_GPY2(_nr)   (EXYNOS4_GPIO_Y2_START + (_nr))
#define EXYNOS4_GPY3(_nr)   (EXYNOS4_GPIO_Y3_START + (_nr))
#define EXYNOS4_GPY4(_nr)   (EXYNOS4_GPIO_Y4_START + (_nr))
#define EXYNOS4_GPY5(_nr)   (EXYNOS4_GPIO_Y5_START + (_nr))
#define EXYNOS4_GPY6(_nr)   (EXYNOS4_GPIO_Y6_START + (_nr))
#define EXYNOS4_GPZ(_nr)    (EXYNOS4_GPIO_Z_START + (_nr))

说明:上面的宏是获取指定IO口编号,每一个宏代表一组(注意不是一个)`IO“口,参数表示是这组的低第几个引脚。

io口相关的内核API函数

gpio.h linux-3.5includelinux

//作用:获取指定IO口的电平状态
//参数:GPIO口编号
//返回值:io口电平状态 非0高电平 0低电平
static inline int gpio_get_value(unsigned int gpio)
{
    return __gpio_get_value(gpio);
}
//作用:设置指定IO口的电平状态为value
//参数:目标gpio口编号,value 0或者1
static inline void gpio_set_value(unsigned int gpio, int value)
{
    __gpio_set_value(gpio, value);
}
//不常用
static inline int gpio_cansleep(unsigned int gpio)
{
    return __gpio_cansleep(gpio);
}
//作用:通过GPIO口编号获得出现在这个IO口上的外部中断编号
//参数:gpio口编号
//返回值:这个IO口上对应的外部中断编号
static inline int gpio_to_irq(unsigned int gpio)
{
    return __gpio_to_irq(gpio);
}

static inline int irq_to_gpio(unsigned int irq)
{
    return -EINVAL;
}

其它相关函数
gpio.h linux-3.5includeasm-generic

//作用:向内核申请使用指定GPIO口给label使用
//参数:目标GPIO口的编号,label使用者的名字
//返回值:0 表示成功  负数 表示申请失败
//说明:对于IO使用,保险的方法先使用这个函数申请,在使用,防止和其它驱动冲突。只要使用这个函数申请成功后的IO口,在没有使用gpio_free函数释放前,其他驱动在使用这个IO口都会失败
int gpio_request(unsigned gpio, const char *label);

//作用:释放gpio_request申请的IO口,这样其它驱动就可以再次申请
//参数:目标gpio编号
void gpio_free(unsigned gpio);


//作用:把指定的gpio口设置为输入方向
//参数:目标gpio编号
//返回:0表示成功 负数表示设置失败
int gpio_direction_input(unsigned gpio);

//作用:把指定的gpio口设置为输出方向,并且设置指定电平
//参数:目标gpio编号,电平
//返回:0表示成功 负数表示设置失败
int gpio_direction_output(unsigned gpio, int value);
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/z961968549/article/details/78554733
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-07 15:59:05
  • 阅读 ( 1316 )
  • 分类:Linux

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢