虚拟地址与物理地址

虚拟地址与物理地址

本文旨在通过虚拟地址计算物理地址,理解环境实际运行机制以及写时复制技术

/proc/pid/page_map

Linux文件目录中的/proc记录着当前进程的信息,称其为虚拟文件系统。在/proc下有一个链接目录名为self,这意味着哪一个进程打开了它,self中存储的信息就是所链接进程的。self中有一个名为pagemap的文件,专门用来记录所链接进程的物理页号信息。这样通过/proc/pid/pagemap文件,允许一个用户态的进程查看到每个虚拟页映射到的物理页

/proc/pid/pagemap中的每一项都包含了一个64位的值,这个值内容如下所示。每一项的映射方式不同于真正的虚拟地址映射,其文件中遵循独立的对应关系,即虚拟地址相对于0x0经过的页面数是对应项在文件中的偏移量

这两段话目前还是比较难理解
- /proc/pid/pagemap.  This file lets a userspace process find out which
  physical frame each virtual page is mapped to.  It contains one 64-bit
  value for each virtual page, containing the following data (from
  fs/proc/task_mmu.c, above pagemap_read):
  - Bits 0-54  page frame number (PFN) if present//present为1时,bit0-54表示物理页号
  - Bits 0-4   swap type if swapped
  - Bits 5-54  swap offset if swapped
  - Bit  55    pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
  - Bit  56    page exclusively mapped (since 4.2)
  - Bits 57-60 zero
  - Bit  61    page is file-page or shared-anon (since 3.5)
  - Bit  62    page swapped
  - Bit  63    page present//如果为1,表示当前物理页在内存中;为0,表示当前物理页不在内存中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    在计算物理地址时,只需要找到虚拟地址的对应项,再通过对应项中的bit63判断此物理页是否在内存中,若在内存中则对应项中的物理页号加上偏移地址,就能得到物理地址

通过程序获取物理地址并验证写时拷贝技术

计算过程

1、首先获取一个变量的地址;(逻辑地址:段内偏移量)
2、获取系统设定的页面大小getpagesize()
3、计算虚拟地址相对于0x0经过的页面数
unsigned long v_pageIndex = vaddr/pageSize;
4、计算在/proc/pid/page_map文件中的偏移
unsigned long page_offset=v_pageIndex*sizeof(uint64_t);
5、计算虚拟地址的页内偏移量
unsigned long page_offset = vaddr%pageSize;
6、以只读方式打开/proc/pid/page_map
7、将游标移动到相应位置,即对应项的起始地址
lseek(fd,v_offset,SEEK_SET)
8、取8字节对应项
read(fd,&item,sizeof(uint64_t);
9、计算物理页号
uint64_t phy_pageIndex = (((uint64_t)1<<55)-1)&item;
10、计算物理地址
*paddr = (pht_pageIndex*pageSize)+page_offset;

#include <stdio.h>


#include <stdlib.h>


#include <sys/types.h>


#include <unistd.h>


#include <sys/stat.h>


#include <fcntl.h>


#include <stdint.h>


//    计算虚拟地址对应的地址,传入虚拟地址vaddr,通过paddr传出物理地址
void mem_addr(unsigned long vaddr, unsigned long *paddr)
{
    int pageSize = getpagesize();调用此函数获取系统设定的页面大小

        unsigned long v_pageIndex = vaddr / pageSize;//计算此虚拟地址相对于0x0的经过的页面数
        unsigned long v_offset = v_pageIndex * sizeof(uint64_t);//计算在/proc/pid/page_map文件中的偏移量
        unsigned long page_offset = vaddr % pageSize;//计算虚拟地址在页面中的偏移量
        uint64_t item = 0;//存储对应项的值

        int fd = open("/proc/self/pagemap", O_RDONLY);//。。以只读方式打开/proc/pid/page_map
        if(fd == -1)//判断是否打开失败
        {
            printf("open /proc/self/pagemap error\n");//
            return;//
        }

    if(lseek(fd, v_offset, SEEK_SET) == -1)//将游标移动到相应位置,即对应项的起始地址且判断是否移动失败
    {
        printf("sleek error\n");//
        return;// 
    }

    if(read(fd, &item, sizeof(uint64_t)) != sizeof(uint64_t))//读取对应项的值,并存入item中,且判断读取数据位数是否正确
    {
        printf("read item error\n");//
        return;//
    }

    if((((uint64_t)1 << 63) & item) == 0)//判断present是否为0
    {
        printf("page present is 0\n");//
        return ;//
    }

    uint64_t phy_pageIndex = (((uint64_t)1 << 55) - 1) & item;//计算物理页号,即取item的bit0-54
        printf("phypageIndex:%d\n pageSize:%d\n pageoffset:%d\n",phy_pageIndex,pageSize,page_offset);

        *paddr = (phy_pageIndex * pageSize) + page_offset;//再加上页内偏移量就得到了物理地址
}

int a = 100;//全局常量

int main()
{
    int b = 100;//局部变量
        static c = 100;//局部静态变量
        const int d = 100;//局部常量
        char *str = "Hello World!";//

    unsigned long phy = 0;//物理地址

        char *p = (char*)malloc(100);//动态内存

        int pid = fork();//创建子进程
        if(pid == 0)
        {
            sleep(10);
            a=98;
            printf("子进程中修改动态内存\n");
            p[0] = '1';//子进程中修改动态内存
                mem_addr((unsigned long)&a, &phy);//
            printf("pid = %d, virtual addr = %x , physical addr = %x\n", getpid(), &a, phy);//
        }
        else
        { 
            mem_addr((unsigned long)&a, &phy);//
            printf("pid = %d, virtual addr = %x , physical addr = %x\n", getpid(), &a, phy);//
        }

    sleep(100);//
    free(p);//
    waitpid();//
    printf("父进程安全退出\n");
    return 0;//
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-33787By5-1618144263365)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618122377036.png)]

