Linux之中断子系统-中断控制器的中断函数gic_handle_irq分析(5)

1.概述

在前面文章中,已经分析汇编函数, 最终调用的是handle_arch_irq或arch_irq_handler_default函数。以zynq7k为例,其采用的是GIC pl390中断控制器,gic初始化的时候调用set_handle_irq函数,将handle_arch_irq函数指针指向了 gic_handle_irq 。因此这里以zynq7为例,介绍Linux内核对uart0的中断处理过程。
 

2.GIC中断入口函数

底层irq_handler调用gic_handle_irq处理中断,从函数名可以看出,此时的中断处理和具体的中断控制器有关,arm平台上中断控制器一般都为gic。传入的参数为struct pt_regs *regs,指向了保存中断发生时刻的寄存器信息地址。首先读取中断响应寄存器ICCIAR,获取硬件中断号及产生SGI中断的CPU ID,根据硬件中断号,判断中断类型。当读取了ICCIAR寄存器后,表示此CPU响应了中断,在此cpu处理完该中断之前,gic中断控制器不会再向此cpu发送任何中断。PPI中断和SPI中断调用handle_domain_irq处理,SGI中断调用handle_IPI处理。当然,SGI中断在SMP系统中才有效,在UP系统中无效,不做处理。 从下面的代码可以看出,整个中断处理逻辑是在一个死循环中,只要能从ICCIAR寄存器中读出有效的硬件中断号,就会一直进行中断处理,也就是说,只要中断一直产生,则中断服务函数gic_handle_irq会一直运行,不会退出。这正是Linux内核设计巧妙的地方,进入中断处理程序后,会处理所有pending或者active and pending状态的中断,直到没有中断产生,中断处理函数才退出,避免频繁进入和退出中断处理程序导致的性能下降。
 



    [drivers/irqchip/irq-gic.c]
    static struct gic_chip_data gic_data[CONFIG_ARM_GIC_MAX_NR] __read_mostly;
    gic_handle_irq
      struct gic_chip_data *gic = &gic_data[0]  // 获取gic设备结构体指针
      ->gic_data_cpu_base  // 获取GIC CPU interface的基地址空间
      do {
          // 读取中断响应寄存器ICCIAR,读取后就清除了gic中的中断,gic中对应的中断状态由pending变为acting
          // ICCIAR的[bit9-0]为硬件中断ID,SPI和PPI中断用到,[bit12-10]产生中断的CPU的ID,SGI中断用到
          irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
          irqnr = irqstat & GICC_IAR_INT_ID_MASK;  // 获取ICCIAR的[bit9-0],即硬件中断号
          if (likely(irqnr > 15 && irqnr < 1020)) {  // PPI和SPI中断走此流程
              // 如果支持priority drop(降低优先级)and deactivation operations(中断状态变化)分离,
              // 则先写ICCEOIR寄存器,中断完成后再写ICCDIR寄存器,表示中断处理完成,zynq7不支持
              if (static_key_true(&supports_deactivate))
                  writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
              handle_domain_irq(gic->domain, irqnr, regs);
                ->__handle_domain_irq
              continue;
          }
          if (irqnr < 16) {  // SGI中断走此流程
              writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
              if (static_key_true(&supports_deactivate))
                  writel_relaxed(irqstat, cpu_base + GIC_CPU_DEACTIVATE);
            #ifdef CONFIG_SMP  // SMP系统中,SGI中断才真正有意义
            /*
             * Ensure any shared data written by the CPU sending
             * the IPI is read after we've read the ACK register
             * on the GIC.
             *
             * Pairs with the write barrier in gic_raise_softirq
             */
              smp_rmb();
              handle_IPI(irqnr, regs);  // SGI中断处理函数,内核中将软件产生的中断叫IPI中断
            #endif
              continue;
          }
          break;  // 直到没有处于pending或者active and pending状态的中断,才跳出循坏,退出中断处理程序
      } while (1)

