FreeFlyOS【十六】:file部分详解

本文详细介绍了FreeFlyOS的文件系统实现,包括主分区的初始化、打开/关闭文件、写/读文件、删除文件、创建目录、删除目录以及更改和获取当前工作目录的操作。整个文件系统基于《操作系统真相还原》中的ext2实现,通过分析硬盘分区、超级块和inode等结构,阐述了文件系统的内部工作原理。
摘要由CSDN通过智能技术生成

bitmap.h

#ifndef _BITMAP_H_
#define _BITMAP_H_
/* 
** 在遍历位图时,整体上以字节为单位,细节上是以位为单位,
**     所以此处位图的指针必须是单字节 
*/
struct bitmap {
   unsigned int btmp_bytes_len;
   unsigned char* bits;
};
#endif

dir.c

#include "dir.h"
#include "inode.h"
#include "file.h"
#include "../mem/vmm.h"
#include "../asm/asm.h"
#include "../debug/debug.h"
#define NULL (void *)0
#define BITMAP_MASK 1

struct dir *root_dir;             // 根目录
extern struct partition *cur_part;

/*
** 打开根目录 
*/
void 
open_root_dir(struct partition* part) {
   root_dir=(struct dir *)vmm_malloc(sizeof(struct dir),2);
   root_dir->inode = inode_open(part, part->sb->root_inode_no);
   root_dir->dir_pos = 0;
}

/* 在分区part上打开i结点为inode_no的目录并返回目录指针 */
struct dir* 
dir_open(struct partition* part, unsigned int inode_no) {
   struct dir* pdir = (struct dir*)vmm_malloc(sizeof(struct dir),2);
   pdir->inode = inode_open(part, inode_no);
   pdir->dir_pos = 0;
   return pdir;
}

/* 在part分区内的pdir目录内寻找名为name的文件或目录,
 * 找到后返回1并将其目录项存入dir_e,否则返回0 */
char 
search_dir_entry(struct partition* part, struct dir* pdir,
const char* name, struct dir_entry* dir_e) {
   unsigned int block_cnt = 140;	 // 12个直接块+128个一级间接块=140块

   /* 12个直接块大小+128个间接块,共560字节 */
   unsigned int* all_blocks = (unsigned int*)vmm_malloc(48 + 512,2);
   if (all_blocks == NULL) {
      printk("search_dir_entry: sys_malloc for all_blocks failed");
      return 0;
   }

   unsigned int block_idx = 0;
   while (block_idx < 12) {
      all_blocks[block_idx] = pdir->inode->i_sectors[block_idx];
      block_idx++;
   }
   block_idx = 0;

   if (pdir->inode->i_sectors[12] != 0) {	// 若含有一级间接块表
      ide_read(all_blocks + 12, pdir->inode->i_sectors[12] , 1);
   }
/* 至此,all_blocks存储的是该文件或目录的所有扇区地址 */

   /* 写目录项的时候已保证目录项不跨扇区,
    * 这样读目录项时容易处理, 只申请容纳1个扇区的内存 */
   unsigned char* buf = (unsigned char*)vmm_malloc(SECTSIZE,2);
   struct dir_entry* p_de = (struct dir_entry*)buf;	    // p_de为指向目录项的指针,值为buf起始地址
   unsigned int dir_entry_size = part->sb->dir_entry_size;
   unsigned int dir_entry_cnt = SECTSIZE / dir_entry_size;   // 1扇区内可容纳的目录项个数

   /* 开始在所有块中查找目录项 */
   while (block_idx < block_cnt) {		  
   /* 块地址为0时表示该块中无数据,继续在其它块中找 */
      if (all_blocks[block_idx] == 0) {
        block_idx++;
        continue;
      }
      ide_read(buf, all_blocks[block_idx], 1);
      
      unsigned int dir_entry_idx = 0;
      /* 遍历扇区中所有目录项 */
      while (dir_entry_idx < dir_entry_cnt) {
	   /* 若找到了,就直接复制整个目录项 */
         if (!strcmp(p_de->filename, name)) {
            memcpy(dir_e, p_de, dir_entry_size);
            vmm_free(buf,SECTSIZE);
            vmm_free(all_blocks,48 + 512);
            return 1;
         }
         dir_entry_idx++;
         p_de++;
      }
      block_idx++;
      p_de = (struct dir_entry*)buf;  // 此时p_de已经指向扇区内最后一个完整目录项了,需要恢复p_de指向为buf
      memset(buf, 0, SECTSIZE);	  // 将buf清0,下次再用
   }
	vmm_free(buf,SECTSIZE);
	vmm_free(all_blocks,48 + 512);
   return 0;
}

