Linux中断系列之中断或异常处理(四)

中断处理基本过程:首先设备产生中断,通过中断线将电信号传递到中断控制器,如果未被屏蔽则会送往CPU的INTR引脚。CPU停止当前任务,根据得到的中断向量,从IDT找到相应的门描述符,可从中获取中断服务程序的地址并执行。
异常处理过程:不需要产生电信号,当异常发生时,CPU通过特定的中断向量,从IDT找到相应的门描述符,可从中获取异常服务程序的地址并执行。
一、中断控制器的工作 (以8259A为例)
    1、中断控制器是外部设备和CPU之间中断信号的桥梁,中断控制器主要有三个寄存器:IRR ISR IMR
  (1) IRR:中断请求队列,共8bit,对应八个引脚。中断到来时,若未被屏蔽,则IRR相应位置置1,表示收到中断请求,但未交给CPU。
  (2) ISR:中断服务寄存器,共8bit,对应八个引脚。当IRR中某个中断请求送往CPU后,该位置1,表示已递交给CPU,但CPU未处理完。
  (3) IMR:中断屏蔽寄存器,共8bit,对应八个引脚,当某位置1,表示对应中断引脚被屏蔽。
2、引脚的优先级问题:有两种优先级管理方式,固定优先级和循环优先级,固定优先级不可改变,而循环优先级在系统工作过程中可以动态改变。
3、8259A支持两种嵌套模式:一般嵌套模式和特殊嵌套模式
  一般嵌套模式,指在一个中断被处理的时候,一直相比低优先级或同等优先级会自动被屏蔽,而高优先级则可以响应。
  特殊嵌套模式,与一般不同的是,同优先级的中断也能响应。
4、8259A在中断处理过程中所要完成的工作:
  (1) 中断请求:设备发起中断,与之相连的IR引脚产生电信号,若对应中断未被屏蔽,IMR对应位为0,则将IRR对应位置1,通过INT引脚向CPU的INTR引脚发出请求信号。若被屏蔽,则丢弃。
  (2) 中断响应:CPU响应中断三个条件:至少有一个中断请求;CPU允许中断;当前指令执行完毕。
      8259A在提交中断请求信号,CPU可能在执行一条指令,并不会立即响应。此时还可能有其他IRQ线也产生了中断请求。
  当CPU执行完一条指令后,回去检查INTR管脚是否有信号,若有则检查EFLAGS寄存器中断允许标志IF是否为1,为1,则通过INTA引脚应答8259A。
  (3) 优先级判定:8259A收到CPU应答后,在IRR挑取优先级最高的中断,将其ISR对应的位置1。表示中断正在接收CPU的处理。
  (4) 提交中断向量:CPU通过INTA第二次发送脉冲,8259A根据被设置的起始信号量,计算最高的优先级的中断向量(比如起始向量号为16,当前中断请求为IRQ3,则得到的中断向量号为19),通过数据线递交给CPU。
  (5) 中断结束:递交中断向量后,8259A会检测是否AEOI模式,若是自动清除中断请求信号,将ISR相应位置0。若不是则等待CPU发送EOI指令(由中断服务程序发起),收到EOI,ISR相应位会清零。
  注:也就是说,在递交中断向量之前,高优先级的中断可以打断低优先级,而递交之后则不能。因为内核接收到中断向量后直接进入中断门,同时CPU会清零EFLAGS的IF位。    
      当CPU在正在处理IRQ1时,IRQ2线来了信号,8259A会自动将其与ISR中的值比较,ISR上有IRQ1置1,则仅仅IRR上置1。若IRQ0来了信号,相同的比较,发现IRQ0优先级大,此时8259A将IRQ0的ISR位置1,并向CPU发出中断请求。因为此时正在处理IRQ1,所以ISR上ISR0和ISR1均为1。
  8259A是AEOI模式,ISR中的位总是被清零(在提交中断向量号后),意味着如果有新的中断线来了信号,8259A会立刻向CPU提交中断请求,即使在处理IRQ0的中断,CPU只会简单应答8259A。因此这种情况下低优先级的中断会打断高优先级的中断服务程序。