gic_handle_irq函数调用handle_domain_irq处理PPI和SPI中断,首先根据硬件中断号找到软件中断号,根据软件中断号找到对应的中断描述符irq_desc,调用中断描述符中的通用中断处理函数handle_irq,zynq7k平台的通用中断处理函数handle_irq指向了handle_fasteoi_irq函数。__handle_domain_irq开始调用了irq_enter,用来增加preempt_count成员里HARDIRQ域的值,HARDIRQ域的值大于0,则表示此进程处于硬件中断上下文中,否则表示此进程不处于硬件中断上下文,__handle_domain_irq退出时调用irq_enter,递减preempt_count成员里HARDIRQ域的值,并判断当前上下文是否处于硬件中断和是否有软中断pending,如当前上下文不处于硬件中断并且有软中断pending,则处理软件中断。



    handle_domain_irq
    ->__handle_domain_irq
        ->irq_enter  // 进入中断上下文
          ->__irq_enter
            // 增加当前进程struct thread_info中的preempt_count成员里HARDIRQ域的值
            // HARDIRQ域用于判断当前进程是否处于硬件中断上下文
            ->preempt_count_add(HARDIRQ_OFFSET)
        ->irq_find_mapping  // 根据中断控制的irq_domain和硬件中断号,获取软件中断号
            // 如果硬件中断号小于直接映射的最大中断号,则直接比较domain中的hwirq和硬件中断号是否相等,
            // 相等,则直接返回,zynq7k gic初始化时将revmap_direct_max_irq设为0,不满足条件
            if (hwirq < domain->revmap_direct_max_irq)
            {
                data = irq_domain_get_irq_data(domain, hwirq);
                if (data && data->hwirq == hwirq)
                    return hwirq;
            }
            // zynq7k gic初始化时将revmap_size设为96,满足线性映射的要求,则直接在映射数组中查找软件中断号
            if (hwirq < domain->revmap_size)  // 如果是线性映射
                return domain->linear_revmap[hwirq];  // 返回硬件中断号对应的软件中断号
 
            ->rcu_read_lock();
            // 上面的两个条件都不满足,则是非线性映射,则直接在revmap_tree树中查找软件中断号
            ->data = radix_tree_lookup(&domain->revmap_tree, hwirq);
            ->rcu_read_unlock();
        // 如果软件中断号为0或者大于中断号的数量,返回-EINVAL
        if (unlikely(!irq || irq >= nr_irqs)) 
        {
            ack_bad_irq(irq);
            ret = -EINVAL;
        } 
        else 
        {
            generic_handle_irq(irq);
            ->irq_to_desc  // 通过软件中断号获取中断描述符irq_desc
                // 非线性映射,根据软件中断号在irq_desc_tree树中查找irq_desc
                return radix_tree_lookup(&irq_desc_tree, irq);  
                // 线性映射,根据软件中断号在irq_desc数组中查找irq_desc
                return (irq < NR_IRQS) ? irq_desc + irq : NULL; 
            ->generic_handle_irq_desc
                ->desc->handle_irq(desc)  // 调用通用的中断处理函数handle_fasteoi_irq
        }
        ->irq_exit  // 此时硬件中断处理部分已经完成,中断已打开
          // 递减当前进程struct thread_info中的preempt_count成员里HARDIRQ域的值
          // HARDIRQ域大于0,表示当前进程处于硬件中断上下文中,否则不处于硬件中断上下文中
          ->preempt_count_sub(HARDIRQ_OFFSET)
          if (!in_interrupt() && local_softirq_pending())
              invoke_softirq();  // 处理软中断,软中断不能在硬件中断上下文中处理,软件中断将在第六章介绍
        ->set_irq_regs

从上面的分析可以看出,中断处理函数handle_domain_irq首先从中断控制器ICCIAR寄存器中获取硬件中断号。若硬件中断号到软件中断号采用的是线性映射方式,则以硬件中断号为下表,在数组linear_revmap中的查找软件中断号,然后以软件中断号为下表,在irq_desc数组中查找该中断对应的irq_desc数据结构。若硬件中断号到软件中断号采用的是非线性映射方式,则l利用硬件中断号在树revmap_tree中查找软件中断号,然后利用软件中断号在树irq_desc_tree中查找irq_desc数据结构。
 

2.GIC SPI和PPI中断通用处理函数

