libusb阻塞

本文探讨了在使用libusb进行USB指纹头数据传输时遇到的同步方式下阻塞问题,分析了超时处理机制及问题根源,并提供了相应的解决策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这两天发现手头一个usb指纹头出现了一点状况,若libusb以同步方式发送bulk transfer出现阻塞。经过测试发现跟timeout有些关系:若timeout为0(无timeout),不会阻塞;若timeout为1000或者2000,则会。另外,若采用异步方式传送bulk transfer,则不会阻塞。
 
同步方式
libusb_bulk_transfer(devh, ep_bulk, buf, CAM_BUF_SZ, &len, timeout);
进入libusb研究,发现libusb是采用异步方式来实现的。在do_sync_bulk_transfer中:
 

 

staticint do_sync_bulk_transfer(struct libusb_device_handle *dev_handle,
unsignedchar endpoint,unsignedchar*buffer,int length,
int*transferred,unsignedint timeout,unsignedchar type)
{
libusb_fill_bulk_transfer(transfer, dev_handle, endpoint, buffer, length,
bulk_transfer_cb,&completed, timeout);
transfer->type = type;
 
r = libusb_submit_transfer(transfer);
if(r <0){
libusb_free_transfer(transfer);
return r;
}
 
while(!completed){
r = libusb_handle_events(HANDLE_CTX(dev_handle));
}
}
这里libusb_fill_bulk_transfer来填充bulk transfer,然后libusb_submit_transfer提交bulk transfer,最后用libusb_handle_events来等待完成。当收到回应后,bulk_transfer_cb回调设置completed,从而阻塞被接触,函数返回。
 
分段处理bulk transfer
libusb_submit_transfer最终调到了submit_bulk_transfer。该函数会检查buffer大小,如果大于MAX_BULK_BUFFER_LENGTH(16K),则分成若干16K大小的urb,每个urb指向用户buffer的适当位置,然后用ioctl(IOCTL_USBFS_SUBMITURB)提交每个urb。
 
收到数据时会判断是否收齐所有urb。在handle_bulk_completion中,若urb_idx为最后一个urb,则认为收齐了所有的urb。最终会调用usbi_handle_transfer_completion来调用urb callback。
 
timeout处理
若libusb_bulk_transfer传入的timeout为0,则没有timeout,libusb会一直等待数据。在libusb_handle_events中设置了一个2s的poll timeout,libusb会在while中一直poll,每次poll的timeout为2s。
 
若设置了timeout,libusb_submit_transfer会按照timeout升序将transfer插入到libusb_context的flying_transfers列表中,然后提交transfer(当然会分段了,如上所述)
 
libusb_handle_events会根据自己设置的2s timeout和flying_transfers中的timeout得出实际timeout,然后用poll查询usb fd。
 
问题根源
libusb阻塞的原因就是超时。有时usb指纹头返回数据较慢,在指定的timeout时间内没有完成所有urb请求,进入超时处理。handle_timeout()会cancel掉为完成的urb(IOCTL_USBFS_DISCARDURB)。在do_sync_bulk_transfer中,由于未完成所有urb,bulk_transfer_cb没有被调用,从而会阻塞。libusb_handle_events会继续以2s超时来查询fd,但由于urb已经取消,设备会返回-2(ENOENT)。
 
按道理讲,即使超时,libusb也应该在超时后返回。libusb为什么没有返回呢?handle_bulk_completion函数中,若读到ENOENT,awaiting_discard会递减至0。与awaiting_discard一起的还有awaiting_reap,若二者均为0,也会调用bulk_transfer_cb通知用户。
 
awaiting_discard在超时时cancel urb时被设置。若ioctl(IOCTL_USBFS_DISCARDURB)返回EINVAL,则awaiting_reap会递增。
 
经过打印验证,发现在cancel urb时,对于已经完成的urb,会返回EINVAL。而未完成的urb,则返回0。我想这大概是一个bug,正确做法应该是不要取消已经完成的urb。

 