二、CPU的工作
1、确定中断或异常的中断向量i(0~255之间)。可屏蔽中断的中断向量从中断控制器获得,不可屏蔽中断或异常的中断向量是固定的。
2、通过IDTR寄存器找到IDT,读取第i项(即第i个门描述符)。
3、进行特权级检查,将当前CPU的特权级CPL与门描述符的DPL比较,若小于或等于DPL,则进入门,之后,将获取门描述符中所指向代码段的DPL与CPL比较,如果小于或等于CPL,则执行中断服务程序。如果两次比较其中一次不符,则进入通用保护异常(中断向量13)。
4、若上述过程(1-3)检测到特权级发生变化,则需要进行堆栈转换。中断或异常程序运行在内核态(特权级0),而中断或异常在用户态(特权级0)时发生,此时需要从用户态(特权级3)变成内核态(特权级0),所以要将堆栈从用户态堆栈切换到内核态堆栈。
5、如果是异常,则CPU将异常码压入当前的堆栈。
6、如果是中断,进入中断门后,CPU会清零EFLAGS的IF位,即关闭所有的可屏蔽中断;如果是异常则不清零此位。(见handle_IRQ_event代码)
7、进入中断或异常服务程序执行
三、内核对中断的处理
过程如下:
      common_interrupt
              ↓
         do_IRQ()                     handle_IRQ_event               →                             运行该IRQ中断请求队列中的所有中断服务程序
              ↓                                          ↑(否)                                                                    ↓
      该IRQ公共服务程序 → 该IRQ中断请求队列是否为空?  (是)→              ret_from_intr      
    
1、所有中断(不可屏蔽中断除外)的服务程序在init_IRQ函数均被初始化为interrupt[i],数组每一项指向一个代码片段,此代码片段将中断向量压入堆栈外,之后调用了一个公共处理程序common_interrupt
  interrupt数组的代码片段                        例如:中断向量32 即IRQ0
     pushl $~(vector)//vector表示中断向量号            pushl $~(0)                 
 CFI_ADJUST_CFA_OFFSET 4                           CFI_ADJUST_CFA_OFFSET 4
 jmp common_interrupt                              jmp common_interrupt

  关于common_interrupt公共处理程序,每个中断发生后都会调用此函数。此程序功能如下:

  (1) 保存现场,将中断发生前所有的寄存器的值保存在堆栈中;

  (2) 调用do_IRQ函数,函数功能:

(注:在IRQ层引入之前,所有的IRQ线都共用一个公共的服务程序__do_IRQ对中断进行处理,它们对应的结构体都不同引入IRQ层以后的;引入后,后面讲解)

      ① 从堆栈中取出中断向量号,由中断向量号找到
      ② 调用irq_enter函数进入中断上下文,preempt_count加一,表示禁止其他进程抢占,调用irq_exit函数,preempt_count减一;
      ③ 调用函数__do_IRQ,看源码
      其功能:<1> 屏蔽当前的IRQ,禁止该IRQ线的中断传递。
                      <2> 判断中断请求队列是否为空,即是否有一个或多个中断服务程序,

     如果有则调用handle_IRQ_event函数进行遍历,执行相应程序处理中断

/** 
 * do_IRQ执行与一个中断相关的所有中断服务例程.这是在引用IRQ层之前的处理方式,也属于插入讲解引入IRQ层后面讲解
 */
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{	
	/* high bits used in ret_from_ code */
	int irq = regs->orig_eax & 0xff;
#ifdef CONFIG_4KSTACKS
	union irq_ctx *curctx, *irqctx;
	u32 *isp;
#endif


	/**
	 * irq_enter增加中断嵌套计数
	 */
	irq_enter();
#ifdef CONFIG_DEBUG_STACKOVERFLOW
	/* Debugging check for stack overflow: is there less than 1KB free? */
	{
		long esp;


		__asm__ __volatile__("andl %%esp,%0" :
					"=r" (esp) : "0" (THREAD_SIZE - 1));
		if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) {
			printk("do_IRQ: stack overflow: %ld\n",
				esp - sizeof(struct thread_info));
			dump_stack();
		}
	}
#endif