/* 关闭目录 */
void 
dir_close(struct dir* dir) {
/*************      根目录不能关闭     ***************
 *1 根目录自打开后就不应该关闭,否则还需要再次open_root_dir();
 *2 root_dir所在的内存是低端1M之内,并非在堆中,free会出问题 */
   if (dir == root_dir) {
   /* 不做任何处理直接返回*/
      return;
   }
   inode_close(dir->inode);
   vmm_free(dir,sizeof(struct dir));
}

/* 在内存中初始化目录项p_de */
void 
create_dir_entry(char* filename, unsigned int inode_no, 
unsigned char file_type, struct dir_entry* p_de) {
   ASSERT(strlen(filename) <=  MAX_FILE_NAME_LEN);

   /* 初始化目录项 */
   memcpy(p_de->filename, filename, strlen(filename));
   p_de->i_no = inode_no;
   p_de->f_type = file_type;
}

/* 将目录项p_de写入父目录parent_dir中,io_buf由主调函数提供 */
char sync_dir_entry(struct dir* parent_dir, struct dir_entry* p_de, void* io_buf) {
   struct inode* dir_inode = parent_dir->inode;
   unsigned int dir_size = dir_inode->i_size;
   unsigned int dir_entry_size = cur_part->sb->dir_entry_size;

   ASSERT(dir_size % dir_entry_size == 0);	 // dir_size应该是dir_entry_size的整数倍

   unsigned int dir_entrys_per_sec = (512 / dir_entry_size);       // 每扇区最大的目录项数目
   int block_lba = -1;

   /* 将该目录的所有扇区地址(12个直接块+ 128个间接块)存入all_blocks */
   unsigned char block_idx = 0;
   unsigned int all_blocks[140] = {0};	  // all_blocks保存目录所有的块

   /* 将12个直接块存入all_blocks */
   while (block_idx < 12) {
      all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
      block_idx++;
   }

   struct dir_entry* dir_e = (struct dir_entry*)io_buf;	       // dir_e用来在io_buf中遍历目录项
   int block_bitmap_idx = -1;

   /* 开始遍历所有块以寻找目录项空位,若已有扇区中没有空闲位,
    * 在不超过文件大小的情况下申请新扇区来存储新目录项 */
   block_idx = 0;
   while (block_idx < 140) {  
      // 文件(包括目录)最大支持12个直接块+128个间接块=140个块
      block_bitmap_idx = -1;
      if (all_blocks[block_idx] == 0) {   
         // 在三种情况下分配块
         block_lba = block_bitmap_alloc(cur_part);

         if (block_lba == -1) {
            printk("alloc block bitmap for sync_dir_entry failed\n");
            return 0;
	      }

         /* 每分配一个块就同步一次block_bitmap */
	      block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
	      ASSERT(block_bitmap_idx != -1);
	      bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

	      block_bitmap_idx = -1;
	      if (block_idx < 12) {	    // 若是直接块
	         dir_inode->i_sectors[block_idx] = all_blocks[block_idx] = block_lba;
	      } 
         else if (block_idx == 12) {	  // 若是尚未分配一级间接块表(block_idx等于12表示第0个间接块地址为0)
	         dir_inode->i_sectors[12] = block_lba;       // 将上面分配的块做为一级间接块表地址
            block_lba = -1;
            block_lba = block_bitmap_alloc(cur_part);	       // 再分配一个块做为第0个间接块
            
            if (block_lba == -1) {
               block_bitmap_idx = dir_inode->i_sectors[12] - cur_part->sb->data_start_lba;
               unsigned int byte_idx = block_bitmap_idx / 8;    // 向下取整用于索引数组下标
               unsigned int bit_odd  = block_bitmap_idx % 8;    // 取余用于索引数组内的位
               cur_part->block_bitmap.bits[byte_idx]&= ~ (BITMAP_MASK << bit_odd); //将空闲位置0
               dir_inode->i_sectors[12] = 0;
               printk("alloc block bitmap for sync_dir_entry failed\n");
               return 0;
            }
         /* 每分配一个块就同步一次block_bitmap */
            block_bitmap_idx = block_lba - cur_part->sb->data_start_lba;
            ASSERT(block_bitmap_idx != -1);
            bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

            all_blocks[12] = block_lba;
            /* 把新分配的第0个间接块地址写入一级间接块表 */
            ide_write(all_blocks + 12, dir_inode->i_sectors[12] , 1);
         } 
         else {	   
            // 若是间接块未分配
            all_blocks[block_idx] = block_lba;
            /* 把新分配的第(block_idx-12)个间接块地址写入一级间接块表 */
            ide_write(all_blocks + 12, dir_inode->i_sectors[12], 1);
         }
         /* 再将新目录项p_de写入新分配的间接块 */
         memset(io_buf, 0, 512);
         memcpy(io_buf, p_de, dir_entry_size);
         ide_write(io_buf, all_blocks[block_idx], 1);
         dir_inode->i_size += dir_entry_size;
         return 1;
      }
      /* 若第block_idx块已存在,将其读进内存,然后在该块中查找空目录项 */
      ide_read(io_buf, all_blocks[block_idx] , 1); 
      /* 在扇区内查找空目录项 */
      unsigned char dir_entry_idx = 0;
      while (dir_entry_idx < dir_entrys_per_sec) {
         if ((dir_e + dir_entry_idx)->f_type == FT_UNKNOWN) {	// FT_UNKNOWN为0,无论是初始化或是删除文件后,都会将f_type置为FT_UNKNOWN.
            memcpy(dir_e + dir_entry_idx, p_de, dir_entry_size);    
            ide_write(io_buf, all_blocks[block_idx], 1);

            dir_inode->i_size += dir_entry_size;
            return 1;
	      }
	      dir_entry_idx++;
      }
      block_idx++;
   }   
   printk("directory is full!\n");
   return 0;
}

