add_partition 函数学习

46 篇文章 4 订阅
  文章结尾绿色粗字体是本人添加的内容。

 add_partition,是添加磁盘分区信息的函数,负责向通用磁盘数据结构添加一个新的分区:
    通用磁盘数据结构如下:

  1. struct gendisk {
  2.     int major;            /* major number of driver */
  3.     int first_minor;
  4.     int minors; /* maximum number of minors, =for
  5.                                          * disks that can't be partitioned. */
  6.     char disk_name[32];        /* name of major driver */
  7.     struct hd_struct **part;    /* [indexed by minor] */
  8.     struct block_device_operations *fops;
  9.     struct request_queue *queue;
  10.     void *private_data;
  11.     sector_t capacity;

  12.     int flags;
  13.     struct device *driverfs_dev; // FIXME: remove
  14.     struct device dev;
  15.     struct kobject *holder_dir;
  16.     struct kobject *slave_dir;

  17.     struct timer_rand_state *random;
  18.     int policy;

  19.     atomic_t sync_io;        /* RAID */
  20.     unsigned long stamp;
  21.     int in_flight;
  22. #ifdef    CONFIG_SMP
  23.     struct disk_stats *dkstats;
  24. #else
  25.     struct disk_stats dkstats;
  26. #endif
  27.     struct work_struct async_notify;
  28. };
  29. vents *ev;
  30. #ifdef CONFIG_BLK_DEV_INTEGRITY
  31.     struct blk_integrity *integrity;
  32. #endif
  33.     int node_id;
  34. };
    蓝色的struct hd_struct类型 指向分区相关的数据区。看下下面的函数,

    首先是分配一个hd_struct类型的变量作为分区的结构:disk的第part个分区指针指向该结构。
  1. = kzalloc(sizeof(*p), GFP_KERNEL);
  1. disk->part[part-1] = p;
    然后是将分区的起始扇区赋值为入参start,扇区个数赋值为入参len,partno赋值为入参part。SCSI磁盘的分区个数最多为15个。

  1. void add_partition(struct gendisk *disk, int part, sector_t start, sector_t len, int flags)
  2. {
  3.     struct hd_struct *p;
  4.     int err;

  5.     p = kzalloc(sizeof(*p), GFP_KERNEL);
  6.     if (!p)
  7.         return;

  8.     if (!init_part_stats(p)) {
  9.         kfree(p);
  10.         return;
  11.     }
  12.     p->start_sect = start;
  13.     p->nr_sects = len;
  14.     p->partno = part;
  15.     p->policy = disk->policy;

  16.     if (isdigit(disk->dev.bus_id[strlen(disk->dev.bus_id)-1]))
  17.         snprintf(p->dev.bus_id, BUS_ID_SIZE,
  18.         "%sp%d", disk->dev.bus_id, part);
  19.     else
  20.         snprintf(p->dev.bus_id, BUS_ID_SIZE,
  21.              "%s%d", disk->dev.bus_id, part);

  22.     device_initialize(&p->dev);
  23.     p->dev.devt = MKDEV(disk->major, disk->first_minor + part);
  24.     p->dev.class = &block_class;
  25.     p->dev.type = &part_type;
  26.     p->dev.parent = &disk->dev;
  27.     disk->part[part-1] = p;

  28.     /* delay uevent until 'holders' subdir is created */
  29.     p->dev.uevent_suppress = 1;
  30.     device_add(&p->dev);
  31.     partition_sysfs_add_subdir(p);
  32.     p->dev.uevent_suppress = 0;
  33.     if (flags & ADDPART_FLAG_WHOLEDISK)
  34.         err = device_create_file(&p->dev, &dev_attr_whole_disk);

  35.     /* suppress uevent if the disk supresses it */
  36.     if (!disk->dev.uevent_suppress)
  37.         kobject_uevent(&p->dev.kobj, KOBJ_ADD);
  38. }

    接下来的问题是,每个磁盘有几个分区,每个分区的起始扇区是多少,每个分区的扇区个数是多少是怎么来的。

    我们先看下rescan_partitions 中添加分区的流程
  1. struct parsed_partitions *state;
 ...

  1. for (= 1; p < state->limit; p++) {
  2.         sector_t size = state->parts[p].size;
  3.         sector_t from = state->parts[p].from;
  4.         if (!size)
  5.             continue;
  6.         if (from + size > get_capacity(disk)) {
  7.             printk(" %s: p%d exceeds device capacity\n",
  8.                 disk->disk_name, p);
  9.         }
  10.         add_partition(disk, p, from, size, state->parts[p].flags);
  11. #ifdef CONFIG_BLK_DEV_MD
  12.         if (state->parts[p].flags & ADDPART_FLAG_RAID)
  13.             md_autodetect_dev(bdev->bd_dev+p);
  14. #endif
  15.     }
    我们看到,add_partition的入参比如分区起始扇区,分区扇区的个数等都是从struct parsed_partitions *state 获取到的,那么state指向的数据是从哪里来的呢,这个问题浮出水面,就到了检测分区的函数check_partition闪亮登场的时刻了。

  1. static struct parsed_partitions *
  2. check_partition(struct gendisk *hd, struct block_device *bdev)
  3. {
  4.     struct parsed_partitions *state;
  5.     int i, res, err;

  6.     state = kzalloc(sizeof(struct parsed_partitions), GFP_KERNEL);
  7.     if (!state)
  8.         return NULL;
  9.     state->pp_buf = (char *)__get_free_page(GFP_KERNEL);
  10.     if (!state->pp_buf) {
  11.         kfree(state);
  12.         return NULL;
  13.     }
  14.     state->pp_buf[0] = '\0';

  15.     state->bdev = bdev;
  16.     disk_name(hd, 0, state->name);
  17.     snprintf(state->pp_buf, PAGE_SIZE, " %s:", state->name);
  18.     if (isdigit(state->name[strlen(state->name)-1]))
  19.         sprintf(state->name, "p");

  20.     state->limit = disk_max_parts(hd);
  21.     i = res = err = 0;
  22.     while (!res && check_part[i]) {
  23.         memset(&state->parts, 0, sizeof(state->parts));
  24.         res = check_part[i++](state);
  25.         if (res < 0) {
  26.             /* We have hit an I/error which we don't report now.
  27.              * But record it, and let the others do their job.
  28.              */
  29.             err = res;
  30.             res = 0;
  31.         }

  32.     }
  33.     if (res > 0) {
  34.         printk(KERN_INFO "%s", state->pp_buf);

  35.         free_page((unsigned long)state->pp_buf);
  36.         return state;
  37.     }
  38.     if (state->access_beyond_eod)
  39.         err = -ENOSPC;
  40.     if (err)
  41.     /* The partition is unrecognized. So report I/O errors if there were any */
  42.         res = err;
  43.     if (!res)
  44.         strlcat(state->pp_buf, " unknown partition table\n", PAGE_SIZE);
  45.     else if (warn_no_part)
  46.         strlcat(state->pp_buf, " unable to read partition table\n", PAGE_SIZE);

  47.     printk(KERN_INFO "%s", state->pp_buf);

  48.     free_page((unsigned long)state->pp_buf);
  49.     kfree(state);
  50.     return ERR_PTR(res);
  51. }
    很吓人是不?原理起始很简单,最关键语句是绿色的那一句。我们知道有很多种类型的磁盘,不可能存在一个处理函数,能够解析出所有磁盘分区信息。那怎么办呢。神农 尝百草,一个一个的实验。我们有个函数数组 check_part,每种函数解析一种磁盘分区信息。那个函数能成功的解析磁盘分区信息,就不需要继续尝试了,可以打完收工了。当然解析出来的数据 存放在struct parsed_partitions 类型的结构体state中。

    那么神农尝百草,那么一共有多少种草呢?
    看下函数数组的定义:
  1. static int (*check_part[])(struct parsed_partitions *, struct block_device *) = {
  2.     /*
  3.      * Probe partition formats with tables at disk address 0
  4.      * that also have an ADFS boot block at 0xdc0.
  5.      */
  6. #ifdef CONFIG_ACORN_PARTITION_ICS
  7.     adfspart_check_ICS,
  8. #endif
  9. #ifdef CONFIG_ACORN_PARTITION_POWERTEC
  10.     adfspart_check_POWERTEC,
  11. #endif
  12. #ifdef CONFIG_ACORN_PARTITION_EESOX
  13.     adfspart_check_EESOX,
  14. #endif

  15.     /*
  16.      * Now move on to formats that only have partition info at
  17.      * disk address 0xdc0. Since these may also have stale
  18.      * PC/BIOS partition tables, they need to come before
  19.      * the msdos entry.
  20.      */
  21. #ifdef CONFIG_ACORN_PARTITION_CUMANA
  22.     adfspart_check_CUMANA,
  23. #endif
  24. #ifdef CONFIG_ACORN_PARTITION_ADFS
  25.     adfspart_check_ADFS,
  26. #endif

  27. #ifdef CONFIG_EFI_PARTITION
  28.     efi_partition,        /* this must come before msdos */
  29. #endif
  30. #ifdef CONFIG_SGI_PARTITION
  31.     sgi_partition,
  32. #endif
  33. #ifdef CONFIG_LDM_PARTITION
  34.     ldm_partition,        /* this must come before msdos */
  35. #endif
  36. #ifdef CONFIG_MSDOS_PARTITION
  37.     msdos_partition,
  38. #endif
  39. #ifdef CONFIG_OSF_PARTITION
  40.     osf_partition,
  41. #endif
  42. #ifdef CONFIG_SUN_PARTITION
  43.     sun_partition,
  44. #endif
  45. #ifdef CONFIG_AMIGA_PARTITION
  46.     amiga_partition,
  47. #endif
  48. #ifdef CONFIG_ATARI_PARTITION
  49.     atari_partition,
  50. #endif
  51. #ifdef CONFIG_MAC_PARTITION
  52.     mac_partition,
  53. #endif
  54. #ifdef CONFIG_ULTRIX_PARTITION
  55.     ultrix_partition,
  56. #endif
  57. #ifdef CONFIG_IBM_PARTITION
  58.     ibm_partition,
  59. #endif
  60. #ifdef CONFIG_KARMA_PARTITION
  61.     karma_partition,
  62. #endif
  63. #ifdef CONFIG_SYSV68_PARTITION
  64.     sysv68_partition,
  65. #endif
  66.     NULL
  67. };
    这么多函数,来自五湖四海,就是为了共同的目的,封装到了一起,就是为了获取分区的信息。把获取到信息存入结构体struct parsed_partitions *state;包含 分区的起始扇区,分区扇区的个数,flag等信息。
    定义如下:
  1. struct parsed_partitions {
  2.     char name[BDEVNAME_SIZE];
  3.     struct {
  4.         sector_t from;
  5.         sector_t size;
  6.         int flags;
  7.     } parts[MAX_PART];
  8.     int next;
  9.     int limit;
  10. };