#ifdef CONFIG_4KSTACKS


	/**
	 * 如果中断栈使用不同的的栈,就需要切换栈.
	 */
	curctx = (union irq_ctx *) current_thread_info();
	irqctx = hardirq_ctx[smp_processor_id()];


	/*
	 * this is where we switch to the IRQ stack. However, if we are
	 * already using the IRQ stack (because we interrupted a hardirq
	 * handler) we can't do that and just have to keep using the
	 * current stack (which is the irq stack already after all)
	 */
	/**
	 * 当前在使用内核栈,而不是硬中断请求栈.就需要切换栈
	 */
	if (curctx != irqctx) {
		int arg1, arg2, ebx;


		/* build the stack frame on the IRQ stack */
		isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
		/**
		 * 保存当前进程描述符指针
		 */
		irqctx->tinfo.task = curctx->tinfo.task;
		/**
		 * 把esp栈指针寄存器的当前值存入irqctx的thread_info(内核oops时使用)
		 */
		irqctx->tinfo.previous_esp = current_stack_pointer;


		/**
		 * 将中断请求栈的栈顶装入esp,isp即为中断栈顶
		 * 调用完__do_IRQ后,从ebx中恢复esp
		 */
		asm volatile(
			"       xchgl   %%ebx,%%esp      \n"
			"       call    __do_IRQ         \n"
			"       movl   %%ebx,%%esp      \n"
			: "=a" (arg1), "=d" (arg2), "=b" (ebx)
			:  "0" (irq),   "1" (regs),  "2" (isp)
			: "memory", "cc", "ecx"
		);
	} else/* 否则,发生了中断嵌套,不用切换 */