/* 把分区part目录pdir中编号为inode_no的目录项删除 */
char delete_dir_entry(struct partition* part, struct dir* pdir, unsigned int inode_no, void* io_buf) {
   struct inode* dir_inode = pdir->inode;
   unsigned int block_idx = 0, all_blocks[140] = {0};
   unsigned int byte_idx;
   unsigned int bit_odd;
   /* 收集目录全部块地址 */
   while (block_idx < 12) {
      all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
      block_idx++;
   }
   if (dir_inode->i_sectors[12]) {
      ide_read(all_blocks + 12, dir_inode->i_sectors[12],  1);
   }

   /* 目录项在存储时保证不会跨扇区 */
   unsigned int dir_entry_size = part->sb->dir_entry_size;
   unsigned int dir_entrys_per_sec = (SEC_SIZE / dir_entry_size);       // 每扇区最大的目录项数目
   struct dir_entry* dir_e = (struct dir_entry*)io_buf;   
   struct dir_entry* dir_entry_found = NULL;
   unsigned char dir_entry_idx, dir_entry_cnt;
   char is_dir_first_block = 0;     // 目录的第1个块 

   /* 遍历所有块,寻找目录项 */
   block_idx = 0;
   while (block_idx < 140) {
      is_dir_first_block = 0;
      if (all_blocks[block_idx] == 0) {
	      block_idx++;
	      continue;
      }
      dir_entry_idx = dir_entry_cnt = 0;
      memset(io_buf, 0, SEC_SIZE);
      /* 读取扇区,获得目录项 */
      ide_read(io_buf, all_blocks[block_idx],  1);

      /* 遍历所有的目录项,统计该扇区的目录项数量及是否有待删除的目录项 */
      while (dir_entry_idx < dir_entrys_per_sec) {
	      if ((dir_e + dir_entry_idx)->f_type != FT_UNKNOWN) {
	         if (!strcmp((dir_e + dir_entry_idx)->filename, ".")) { 
	            is_dir_first_block = 1;
	         } 
            else if (strcmp((dir_e + dir_entry_idx)->filename, ".") 
            && strcmp((dir_e + dir_entry_idx)->filename, "..")) {
	            dir_entry_cnt++;     // 统计此扇区内的目录项个数,用来判断删除目录项后是否回收该扇区
	            if ((dir_e + dir_entry_idx)->i_no == inode_no) {	  // 如果找到此i结点,就将其记录在dir_entry_found
                  ASSERT(dir_entry_found == NULL);  // 确保目录中只有一个编号为inode_no的inode,找到一次后dir_entry_found就不再是NULL
                  dir_entry_found = dir_e + dir_entry_idx;
		  /* 找到后也继续遍历,统计总共的目录项数 */
	            }
	         }
	      }
	      dir_entry_idx++;
      } 

      /* 若此扇区未找到该目录项,继续在下个扇区中找 */
      if (dir_entry_found == NULL) {
         block_idx++;
         continue;
      }

      /* 在此扇区中找到目录项后,清除该目录项并判断是否回收扇区,随后退出循环直接返回 */
      ASSERT(dir_entry_cnt >= 1);
      /* 除目录第1个扇区外,若该扇区上只有该目录项自己,则将整个扇区回收 */
      if (dir_entry_cnt == 1 && !is_dir_first_block) {
         /* a 在块位图中回收该块 */
         unsigned int block_bitmap_idx = all_blocks[block_idx] - part->sb->data_start_lba;
         byte_idx = block_bitmap_idx / 8;
         bit_odd  = block_bitmap_idx % 8;
         part->block_bitmap.bits[byte_idx] &= ~(1 << bit_odd);
         bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);

         /* b 将块地址从数组i_sectors或索引表中去掉 */
         if (block_idx < 12) {
            dir_inode->i_sectors[block_idx] = 0;
         } else {    // 在一级间接索引表中擦除该间接块地址
         /*先判断一级间接索引表中间接块的数量,如果仅有这1个间接块,连同间接索引表所在的块一同回收 */
            unsigned int indirect_blocks = 0;
            unsigned int indirect_block_idx = 12;
            while (indirect_block_idx < 140) {
               if (all_blocks[indirect_block_idx] != 0) {
                  indirect_blocks++;
               }
            }
            ASSERT(indirect_blocks >= 1);  // 包括当前间接块

            if (indirect_blocks > 1) {	  // 间接索引表中还包括其它间接块,仅在索引表中擦除当前这个间接块地址
               all_blocks[block_idx] = 0; 
               ide_write(all_blocks + 12, dir_inode->i_sectors[12], 1); 
            } else {	// 间接索引表中就当前这1个间接块,直接把间接索引表所在的块回收,然后擦除间接索引表块地址
               /* 回收间接索引表所在的块 */
               block_bitmap_idx = dir_inode->i_sectors[12] - part->sb->data_start_lba;
               byte_idx = block_bitmap_idx / 8;
               bit_odd  = block_bitmap_idx % 8;
               part->block_bitmap.bits[byte_idx] &= ~(1 << bit_odd);
               bitmap_sync(cur_part, block_bitmap_idx, BLOCK_BITMAP);
               
               /* 将间接索引表地址清0 */
               dir_inode->i_sectors[12] = 0;
            }
         }
      } else { // 仅将该目录项清空
            memset(dir_entry_found, 0, dir_entry_size);
            ide_write(io_buf,  all_blocks[block_idx], 1);
         }

      /* 更新i结点信息并同步到硬盘 */
      ASSERT(dir_inode->i_size >= dir_entry_size);
      dir_inode->i_size -= dir_entry_size;
      memset(io_buf, 0, SEC_SIZE * 2);
      inode_sync(part, dir_inode, io_buf);

      return 1;
   }
   /* 所有块中未找到则返回false,若出现这种情况应该是serarch_file出错了 */
   return 0;
}
/* 读取目录,成功返回1个目录项,失败返回NULL */
struct dir_entry* dir_read(struct dir* dir) {
   struct dir_entry* dir_e = (struct dir_entry*)dir->dir_buf;
   struct inode* dir_inode = dir->inode; 
   unsigned int all_blocks[140] = {0}, block_cnt = 12;
   unsigned int block_idx = 0, dir_entry_idx = 0;
   while (block_idx < 12) {
      all_blocks[block_idx] = dir_inode->i_sectors[block_idx];
      block_idx++;
   }
   if (dir_inode->i_sectors[12] != 0) {	     // 若含有一级间接块表
      ide_read(all_blocks + 12, dir_inode->i_sectors[12],  1);
      block_cnt = 140;
   }
   block_idx = 0;