参考文献:
1 Linux 那些事儿
2 Linux Kernel Source Code

一、添加块设备分区
块设备及分区:
手机系统中一个mmc对应一个块设备/dev/block/mmcblk0 ,
在一个mmc块设备基础上可以逻辑上可以分出多个分区/dev/block/mmcblk0pXX。就相当于PC硬盘上,C盘,D盘,E、F多个盘符。
如下:
root@xxx:/dev/block # ls
bootdevice
mmcblk0
mmcblk0p1
mmcblk0p10
mmcblk0p11
mmcblk0p12
mmcblk0p13
mmcblk0p14
mmcblk0p15
mmcblk0p16
mmcblk0p17
mmcblk0p18
mmcblk0p19
mmcblk0p2
mmcblk0p20
mmcblk0p21
mmcblk0p22
mmcblk0p23
mmcblk0p24
mmcblk0p25
mmcblk0p26
mmcblk0p27
mmcblk0p28
mmcblk0p3
mmcblk0p4
mmcblk0p5
mmcblk0p6
mmcblk0p7
mmcblk0p8
mmcblk0p9
mmcblk0rpmb

软件调用流程如下:
<4>[    7.189711] [<c0326f7c>] (check_partition+0xa8/0x1f4) from [<c0326c60>] (rescan_partitions+0x78/0x27c)
<4>[    7.189722] [<c0326c60>] (rescan_partitions+0x78/0x27c) from [<c022115c>] (__blkdev_get+0x14c/0x350)
<4>[    7.189733] [<c022115c>] (__blkdev_get+0x14c/0x350) from [<c02214b8>] (blkdev_get+0x158/0x2c8)
<4>[    7.189742] [<c02214b8>] (blkdev_get+0x158/0x2c8) from [<c03249d0>] (add_disk+0x270/0x414)
<4>[    7.189753] [<c03249d0>] (add_disk+0x270/0x414) from [<c067b3e8>] (mmc_add_disk+0x18/0x1fc)
<4>[    7.189766] [<c067b3e8>] (mmc_add_disk+0x18/0x1fc) from [<c067cb30>] (mmc_blk_probe+0x290/0x2c0)
<4>[    7.189778] [<c067cb30>] (mmc_blk_probe+0x290/0x2c0) from [<c066d620>] (mmc_bus_probe+0x14/0x18)
<4>[    7.189792] [<c066d620>] (mmc_bus_probe+0x14/0x18) from [<c045315c>] (driver_probe_device+0x12c/0x33c)
<4>[    7.189803] [<c045315c>] (driver_probe_device+0x12c/0x33c) from [<c04515d0>] (bus_for_each_drv+0x48/0x8c)
<4>[    7.189814] [<c04515d0>] (bus_for_each_drv+0x48/0x8c) from [<c0452fb8>] (device_attach+0x64/0x88)
<4>[    7.189824] [<c0452fb8>] (device_attach+0x64/0x88) from [<c04524ac>] (bus_probe_device+0x28/0x98)
<4>[    7.189835] [<c04524ac>] (bus_probe_device+0x28/0x98) from [<c0450b44>] (device_add+0x400/0x5d0)
<4>[    7.189846] [<c0450b44>] (device_add+0x400/0x5d0) from [<c066dd28>] (mmc_add_card+0x22c/0x33c)
<4>[    7.189856] [<c066dd28>] (mmc_add_card+0x22c/0x33c) from [<c0671a14>] (mmc_attach_mmc+0x134/0x1d0)
<4>[    7.189866] [<c0671a14>] (mmc_attach_mmc+0x134/0x1d0) from [<c066c928>] (mmc_rescan+0x2a0/0x314)
<4>[    7.189878] [<c066c928>] (mmc_rescan+0x2a0/0x314) from [<c01381bc>] (process_one_work+0x254/0x448)

