linux中断申请之request_threaded_irq

本文介绍了 Linux 内核中的中断处理机制,重点讲解了 threaded_irq 的使用方法及其实现原理。threaded_irq 通过将部分中断处理任务交给内核线程执行,能够有效减少系统延迟并简化中断处理流程。

http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=21977330&id=3755609

在linux里,中断处理分为顶半(top half),底半(bottom half),在顶半里处理优先级比较高的事情,要求占用中断时间尽量的短,在处理完成后,就激活底半,有底半处理其余任务。底半的处理方式主要有soft_irq, tasklet, workqueue三种,他们在使用方式和适用情况上各有不同。soft_irq用在对底半执行时间要求比较紧急或者非常重要的场合,主要为一些subsystem用,一般driver基本上用不上。 tasklet和work queue在普通的driver里用的相对较多,主要区别是tasklet是在中断上下文执行,而work queue是在process上下文,因此可以执行可能sleep的操作。


request_threaded_irq()是Linux kernel 2.6.30 之后新加的irq handler API 

如何
确定可以用到 request_threaded_irq() ? 
Linux kernel config 需要定义CONFIG_GENERIC_HARDIQS 
kernel config 才有支援
threaded irq 

    Moving interrupts to threads 介绍request_threaded_irq() 的由来 
    
http://lwn.net/Articles/302043/ 
    
从realtime tree 移植而来,为了减少kernel 因为要等待每一个硬件中断处理的时间 
    
,就另外交给kernel thread 处理中断后续工作。 
    
优点: 
        
1 减少 kernel 延迟时间 
        
2 避免处理中断时要分辨是在硬体中断或软体中断? 
        
3 更容易为kernel 中断处理除错,可能可完全取代tasklet 
    
原本的中断处理分上半部(硬体中断处理,必须关闭中断无法处理新的中断)跟下半部( 
    
软体中断处理),因此上半部的硬体中断处理必须尽可能简短,让系统反应速度更快。 

    
request_threaded_irq 是在将上半部的硬件中断处理缩短为只确定硬体中断来 
    
自我们要处理的装置,唤醒kernel thread 执行后续中断任务。

    
缺点: 
    
对于非irq 中断的kernel threads ,需要在原本task_struct 新增struct 
    
irqaction 多占 4/8 bytes 记忆体空间 
    
linux kernel 2.6.29 之后(2.6.30)加入request_threaded_irq 


    
跟传统top/bottom havles 的差异是threaded_irq 受Linux kernel system 
    
的 process scheduling 控制,不会因为写错的bottom half 代码造成整个系统 
    
延迟的问题。 

    
也可以透过RT/non RT 跟nice 等工具调整各个thread 优先权,丢给使用率较低的 
    
cpu 以及受惠于kernel 原本可以对threads 做的各种控制,包括但不限于sleep, 
    
lock, allocate 新的记忆体区块。 

    
受惠最大的是shared irq line 的多个中断处理。除了可以加速共享中断造成的延迟 
    
,threaded_irq 也可以降低在同一段程式码处理多个装置中断的复杂度。 

    
threaded irq 在使用性上也比tasklet(接着top half 直接执行,无法sleep) 
    
/workqueue(kernel context?) 等需要在top half 增加跟bottom half 连结与沟通 
    
的麻烦。 