   unsigned int cur_dir_entry_pos = 0;	  // 当前目录项的偏移,此项用来判断是否是之前已经返回过的目录项
   unsigned int dir_entry_size = cur_part->sb->dir_entry_size;
   unsigned int dir_entrys_per_sec = SEC_SIZE / dir_entry_size;	 // 1扇区内可容纳的目录项个数
   /* 因为此目录内可能删除了某些文件或子目录,所以要遍历所有块 */
   while (block_idx < block_cnt) {
      if (dir->dir_pos >= dir_inode->i_size) {
	      return NULL;
      }
      if (all_blocks[block_idx] == 0) {     // 如果此块地址为0,即空块,继续读出下一块
	      block_idx++;
	      continue;
      }
      memset(dir_e, 0, SEC_SIZE);
      ide_read(dir_e, all_blocks[block_idx],  1);
      dir_entry_idx = 0;
      /* 遍历扇区内所有目录项 */
      while (dir_entry_idx < dir_entrys_per_sec) {
	      if ((dir_e + dir_entry_idx)->f_type) {	 // 如果f_type不等于0,即不等于FT_UNKNOWN
	      /* 判断是不是最新的目录项,避免返回曾经已经返回过的目录项 */
	         if (cur_dir_entry_pos < dir->dir_pos) {
	            cur_dir_entry_pos += dir_entry_size;
	            dir_entry_idx++;
	            continue;
	         }
	         ASSERT(cur_dir_entry_pos == dir->dir_pos);
            dir->dir_pos += dir_entry_size;	      // 更新为新位置,即下一个返回的目录项地址
            return dir_e + dir_entry_idx; 
	      }
	      dir_entry_idx++;
      }
      block_idx++;
   }
   return NULL;
}
/* 判断目录是否为空 */
char dir_is_empty(struct dir* dir) {
   struct inode* dir_inode = dir->inode;
   /* 若目录下只有.和..这两个目录项则目录为空 */
   return (dir_inode->i_size == cur_part->sb->dir_entry_size * 2);
}