#endif
		__do_IRQ(irq, regs);


	/**
	 * 递减中断计数器并检查是否有可延迟函数
	 */
	irq_exit();


	/**
	 * 结束后,会返回ret_from_intr函数. 
	 */
	return 1;
}
//
fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
	//得到对应irq号的irq_desc结构体
	irq_desc_t *desc = irq_desc + irq;
	struct irqaction * action;
	unsigned int status;

	/**
	 * 中断发生次数计数.
	 */
	kstat_this_cpu.irqs[irq]++;
	if (desc->status & IRQ_PER_CPU) {
		irqreturn_t action_ret;
		/*
		 * No locking required for CPU-local interrupts:
		 */
		desc->handler->ack(irq);
		action_ret = handle_IRQ_event(irq, regs, desc->action);
		if (!noirqdebug)
			note_interrupt(irq, desc, action_ret);
		desc->handler->end(irq);
		return 1;
	}
	/**
	 * 虽然中断是关闭的,但是还是需要使用自旋锁保护desc,在多处理器中,防止多CPU对主IRQ描述符进行操作,但CPU无影响
	 */
	spin_lock(&desc->lock);
	/**
	 * 如果是旧的8259A PIC,ack就是mask_and_ack_8259A,它应答PIC上的中断并禁用这条IRQ线.屏蔽IRQ线是为了确保在这个中断处理程序结束前,
	 * CPU不进一步接受这种中断的出现.
	 * do_IRQ是以禁止本地中断运行,事实上,CPU控制单元自动清eflags寄存器的IF标志.因为中断处理程序是通过IDT中断门调用的.
	 * 不过,内核在执行这个中断的中断服务例程之前可能会重新激活本地中断.
	 * 在使用APIC时,应答中断信赖于中断类型,可能是ack,也可能延迟到中断处理程序结束(也就是应答由end方法去做).
	 * 无论如何,中断处理程序结束前,本地APIC不进一步接收这种中断,尽管这种中断可能会被其他CPU接受.
	 */
	desc->handler->ack(irq);
	/*
	 * REPLAY is when Linux resends an IRQ that was dropped earlier
	 * WAITING is used by probe to mark irqs that are being tested
	 */
	/**
	 * 初始化主IRQ描述符的几个标志.设置IRQ_PENDING标志.也清除IRQ_WAITING和IRQ_REPLAY
	 * 这几个标志可以很好的解决中断重入的问题.
	 * IRQ_REPLAY标志是"挽救丢失的中断"所用.在此不详述.
	 */
	status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
	status |= IRQ_PENDING; /* we _want_ to handle it */

	/*
	 * If the IRQ is disabled for whatever reason, we cannot
	 * use the action we have.
	 */
	action = NULL;
	/**
	 * IRQ_DISABLED和IRQ_INPROGRESS被设置时,什么都不做(action==NULL)
	 * 即使IRQ线被禁止,CPU也可能执行do_IRQ函数.首先,可能是因为挽救丢失的中断,其次,也可能是有问题的主板产生伪中断.
	 * 所以,是否真的执行中断代码,需要根据IRQ_DISABLED标志来判断,而不仅仅是禁用IRQ线.
	 * IRQ_INPROGRESS标志的作用是:如果一个CPU正在处理一个中断,那么它会设置它的IRQ_INPROGRESS.这样,其他CPU上发生同样的中断
	 * 就可以检查是否在其他CPU上正在处理同种类型的中断,如果是,就什么都不做,这样做有以下好处:
	 * 一是使内核结构简单,驱动程序的中断服务例程式不必是可重入的.二是可以避免弄脏当前CPU的硬件高速缓存.
	 */
	if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
		action = desc->action;//得到此irq的中断服务程序的地址
		status &= ~IRQ_PENDING; /* we commit to handling */
		status |= IRQ_INPROGRESS; /* we are handling it */
	}
	desc->status = status;
	/*
	 * If there is no IRQ handler or it was disabled, exit early.
	 * Since we set PENDING, if another processor is handling
	 * a different instance of this same irq, the other processor
	 * will take care of it.
	 */
	/**
	 * 当前面两种情况出现时,不需要(或者是不需要马上)处理中断.就退出
	 * 或者没有相关的中断服务例程时,也退出.当内核正在检测硬件设备时就会发生这种情况.
	 */
	if (unlikely(!action))
		goto out;
	/*
	 * Edge triggered interrupts need to remember
	 * pending events.
	 * This applies to any hw interrupts that allow a second
	 * instance of the same irq to arrive while we are in do_IRQ
	 * or in the handler. But the code here only handles the _second_
	 * instance of the irq, not the third or fourth. So it is mostly
	 * useful for irq hardware that does not mask cleanly in an
	 * SMP environment.
	 */
	/**
	 * 这里是需要循环处理的,并不是说调用一次handle_IRQ_event就行了.
	 */
	for (;;) {
		irqreturn_t action_ret;
		/**
		 * 现在打开自旋锁了,那么,其他CPU可能也接收到同类中断,并设置IRQ_PENDING标志.
		 * xie.baoyou注:请注意开关锁的使用方法.有点巧妙,不可言传.
		 */
		spin_unlock(&desc->lock);
		/**
		 * 调用中断服务例程.处理挂在此IRQ线上的所有中断例程
		 */
		action_ret = handle_IRQ_event(irq, regs, action);
		spin_lock(&desc->lock);
		if (!noirqdebug)
			note_interrupt(irq, desc, action_ret);
		/**
		 * 如果其他CPU没有接收到同类中断,就退出
		 * 否则,继续处理同类中断.
		 */
		if (likely(!(desc->status & IRQ_PENDING)))
			break;
		/**
		 * 清除了IRQ_PENDING,如果再出现IRQ_PENDING,就说明是其他CPU上接收到了同类中断.
		 * 注意,IRQ_PENDING仅仅是一个标志,如果在调用中断处理函数的过程中,来了多次的同类中断,则意味着只有一次被处理,其余的都丢失了.
		 */
		desc->status &= ~IRQ_PENDING;
	}
	desc->status &= ~IRQ_INPROGRESS;

out:
	/*
	 * The ->end() handler has to deal with interrupts which got
	 * disabled while the handler was running.
	 */
	/**
	 * 现在准备退出了,end方法可能是应答中断(APIC),也可能是通过end_8259A_irq方法重新激活IRQ(只要不是伪中断).
	 */
	desc->handler->end(irq);
	/**
	 * 好,工作已经全部完成了,释放自旋锁吧.注意两个锁的配对使用方法.
	 */
	spin_unlock(&desc->lock);

	return 1;
}
		    //插入handle_IRQ_event代码
irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
{
	irqreturn_t ret, retval = IRQ_NONE;
	unsigned int status = 0;
    
	handle_dynamic_tick(action);
    /*因为CPU再通过中断门后会自动关闭所有中断,即CPU会清零EFLAGS的IF位,如果这里希望能打开,
	需要在使用request_irq函数注册中断程序时,不要设置IRQF_DISABLED标志(见CPU工作,第六步)*/
	if (!(action->flags & IRQF_DISABLED))/* 第一个中断ISR不要求关中断运行 */
		local_irq_enable_in_hardirq();/* 开中断 */

	/* 遍历ISR链表 */
	do {
		/* 回调ISR处理函数 */
		ret = action->handler(irq, action->dev_id);
		if (ret == IRQ_HANDLED)/* 该ISR响应了中断 */
			status |= action->flags;
		retval |= ret;
		/* 下一个ISR */
		action = action->next;
	} while (action);

	if (status & IRQF_SAMPLE_RANDOM)/* 处理随机数种子 */
		add_interrupt_randomness(irq);
	local_irq_disable();/* 回到上层函数,关闭中断 */

	return retval;
}


(承接上面)关于common_interrupt公共处理程序,每个中断发生后都会调用此函数。此程序功能如下:
  (1) 保存现场,将中断发生前所有的寄存器的值保存在堆栈中;
  (2) 调用do_IRQ函数,函数功能:
(注:在IRQ层引入之后,对不同类型中断的处理流程做出了区分,各类型中断有单独的公共服务程序,比如:边沿触发中断handle_edge_irq,电平触发中断对应handle_level_irq函数,8259A就是对应电平触发。          中断系列六讲解:通用IRQ层)
      ① 从堆栈中取出中断向量号,由中断向量号找到
      ② 调用irq_enter函数进入中断上下文,preempt_count加一,表示禁止其他进程抢占,调用irq_exit函数,preempt_count减一;
      ③ 调用IRQ自身的公共服务程序,详细看源码。
      ④ 调用每个IRQ自身的公共服务程序,对不同类型中断的处理流程做出了区分,并分别设置了一些对应的服务程序;
      其功能:<1> 屏蔽当前的IRQ,禁止该IRQ线的中断传递。
                      <2> 判断中断请求队列是否为空,即是否有一个或多个中断服务程序,
     如果有则调用handle_IRQ_event函数进行遍历,执行相应程序处理中断

fastcall unsigned int do_IRQ(struct pt_regs *regs)
{	
	struct pt_regs *old_regs;
	/* high bit used in ret_from_ code */
	int irq = ~regs->orig_eax;//去除IRQ号
	struct irq_desc *desc = irq_desc + irq;//得到对应的irq_desc结构体
#ifdef CONFIG_4KSTACKS
	union irq_ctx *curctx, *irqctx;
	u32 *isp;
#endif
	if (unlikely((unsigned)irq >= NR_IRQS)) {
		printk(KERN_EMERG "%s: cannot handle IRQ %d\n",
					__FUNCTION__, irq);
		BUG();
	}

	old_regs = set_irq_regs(regs);
	irq_enter();//进入中断上下文
#ifdef CONFIG_DEBUG_STACKOVERFLOW
	/* Debugging check for stack overflow: is there less than 1KB free? */
	{
		long esp;

		__asm__ __volatile__("andl %%esp,%0" :
					"=r" (esp) : "0" (THREAD_SIZE - 1));
		if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) {
			printk("do_IRQ: stack overflow: %ld\n",
				esp - sizeof(struct thread_info));
			dump_stack();
		}
	}
#endif

#ifdef CONFIG_4KSTACKS

	curctx = (union irq_ctx *) current_thread_info();
	irqctx = hardirq_ctx[smp_processor_id()];

	/*
	 * this is where we switch to the IRQ stack. However, if we are
	 * already using the IRQ stack (because we interrupted a hardirq
	 * handler) we can't do that and just have to keep using the
	 * current stack (which is the irq stack already after all)
	 */
	if (curctx != irqctx) {
		int arg1, arg2, ebx;

		/* build the stack frame on the IRQ stack */
		isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
		irqctx->tinfo.task = curctx->tinfo.task;
		irqctx->tinfo.previous_esp = current_stack_pointer;

		/*
		 * Copy the softirq bits in preempt_count so that the
		 * softirq checks work in the hardirq context.
		 */
		irqctx->tinfo.preempt_count =
			(irqctx->tinfo.preempt_count & ~SOFTIRQ_MASK) |
			(curctx->tinfo.preempt_count & SOFTIRQ_MASK);

		asm volatile(
			"       xchgl  %%ebx,%%esp      \n"
			"       call   *%%edi           \n"
			"       movl   %%ebx,%%esp      \n"
			: "=a" (arg1), "=d" (arg2), "=b" (ebx)
			:  "0" (irq),   "1" (desc),  "2" (isp),
			   "D" (desc->handle_irq)
			: "memory", "cc"
		);
	} else