handle_irq是中断引脚号映射过程中设置的通用中断处理函数。 在第三章《Linux之中断子系统-内核中断号映射过程(3)》中,uart0在gic_irq_domain_alloc函数中,调用了__irq_do_set_handler函数进行了设置,使handle_irq指向了handle_fasteoi_irq函数。下面分析一下handle_fasteoi_irq的执行流程,其主要工作是遍历irq_desc中的action链表,执行注册中断时提供的中断处理函数handler,如在第四章《Linux之中断子系统-内核中断注册源代码分析(4)》中,zynq7k串口0注册的中断处理函数为cdns_uart_isr,则此处执行的就是cdns_uart_isr函数。执行完所有的中断处理函数,调用gic_eoi_irq通知gic中断控制器中断已处理完毕,gic中断控制器将此中断的中断状态变更为inactive,同时也标志着gic又可以将中断发送到此cpu上。
这里需要注意的是同一个中断在SMP系统上的执行问题。假设有2个cpu组成的SMP系统,中断触发方式为边缘触发(电平触发的中断不会在中断处理完成之前重复触发):
(1)gic向SMP系统发出了中断。
(2)cpu0响应了此中断后开始处理,同时设置IRQD_IRQ_INPROGRESS标志。在cpu0处理完此中断前,gic不会再向cpu0发送任何中断。
(3)紧接着gic又向SMP系统发送了同样的中断。
(4)cpu1响应了此中断(读ICCIAR寄存器)。由于cpu0还为处理完中断,gic不会向cpu0发送此中断,只能由cpu1响应此中断。
(5)cpu1响应此中断(读ICCIAR寄存器),发现此中断描述符已经设置了IRQD_IRQ_INPROGRESS标志,说明此中断正在其他cpu上处理。
(6)cpu1不会处理此中断,直接向中断控制器发送eoi信号(写ICCEOIR寄存器),结束中断处理,退出中断处理程序。
(7)cpu0处理完中断,向中断控制器发送eoi信号(写ICCEOIR寄存器),结束中断处理,退出中断处理程序。
步骤(6)的中断实际上没有被处理,可以认为此中断被丢弃了。
 



    [kernel/irq/chip.c]
    handle_fasteoi_irq
      struct irq_chip *chip = desc->irq_data.chip
      ->raw_spin_lock  // 获取自旋锁
        // 判断当前中断的执行状态,同一个硬件中断,不能同时在两个CPU上执行,
        // 如此中断正在另外一个cpu上执行,则本cpu不处理中断,直接结束此次中断处理
        if (!irq_may_run(desc))
            goto out;
      // 清空标志:IRQS_REPLAY和IRQS_WAITING
      desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING)
      // 如果action链表为空或者此中断被关闭了,则将中断状态设置为IRQS_PENDING
      if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
          desc->istate |= IRQS_PENDING;
          mask_irq(desc);  // 屏蔽中断,最终调用的是gic_mask_irq向ICDICER寄存器中写1
          goto out;
      }
      ->kstat_incr_irqs_this_cpu  // 内核中断统计相关
      // 如果是IRQS_ONESHOT类型的中断,需要屏蔽中断,等中断线程处理完中断再解除屏蔽
      if (desc->istate & IRQS_ONESHOT)
          mask_irq(desc);
      ->handle_irq_event  // 处理硬件中断的入口
        desc->istate &= ~IRQS_PENDING  // 清除中断挂起标志
        ->irqd_set  // 设置IRQD_IRQ_INPROGRESS标志,说明中断正在处理中
        ->raw_spin_unlock  // 解锁
        ->handle_irq_event_percpu
            // 遍历action链表,调用注册中断时提供的中断处理函数handler
            for_each_action_of_desc(desc, action) {
                irqreturn_t res;
                // 调用中断处理函数,对于zynq7k的串口0中断,调用的是cdns_uart_isr函数
                res = action->handler(irq, action->dev_id);
                switch (res) {
                // 如果中断处理函数返回IRQ_WAKE_THREAD,则唤醒中断处理线程,
                case IRQ_WAKE_THREAD: 
                    __irq_wake_thread(desc, action);
                    // 如果中断处理函数返回IRQ_HANDLED,则读取相关标志
                    case IRQ_HANDLED:
                    flags |= action->flags;
                    break;
                default:
                    break;
            }
                retval |= res;
            }
        ->raw_spin_lock  // 加锁
        ->irqd_clear  // 处理完中断,清除IRQD_IRQ_INPROGRESS标志
      ->cond_unmask_eoi_irq
          // 如果不是IRQS_ONESHOT类型的中断,则调用gic提供的回调函数gic_eoi_irq,向ICCEOIR寄存器
          // 写入硬件中断号,表示此中断已处理完毕,此后中断控制器可以继续向该cpu发送中断了
          if (!(desc->istate & IRQS_ONESHOT)) {
              chip->irq_eoi(&desc->irq_data);
              return;
           }
      ->raw_spin_unlock(&desc->lock)  // 解锁
      return;
      out:
          // 向中断控制器发送eoi信号结束此次中断处理
          if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED))
              chip->irq_eoi(&desc->irq_data);  // 具体调用的是gic_eoi_irq函数
          raw_spin_unlock(&desc->lock);  // 解锁
 
    [kernel/irq/chip.c]
    // 返回true,则该中断会被处理;返回false,则该中断不会被处理,handle_fasteoi_irq直接跳转到out处执行
    static bool irq_may_run(struct irq_desc *desc)
    {
        // IRQD_IRQ_INPROGRESS表示该硬件中断正在被处理
        // IRQD_WAKEUP_ARMED表示该中断被用作唤醒系统
        unsigned int mask = IRQD_IRQ_INPROGRESS | IRQD_WAKEUP_ARMED;
 
        // 判断irq_common_data结构体中的state_use_accessors变量是否设置了
        // IRQD_IRQ_INPROGRESS或IRQD_WAKEUP_ARMED标志,没有设置返回true处理中断
        if (!irqd_has_set(&desc->irq_data, mask))
            return true;
        /*
        * If the interrupt is an armed wakeup source, mark it pending
        * and suspended, disable it and notify the pm core about the
        * event.
        * 该中断被用作唤醒源,将中断标记为pending and suspended,禁止该中断,同时向其他核通知此事,
        * 返回false,不处理此中断
        */
        if (irq_pm_check_wakeup(desc))
            return false;
        /* Handle a potential concurrent poll on a different core. */
        return irq_check_poll(desc);
    }
    [kernel/irq/chip.c]
    // 检查该中断是否被多核轮训
    static bool irq_check_poll(struct irq_desc *desc)
    {
        // 检查irq_desc结构体中的istate成员是否设置了IRQS_POLL_INPROGRESS标志
        if (!(desc->istate & IRQS_POLL_INPROGRESS))
            return false;  // 没有设置,直接返回false,不处理中断
        // 如果设置了IRQS_POLL_INPROGRESS标志,说明此中断正在被轮训,需要等待轮训结束
        return irq_wait_for_poll(desc);
    }