/* 在父目录parent_dir中删除child_dir */
int dir_remove(struct dir* parent_dir, struct dir* child_dir) {
   struct inode* child_dir_inode  = child_dir->inode;
   /* 空目录只在inode->i_sectors[0]中有扇区,其它扇区都应该为空 */
   int block_idx = 1;
   while (block_idx < 13) {
      ASSERT(child_dir_inode->i_sectors[block_idx] == 0);
      block_idx++;
   }
   void* io_buf = vmm_malloc(SEC_SIZE * 2,2);
   if (io_buf == NULL) {
      printk("dir_remove: malloc for io_buf failed\n");
      return -1;
   }

   /* 在父目录parent_dir中删除子目录child_dir对应的目录项 */
   delete_dir_entry(cur_part, parent_dir, child_dir_inode->i_num, io_buf);

   /* 回收inode中i_secotrs中所占用的扇区,并同步inode_bitmap和block_bitmap */
   inode_release(cur_part, child_dir_inode->i_num);
   vmm_free(io_buf,SEC_SIZE * 2);
   return 0;
}

dir.h

#ifndef _DIR_H_
#define _DIR_H_
#include "fs.h"
#include "ide-dev.h"
#define MAX_FILE_NAME_LEN 16

/* 目录结构 */
struct dir
{
    struct inode *inode;
    unsigned int dir_pos;  //记录在目录内的偏移
    unsigned char dir_buf[512];  //目录的数据缓存
};
/* 目录项结构 */
struct  dir_entry
{
    char filename[MAX_FILE_NAME_LEN]; //普通文件或目录名称
    unsigned i_no;  //普通文件或目录对应的inode结点号
    enum file_types f_type; //文件类型
};
void open_root_dir(struct partition* part);
struct dir* dir_open(struct partition* part, unsigned int inode_no);
char search_dir_entry(struct partition* part, struct dir* pdir, const char* name, struct dir_entry* dir_e);
void dir_close(struct dir* dir);
void create_dir_entry(char* filename, unsigned int inode_no, unsigned char file_type, struct dir_entry* p_de);
struct dir_entry* dir_read(struct dir* dir);
#endif

