在内核态Block Layer访问块设备

Linux内核本质:操作系统的心脏与硬件资源总控中心 内核是操作系统最核心的特权级程序,运行于CPU Ring 0,为上层应用提供进程管理、内存虚拟化、文件系统抽象和设备驱动四大支柱能力。其设计基于特权级隔离(如系统调用)、硬件抽象层(HAL)和安全仲裁机制,确保多任务并发下的稳定性与安全性。理解内核即理解为什么程序不能直接操作物理内存、为何进程崩溃不致系统瘫痪、以及‘kernel panic’和‘unable to find the kernel source tree’等典型问题的技术根源。它既是硬件资源的中央银行,也是软件生态得以统一运行的底层契约。 阅读详情

为减少内核态与用户态之间大量的copy_from/to_user 内存拷贝以及定制I/O软件流程,可以在内核态直接访问块设备。

代码示例

struct block_device *blkdev = blk_get_by_path("/dev/block/by-name/boot", FMODE_READ, THIS_MODULE);

loff_t blkdev_size = i_size_read(blkdev->bd_inode);

#define MAX_BIOVEC 256
struct bio_vec bio_vec[MAX_BIOVEC];
struct page* page_buffer[MAX_BIOVEC];

for (i = 0; i < MAX_BIOVEC; i++) {
    page_buffer[i] = alloc_page(GFP_KERNEL);
}

注:每个 struct bio_vec 只能指向一个 page 或者多个物理内存连续的 pages;

struct bio *bio = bio_alloc(GFP_KERNEL, 1);

loff_t page_count = blkdev_size / PAGE_SIZE;
for (page_off = 0; page_off < page_count; page_off += MAX_BIOVEC) {
    loff_t read_pages = (page_off+MAX_BIOVEC) > page_count? page_count - page_off : MAX_BIOVEC;
    bio_reset(bio);
    bio_init(bio, bio_vec, read_pages);
    bio_set_dev(bio, blkdev);

    for (i = 0; i < read_pages; i++) {
        bio_add_page(bio, page_buffer[i], PAGE_SIZE, 0);
    }

    bio->bi_iter.bi_sector = page_off * (PAGE_SIZE / 512);

    bio_set_op_attrs(bio, REQ_OP_READ | REQ_SYNC, 0);

    submit_bio_wait(bio);

    struct bio_vec bvec;
    struct bvec_iter iter;
    bio_for_each_bvec(bvec, bio, iter) {
        // bvec.bv_page
        // bvec.bv_len
    }
}

重要的数据结构和函数

// include/linux/blk_types.h
/*
 * main unit of I/O for the block layer and lower layers (ie drivers and
 * stacking drivers)
 */
struct bio {
    struct bio            *bi_next;    /* request queue link */
    struct block_device   *bi_bdev;
    blk_opf_t             bi_opf;        /* bottom bits REQ_OP, top bits req_flags.*/
    unsigned short        bi_flags;    /* BIO_* below */
    blk_status_t          bi_status;
    struct bvec_iter      bi_iter;
    bio_end_io_t          *bi_end_io;
    void                  *bi_private;
    ......
    unsigned short        bi_vcnt;    /* how many bio_vec's */
    /*
     * Everything starting with bi_max_vecs will be preserved by bio_reset()
     */
    unsigned short        bi_max_vecs;    /* max bvl_vecs we can hold */
    struct bio_vec        *bi_io_vec;    /* the actual vec list */
};

//include/linux/bvec.h
struct bio_vec {
    struct page    *bv_page;
    unsigned int    bv_len;
    unsigned int    bv_offset;
};

struct bvec_iter {
    sector_t         bi_sector;    /* device address in 512 byte sectors */
    unsigned int     bi_size;     /* residual I/O count */
    unsigned int     bi_idx;        /* current index into bvl_vec */
    unsigned int     bi_bvec_done;    /* number of bytes completed in current bvec */
} __packed;

Immutable biovecs and biovec iterators

从 3.13 开始,biovecs 不应在提交 bio 后进行修改。相反,我们有一个新的 struct bvec_iter 表示一个 biovec 的范围——这个迭代器将在 bio 完成时被修改,而不是 biovec。