如果硬件中断处理函数返回了IRQ_WAKE_THREAD,此时调用__irq_wake_thread唤醒中断处理线程。首选判断线程是否设置了PF_EXITING标志,如设置,则说明线程要退出,则直接返回;然后测试并设置IRQTF_RUNTHREAD标志,如设置了说明中断线程正在运行,返回即可,反之则设置IRQTF_RUNTHREAD标志,表示中断线程将要运行;中断描述符irq_desc结构体中的threads_oneshot和threads_active都是为了处理oneshot类型的中断,oneshot类型的中断(包括硬件中断和中断线程)处理不允许嵌套。threads_oneshot是一个位图,每个比特位代表正在处理的(共享)oneshot类型中断线程,threads_active表示正在运行的中断线程数量。irqaction结构体中的thread_mask也是一个位图,在共享中断中每一个action由一个bit位来表示,此bit保存在thread_mask中;接着将中断线程所在的irqaction的位图保存到threads_oneshot中,记录运行的中断线程;随后将threads_active字段加1,增加唤醒的中断线程数量;最后调用wake_up_process唤醒中断线程。
 



    // 唤醒中断处理线程
    void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
    {
        // 如果中断线程设置了关闭标志PF_EXITING,则直接返回
        if (action->thread->flags & PF_EXITING)
            return;
        // 测试并设置IRQTF_RUNTHREAD标志,若已经设置,说明中断线程正在运行,直接返回
        if (test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags))
            return;
        // threads_oneshot代表一个位图,每一位代表一个运行的中断线程,thread_mask为本
        // action的中断线程,将要运行的中断线程设置到irq_desc结构体的threads_oneshot成员中
        desc->threads_oneshot |= action->thread_mask;
        // 增加唤醒的中断线程数量
        atomic_inc(&desc->threads_active);
        // 唤醒线程
        wake_up_process(action->thread);
          ->try_to_wake_up
    }