测试效果没有达到预期

解决方法

with a gap in time

访问物理内存

mknod /dev/dram c 85 0
./fileview /dev/dram

mknod

手动创建设备文件
mknod DEVNAME {b|c} MAJOR MINOR
 1,DEVNAME是要创建的设备文件名,如果想将设备文件放在一个特定的文件夹下,就需要先用mkdir在dev目录下新建一个目录;
       2, b和c 分别表示块设备和字符设备:
                  b表示系统从块设备中读取数据的时候,直接从内存的buffer中读取数据,而不经过磁盘;
                  c表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符,比如打印机、终端都是以字符的形式传送数据;
       3,MAJOR和MINOR分别表示主设备号和次设备号:
             为了管理设备,系统为每个设备分配一个编号,一个设备号由主设备号和次设备号组成。主设备号标示某一种类的设备,次设备号用来区分同一类型的设备。linux操作系统中为设备文件编号分配了32位无符号整数,其中前12位是主设备号,后20位为次设备号,所以在向系统申请设备文件时主设备号不好超过4095,次设备号不好超过2^20 -1。

wait

通过分析各个回收方法理解进程之间的关系

父进程一旦调用了wait就立即阻塞自己,由wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,
如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
 wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

  如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态.

  参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

  如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
————————————————
版权声明:本文为CSDN博主「DNFK」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyhh_0101/article/details/83933308

WIFEXITED

这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非0值
请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数–指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)

WEXITSTATUS

 WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
————————————————
版权声明:本文为CSDN博主「DNFK」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyhh_0101/article/details/83933308

waitpid

从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数:
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
  pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
  pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
  pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
————————————————
版权声明:本文为CSDN博主「DNFK」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyhh_0101/article/details/83933308
返回值和错误

waitpid的返回值比wait稍微复杂一些,一共有3种情况:

      1、当正常返回的时候,waitpid返回收集到的子进程的进程ID;

      2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

      3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
————————————————
版权声明:本文为CSDN博主「DNFK」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyhh_0101/article/details/83933308

举例监视进程

目的:监视守护进程

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/stat.h>
void Daemon(void)
{
    pid_t pid = -1;
    int ret = 01;
    int fd = -1;
    int status = 0;
    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        exit(1);
    }
    else if(pid >0)
    {
        //父进程退出
        exit(1);
    }
    //创建会话
    ret = setsid();
    //当进程是会话的领头进程时setsid调用失败并返回(-1)
    //调用setsid函数的进程成为新的会话的领头进程,并与其父进程的绘画组和进程组脱离
    //由于会话对控制终端的独占性,进程同时与控制终端脱离
    if(-1 == ret)
    {
        perror("setsid");
        exit(1);
    }
    //设置权限掩码
    //
    umask(0);
    //改变当前进程工作目录
    //printf("当前目录:%s\n",get_current_dir_name());
    char buf[1024];
    memset(buf,0x00,sizeof(buf));
    printf("当前进程目录:%s\n",getwd(buf));
    //重定向文件描述符
    //守护进程起循环
    while(1)
    {
        pid = fork();
        if(-1 == pid)
        {
            perror("fork");
            exit(1);
        }
        else if(pid > 0)
        {
            //父进程监视异常
            //如果异常会回收资源
            wait(&status);
            //但不会继续向下执行
            //并发的子进程会跳出执行下一次循环使资源重新分配
            if(WIFEXITED(status))
            {
                //子进程正常终止,父进程也退出
                //正常结束
                exit(0);
            }
        }
        else if(pid == 0)
        {
            //父进程阻塞
            //子进程跳出循环去执行任务
           break; 
        }
        else 
        {
            perror("fork");
            exit(1);
        }
    }
}
int main(int argc,char **argv)
{
    if((2 == argc) &&(0 == strcmp("daemon",argv[1])))
    {
        Daemon(); 
    }
    else
    {
        printf("请输入两个参数\n");
        exit(1);
    }
    while(1)
    {
        sleep(2);
        printf("hello world\n");
    }
    return 0;
}