手机系统使用到两种磁盘分区类型:MBR和GPT,
MBR和GPT对应的config:CONFIG_EFI_PARTITION、CONFIG_MSDOS_PARTITION默认都是打开的。
gpt分区的检查函数及结构体如下(efi.c),重点请看find_valid_gpt及for循环。
/**
 * efi_partition(struct parsed_partitions *state)
 * @state
 *
 * Description: called from check.c, if the disk contains GPT
 * partitions, sets up partition entries in the kernel.
 *
 * If the first block on the disk is a legacy MBR,
 * it will get handled by msdos_partition().
 * If it's a Protective MBR, we'll handle it here.
 *
 * We do not create a Linux partition for GPT, but
 * only for the actual data partitions.
 * Returns:
 * -1 if unable to read the partition table
 *  0 if this isn't our partition table
 *  1 if successful
 *
 */
int efi_partition(struct parsed_partitions *state)
{
gpt_header *gpt = NULL;
gpt_entry *ptes = NULL;
u32 i;
unsigned ssz = bdev_logical_block_size(state->bdev) / 512;

if (!find_valid_gpt(state, &gpt, &ptes) || !gpt || !ptes) {
kfree(gpt);
kfree(ptes);
return 0;
}

pr_debug("GUID Partition Table is valid!  Yea!\n");

for (i = 0; i < le32_to_cpu(gpt->num_partition_entries) && i < state->limit-1; i++) {
struct partition_meta_info *info;
unsigned label_count = 0;
unsigned label_max;
u64 start = le64_to_cpu(ptes[i].starting_lba);
u64 size = le64_to_cpu(ptes[i].ending_lba) -
  le64_to_cpu(ptes[i].starting_lba) + 1ULL;

if (!is_pte_valid(&ptes[i], last_lba(state->bdev)))
continue;

put_partition(state, i+1, start * ssz, size * ssz);

/* If this is a RAID volume, tell md */
if (!efi_guidcmp(ptes[i].partition_type_guid,
PARTITION_LINUX_RAID_GUID))
state->parts[i + 1].flags = ADDPART_FLAG_RAID;

info = &state->parts[i + 1].info;
efi_guid_unparse(&ptes[i].unique_partition_guid, info->uuid);

/* Naively convert UTF16-LE to 7 bits. */
label_max = min(sizeof(info->volname) - 1,
sizeof(ptes[i].partition_name));
info->volname[label_max] = 0;
while (label_count < label_max) {
u8 c = ptes[i].partition_name[label_count] & 0xff;
if (c && !isprint(c))
c = '!';
info->volname[label_count] = c;
label_count++;
}
state->parts[i + 1].has_info = true;
}
kfree(ptes);
kfree(gpt);
strlcat(state->pp_buf, "\n", PAGE_SIZE);
return 1;
}
//gpt 的头信息
typedef struct _gpt_header {
__le64 signature;
__le32 revision;
__le32 header_size;
__le32 header_crc32;
__le32 reserved1;
__le64 my_lba;
__le64 alternate_lba;
__le64 first_usable_lba;
__le64 last_usable_lba;
efi_guid_t disk_guid;
__le64 partition_entry_lba;
__le32 num_partition_entries;
__le32 sizeof_partition_entry;
__le32 partition_entry_array_crc32;

/* The rest of the logical block is reserved by UEFI and must be zero.
* EFI standard handles this by:
*
* uint8_t reserved2[ BlockSize - 92 ];
*/
} __attribute__ ((packed)) gpt_header;