中断处理线程在注册中断的时候已经使用kthread_create创建,使用wake_up_process唤醒此线程,第四章《Linux之中断子系统-内核中断注册源代码分析(4)》中有描述。中断线程的入口函数为irq_thread,实质上在中断注册的时候中断线程已经运行起来了,在irq_wait_for_interrupt处等待被唤醒,硬件中断处理函数唤醒后检测到IRQTF_RUNTHREAD标志被设置,则返回0;接着调用irq_thread_fn函数处理中断(强制中断线程化调用的是irq_forced_thread_fn);执行完需要唤醒等待在wait_for_threads上的线程。



    [kernel/irq/manage.c]
    irq_thread
      ->irq_to_desc  // 根据软件中断号查找中断描述符irq_desc
      if (force_irqthreads && test_bit(IRQTF_FORCED_THREAD, &action->thread_flags))
          // 如果强制中断线程化,且设置了IRQTF_FORCED_THREAD标志,则使用irq_forced_thread_fn函数
          handler_fn = irq_forced_thread_fn;
      else
          handler_fn = irq_thread_fn;
      // 判断thread_flags变量是否设置了IRQTF_RUNTHREAD标志,如设置了则开始运行
      while (!irq_wait_for_interrupt(action)) {
          irqreturn_t action_ret;
          irq_thread_check_affinity(desc, action);
          action_ret = handler_fn(desc, action);  // 调用irq_thread_fn函数处理中断
          if (action_ret == IRQ_HANDLED)
             atomic_inc(&desc->threads_handled);  // 增加中断线程处理中断的次数
          if (action_ret == IRQ_WAKE_THREAD)  // 如果需要唤醒下半段的线程,则唤醒
              irq_wake_secondary(desc, action);
          // 活动的中断线程计数threads_active变量减1,如减到0,则说明所有线程执行完毕,
          // 此时需要唤醒等待在wait_for_threads上的线程。关闭中断的函数disable_irq内部会调用
          // synchornize_irq,如中断线程还未处理完毕,则会睡眠在wait_for_threads上,所有中断
          // 线程执行完毕,需要唤醒等待的线程
          wake_threads_waitq(desc);
      }


irq_thread_fn
函数调用了注册中断时提供的函数
thread_fn
,执行完毕调用
irq_finalize_oneshot
函数处理oneshot类型的中断。



    [kernel/irq/manage.c]
    // irq_thread调用此函数执行注册的中断线程化函数thread_fn
    static irqreturn_t irq_thread_fn(struct irq_desc *desc, struct irqaction *action)
    {
        irqreturn_t ret;
        // 执行注册的中断线程化函数
        ret = action->thread_fn(action->irq, action->dev_id);
        irq_finalize_oneshot(desc, action);  // 处理oneshot类型的中断
        return ret;
    }
    [kernel/irq/manage.c]
    static void irq_finalize_oneshot(struct irq_desc *desc, struct irqaction *action)
    {
        // 如果不是oneshot类型的中断或者强制线程中断化了,则直接退出
        if (!(desc->istate & IRQS_ONESHOT) ||
            action->handler == irq_forced_secondary_handler)
            return;
    again:
        // 循环判断硬件中断处理函数是否清除了IRQD_IRQ_INPROGRESS标志,清除了则跳出循环
        chip_bus_lock(desc);
        raw_spin_lock_irq(&desc->lock);
        if (unlikely(irqd_irq_inprogress(&desc->irq_data))) {
            raw_spin_unlock_irq(&desc->lock);
            chip_bus_sync_unlock(desc);
            cpu_relax();
            goto again;
        }
        // 测试是否设置了IRQTF_RUNTHREAD标志,如设置了,直接跳到out_unlock处,不清除oneshot中断
        if (test_bit(IRQTF_RUNTHREAD, &action->thread_flags))
            goto out_unlock;
        // 清除oneshot线程运行的位图
        desc->threads_oneshot &= ~action->thread_mask;
        // oneshot类型的中断线程执行完毕后,需要打开中断
        if (!desc->threads_oneshot && !irqd_irq_disabled(&desc->irq_data) &&
            irqd_irq_masked(&desc->irq_data))
            unmask_threaded_irq(desc);  // 打开中断
    out_unlock:
        raw_spin_unlock_irq(&desc->lock);
        chip_bus_sync_unlock(desc);
    }