测试效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sNBi9oyy-1618144263368)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618131481453.png)]

未开启守护进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-94mV0k7d-1618144263370)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618131516675.png)]

开启守护进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0xBw0dj5-1618144263372)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618132328198.png)]

pid查看

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ssNituaB-1618144263374)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618132361088.png)]

关闭终端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gGOLo3C8-1618144263375)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618132432921.png)]

将后台任务恢复到前台

查看后台运行任务

杀掉子进程
kill -9 11049

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o8B4RPYa-1618144263376)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618133073342.png)]

描述执行情况

守护方法中增加一个循环
开一个进程负责阻塞wait
通过WIFEXITED(status)
监视是否正常退出,
如果返回真,则正常退出
如果返回假,则子进程异常结束(比如接收一个kill 信号),再次执行循环fork一个子进程去执行任务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8G4qGKR-1618144263377)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618133440221.png)]

杀掉父进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uLq2mt5v-1618144263377)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618133491720.png)]

makefile实例


#获取所有的源文件
SRCS:=$(wildcard *.c*)
#获取所有的目标文件
OBJS:=$(patsubst %.c*, %.o, $(SRCS))

#指定编译器
CC:= g++
#指定目标
TARGET:=write

$(TARGET):$(OBJS)
	$(CC) $^ -L/u01/app/oracle/product/11.2.0/db_1/lib -I/u01/app/oracle/product/11.2.0/db_1/rdbms/public -o $@ -lprotobuf -locci -lclntsh -lssl -lcrypto -lpthread

%.o:%.c*
	$(CC) -c -std=c++11 $< -o $@ -lprotobuf -locci -lclntsh 

.PHONY:clean
clean:
	rm -rf $(OBJS) $(TARGET)


理解setsid

#include <sys/file.h>  
#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
#include <sys/file.h>  
                                          
int main (int argc, char ** argv)  
{  
     if ( fork() > 0 ) {  
          printf ( "parent begin\n" ) ;  
          sleep(10);  
            
        printf ( "parent exit\n" ) ;  
        exit ( 0 ) ;  
     }  
       
     printf ( "child begin\n" ) ;  
     sleep(100);  
       
     printf ( "child exit\n" ) ;  
     return 0 ;  
}  
————————————————
版权声明:本文为CSDN博主「sweetfather」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sweetfather/article/details/79457261

测试结果

错误

c:23:2: error: stray ‘\302’ in program
分析
od -c *.c
这个错误一般是源代码中含有一些隐藏的非ascii字符。你把东西copy到文本编辑器中,再copy回来试试。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UozOfyi2-1618144263378)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129119457.png)]

打开另一个终端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xRga0GSo-1618144263379)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129158583.png)]

关闭运行test的窗口,父子进程都不在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgMq7af4-1618144263379)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129226187.png)]

子进程开始调用setsid

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wVahipfs-1618144263380)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129559733.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pl9ZqLOb-1618144263381)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129633493.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-okjnclhg-1618144263381)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129664830.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fnLP5dXl-1618144263382)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129689980.png)]

testSid分为两个进程组

父进程退出,子进程继续存在

说明:setsid后子进程不受终端影响,终端退出,不影响子进程

umask

linux中的 umask 函数主要用于:在创建新文件或目录时 屏蔽掉新文件或目录不应有的访问允许权限。文件的访问允许权限共有9种,分别是:r w x r w x r w x(它们分别代表:用户读 用户写 用户执行 组读 组写 组执行 其它读 其它写 其它执行)。
其实这个函数的作用,就是设置允许当前进程创建文件或者目录最大可操作的权限,比如这里设置为0,它的意思就是0取反再创建文件时权限相与,也就是:(~0) & mode 等于八进制的值0777 & mode了,这样就是给后面的代码调用函数mkdir给出最大的权限,避免了创建目录或文件的权限不确定性。
————————————————
版权声明:本文为CSDN博主「sweetfather」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sweetfather/article/details/79457261
pmap命令