file.c

#include "file.h"
#include "fs.h"
#include "inode.h"
#include "../debug/debug.h"
#include "../vga/vga.h"
#include "../task/task.h"
#define NULL (void *)0
#define BITMAP_MASK 1
/* 文件表 */
struct file file_table[MAX_FILE_OPEN];
extern struct task_struct *current;
extern struct partition *cur_part;
/* 从文件表file_table中获取一个空闲位,成功返回下标,失败返回-1 */
int get_free_slot_in_global(void){
   unsigned int fd_idx=3;
   while(fd_idx<MAX_FILE_OPEN){
      if(file_table[fd_idx].fd_inode==NULL)
         break;
      fd_idx++;
   }
   if(fd_idx==MAX_FILE_OPEN){
      return -1;
   }
   return fd_idx;
}
/* 将全局描述符下标安装到进程或线程自己的文件描述符数组fd_table中,成功返回下标,失败返回-1 */
int task_fd_install(int global_fd_idx){
   char local_fd_idx=3; 
   while(local_fd_idx<MAX_FILE){
      if(current->fd_table[local_fd_idx]==-1){
         current->fd_table[local_fd_idx]=global_fd_idx;
         break;
      }
      local_fd_idx++;
   }
   if(local_fd_idx==MAX_FILE){
     return -1;
   }
   return local_fd_idx;
}
/* 分配一个i节点,返回i节点号 */
int inode_bitmap_alloc(struct partition *part){
   unsigned int idx_byte=0;//记录空闲位所在字节
    /* 先逐字节比较,蛮力法 */
   while (( 0xff == part->inode_bitmap.bits[idx_byte]) && (idx_byte < part->inode_bitmap.btmp_bytes_len)) {
   /* 1表示该位已分配,所以若为0xff,则表示该字节内已无空闲位,向下一字节继续找 */
      idx_byte++;
   }
   if (idx_byte == part->inode_bitmap.btmp_bytes_len) {  // 若该内存池找不到可用空间		
      return -1;
   } 

   /* 若在位图数组范围内的某字节内找到了空闲位,
  * 在该字节内逐位比对,返回空闲位的索引。*/
   int idx_bit = 0;
 /* 和btmp->bits[idx_byte]这个字节逐位对比 */
   while ((unsigned char)(BITMAP_MASK << idx_bit) & part->inode_bitmap.bits[idx_byte]) { 
	   idx_bit++;
   }
   int bit_idx_start = idx_byte * 8 + idx_bit;    // 空闲位在位图内的下标
   part->inode_bitmap.bits[idx_byte]|= (BITMAP_MASK << idx_bit); //将空闲位置1
   return bit_idx_start;
}

/* 分配1个扇区,返回扇区地址 */
int block_bitmap_alloc(struct partition *part){
   unsigned int idx_byte=0;//记录空闲位所在字节
   /* 先逐字节比较,蛮力法 */
   while (( 0xff == part->block_bitmap.bits[idx_byte]) && (idx_byte < part->block_bitmap.btmp_bytes_len)) {
   /* 1表示该位已分配,所以若为0xff,则表示该字节内已无空闲位,向下一字节继续找 */
      idx_byte++;
   }
   if (idx_byte == part->block_bitmap.btmp_bytes_len) {  // 若该内存池找不到可用空间		
      return -1;
   } 

   /* 若在位图数组范围内的某字节内找到了空闲位,
   * 在该字节内逐位比对,返回空闲位的索引。*/
   int idx_bit = 0;
   /* 和btmp->bits[idx_byte]这个字节逐位对比 */
   while ((unsigned char)(BITMAP_MASK << idx_bit) & part->block_bitmap.bits[idx_byte]) { 
	   idx_bit++;
   }
   int bit_idx_start = idx_byte * 8 + idx_bit;    // 空闲位在位图内的下标
   part->block_bitmap.bits[idx_byte]|= (BITMAP_MASK << idx_bit); //将空闲位置1
   return part
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值