更具体地说,需要部分完成 bio 的旧代码将更新 bi_sector 和 bi_size,并将 bi_idx 推进到下一个 biovec。如果它在 biovec 中途结束,它将增加 bv_offset 并减少 bv_len,增加的字节数为该 biovec 中完成的字节数。

在新方案中,为了部分完成 bio 而必须改变的所有内容都被隔离到结构 bvec_iter 中:bi_sector、bi_size 和 bi_idx 已移到那里;而不是修改bv_offset和bv_len,struct bvec_iter有bi_bvec_done,代表当前bvec完成的字节数。

有一堆新的辅助宏用于隐藏血淋淋的细节——特别是,呈现部分完成的 biovecs 的错觉,以便普通代码不必处理 bi_bvec_done。

  • 驱动程序代码不应再直接引用 biovecs;我们现在有 bio_iovec() 和 bio_iter_iovec() 宏,它们返回文字结构 biovecs,从原始 biovecs 构造但考虑了 bi_bvec_done 和 bi_size

bio_for_each_segment() 已更新为采用 bvec_iter 参数而不是整数(对应于 bi_idx);对于很多代码,转换只需要将参数类型更改为 bio_for_each_segment()。

  • 推进 bvec_iter 是用 bio_advance_iter() 完成的; bio_advance() 是 bio_advance_iter() 的包装器,它在 bio->bi_iter 上运行,并且如果存在 bio 完整性的 iter 也会提升它。

有一个较低级别的高级函数 - bvec_iter_advance() - 它接受一个指向 biovec 的指针,而不是 bio;这是由bio完整性代码使用的。

从 5.12 开始,不支持 bv_len 为零的 bvec 段。

这一切给我们带来了什么?

拥有一个真正的迭代器,并使 biovecs 不可变,具有许多优点:

  • 以前,当您一次不只处理一个 bvec 时,遍历 bios 是非常尴尬的 - 例如,block/bio.c 中的 bio_copy_data(),它将一个 bio 的内容复制到另一个。因为 biovec 的大小不一定相同,所以旧代码非常复杂 - 它必须同时遍历两个不同的 bios,同时将 bi_idx 和 offset 都保留到当前的 biovec 中。

新代码更简单 - 看看。这种模式出现在很多地方;许多驱动程序之前本质上是开放编码的 bvec 迭代器,并且具有通用实现大大简化了很多代码。

  • 以前,任何可能需要在 bio 完成后使用 biovec 的代码(可能将数据复制到其他地方,或者如果出现错误可能需要在其他地方重新提交)必须保存整个 bvec 数组 - 同样,这在很多地方都在进行。

  • biovecs 可以在多个 bios 之间共享 - bvec iter 可以表示现有 biovec 的任意范围,从 biovecs 的中间开始和结束。这就是能够有效拆分任意 bios 的原因。请注意,这意味着我们_仅_使用 bi_size 来确定何时到达 bio 的末尾,而不是 bi_vcnt - 并且 bio_iovec() 宏在构建 biovecs 时会考虑 bi_size。

  • 拆分 bios 现在要简单得多。旧的 bio_split() 甚至不能在超过一个 bvec 的 bios 上工作!现在,我们可以有效地拆分任意大小的 bios——因为新 bio 可以共享旧 bio 的 biovec。

必须注意确保 biovec 在拆分 bio 仍在使用时不被释放,以防原始 bio 先完成。拆分 bios 时使用 bio_chain() 有助于此。

  • 提交部分完成的 bios 现在完全没问题 - 这偶尔会出现在堆栈块驱动程序中,各种代码(例如 md 和 bcache)对此有一些丑陋的解决方法。

过去,提交部分完成的 bio 对_大多数_设备都可以正常工作,但由于访问原始 bvec 数组是常态,因此并非所有驱动程序都会尊重 bi_idx 并且那些会中断。现在,由于所有驱动程序_必须_通过 bvec 迭代器 - 并且已经过审核以确保它们是 - 提交部分完成的 bios 是完全可以的。