//每个gpt实体,对应一个partition
typedef struct _gpt_entry {
efi_guid_t partition_type_guid;
efi_guid_t unique_partition_guid;
__le64 starting_lba;
__le64 ending_lba;
gpt_entry_attributes attributes;
efi_char16_t partition_name[72 / sizeof (efi_char16_t)];
} __attribute__ ((packed)) gpt_entry;

mbr分区的检查函数及结构体如下(msdoc.c),
int msdos_partition(struct parsed_partitions *state)
{
sector_t sector_size = bdev_logical_block_size(state->bdev) / 512;
Sector sect;
unsigned char *data;
struct partition *p;
struct fat_boot_sector *fb;
int slot;
u32 disksig;

data = read_part_sector(state, 0, &sect);
if (!data)
return -1;
/*
* Note order! (some AIX disks, e.g. unbootable kind,
* have no MSDOS 55aa)
*/
if (aix_magic_present(state, data)) {
put_dev_sector(sect);
strlcat(state->pp_buf, " [AIX]", PAGE_SIZE);
return 0;
}

if (!msdos_magic_present(data + 510)) {
put_dev_sector(sect);
return 0;
}

/*
* Now that the 55aa signature is present, this is probably
* either the boot sector of a FAT filesystem or a DOS-type
* partition table. Reject this in case the boot indicator
* is not 0 or 0x80.
*/
p = (struct partition *) (data + 0x1be);
for (slot = 1; slot <= 4; slot++, p++) {
if (p->boot_ind != 0 && p->boot_ind != 0x80) {
/*
* Even without a valid boot inidicator value
* its still possible this is valid FAT filesystem
* without a partition table.
*/
fb = (struct fat_boot_sector *) data;
if (slot == 1 && fb->reserved && fb->fats
&& fat_valid_media(fb->media)) {
strlcat(state->pp_buf, "\n", PAGE_SIZE);
put_dev_sector(sect);
return 1;
} else {
put_dev_sector(sect);
return 0;
}
}
}
#ifdef CONFIG_EFI_PARTITION
p = (struct partition *) (data + 0x1be);
for (slot = 1 ; slot <= 4 ; slot++, p++) {
/* If this is an EFI GPT disk, msdos should ignore it. */
if (SYS_IND(p) == EFI_PMBR_OSTYPE_EFI_GPT) {
put_dev_sector(sect);
return 0;
}
}
#endif
p = (struct partition *) (data + 0x1be);

disksig = le32_to_cpup((__le32 *)(data + 0x1b8));

/*
* Look for partitions in two passes:
* First find the primary and DOS-type extended partitions.
* On the second pass look inside *BSD, Unixware and Solaris partitions.
*/

state->next = 5;
for (slot = 1 ; slot <= 4 ; slot++, p++) {
sector_t start = start_sect(p)*sector_size;
sector_t size = nr_sects(p)*sector_size;
if (!size)
continue;
if (is_extended_partition(p)) {
/*
* prevent someone doing mkfs or mkswap on an
* extended partition, but leave room for LILO
* FIXME: this uses one logical sector for > 512b
* sector, although it may not be enough/proper.
*/
sector_t n = 2;
n = min(size, max(sector_size, n));
put_partition(state, slot, start, n);

strlcat(state->pp_buf, " <", PAGE_SIZE);
parse_extended(state, start, size, disksig);
strlcat(state->pp_buf, " >", PAGE_SIZE);
continue;
}
put_partition(state, slot, start, size);
set_info(state, slot, disksig);
if (SYS_IND(p) == LINUX_RAID_PARTITION)
state->parts[slot].flags = ADDPART_FLAG_RAID;
if (SYS_IND(p) == DM6_PARTITION)
strlcat(state->pp_buf, "[DM]", PAGE_SIZE);
if (SYS_IND(p) == EZD_PARTITION)
strlcat(state->pp_buf, "[EZD]", PAGE_SIZE);
}
strlcat(state->pp_buf, "\n", PAGE_SIZE);
/* second pass - output for each on a separate line */
p = (struct partition *) (0x1be + data);
for (slot = 1 ; slot <= 4 ; slot++, p++) {
unsigned char id = SYS_IND(p);
int n;
if (!nr_sects(p))
continue;

for (n = 0; subtypes[n].parse && id != subtypes[n].id; n++)
;
if (!subtypes[n].parse)
continue;
subtypes[n].parse(state, start_sect(p) * sector_size,
 nr_sects(p) * sector_size, slot);
}
put_dev_sector(sect);
return 1;
}