handle_fasteoi_irq是现代形式的中断处理程序,需要中断控制器具有eoi功能。同时Linux内核也保留了之前使用的通用中断处理程序,这些函数都在[kernel/irq/chip.c]目录下,具体信息如下,这些函数的主要区别在于中断控制器的流控不同。
(1)handle_level_irq   处理电平触发的中断
(2)handle_simple_irq   通常用于多路复用中的子中断,由父中断的流控回调调用
(3)handle_nested_irq   处理中断线程发送的中断
(4)handle_edge_irq   处理边缘触发的中断
(5)handle_edge_eoi_irq   处理边缘触发的具有eoi功能中断控制器的中断
(6)handle_percpu_irq   处理本地cpu的中断
 

3.GIC SGI中断通用处理函数

硬件中断号小于16的为SGI中断,SGI中断调用
handle_IPI
函数处理,ipi_types中的handler处理函数由set_ipi_handler函数设置。



    void handle_IPI(int ipinr, struct pt_regs *regs)
    {
        unsigned int cpu = smp_processor_id();  // 获取被中断打断进程执行的cpu编号
        struct pt_regs *old_regs = set_irq_regs(regs);
 
        if (ipi_types[ipinr].handler) {
            __inc_irq_stat(cpu, ipi_irqs[ipinr]);
            irq_enter();  // 进入中断
            // 根据SGI硬件中断号,调用相应的中断处理函数
            (*ipi_types[ipinr].handler)();
            irq_exit();  // 退出中断
        } else
            pr_debug("CPU%u: Unknown IPI message 0x%x
", cpu, ipinr);
            set_irq_regs(old_regs);
    }

4.具体外设的中断处理函数


cdns_uart_isr
为zynq7k注册的串口中断处理函数,首先读取串口0中断状态寄存器,然后清除中断,接着根据中断状态寄存器判断是发送中断还是接收中断,根据判断结果进行相应的处理,处理完成,返回IRQ_HANDLED。



    [drivers/tty/serial/xilinx_uartps.c]
    cdns_uart_isr
      // 可以看出,串口中断服务函数不允许并发的执行
      ->spin_lock  // 加锁
      isrstatus = cdns_uart_readl(CDNS_UART_ISR_OFFSET);  // 获取中断状态
      cdns_uart_writel(isrstatus, CDNS_UART_ISR_OFFSET);  // 清中断
 
    static irqreturn_t cdns_uart_isr(int irq, void *dev_id)
    {
        struct uart_port *port = (struct uart_port *)dev_id;
        unsigned int isrstatus;
        spin_lock(&port->lock);
        // 获取中断状态寄存器信息
        isrstatus = cdns_uart_readl(CDNS_UART_ISR_OFFSET);
        cdns_uart_writel(isrstatus, CDNS_UART_ISR_OFFSET);  // 清中断
 
        if (isrstatus & CDNS_UART_IXR_TXEMPTY) {
            // 串口发送中断,处理发送逻辑
            cdns_uart_handle_tx(dev_id);
            isrstatus &= ~CDNS_UART_IXR_TXEMPTY;
        }
        if (isrstatus & CDNS_UART_IXR_MASK)
            // 串口接收中断,处理接收逻辑
            cdns_uart_handle_rx(dev_id, isrstatus);
 
        spin_unlock(&port->lock);  // 解锁
        // 中断处理完成,返回IRQ_HANDLED
        return IRQ_HANDLED;
    }

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
霍霍霍伟伟的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容