其他影响:

  • 几乎所有 bi_idx 的用法现在都是不正确的并且已被删除;相反,以前使用 bi_idx 的地方现在使用 bvec_iter,可能将其传递给辅助宏之一。

IE。不再使用 bio_iovec_idx()(或 bio->bi_iovec[bio->bi_idx]),您现在使用 bio_iter_iovec(),它接受一个 bvec_iter 并返回一个文字结构 bio_vec - 从原始 biovec 动态构建,但考虑到bi_bvec_done(和 bi_size)。

  • bi_vcnt 不能被驱动程序代码信任或依赖 - 即任何实际上不拥有 bio 的东西。原因有两个:首先,实际上不再需要迭代 bio - 我们只使用 bi_size。其次,当克隆一个 bio 并重用(一部分)原始 bio 的 biovec 时,为了计算新 bio 的 bi_vcnt,我们必须迭代新 bio 中的所有 biovec——这很愚蠢,因为它不需要。

所以,不要再使用 bi_vcnt 了。

  • 当前接口允许块层根据需要拆分 bios,因此我们可以消除很多复杂性,尤其是在堆叠驱动程序中。创建 bios 的代码可以创建方便的任何大小的 bios,更重要的是,堆栈驱动程序不必处理它们自己的 bio 大小限制和底层设备的限制。因此,无需为各个块驱动程序定义 ->merge_bvec_fn() 回调。

助手的使用:

  • 以下名称后缀为 _all 的助手只能用于non-BIO_CLONED bio。它们通常由文件系统代码使用。Drivers 不应该使用它们,因为bio可能在到达driver之前就已经分裂了。

bio_for_each_segment_all()
bio_for_each_bvec_all()
bio_first_bvec_all()
bio_first_page_all()
bio_last_bvec_all()
  • 以下助手遍历单页段。传递的 'struct bio_vec' 将在迭代期间包含一个单页 IO 向量:

bio_for_each_segment()
bio_for_each_segment_all()
  • 以下助手迭代多页 bvec。传递的 'struct bio_vec' 将在迭代期间包含一个多页 IO 向量:

bio_for_each_bvec()
bio_for_each_bvec_all()
rq_for_each_bvec()

【Linux 内核中等待 completion 事件 API--wait_for_completion_interruptible_timeout 】 是 Linux 内核中用于等待 completion 事件的一个函数,它结合了等待 completion 的功能、可中断性(即如果线程在等待过程中接收到信号,它会提前返回)以及超时机制。如果等待时间超过5秒,或者等待过程中。函数,并传入相应的 completion 变量和超时时间。将返回,然后可以根据返回值判断是超时还是被信号中断。初始化一个 completion 变量,通常使用。在需要等待 completion 的线程中,调用。在另一个线程中,当某个任务完成时,调用。函数来标记 completion。 阅读详情

相关推荐

【信息科学与工程学】计算机科学与自动化——第十八篇 存储系统设计 10 存储器/存储软件/存储芯片/存储盘/存储系统/存储网络01

③ MTJ电阻 → 界面:TMR=(R_AP-R_P)/R_P = 2P₁P₂/(1-P₁P₂) (Julliere模型);③ 访问延迟模型 → 统计:平均访问延迟 = p_hot * t_dram + (1-p_hot) * t_nvm + t_migration_overhead;:① 储能电容计算 → 数学物理:所需能量 E = P * t_backup,电容 C = 2E / (V_start² - V_end²);:① B-tree平衡 → 代数:树高 h = log_m(N),m为扇出;

weixin_49199313的博客 262

.Clang-format格式化code

一款致力于vscode,代码格式的执行code;使用方便

Controller_wgy的博客 352

【合规领域】【信息科学与工程学】计算机科学与自动化——第五百篇 国产化/XC解决方案 第一篇 国产化产品适配