附:
阻塞时libusb打印

found usbfs at /dev/bus/usb
found usb devices in sysfs
add fd 9 events 1
created default context

scan usb1
sysfs descriptors available
bus=1 dev=1
busnum 1 devaddr 1 session_id 257
allocating new device for1/1(session 257)
scan 1-1
bus=1 dev=2
busnum 1 devaddr 2 session_id 258
allocating new device for1/2(session 258)
scan 1-1.1
bus=1 dev=3
busnum 1 devaddr 3 session_id 259
allocating new device for1/3(session 259)



open 1.3
add fd 11 events 4
destroy device 1.1
destroy device 1.2
configuration 1
interface0
next timeout in2.499878s
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=2 status=0 transferred=0
handling completion status 0
actual_length=0control transfer的回调)
next timeout in2.499909s
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=2 status=0 transferred=0
handling completion status 0
actual_length=0(另一个control transfer的回调)
need 10 urbs fornew transfer with length 153600
next timeout in0.997836s(从这里开始查询bulk
poll()2 fds with timeout in998ms
poll() returned 1
urb type=3 status=0 transferred=16384
handling completion status 0 of bulk urb 1/10
next timeout in0.379086s
poll()2 fds with timeout in380ms
poll() returned 1
urb type=3 status=0 transferred=16384
handling completion status 0 of bulk urb 2/10
next timeout in0.253080s
poll()2 fds with timeout in254ms
poll() returned 0

all URBs have already been processed for timeouts (超时)
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=3 status=-2 transferred=9728
handling completion status -2 of bulk urb 3/10
CANCEL: detected a cancelled URB
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=3 status=-2 transferred=0
handling completion status -2 of bulk urb 4/10
CANCEL: detected a cancelled URB
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=3 status=-2 transferred=0
handling completion status -2 of bulk urb 5/10
CANCEL: detected a cancelled URB
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=3 status=-2 transferred=0
handling completion status -2 of bulk urb 6/10
CANCEL: detected a cancelled URB
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=3 status=-2 transferred=0
handling completion status -2 of bulk urb 7/10
CANCEL: detected a cancelled URB
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=3 status=-2 transferred=0
handling completion status -2 of bulk urb 8/10
CANCEL: detected a cancelled URB
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=3 status=-2 transferred=0
handling completion status -2 of bulk urb 9/10
CANCEL: detected a cancelled URB
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 1
urb type=3 status=-2 transferred=0
handling completion status -2 of bulk urb 10/10
CANCEL: detected a cancelled URB
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 0
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 0
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 0
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 0
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 0
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 0
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
poll() returned 0
all URBs have already been processed for timeouts
poll()2 fds with timeout in2000ms
^C

kernel usbmon的打印:

e8c3a240 1032172219 S Co:1:003:0 s 00090001000000000
e8c3a240 1032173347 C Co:1:003:000
e8c3a240 1032174262 S Co:1:003:0 s 40 e0 0000000000000
e8c3a240 1032174597 C Co:1:003:000
e8c3a240 1032175054 S Co:1:003:0 s 02010000008100000
e8c3a240 1032249749 C Co:1:003:000
e8c3a240 1032250298 S Co:1:003:0 s 40 e5 0000000000000
e8c3a240 1032250634 C Co:1:003:000
e8c3a240 1032251121 S Bi:1:003:1-11516384<
e8c3a440 1032251365 S Bi:1:003:1-11516384<
e8c3a1c0 1032252890 S Bi:1:003:1-11516384<
e8c3a2c0 1032254018 S Bi:1:003:1-11516384<
e8c3a740 1032254475 S Bi:1:003:1-11516384<
e8c3a7c0 1032254871 S Bi:1:003:1-11516384<
e8c3a5c0 1032255268 S Bi:1:003:1-11516384<
e8c3a0c0 1032255695 S Bi:1:003:1-11516384<
e8c3a640 1032256091 S Bi:1:003:1-11516384<
e849ce20 1032256518 S Bi:1:003:1-1156144<
e8c3a240 1032561640 C Bi:1:003:1016384=40e5000000000000 a59fa89e b3a3a09c a899989b 8b8a9ba09f9392a0929f8498
e8c3a440 1033162310 C Bi:1:003:1016384=909e8a928d99898d908f94909ba197a18ba5a69d9396959d948d9e9e968c9584
e8c3a1c0 1033653347 C Bi:1:003:1016384= d1d9d0d8 d3cdd2d7 d1d8c5d6 c5cfd1d3 d0ccd0d4 d0cbd1d1 bed2d2d0 d9dad4d9
e8c3a2c0 1034152250 C Bi:1:003:1016384= a79fa0aa ada2bab2 b6b3a5ad aeae9bb0 b2aa9eaa 9396a8a6 afa5af95 9daaa5ba
e8c3a740 1034753624 C Bi:1:003:1-216320= d6d0d5d2 ccd4c7cc cdd6cfc9 d3d2d2ce cfcfd5c8 d0c4cdc7 cdc9c6ca d4d0cec2
e8c3a2c0 1034753776 S Co:1:002:0 s 23089031000100000
e8c3a2c0 1034754111 C Co:1:002:000
e8c3a7c0 1034755026 C Bi:1:003:1-264= a3a9bda3 a5a4a9a5 93a69e98949d9294 aa9d8d9d 9b91a6a4 a793a097 9b8f8f96
e8c3a2c0 1034755117 S Co:1:002:0 s 23089031000100000
e8c3a2c0 1034755483 C Co:1:002:000
e8c3a5c0 1034756215 C Bi:1:003:1-264= d1c7d2d5 dad6d6db ded1d0da d8d2d4da d3d8d8d6 dbd4dedd d2d5ded5 dbd6d7d8
e8c3a2c0 1034756459 S Co:1:002:0 s 23089031000100000
e8c3a2c0 1034756855 C Co:1:002:000
e8c3a0c0 1034757587 C Bi:1:003:1-264= b04a5e59 637c82707f8483787f7e7881838f8497988a8389938c8f928b8ca28c
e8c3a2c0 1034757648 S Co:1:002:0 s 23089031000100000
e8c3a2c0 1034757983 C Co:1:002:000
e8c3a640 1034758715 C Bi:1:003:1-20
e8c3a2c0 1034758776 S Co:1:002:0 s 23089031000100000
e8c3a2c0 1034759111 C Co:1:002:000
e849ce20 1034759569 C Bi:1:003:1-20
e8c3a2c0 1034759630 S Co:1:002:0 s 23089031000100000
e8c3a2c0 1034759995 C Co:1:002:000

 

转载于:https://www.cnblogs.com/heyp/p/3421800.html

libusb1.0学习(一) 首先声明,这是看到国外论坛上的学习文章后,独立翻译过来作为笔记用,加入部分自我理解,并且全部原创。 介绍: libusb是一个开源库,可以帮助开发者在用户空间的层面上与UBS设备进行通讯。如果想了解更多,可以查看他们的页:http://libusb.org/ 在其文档中,建议首先阅读USB2的规格说明:http://www.usb.org/developers/docs/,这可以帮助真正地了解USB是如何工作的。 libusb的安装: 你可以从官方的页上获取源代码,并且编译安装。或者使用的发行版已经包含了软件包,可以很方便地安装。 如果已经安装完毕,请继续往下 通讯: 设备和客户端之间的通讯是个清楚的概念叫做使用管道。每个管道都是一个机上的一个软件和设备上一个端点的通道。每个端点对于设备来说完成一部分特殊的目标,比如接受命令或者传输数据。一个全速设备最高可以拥有16个端点,然后低速的设备只拥有三个端点。 所有的USB设备当供电时都支持端口0。这个端口是默认的目标管道。当设备连接被检测到后,USBD软件会使用端口0来初始化设备,执行普通的(非特殊)配置,并且获得有设备提供的其他端点的信息。端点是以他们的端点数目(取决于设计的时间),总线宽带,访问频率,延迟和处理错误要求为特征区分的。 一旦设备里的端点识别并且配置完毕,管道就产生允许客户端软件与设备进行通讯。跟一个管道产生联系是以对总线的访问和带宽,传输的类型,传输的方向和最大数据负载大小为描述特征的。 USB定义了四种传输方式:控制传输,通常用来传输命令和状态操作;中断传输,通过设备初始化一些来自机的请求;同步传输,用来传输投递关键事件的数据(比如视频和对话);批量传输,使用全部可以用的带宽但不是特定时间的。所有的数传使用相同格式的包装,包括控制信息,数据和错误效验区域。 这里有两种管道:消息管道和流管道。控制传输是使用消息管道。在消息管道中,在每个包中的数据部分对于USB系统软件是有意义的。 流管道被中断传输,同步传输和批量传输使用。在流管道中,在每个包中的数据部分对于USB是没有意义,仅仅在客户端软件和设备间传输。 同步接口: 同步接口允许你使用单独一个函数调用一个USB传输。当这个函数调用返回时,这个传输也已经完成并且返回结果供解析用。这种方式的优点是十分清晰的:你可以通过一个简单的函数调用做任何事。 尽管如此,这个接口还是有它的局限性。你的程序会进入休眠当在libusb_bulk_transfer()(当进行批量传输),直到传输完成。假如这需要花费三个小时,你的程序同样需要休眠同样的时间。实现将会在库里面结束,整体线程这期间是无用的。另一个问题是,当一个单独的传输线程结束,这里不存在可能多个端点和多个设备同时地进行I/O操作,除非你借助创造新的线程处理。另外的,当请求被提交后,这里没有机会可能取消传输。 设备和接口: 在libusb中,每个USB设备通过libusb_device和libusb_device_handle对象操作。libusb API 连接一个打开的设备至特定的接口。这意味着如果你在设备上请求多个接口,你必须同样多次打开设备来接受一个libusb_dev_handle,对应每个你想进行通讯的接口。不要忘记调用libusb_dev_handle。 这些意味着什么?这意味你在设备上操作以前,可以完成请求接口,同样,你可以在完成设备操作前,先释放接口。 每个设备都有自己独属的配置,比如vendor id,product id等。我们使用这些设置去发现需求的设备,并且通过这些配置来工作。首先我们写一个函数来查明这些配置并且打印出来,以便我们找到正确的一个;我们基本的操作如下: 1.通过调用libusb_init来初始化库,同时创建一个对话; 2.调用libusb_get_device_list来获得已经连接的设备的队列。这会创建一个libusb_device的数组,包含了所有连接到系统上的usb设备; 3.循环遍历所有的设备来检查他们的选项; 4.发现其中需要的一个,使用libusb_open或者libusb_open_device_with_vid_pid(当你知道这个设备vendor id和product id)来打开设备; 5.使用libusb_free_device_list清除使用libusb_get_device_list获得的队列; 6.通过libusb_claim_interface请求接口(需要你知道设备的接口数值); 7.操作想得到的I/O; 8.通过libusb_release_interface释放设备; 9.通过libusb_close将你之前打开的设备关闭; 10.通过libusb_exit来关闭对话; PS:英文需要苦练啊,一篇短的教程文章看的结结巴巴的 libusb1.0学习(二) 接学习一,学习二要是看例程 ok,现在最简单的想法看看有多少信息包含在你的设备里,程序代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <iostream> #include <libusb.h> using namespace std; void printdev(libusb_device *dev); //prototype of the function int main() { libusb_device **devs; //pointer to pointer of device, used to retrieve a list of devices libusb_context *ctx = NULL; //a libusb session int r; //for return values ssize_t cnt; //holding number of devices in list r = libusb_init(&ctx;); //initialize a library session if(r < 0) { cout<<"Init Error "<<r<<endl; //there was an error return 1; } libusb_set_debug(ctx, 3); //set verbosity level to 3, as suggested in the documentation cnt = libusb_get_device_list(ctx, &devs;); //get the list of devices if(cnt < 0) { cout<<"Get Device Error"<<endl; //there was an error } cout<<cnt<<" Devices in list."<<endl; //print total number of usb device ssize_t i; //for iterating through the list for(i = 0; i < cnt; i++) { printdev(devs[i]); //print specs of this device } libusb_free_device_list(devs, 1); //free the list, unref the devices in it libusb_exit(ctx); //close the session return 0; } void printdev(libusb_device *dev) { libusb_device_descriptor desc; int r = libusb_get_device_descriptor(dev, &desc;); if (r < 0) { cout<<"failed to get device descriptor"<<endl; return; } cout<<"Number of possible configurations: "<<(int)desc.bNumConfigurations<<" "; cout<<"Device Class: "<<(int)desc.bDeviceClass<<" "; cout<<"VendorID: "<<desc.idVendor<<" "; cout<<"ProductID: "<<desc.idProduct<<endl; libusb_config_descriptor *config; libusb_get_config_descriptor(dev, 0, &config;); cout<<"Interfaces: "<<(int)config->bNumInterfaces<<" ||| "; const libusb_interface *inter; const libusb_interface_descriptor *interdesc; const libusb_endpoint_descriptor *epdesc; for(int i=0; i<(int)config->bNumInterfaces; i++) { inter = &config;->interface[i]; cout<<"Number of alternate settings: "<<inter->num_altsetting<<" | "; for(int j=0; j<inter->num_altsetting; j++) { interdesc = &inter;->altsetting[j]; cout<<"Interface Number: "<<(int)interdesc->bInterfaceNumber<<" | "; cout<<"Number of endpoints: "<<(int)interdesc->bNumEndpoints<<" | "; for(int k=0; k<(int)interdesc->bNumEndpoints; k++) { epdesc = &interdesc;->endpoint[k]; cout<<"Descriptor Type: "<<(int)epdesc->bDescriptorType<<" | "; cout<<"EP Address: "<<(int)epdesc->bEndpointAddress<<" | "; } } } cout<<endl<<endl<<endl; libusb_free_config_descriptor(config); } 写完之后,编译看看会出现什么。首先运行程序并检查设备,然后连上我自己的设备在执行程序。会发现有新的内容出现,可以根据vendor id和product id发现这正是我自己连上打开的设备。 注意:发现设备(调用libusb_get_device_list())会返回新的内存分配的设备队列。当你完成这个队列的使用后必须释放他。 Libusb同样需要知道当一切完成时清除队列的内容;设备的本身。 为处理这些问题,libusb提供了两个单独的条目: 一个释放队列本身的函数 一个针对设备内部的参考计数系统 新的设备由libusb_get_device_list()函数展示,都拥有一个参考计数1。你可以使用libubs_ref_device()和libusb_unref_device()增加或减少参考计数。当一个设备的参考计数为0时,该设备就被销毁 通过以上的信息,打开设备的基本流程可以视为如下步骤: 1. 使用libusb_get_device_list()发现设备; 2. 选择你想操作的设备,调用libusb_open(); 3. 在设备队列中unref所有的设备; 4. 释放已经发现的设备队列; 这个次序是十分重要的,在尝试打开设备之前,你不能够unreference设备,因此unreference操作有可能导致设备的销毁。 为了方便起见,libusb_free_device_list()函数包含一个参数,在释放队列本身前,该参数能够选择性地在队列中unreference所有设备。这包含了以上的步骤3和步骤4。 如果还有需要,可以去libusb1’s API(http://libusb.sourceforge.net/api-1.0/index.html)文档参考你需要的函数。 好了,现在你可以找到你需要的设备了。现在是打开设备,请求并且执行一个简单的I/O。如果你知道vendor ID和prouct ID,使用libusb_open_device_with_vid_pid。 另外需要注意的,如果内核(你的OS)已经连接到这个设备,你将无法请求到它。在这种情况下,你需要调用libusb_detach_kernel_drive来从内核中检测设备。如果你想知道内核是否可用的,使用libusb_kernel_drive_active,如果返回值为1,对于你的设备内核可以加载驱动。 批量传输 为了在你的设备上使用批量传输,你应该获得为你的USB设备获得一个设备句柄,并且你应该知道使用哪个端点(从之前设备说明获得)。 关于语法上的信息参考这里(http://libusb.sourceforge.net/api-1.0/group__syncio.html#gab8ae853ab492c22d707241dc26c8a805) 这里有个简单的例子包含所有我提到的相关部分: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <iostream> #include <libusb.h> using namespace std; int main() { libusb_device **devs; //pointer to pointer of device, used to retrieve a list of devices libusb_device_handle *dev_handle; //a device handle libusb_context *ctx = NULL; //a libusb session int r; //for return values ssize_t cnt; //holding number of devices in list r = libusb_init(&ctx;); //initialize the library for the session we just declared if(r < 0) { cout<<"Init Error "<<r<<endl; //there was an error return 1; } libusb_set_debug(ctx, 3); //set verbosity level to 3, as suggested in the documentation cnt = libusb_get_device_list(ctx, &devs;); //get the list of devices if(cnt < 0) { cout<<"Get Device Error"<<endl; //there was an error return 1; } cout<<cnt<<" Devices in list."<<endl; dev_handle = libusb_open_device_with_vid_pid(ctx, 5118, 7424); //these are vendorID and productID I found for my usb device if(dev_handle == NULL) cout<<"Cannot open device"<<endl; else cout<<"Device Opened"<<endl; libusb_free_device_list(devs, 1); //free the list, unref the devices in it unsigned char *data = new unsigned char[4]; //data to write data[0]='a';data[1]='b';data[2]='c';data[3]='d'; //some dummy values int actual; //used to find out how many bytes were written if(libusb_kernel_driver_active(dev_handle, 0) == 1) { //find out if kernel driver is attached cout<<"Kernel Driver Active"<<endl; if(libusb_detach_kernel_driver(dev_handle, 0) == 0) //detach it cout<<"Kernel Driver Detached!"<<endl; } r = libusb_claim_interface(dev_handle, 0); //claim interface 0 (the first) of device (mine had jsut 1) if(r < 0) { cout<<"Cannot Claim Interface"<<endl; return 1; } cout<<"Claimed Interface"<<endl; cout<<"Data->"<<data<<"<-"<<endl; //just to see the data we want to write : abcd cout<<"Writing Data..."<<endl; r = libusb_bulk_transfer(dev_handle, (2 | LIBUSB_ENDPOINT_OUT), data, 4, &actual;, 0); //my device's out endpoint was 2, found with trial- the device had 2 endpoints: 2 and 129 if(r == 0 && actual == 4) //we wrote the 4 bytes successfully cout<<"Writing Successful!"<<endl; else cout<<"Write Error"<<endl; r = libusb_release_interface(dev_handle, 0); //release the claimed interface if(r!=0) { cout<<"Cannot Release Interface"<<endl; return 1; } cout<<"Released Interface"<<endl; libusb_close(dev_handle); //close the device we opened libusb_exit(ctx); //needs to be called to end the delete[] data; //delete the allocated memory for data return 0; } 结尾:这个教程对于这个话题来说是十分简单的介绍,需要时间需联系同步传输,然后移动是异步的,这还有很多需要学习。 希望这些对你的初学有帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值