struct partition {
unsigned char boot_ind;/* 0x80 - active */
unsigned char head;/* starting head */
unsigned char sector;/* starting sector */
unsigned char cyl;/* starting cylinder */
unsigned char sys_ind;/* What partition type */
unsigned char end_head;/* end head */
unsigned char end_sector;/* end sector */
unsigned char end_cyl;/* end cylinder */
__le32 start_sect;/* starting sector counting from 0 */
__le32 nr_sects;/* nr of sectors in partition */
} __attribute__((packed));

struct fat_boot_sector {
__u8 ignored[3];/* Boot strap short or near jump */
__u8 system_id[8];/* Name - can be used to special case
  partition manager volumes */
__u8 sector_size[2];/* bytes per logical sector */
__u8 sec_per_clus;/* sectors/cluster */
__le16 reserved; /* reserved sectors */
__u8 fats;/* number of FATs */
__u8 dir_entries[2];/* root directory entries */
__u8 sectors[2];/* number of sectors */
__u8 media;/* media code */
__le16 fat_length; /* sectors/FAT */
__le16 secs_track; /* sectors per track */
__le16 heads; /* number of heads */
__le32 hidden; /* hidden sectors (unused) */
__le32 total_sect; /* number of sectors (if sectors == 0) */

union {
struct {
/*  Extended BPB Fields for FAT16 */
__u8 drive_number;/* Physical drive number */
__u8 state;/* undocumented, but used
  for mount state. */
/* other fiealds are not added here */
} fat16;

struct {
/* only used by FAT32 */
__le32 length; /* sectors/FAT */
__le16 flags; /* bit 8: fat mirroring,
  low 4: active fat */
__u8 version[2];/* major, minor filesystem
  version */
__le32 root_cluster; /* first cluster in
  root directory */
__le16 info_sector; /* filesystem info sector */
__le16 backup_boot; /* backup boot sector */
__le16 reserved2[6]; /* Unused */
/* Extended BPB Fields for FAT32 */
__u8 drive_number;   /* Physical drive number */
__u8    state;       /* undocumented, but used
  for mount state. */
/* other fiealds are not added here */
} fat32;
};
};

参考文献:
http://www.uefi.org/specs/
UEFI_2_4_Errata_B.pdf
80-N7350-1_B_GUID_Partition_Tables_Programming.pdf

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值