编号类型信创硬件及国产化硬件信创及国产化软件领域算法应用场景算法名称算法中的函数列表及每个函数逐步推理思考的数学表达式及函数调用关系及参数列表及参数数值设计及每个函数的完整代码及数学复杂度(时间复杂度、空间复杂度、逻辑复杂度)及时序流程图、通信图、架构图满足的用户/管理员规模关联知识和法律法规1适配方法华为鲲鹏920处理器(ARMv8架构)、飞腾FT-2000/4(ARMv8)银河麒麟V10(内核5.10)、达梦DM8数据库、OpenSSL 1.1.1(国密补丁)信息安全。

weixin_49199313的博客 392

BLOCK层代码分析(4)IO下发之BIO的切分和合并

之前计划先将bounce过程,但bounce过程涉及bio的切分,因此本节将bio的操作:切分和合并。前面对bio的组织和分配做了基本的介绍,bio的切分是将一个bio分成两个bio,而合并准确来说是将bio与IO请求request进行合并。 1. IO请求request和bio的关系 下图反应了request和bio之间的关系。request是由在硬盘位置连续的bio链接起来的。图中request中有3个bio,每个分别指向硬盘中一段数据。只有硬盘上相邻的bio才可以合...

flyingnosky的专栏 4607

【信息科学与工程学】计算机科学与自动化——第三百零五篇 数据中心 Scale-Up、Scale-Out、Scale-Across 01

临界带宽B_critical满足:V_orig/B_critical = V_orig/(r·B_critical) + V_orig/R_compress + r·V_orig/R_decompress ⇒ 1/B_critical = 1/(r·B_critical) + 1/R_compress + r/R_decompress ⇒ B_critical = (1 - 1/r) / (1/R_compress + r/R_decompress)。T_original中所有数据均经内存总线传输。

weixin_49199313的博客 475

【信息科学与工程学】【通信工程】第七十九篇 Infiniband网络02

管理员通过 SM 定义分区,指定其 P_Key 和期望的成员节点(CA/交换机端口)。2.

weixin_49199313的博客 381

linux block layer第一篇bio 子系统数据结构及初始化

介绍: block layer在内核中起着承上(文件系统层)启下(设备驱动层)的作用,struct bio是block层的基本数据结构,可以理解成将bio输入block层,由block layer对其加工处理,然后再输出给底层。与bio相关的代码涉及bio内存管理、提交、合并拆分、完成时的回调处理等内容,是内核中一个很小的子系统,该文对其中涉及的代码进行分析,作为笔记,以备查阅。 内核源码:linux-5.10.3 bio数据结构 bio是block层最基本的结构,用来描述io请求属性及状态。从内

geshifei的博客 8829

基于Python的携程景点评价爬虫与情感评分分析

摘要: 本项目通过Python技术栈实现携程景点评论的情感分析系统,包含数据爬取、文本预处理、情感评分和可视化分析全流程。使用requests抓取评论接口,jieba进行中文分词,SnowNLP计算情感得分(0-1),并通过词云、情感分布图呈现结果。典型分析显示72%评论为正面,负面反馈多涉及"排队久""票价贵"等问题。系统可辅助景点口碑管理,扩展方向包括多景点对比、BERT模型优化等。项目强调需遵守爬虫伦理,控制请求频率,适用于非商业用途的旅游数据分析。(150字)

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 975

linux block layer第二篇bio 的操作

摘要 linux block layer第一篇介绍了bio数据结构及bio内存管理,本文章介绍bio的提交、拆分、io请求合并、io请求完成时的回调处理。由于“bio的提交”涉及内容太多,所以该小节只描述一些概要信息,在介绍完multi-queue机制后(待整理),再对着代码细说submit_bio流程。 内核源码:linux-5.10.3 一、bio的提交(submit) 提交bio的函数是submit_bio,这个函数做完account工作统计读写量后,调用submit_bio_noacct进行

geshifei的博客 9693

BUCK降压斩波电路设计实战:从IGBT选型到续流二极管优化

本文详细解析了BUCK降压斩波电路的设计实战,从IGBT选型到续流二极管优化,涵盖了关键参数设计、调试技巧及电磁兼容问题解决方案。通过实际案例和数据对比,帮助工程师高效完成DC-DC转换电路设计,提升系统性能和可靠性。

weixin_29253711的博客 95

避坑指南:从聚宽迁移到QMT必须知道的5个细节(含Redis连接异常处理)