举例

进程1

pmap -d 1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z2aPUZHq-1618144263383)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618142968796.png)]

最后一行的值
mapped 表示该进程映射的虚拟地址空间大小,也就是该进程预先分配的虚拟内存大小,即ps出的vsz
writeable/private  表示进程所占用的私有地址空间大小,也就是该进程实际使用的内存大小      
shared 表示进程和其他进程共享的内存大小
扩展格式
pmap -x 1
循环显示pid的设备格式的最后一行
间隔2秒
while true;
pmap -d pid|tail -1;sleep 2;done

分析cat maps

第一行:从r-xp可知其权限为只读、可执行,该段内存地址对应于执行文件的代码段,程序的代码段需加载到内存中才可以执行。由于其只读,不会被修改,所以在整个系统内共享。
第二行:从rw-p可知其权限为可读写,不可执行,该段内存地址对应于执行文件的数据段,存放执行文件所用到的全局变量、静态变量。
第三行:从rwxp可知其权限是可读写,可执行,地址空间向上增长,而且不对应文件,是堆段,进程使用malloc申请的内存放在堆段。每个进程只有一个堆段,不论是主进程,还是不同的线程申请的内存,都反映到到进程的堆段。堆段向上增长,最大可以增长到1GB的位置,即0x40000000,如果大于1GB,glibc将采用mmap的方式,为堆申请一块内存。
第四行:是程序连接的共享库的内存地址。
第五行:是以mmap方式映射的虚拟地址空间。
第六、七行:是线程的栈区地址段,每个线程的栈大小都是16K。
第八行:是进程的栈区。关于栈段,每个线程都有一个,如果进程中有多个线程,则包含多个栈段。

系统总内存的统计

四、对调试内存泄露类问题的一些启示

 当进程申请内存时,实际上是glibc中内置的内存管理器接收了该请求,随着进程申请内存的增加,内存管理器会通过系统调用陷入内核,从而为进程分配更多的内存。
 针对堆段的管理,内核提供了两个系统调用brk和mmap,brk用于更改堆顶地址,而mmap则为进程分配一块虚拟地址空间。

当进程向glibc申请内存时,如果申请内存的数量大于一个阀值的时候,glibc会采用mmap为进程分配一块虚拟地址空间,而不是采用brk来扩展堆顶的指针。缺省情况下,此阀值是128K,可以通过函数来修改此值。
修改阈值:
#include <malloc.h>
intmallopt(int param,int value);
如果在实际的调试过程中,怀疑某处发生了内存泄露,可以查看该进程的maps表,看进程的堆段或者mmap段的虚拟地址空间是否持续增加,如果是,说明很可能发生了内存泄露,如果mmap段虚拟地址空间持续增加,还可以看到各个段的虚拟地址空间的大小,从而可以确定是申请了多大的内存,对调试内存泄露类问题可以起到很好的定位作用。

程的栈区。关于栈段,每个线程都有一个,如果进程中有多个线程,则包含多个栈段。


### 系统总内存的统计

### 四、对调试内存泄露类问题的一些启示

当进程申请内存时,实际上是glibc中内置的内存管理器接收了该请求,随着进程申请内存的增加,内存管理器会通过系统调用陷入内核,从而为进程分配更多的内存。
针对堆段的管理,内核提供了两个系统调用brk和mmap,brk用于更改堆顶地址,而mmap则为进程分配一块虚拟地址空间。

当进程向glibc申请内存时,如果申请内存的数量大于一个阀值的时候,glibc会采用mmap为进程分配一块虚拟地址空间,而不是采用brk来扩展堆顶的指针。缺省情况下,此阀值是128K,可以通过函数来修改此值。
修改阈值:
#include <malloc.h>
intmallopt(int param,int value);


如果在实际的调试过程中,怀疑某处发生了内存泄露,可以查看该进程的maps表,看进程的堆段或者mmap段的虚拟地址空间是否持续增加,如果是,说明很可能发生了内存泄露,如果mmap段虚拟地址空间持续增加,还可以看到各个段的虚拟地址空间的大小,从而可以确定是申请了多大的内存,对调试内存泄露类问题可以起到很好的定位作用。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值