#endif          
  //比如8259A,handle_irq就指向用户设定与其对应的公共服务函数,8259A上的所有中断线的函数指针handle_irq都会指向这个公共服务函数。在初始化时                                     ,set_irq_chip_and_handler_name函数,将handle_level_irq作为IRQ0~IRQ15的公共服务函数,因为他们触发的是这一类型的中断
  desc->handle_irq(irq, desc);//根据irq号和其对应的irq_desc结构体,执行对应的中断服务程序
  irq_exit();
  set_irq_regs(old_regs);
  return 1;
}


2、中断结束后,返回common_interrupt,然后调用ret_form_intr函数从中断返回,如果返回到用户空间,并且被中断进程设置了标志TIF_NEED_RESCHD,则ret_form_intr会调用schedule函数进行重新调度。
  如果返回内核空间,则会检查被中断进程的preempt_coount是否为0,只有为0时才能调用schedule函数。
四、内核对异常的处理
    与中断不同,各种异常都具有固定的中断向量和固定的异常服务程序(在trap_init中指定)。当异常发生时,将直接跳入相应的服务程序中。每个异常都有自己的代码段
    如同中断跳入common_interrupt函数中一样,异常则跳入汇编函数error_code中,也是公共处理程序,这个程序对每个异常都一样
    例如堆栈异常:
    ENTRY(stack_segment)    //堆栈异常处理程序入口
RING0_EC_FRAME
pushl $do_satck_segment  //异常服务程序地址压入堆栈
CFI_ADJUST_CFA_OFFSET 4 
jmp error_code           //调用对应的服务函数,这个异常就调用do_satck_segment程序
CFI_ENDPROC
END(stack_sehment)

五、总结:中断如何进行的
1、8259A接收到中断后,通过INTR向CPU发起中断请求;
2、CPU响应(INTA)后,8259A进行优先级判定,将寄存器ISR此时最高优先级位置1,将IRR对应位置0;
3、8259A等待CPU二次信号(INTA),提交中断向量i,可能为(0~255)之间的值,其中异常除外;
4、基本上8259A都是可屏蔽中断(32~255),还有不可屏蔽中断和异常中断向量是确定的(0~31);
5、根据IDTR找到的IDT,CPU用中断向量从IDT中读取第i项(即第i个门描述符);
6、CPU进行特权级检查,来决定是否能通过门;
7、CPU判定上述过程特权级是否发生变化;
8、进行异常与中断的不同处理;(5~8)此过程是由硬件完成;
9、如果i(0~31)是此时CPU通过门描述直接进入服务程序;
  如果i(32~255)此时CPU通过门描述符跳到其对应的代码段(在初始化的时候代码段的地址也存储在interrupt[i]中,然后存入中断门中)。
10、如果是0~31就直接执行中断服务程序了,看13步;如果是32~255先执行代码片段,将中断向量号压入堆栈中,然后跳到公共处理程序comm_interrupt。
11、comm_interrupt执行的任务是先保存现场,中断前寄存器的值保存在堆栈中,然后调用函数do_IRQ;
12、do_IRQ函数任务(参看三),其实对于32~255号,中断的处理是’分-总-分‘的结构,分:先是硬件通过IRQ号找到门描述符,然后找到各自的代码片段,转向内核处理,总:每个代码片段又会调用同一个汇编函数common_interrupt,
   分:接下来又通过irq号找到对应的irq_desc结构体,然后根据这个结构体执行自身的中断服务程序。
13、第9步,对于0~31号,看四



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值