本文详细解析了从聚宽迁移到QMT平台时需注意的5个关键细节,特别是Redis连接池优化和xtquant重连机制。通过实战代码示例,帮助量化团队解决信号传递稳定性、时间同步、滑点控制等核心问题,确保交易策略平稳过渡。

weixin_27303545的博客 426

内核IO栈 | 反弹缓冲区、bio切分、完整性保护和蓄流

line17 ※1 反弹缓冲区blk_queue_bounce函数的作用是,尝试创建一个反弹缓冲区,通常是在bio给定的内存区域在外围设备不可达(例如高端内存上执行DMA)的地址时才需要去创建这么一个特殊的区域,以确保内存区域是外围设备可达的,这是bio能够完成的必要条件。static inline void blk_queue_bounce(struct request_queue *q, st...

marlos的博客 1265

OpenClaw Docker 安装问题解决全记录

控制面板无法加载(WebSocket 错误) | 确认 Nginx 配置中的 `Upgrade` 和 `Connection` 头正确 || 网关容器无法启动 | `docker compose logs openclaw-gateway`,检查权限、镜像名 |如需消除,使用 Let's Encrypt |- `<your-token>`:你的 OpenClaw 网关令牌(如 `b0e5ab...`)将 `<nas-ip>` 替换为你的群晖内网 IP(例如 `192.168.1.100`)。

u014490376的博客 2308

基于闪存写入特性构建勒索软件防御:FlashGuard项目原理与实现

在数据安全领域,勒索软件已成为企业和个人面临的主要威胁之一,其核心攻击手段是对存储设备上的文件进行加密覆盖。理解计算机存储系统的工作原理,特别是闪存(Flash Memory)的物理特性,是构建有效防御的基础。闪存具有“先擦后写”的固有约束,其写入和擦除操作以块为单位进行,且擦写次数有限,这一特性为数据保护提供了新的思路。从技术原理上看,通过监控和管理写入操作,可以在存储栈的底层实施访问控制,从而在恶意加密行为发生时进行阻断。这种硬件特性结合软件策略的方法,具有很高的技术价值,能够为关键数据提供一道底层防线

weixin_30294295的博客 577

HTOL集成电路老化测试学习总结-20250510

电子元器件的可靠性在产品开发中至关重要,尤其是在微电子技术迅速发展的今天。电子元器件的失效率通常遵循“浴盆曲线”,分为早期失效期、可使用期和老化期。为了评估芯片的可靠性,常采用高温操作寿命测试(HTOL),通过高温和高压加速芯片老化,以筛选出早期失效的芯片并评估其长期可靠性。HTOL测试中,芯片在高温下运行并监控其性能变化,通过阿伦尼乌斯公式计算加速因子,预测芯片的失效率和寿命。此外,Burn-in测试用于加速芯片老化,提前发现并剔除不合格产品,而早期失效率测试(ELFR)则用于评估产品初期使用阶段的可靠性

歌颂苦难的行为是无耻的; 2101

TIA博途中通过UDT实现IO地址映射到DB块中的具体方法

TIA博途中通过UDT实现IO地址映射到DB块中的具体方法

Robot_PLC_HMI_Automation 1万+

Storage Spaces Direct(S2D)的数据块映射

摘要: S2D(Storage Spaces Direct)的chunkmapping是其分布式存储系统的核心地址系统,负责将逻辑数据块映射到物理磁盘的分布式坐标。它采用三层虚拟化分层结构:上层为虚拟卷(CSVFS/VHDX),中层通过Slab/Chunk映射表(核心层,1GB粒度)分配副本策略与物理位置,底层为物理磁盘Extent。

徐火军博客 406

RH850 GreenHillSoftware Install

IDE一次链接到一个Compiler/Green Hills Debug Probe安装目录。如果您安装了IDE、编译器和绿山调试探测器软件,安装程序会将安装链接在一起。如果您只使用Advanced Install选项安装IDE,那么当您第一次启动它时,IDE会提示您链接到编译器和绿山调试探针安装*。如果您使用Advanced Install选项只安装编译器和绿山调试探针软件,安装程序会提示您将IDE的安装链接到新的编译器和绿山调试探针安装。

舊梦的博客 1212

计算型存储(Computational Storage)深度解析:SNIA架构、NVMe TP4073计算程序命令集、FPGA/ASIC引擎实现与AI盘内加速全链路

计算型存储将计算引擎集成到SSD内部,实现"数据不动、计算就近"。本文深度剖析SNIA CS架构(CSP/CSD/CSA、CSF/CSE/FDM模型),NVMe 2.0计算程序命令集(CSI=0x04、Execute Program、Program List LID 0x82),Linux内核驱动与SNIA CS API六阶段流程,对比FPGA/ASIC方案,解析FMS 2026趋势:认知文件系统、盘内ANN检索、GPU-SSD直连。

专注分享DPU、RDMA智能网卡/SSD(固态硬盘)产品测试与优化 492

AutoSAR MemMap模块详解及使用注意事项

在本文中,我们将深入探讨MemMap模块的实例解析,并介绍在使用该模块时需要注意的一些事项。在上述代码中,我们使用了MEMMAP_START_SEC_VAR_32BIT和MEMMAP_START_SEC_CODE来定义MemMap区域的起始位置,使用MEMMAP_STOP_SEC_VAR_32BIT和MEMMAP_STOP_SEC_CODE来定义MemMap区域的结束位置。1.合理划分MemMap区域:根据实际需求,合理划分MemMap区域,将不同类型的变量放置在不同的区域中,以便于管理和访问。

weixin_50547796的博客 2530

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

实战Linux内核块设备BIO--在内核中直接读写块设备

本文介绍了在Linux内核中使用BIO(Block I/O)机制直接对块设备进行读写操作的方法。首先,文章概述了BIO的基本概念及其优势,包括高效的数据管理和强大的I/O调度机制。接着,通过一个具体的内核模块示例,展示了如何创建BIO请求、设置请求参数、分配页面并复制数据至页面、将页面添加到BIO、提交BIO请求并等待完成等关键步骤。此外,还提供了一个测试脚本`test.sh`,该脚本自动完成了模块加载、测试文件创建及回环设备绑定等操作,便于验证内核模块的功能。

Linux内核研究者 1937

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

【问题实操】银河高级服务器操作系统实例分享,三台服务器宕机解决方式

【问题实操】银河高级服务器操作系统实例分享,三台服务器宕机解决方式。

银河麒麟操作系统的博客 1872

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

bio.c

1 bio_add_page 函数功能:把page加入bio的bio_vec链表 参数:bio-目标bio;page-待加入的page;len-加入长度;offset-在页中偏移地址;             注意到bio->bi_vec的结构,后三个参数用来给bio->bi_vec赋值 /*include/linux/blk_types.h*/ struct bio_vec {

cherry_hust的专栏 1485

linux bio bh关系,Linux BIO

blk_sysfs.cstaticstructqueue_sysfs_entryqueue_ra_entry={.attr={.name="read_ahead_kb",.mode=S_IRUGO|S_IWUSR},.show=queue_ra_show,.store=queue_ra_store,};staticstructqueue_sysfs_ent...

weixin_39800918的博客 295

Linux下wait_for_completion函数分析

先看个使用实例 init_completion(&tx_done_complete); wait_for_completion_timeout(&tx_done_complete,msecs_to_jiffies(100)); complete(&tx_done_complete); 分析围绕这三个函数进行 init_completion struct completion { unsigned int done; wait_queue_head_t wait; };

Android开发 3737

linux kernel中的wait_for_completion和complete总结

文章目录结构体init_completioncompletewait_for_completion 结构体 struct completion { unsigned int done; wait_queue_head_t wait; }; #define UINT_MAX (~0U) init_completion #define init_completion(x) __init_completion(x) static inline void __init_completion(struct

baron-周贺贺-代码改变世界ctw 7801
上一篇: 【io_uring】使用 io_uring 的高效 IO
下一篇: Linux Task 状态变迁
zs.w
博客等级 码龄4年 133粉丝 16原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zs.w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值