int  request_threaded_irq (unsigned int  irq irq_handler_t   handler irq_handler_t   thread_fn , unsigned long  irqflags const char * devname , void * dev_id

IRQF_SHARED 共享中断时,dev_id不能为空,因为释放irq时要区分哪个共享中断
irq:中断号
handler:发生中断时首先要执行的硬中断处理函数,这个函数可以通过返回 IRQ_WAKE_THREADED唤醒中断线程,也可
返回IRQ_HANDLE不执行中断线程
thread_fn : 中断线程,类似于中断下半部
后三个参数与request_irq中的一致

关于IRQF_ONESHOT, 直到线程函数执行完毕才会开启该中断
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. 这里linus在邮件列表里指明IRQF_ONESHOT 的原因
Making the IRQF_ONESHOT explicit does two things:
 - it makes people who read the code *aware* of things
 - if/when you have irq conflicts and two drivers want to attach to
the same interrupt, at least you can see directly from the source what
flags they used (and again, not have to even *think* about it).

IRQF_ONESHOT 与 IRQF_SHARED 不能同时使用
当多个设备共享中断时,由于IRQF_ONESHOT会关闭中断线程的中断,而线程一般执行时间会比较长,所以是不允许的
当hardirq函数为NULL时,必须声明IRQF_ONESHOT, 表示threadirq线程中关闭该中断,在某些情况下,这个标志会非常有用
例如:设备是低电平产生中断,而硬中断函数为NULL,如果不使用IRQF_ONESHOT,就会一直产生中断执行NULL函数,中断线程
得不到执行,声明IRQF_ONESHOT后,会执行完线程才使能该中断  


点击(此处)折叠或打开

  1. /* 
  2.  * gpio_irqTest.
  3.  * PB27 receive this signal as IRQ and make the LED linking on PB17 turn on or turn off 
  4.  * 
  5.  */ 
  6.   
  7. #include <linux/types.h> 
  8. #include <linux/kernel.h> 
  9. #include <linux/module.h> 
  10. #include <linux/init.h> 
  11. #include <linux/platform_device.h> 
  12. #include <linux/cdev.h> 
  13. #include <linux/ioctl.h> 
  14. #include <linux/fs.h> 
  15. #include <linux/gpio.h>
  16. #include <linux/delay.h>
  17. #include <linux/cdev.h> 
  18. #include <linux/interrupt.h>

  19. #include <asm/io.h> 
  20. #include <asm/io.h>

  21. #include <mach/gpio.h> 
  22. #include <mach/hardware.h> 
  23. #include <mach/board.h> 
  24. #include <mach/gpio.h> 
  25. #include <mach/at91_pio.h> 
  26. #include <mach/at91_aic.h> 
  27. #include <mach/at91_pmc.h> 

  28. void led_on() 
  29. { 
  30. // at91_set_gpio_output(AT91_PIN_PB17,1);
  31.     printk("led on\n");
  32. } 
  33.   
  34. void led_off() 
  35. { 
  36. // at91_set_gpio_output(AT91_PIN_PB17 ,0);
  37.     printk("led off.\n");
  38. } 
  39.   
  40. struct light_dev *light_devp; 
  41. int light_major = 200; 
  42.   
  43. struct light_dev 
  44. { 
  45.     struct cdev cdev; 
  46.     unsigned char value; 
  47. }; 
  48.   

  49.   
  50. static void io_init(void) 
  51. {
  52.     at91_set_GPIO_periph(AT91_PIN_PB27, 0);
  53.     at91_set_gpio_input(AT91_PIN_PB27, 1); 
  54.     at91_set_deglitch(AT91_PIN_PB27, 1); 

  55. } 
  56.   
  57.   
  58. struct gpio_irq_desc 
  59. {
  60.     int pin;
  61.     int irq; 
  62.     unsigned long flags; 
  63.     char *name; 
  64. }; 
  65.   
  66. static struct gpio_irq_desc gpio_irq={AT91_PIN_PB27, AT91_PIN_PB27,IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING|IRQF_ONESHOT,"PB27"};
  67. static irqreturn_t gpio_irqhandler(int irq, void *dev_id)
  68. {
  69.     printk(KERN_INFO "In hard irq handler.\n");
  70.     return IRQ_WAKE_THREAD;
  71. }

  72. static irqreturn_t gpio_threadhandler(int irq, void *dev_id) 
  73. {
  74.     int rst;
  75.     rst = at91_get_gpio_value(gpio_irq.pin);
  76.     printk(KERN_INFO "gpio stat: %d\n", rst);
  77.     if(rst == 0){
  78.         led_on();
  79.     }else{
  80.         led_off();
  81.     }
  82.     printk(KERN_INFO "sleep 3000ms\n");
  83.     msleep(3000);
  84.     printk(KERN_INFO "awake after sleep\n");
  85.     return IRQ_HANDLED;
  86. }
  87.   
  88. int light_open(struct inode *inode,struct file *filp) 
  89. { 
  90.     int err; 
  91.     struct light_dev *dev;
  92.     dev = container_of(inode->i_cdev,struct light_dev,cdev); 
  93.     filp->private_data = dev;
  94.     printk(KERN_DEBUG "%s", __FUNCTION__);
  95.     io_init(); 
  96. // err = request_threaded_irq(gpio_irq.irq,gpio_irqhandler,gpio_threadhandler,gpio_irq.flags,gpio_irq.name,(void*)0);
  97.     err = request_threaded_irq(gpio_irq.irq,NULL,gpio_threadhandler,gpio_irq.flags,gpio_irq.name,(void*)0);
  98.     if(err) 
  99.     { 
  100. // free_irq(gpio_irq.irq,(void*)0);
  101.         printk(KERN_DEBUG "request irq failed.\n");
  102.         return -EBUSY; 
  103.     } 
  104.       
  105.     return 0; 
  106. } 
  107.   
  108. int light_release(struct inode *inode,struct file *filp) 
  109. { 
  110.     free_irq(gpio_irq.irq,(void*)0); 
  111.     return 0; 
  112. } 
  113.   
  114. int light_ioctl(struct inode *inode,struct file *filp,unsigned int cmd, unsigned long arg)
  115. { 
  116.     struct light_dev *dev = filp->private_data; 
  117.   
  118.     switch(cmd) 
  119.     { 
  120.         case 0: 
  121.             at91_set_gpio_output(AT91_PIN_PB19,0); 
  122.         break; 
  123.   
  124.         case 1: 
  125.           at91_set_gpio_output(AT91_PIN_PB19,1); 
  126.        led_off(); 
  127.         break; 
  128.   
  129.     default: 
  130.   
  131.             return -ENOTTY;
  132.         // break;
  133.     }
  134.     return 0;
  135. } 
  136.   
  137. struct file_operations light_fops = 
  138. { 
  139.     .owner = THIS_MODULE,
  140.     .open = light_open,
  141.     .release = light_release,
  142.     .unlocked_ioctl = light_ioctl,
  143. }; 
  144.   
  145.   
  146. static void light_setup_cdev(struct light_dev *dev,int index) 
  147. { 
  148.     int err,devno = MKDEV(light_major,index); 
  149.   
  150.     cdev_init(&dev->cdev,&light_fops); 
  151.     dev->cdev.owner = THIS_MODULE; 
  152.     dev->cdev.ops = &light_fops; 
  153.   
  154.     err = cdev_add(&dev->cdev,devno,1); 
  155.   
  156.     if(err) 
  157.     { 
  158.         printk(KERN_NOTICE "Error %d adding LED%d",err,index); 
  159.     }
  160. } 
  161.   
  162.   
  163. int __init light_init(void) 
  164. { 
  165.     int result; 
  166.   
  167.     dev_t dev = MKDEV(light_major,0); 
  168.     if(light_major) 
  169.     { 
  170.           
  171.         result = register_chrdev_region(dev,1,"gpio"); 
  172.     } 
  173.   
  174.     if(result < 0) 
  175.     {
  176.         printk(KERN_DEBUG "%s: register char dev failed.\n", __FUNCTION__);
  177.         return result; 
  178.     } 
  179.   
  180.     light_devp = kmalloc(sizeof(struct light_dev),GFP_KERNEL); 
  181.     if(!light_devp) 
  182.     { 
  183.         result = - ENOMEM; 
  184.         goto fail_malloc; 
  185.     } 
  186.   
  187.     memset(light_devp,0,sizeof(struct light_dev)); 
  188.     light_setup_cdev(light_devp,0); 
  189.       
  190.     printk(KERN_DEBUG "%s done\n", __FUNCTION__);
  191.     return 0; 
  192.   
  193.     fail_malloc:unregister_chrdev_region(dev,light_devp); 
  194.     return result; 
  195.       
  196. } 
  197.   
  198. void __exit light_cleanup(void) 
  199. { 
  200.     cdev_del(&light_devp->cdev); 
  201.     kfree(light_devp); 
  202.     unregister_chrdev_region(MKDEV(light_major,0),1); 
  203. } 
  204.   
  205. module_init(light_init); 
  206. module_exit(light_cleanup);

  207. MODULE_AUTHOR("Enzo Fang"); 
  208. MODULE_LICENSE("Dual BSD/GPL");

结论:
使用
request_threaded_irq(gpio_irq.irq,gpio_irqhandler,gpio_threadhandler,gpio_irq.flags,gpio_irq.name,(void*)0);
hardirq和thread_fn同时出现时,处理thread_fn时该中断是打开的

  err = request_threaded_irq(gpio_irq.irq,NULL,gpio_threadhandler,gpio_irq.flags,gpio_irq.name,(void*)0);
但hardirq和thread_fn只有一个存在时,处理thread_fn时,中断是关闭的
<think> 我们正在比较两个Linux内核中断处理函数:devm_request_threaded_irqrequest_irq。 首先,我们需要明确两者的基本概念和区别。 1. request_irq: 这是传统的中断请求函数。它用于向内核注册一个中断处理程序。 函数原型: int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev); 参数说明: irq: 中断号 handler: 中断处理函数(上半部) flags: 中断标志,如IRQF_SHARED、IRQF_ONESHOT等 name: 中断设备名称 dev: 传递给中断处理程序的设备标识,通常为设备结构体指针 特点: - 需要手动释放中断:free_irq(irq, dev) - 只注册一个中断处理函数(上半部),如果需要进行长时间处理,通常需要配合tasklet或工作队列 2. devm_request_threaded_irq: 这是设备资源管理(device resource management, devres)版本的线程化中断请求函数。 函数原型: int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id); 参数说明: dev: 设备指针,用于资源管理 irq: 中断号 handler: 上半部中断处理函数(硬中断上下文) thread_fn: 下半部线程处理函数(在进程上下文中运行) irqflags: 中断标志 devname: 设备名称 dev_id: 传递给处理程序的设备标识 特点: - 自动资源管理:当设备被卸载或驱动卸载时,内核会自动释放中断,无需手动调用free_irq。 - 支持线程化中断:将中断处理分为上半部(快速处理)和下半部(线程处理)。如果不需要上半部,可以将handler设置为NULL,这样整个中断处理都在线程中运行(需要设置IRQF_ONESHOT标志)。 主要区别总结: 1. 资源管理: - devm_request_threaded_irq 是托管版本,无需手动释放,减少资源泄漏风险。 - request_irq 需要手动调用 free_irq 释放中断。 2. 中断处理方式: - request_irq 注册的处理函数在中断上下文中运行,不能睡眠,需要快速完成。如果处理耗时,需要自行拆分上下半部(如使用tasklet或工作队列)。 - devm_request_threaded_irq 允许指定一个线程函数(下半部),该部分在进程上下文中运行,可以睡眠,适合耗时操作。 3. 使用场景: - request_irq:适用于简单的中断处理,或需要自定义上下半部机制的情况。 - devm_request_threaded_irq:适用于需要线程化中断处理的场景,且希望自动管理资源。 代码示例: 使用 request_irq: static irqreturn_t my_interrupt_handler(int irq, void *dev_id) { /* 上半部处理 */ return IRQ_WAKE_THREAD; // 这会唤醒关联的线程处理函数 } static irqreturn_t my_threaded_handler(int irq, void *dev_id) { /* 下半部处理 */ return IRQ_HANDLED; } /* 注册中断 */ int ret = request_irq(irq, my_interrupt_handler, IRQF_SHARED | IRQF_ONESHOT, "my_device", dev); /* 注意:这里并没有直接关联线程处理函数,所以实际上需要自己实现线程化?实际上,request_irq只能注册一个处理函数,所以通常不直接用于线程化。如果要实现线程化,需要自己创建内核线程或者使用工作队列。*/ 实际上,request_irq并不直接支持线程化。而threaded interrupt是通过devm_request_threaded_irq或者request_threaded_irq(非devres版本)来实现的。 因此,正确的比较应该是 request_irqrequest_threaded_irq(或者devm_request_threaded_irq)的区别。 修正: 实际上,存在一个非托管的线程化中断请求函数:request_threaded_irq。 这里我们比较: request_irqrequest_threaded_irq(以及它们的devm版本) 但问题中提到了devm_request_threaded_irq,所以我们主要对比request_irq和devm_request_threaded_irq。 重新整理: request_irq: 注册一个中断处理函数(在中断上下文中执行),需要手动释放。 devm_request_threaded_irq: 注册一个线程化的中断处理(可以包含上半部和下半部线程处理),自动释放。 另外,还有一个非托管的线程化中断函数:request_threaded_irq,它需要手动释放。 因此,devm_request_threaded_irq 相对于 request_irq 的主要优势在于: - 线程化处理(允许下半部睡眠) - 自动资源管理 注意:实际上,request_irq是一个宏,它内部调用了request_threaded_irq,并将线程处理函数设置为NULL。所以: #define request_irq(irq, handler, flags, name, dev) \ request_threaded_irq(irq, handler, NULL, flags, name, dev) 因此,request_irq实际上只是request_threaded_irq的一个特例(没有线程处理函数)。 结论: devm_request_threaded_irqrequest_irq 的区别在于: 1. 资源管理:devm版本自动释放,request_irq需要手动释放。 2. 处理方式:devm_request_threaded_irq可以指定一个线程处理函数(下半部),而request_irq只能注册一个在中断上下文执行的函数。 3. 使用便利性:devm版本减少了资源泄漏的可能性,特别适合设备驱动。 代码示例: 使用devm_request_threaded_irq: static irqreturn_t hardirq_handler(int irq, void *dev_id) { /* 在中断上下文中快速处理 */ return IRQ_WAKE_THREAD; // 唤醒线程处理函数 } static irqreturn_t threaded_handler(int irq, void *dev_id) { /* 在进程上下文中处理,可以睡眠 */ return IRQ_HANDLED; } int probe(struct device *dev) { ... int ret = devm_request_threaded_irq(dev, irq, hardirq_handler, threaded_handler, IRQF_ONESHOT, "my-device", dev); ... } 在设备卸载时,不需要调用free_irq。 使用request_irq: static irqreturn_t my_handler(int irq, void *dev_id) { /* 全部在中断上下文中处理,不能睡眠 */ return IRQ_HANDLED; } int probe(struct device *dev) { ... int ret = request_irq(irq, my_handler, 0, "my-device", dev); ... } void remove(struct device *dev) { free_irq(irq, dev); } 注意:线程化中断的标志通常需要设置IRQF_ONESHOT,以确保在处理线程完成之前不会重新使能中断(特别是共享中断的情况)。 总结表格: | 特性 | request_irq | devm_request_threaded_irq | |--------------------------|---------------------------------|---------------------------------------| | 资源管理 | 手动 (需free_irq) | 自动 (设备卸载时自动释放) | | 中断处理上下文 | 中断上下文 | 上半部在中断上下文,下半部在进程上下文 | | 是否支持线程化处理 | 否 | 是 | | 是否可睡眠 | 上半部不可睡眠 | 下半部可睡眠 | | 典型应用场景 | 快速中断处理 | 需要长时间处理或可能睡眠的中断 | 因此,对于需要长时间处理的中断,推荐使用devm_request_threaded_irq,以利用线程化处理和自动资源管理。 注意:devm_request_threaded_irq是devm_request_irq的线程化版本,而devm_request_irqrequest_irq的托管版本(但不支持线程化)。 所以,实际上devm_request_irq存在,但它不支持线程化。而devm_request_threaded_irq是同时支持线程化和托管的。 最后,根据问题要求,在回答后提出3-5个相关问题。 </think> `devm_request_threaded_irq()` 和 `request_irq()` 都是 Linux 内核中用于注册中断处理程序的函数,但它们在资源管理、使用场景和功能上有显著区别: --- ### **核心区别对比** | 特性 | `request_irq()` | `devm_request_threaded_irq()` | |--------------------------|------------------------------------|----------------------------------------| | **资源管理** | 需手动释放 (`free_irq()`) | 自动释放(设备卸载时) | | **中断处理模型** | 单一中断上下文处理函数 | 支持上下半部分离(硬中断 + 线程化处理)| | **睡眠支持** | 禁止睡眠 | 线程化下半部允许睡眠 | | **适用场景** | 简单快速的中断处理 | 复杂/耗时的中断处理 | | **函数原型** | `int request_irq(unsigned int, irq_handler_t, ...)` | `int devm_request_threaded_irq(dev, irq, handler, thread_fn, ...)` | --- ### **详细区别解析** #### 1. **资源管理机制** - **`request_irq()`**: 需手动释放中断资源,典型使用流程: ```c request_irq(irq, handler, flags, name, dev); // 注册 free_irq(irq, dev); // 必须手动释放 ``` 若忘记释放会导致资源泄漏。 - **`devm_request_threaded_irq()`**: 通过设备资源管理(Device Resource Management)自动释放: ```c devm_request_threaded_irq(dev, irq, handler, thread_fn, flags, name, dev); // 设备注销时自动调用 free_irq() ``` 避免资源泄漏风险。 #### 2. **中断处理模型** - **`request_irq()`**: 仅支持单一中断处理函数,在**原子上下文**运行: ```c static irqreturn_t handler(int irq, void *dev_id) { // 禁止睡眠/阻塞操作 return IRQ_HANDLED; } ``` - **`devm_request_threaded_irq()`**: 支持**硬中断 + 线程化下半部**: ```c static irqreturn_t hard_handler(int irq, void *dev_id) { // 原子上下文快速处理 return IRQ_WAKE_THREAD; // 唤醒线程处理函数 } static irqreturn_t thread_fn(int irq, void *dev_id) { // 进程上下文,可睡眠/执行复杂逻辑 return IRQ_HANDLED; } ``` #### 3. **阻塞与延迟敏感场景** | **场景** | `request_irq()` | `devm_request_threaded_irq()` | |------------------------|-----------------|-------------------------------| | **硬件中断延迟** | 极低(直接响应)| 略高(需调度线程) | | **执行耗时操作** | 不适用 | 适用(下半部可睡眠) | | **优先级反转风险** | 低 | 需合理设置线程优先级 | --- ### **典型使用场景** #### ✅ `request_irq()` 适用场景 ```c // 简单 GPIO 按键中断 request_irq(gpio_irq, button_handler, IRQF_TRIGGER_FALLING, "gpio_key", NULL); static irqreturn_t button_handler(int irq, void *dev_id) { printk("Button pressed!\n"); // 快速响应 return IRQ_HANDLED; } ``` #### ✅ `devm_request_threaded_irq()` 适用场景 ```c // 网络设备接收数据包中断 devm_request_threaded_irq(dev, irq, eth_hardirq, eth_thread, IRQF_SHARED, "eth0", dev); static irqreturn_t eth_hardirq(int irq, void *dev_id) { disable_hardware_interrupt(); // 快速关闭中断 return IRQ_WAKE_THREAD; // 唤醒线程处理数据 } static irqreturn_t eth_thread(int irq, void *dev_id) { process_packet_queue(); // 耗时操作:数据包处理 enable_hardware_interrupt(); // 重新启用中断 return IRQ_HANDLED; } ``` --- ### **选择建议** 1. **优先 `devm_request_threaded_irq()` 当**: - 需要执行阻塞操作(如 I/O 等待) - 中断处理超过 100μs - 需要自动资源管理 2. **使用 `request_irq()` 当**: - 处理逻辑极短(如标记状态) - 对中断延迟要求极高(实时系统) - 明确需要手动控制中断生命周期 --- ### **线程化中断的特殊标志** 使用 `devm_request_threaded_irq()` 时需注意: ```c // 必须添加 IRQF_ONESHOT 标志(特别是共享中断) devm_request_threaded_irq(..., IRQF_SHARED | IRQF_ONESHOT, ...); ``` - `IRQF_ONESHOT`: 确保线程处理完成前不会重发中断 - 缺少此标志可能导致中断丢失或竞争条件
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值