Linux系统IPC进程间通信

IPC
    进程间通信(两个/多个进程数据交互);
    了解:信号应用中的计时器;

用指定的初始间隔和重复间隔时间为进程设定号一个计时器后,该计时器就会定时的向进程发送时钟信号;
Linux为每个进程维护3个计时器,分别是
    真实计时器SIGALRM
        计算程序运行的实际时间;
    虚拟计时器SIGVTALRM
        计算程序在用户态时所消耗的时间;
    实用计时器SIGPROF
        计算程序处于用户态和内核态所消耗的时间;

获取计时器的设置
int getitimer(int which, struct itimerval *value);
设置计时器:
int setitimer(int which, const struct itimerval *new_value,
                        struct itimerval *old_value);

struct itimerval {
    struct timeval it_interval; //next value
    struct timeval it_value;    //current value
};
struct timeval {
    long tv_sec;       //seconds
    long tv_usec;      //microseconds
};
/*
 * setitimer()定时器
 */
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
void fa(int signo) {
    printf("I am a superman!\n");
}
int main() {
    signal(SIGALRM, fa);
    /* 设置计时器 */
    struct itimerval timer;
    /* 开始时间 */
    timer.it_value.tv_sec = 3;    //开始秒数
    timer.it_value.tv_usec = 0;    //开始微秒数;
    /* 间隔时间 */
    timer.it_interval.tv_sec = 1;    //间隔的秒数;
    timer.it_interval.tv_usec = 0;    //间隔的微秒数
    setitimer(ITIMER_REAL, &timer, NULL);
    while (1);
    return 0;
}

/*
 * setitimer()定时器
 */
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
void fa(int signo) {
    printf("I am a superman %d !\n", signo);
}
int main() {
    signal(SIGPROF, fa);
    /* 设置计时器 */
    struct itimerval timer, otv;
    timer.it_value.tv_sec = 3;    //开始秒数
    timer.it_value.tv_usec = 0;    //开始微秒数;
    timer.it_interval.tv_sec = 1;    //间隔的秒数;
    timer.it_interval.tv_usec = 0;    //间隔的微秒数
    setitimer(ITIMER_PROF, &timer, &otv); //似乎没有捕获到
    //不确定这样的使用方式是否正确,待解
    while (1) {
        printf("hello\n");
        sleep(1);
        /* kill(getpid(), SIGPROF); */
    }
    return 0;
}


IPC进程间通信Inter Process Communication
进程间的通信方式
    1-> 文件;
    2-> 信号;
    3-> 管道;
    4-> 共享内存;
    5-> 消息队列;
    6-> 信号量集(semaphore);
    7-> 网络(套接字socket);
    ...
    其中,共享内存,消息队列和信号量集遵守相同的规范,叫XSI IPC;最重要的是消息队列;
    IPC的应用基本上遵循一个固定的套路,在编程时只需要按照固定的步骤调用相应的函数即可;
    消息队列,信号量和共享内存统称为XSI IPC;

管道
    管道是Unix最古老的IPC方式之一,目前较少使用;
    历史上它是半双工的(数据只能在一个方向流动),现在很多系统都提供全双工管道;
    管道分为有名管道和无名管道;
    有名管道可以用于各种进程的IPC; mkfifio()函数用于创建有名管道文件;

进程A          进程B        动作
建立管道文件                mkfifo
打开管道       打开管道     open
读写数据       读写数据     read/write
关闭管道       关闭管道     close
删除管道                    unlink

    无名管道只能用于fork()创建的父子进程之间的IPC;
    管道(pipe)的交互媒介是一种特殊的文件即管道文件;管道文件的创建必须用mkfifo命令或mkfifo()函数,touch不能创建管道文件;管道文件的后缀是.pipe;
    管道文件只做交互的媒介,不存储数据;因此只有在输入输出都存在时,才畅通,否则就卡住;
练习新建一个管道文件,用echo命令和cat命令测试以下;
        mkfifo test.pipe
        echo "hello" > test.pipe
        会卡在这里等待下一步操作
        再开启一个终端,然后
        cat test.pipe

编写两个程序,一个发送整数0-99,另外一个接收,采用管道的方式;
思路
    新建一个管道文件,然后发送的进程写文件,接收的进程读文件即可;
/*
 * 管道练习a发送
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
    int res = mkfifo("test.pipe", 0666);
    if (res == -1) {
        perror("mkfifo"); exit(-1);
    }
    /* open()的O_CRET是建立不了管道文件的,只能使用mkfifo */
    /* open()函数只能创建普通文件 */
    int fd = open("test.pipe", O_WRONLY); //写
    /* 对管道文件不要使用O_RDWR,
     * 否则系统会认为既是读管道又是写管道,直接运行; */
    if (fd == -1) {
        perror("open"), exit(-1);
    }
    int i;
    for (i = 0; i <= 99; i++) {
        write(fd, &i, 4);
        usleep(100000);    //0.1s
        printf("发送%d ", i);
        if (!(i % 10))
            printf("\n");
    }
    printf("\n");
    close(fd);
    return 0;
}

/*
 * 管道练习b接收
 */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main() {
    int fd = open("test.pipe", O_RDONLY); //读
    if (fd == -1) {
        perror("open"), exit(-1);
    }
    int i;
    for (i = 0; i <= 99; i++) {
        int x;
        read(fd, &x, 4);
        printf("接收到%d\n", x);
    }
    close(fd);
    return 0;
}


共同的使用方法
    1.创建时都需要使用key,key是一个整数,外部程序使用key来获取内核中的IPC结构(共享内存/消息队列和信号量集,也就是交互媒介);
    2.key的生成方式有三种
        a.宏IPC_PRIVATE直接做key,这种基本不使用;只能创建IPC结构,别的进程不能调用;
        b.定义一个头文件,把所有的key写在头文件中;
        c.函数ftok()可以用一个真实存在的目录和一个人工分配的项目ID(0-255有效,即低8位有效)自动生成一个key;
    3.所有的IPC结构在内核中对应一个唯一的ID,标识每一个IPC结构;
    4.key是用来查找ID的,ID是用来定位IPC结构的;key -> ID -> IPC结构;
        //key是外部的,只有找到ID之后才可以定位IPC结构;
        函数xxxget(),比如shmget(key,...)取共享内存;msgget(key,...)取消息队列;可以用key取得ID,后续的代码使用ID即可;
    5.创建IPC结构时,都需要提供一个flags参数,这个参数一般是
        0666 | IPC_CREAT | IPC_EXCL
        权限   创建IPC    如果存在,直接返回-1代表出错;
    6.每种IPC结构都需要提供一个操作函数
        xxxctl(),它至少包括以下功能
        a-> IPC_STAT    取IPC结构的相关属性(查看);
        b-> IPC_SET        修改IPC结构的部分属性;
        c-> IPC_RMID    删除IPC结构;
    注意:IPC结构由内核管理,如果不删除,重启后依然存在;用完记得删除;

关于IPC相关命令
    ipcs    查看IPC结构;
        ipcs -a    查看所有;
        ipcs -m    共享内存;
        ipcs -q    消息队列;
        ipcs -s    信号量集;
    ipcrm    删除IPC结构;需要指定结构的ID;
        ipcrm -m ID 删除共享队列
        ipcrm -q ID 删除消息队列
        ipcrm -s ID 删除信号量集
    
共享内存
    共享内存是以一块内存作为IPC交互的媒介,这块内存由内核维护和管理,允许其他进程映射;
    IPC中共享内存效率高;
    共享内存最大的问题就是多进程同时修改时很难控制;

                 编程模型
     服务进程             客户进程           动作
使用约定文件创建key  使用约定文件创建key    ftok()
使用key创建共享内存  使用key创建共享内存    shmget()
挂接到共享内存       挂接到共享内存         shmat()
使用内存             使用内存
卸载共享内存         卸载共享内存           shmdt()
释放共享内存                                shmctl()

共享内存的使用步骤
    1.创建key;
        可以使用头文件定义或ftok()函数;
        key_t ftok(const char *pathname, int proj_id);
        参数是真实存在的文件路径和非零的项目id;
    2.用key创建/获取ID;
        得到key后使用shmget(key,...)函数创建/获取共享内存ID;
        #include <sys/shm.h>
        int shmget(key_t key, size_t size, int shmflg);
            size//共享内存的大小(字节);//获取时设为0即可;
            shmflag//一般是0666|IPC_CREAT|IPC_EXCL;//获取时设为0即可;
            成功返回虚拟内存首地址,失败返回-1;
    3.挂接;
        使用shmat(ID)函数挂接(映射)共享内存;
        void *shmat(int shmid, const void *shmaddr, int shmflg);
            shmaddr为0时,系统自动分配地址;
            后面两个参数设为0即可;
            成功时返回共享的虚拟内存的首地址;
    4.正常使用;
    5.脱接;
        使用shmdt(ID)函数脱接(解除映射)共享内存;
        int shmdt(const void *shmaddr);
            参数为挂接时返回的首地址;
    6.删除;
        int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        如果确定不再使用,需要使用函数shmctl(id,IPC_RMID,NULL)删除共享内存;
/*
 * IPC共享内存a
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
/* 创建共享内存,并存入数据 */
int main() {
    //1创建key
    key_t key = ftok(".", 100);    //路径不存在会失败;
    if (key == -1) {
        perror("ftok"), exit(-1);
    }
    //2用key创建ID
    int shmid = shmget(key, 4, 0666 | IPC_CREAT | IPC_EXCL);    //
    /* 新建时需要3个参数,获取时后两个参数为0即可; */
    if (shmid == -1) {
        perror("shmget"), exit(-1);
    }
    printf("共享内存创建成功\n");
    //3挂接
    void *p = shmat(shmid, 0, 0);    //挂接
    if (p == (void *)-1) {
        perror("shmat"), exit(-1);
    }
    //4使用
    int *pi = p;
    *pi = 100;
    sleep(10); //使用ipcs -m 可以查看挂接数
    //练习:写shmb.c把100从共享内存中读出来;
    //5脱接
    shmdt(p);        //脱接
    //6删除
    /* shmctl(shmid, IPC_RMID, 0);//删除 */
    return 0;
}

/*
 * IPC共享内存b
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main() {
    //1创建key
    key_t key = ftok(".", 100);
    if (key == -1) {
        perror("ftok"), exit(-1);
    }
    //2用key获取ID
    int shmid = shmget(key, 0, 0);
    /* 获取的时候后两个参数给0就可以 */
    if (shmid == -1) {
        perror("shmget"), exit(-1);
    }
    printf("获取共享内存\n");
    //3挂接
    void *p = shmat(shmid, 0, 0);
    if (p == (void *)-1) {
        perror("shmat"), exit(-1);
    }
    //4使用
    int *pi = p;
    printf("*pi = %d\n", *pi);
    //5脱接
    shmdt(p);
    sleep(1);
    //6删除
    shmctl(shmid, IPC_RMID, 0);
    printf("共享内存已删除\n");
    return 0;
}



消息队列
    应用最广,IPC中的重点
    把数据放入消息中,把消息放入队列中;队列由内核负责创建和维护;
消息队列的使用步骤
    1.用ftok()或头文件定义方式生成key;
    2.用key创建/获取消息队列的ID;
        msgget(key, ...);
        int msgget(key_t key, int msgflg);
        成功返回ID,失败返回-1;
    3.发送消息/接收消息;
    msgsnd()    //放入/发送消息;
    msgrcv()    //取出/接收消息;
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    msgsnd(ID, 消息首地址, 消息长度, 0/IPC_NOWAIT);
    在使用有类型的消息时,第二个参数地址是整个结构的地址,第三个参数大小值是数据区的大小(不包括消息类型mtype);
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
    msgrcv(ID, 接收变量首地址, 接收变量buf大小, 消息类型/*0代表接收所有类型*/, 0/IPC_NOWAIT);
    4.如果确定不再使用消息队列,需要删除;
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
        msgctl(msgid, IPC_RMID, struct msgid_ds *buf);
    注意所有的函数名是msg而不是msq,或许是由于历史原因,应该小心;
可以用msgctl(msgid, IPC_STAT, &msqid_ds)查询消息队列的信息;
    查询结果放在msqid_ds的结构体指针所对应的变量中;
/*
 * 消息队列a
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main() {
    //1生成key
    key_t key = ftok(".", 200);
    //2创建消息队列的ID
    int msgid = msgget(key, 0666 | IPC_CREAT);
    /* IPC_EXCL不要,因为第二次放入时队列已经存在会失败; */
    if (msgid == -1) {
        perror("msgget"), exit(-1);
    }
    printf("消息队列创建成功\n");
    //3发送消息
    int res = msgsnd(msgid, "hello", 5, 0); /*IPC_NOWAIT非阻塞 */
    if (res == -1) {
        perror("msgsnd"), exit(-1);
    }
    printf("send ok !\n");
    /* 练习:写msgb.c用msgrcv()把消息取出来并打印; */
    return 0;
}

/*
 * 消息队列b
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main() {
    //1生成key
    key_t key = ftok(".", 200);
    //2获取消息队列的ID
    int msgid = msgget(key, 0);    //接收时最后一个参数为0
    if (msgid == -1) {
        perror("msgget"), exit(-1);
    }
    printf("消息队列获取成功\n");
    //3接收消息
    printf("消息队列recieving...\n");
    char buf[50] = { };
    ssize_t res = msgrcv(msgid, buf, sizeof(buf), 0, 0);
    /* 前一个0表示接收任意类型的消息,后一个表示阻塞 */
    if (res == -1) {
        perror("msgrcv"), exit(-1);
    }
    printf("收到了%d字节,内容%s\n", res, buf);
    //4删除消息队列
    if (!msgctl(msgid, IPC_RMID, NULL)) {
        printf("消息队列已删除\n");
    }
    return 0;
}

思考,如果消息被放入到消息队列中,本来是发给进程B的,但由于进程A先起来了,于是就把消息取走了,如何才能避免这种情况呢;
给每个消息贴上一个标签用于标识类型;

消息类型
    消息分为有类型消息和无类型消息;
    无类型消息 <==> 数据
        可以使用任意类型;
        比如可以是int,double,字符串,结构,联合;
        这种消息严格遵守先进先出;
    有类型的消息 == 消息类型 + 数据
        因此必须是结构体类型;格式如下
    struct 消息名/*可自定义*/ {
        long mtype;//第一个成员必须是消息类型;
        ...//后面可以随便写,是自定义的数据;
    };
    msgsnd()发送有类型消息时,没有特殊要求;
    msgrcv()接收有类型消息时,可以使用第4个参数选择接收消息的类型;
        其中mtype必须大于0; 负数和0有特殊用途;
    msgrcv()第4个参数(long msgtype)可能的值
        1-> 正数    接收指定类型的消息;
        2->  0      接收任意类型的消息(先进先出);
        3-> 负数    接收小于等于flags绝对值的消息,次序是先小后大;

number  a b c d e f g h
msgtype 2 5 3 1 2 3 1 4
假如msgrcv()函数中接收类型是
    3  : c f 阻塞或返回-1;
    0  : a b c d e f g h 阻塞或返回-1;
    -3 : d g a e c f 阻塞或返回-1;
/*
 * 消息队列,按类型接收消息a
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <signal.h>
struct Msg {
    long mtype;        //消息类型
    char buf[20];    //存储有效数据;
};
/* int msgid; */
void fa(int signo) {
    //信号处理函数无法传参,因此可以定义全局变量,也可重新获取
    key_t key = ftok(".", 100);
    int msgid = msgget(key, IPC_CREAT | 0666);
    //练习:使用msgctl(msgid,IPC_STAT,结构指针)判断消息数是否为0再清空;
    //或者关闭时将消息队列写入文件,启动时再加载进来;
    printf("捕获信号%d\n", signo);
    if (signo == 2) {
        struct msqid_ds mds;
        if (!msgctl(msgid, IPC_STAT, &mds)) {
            if (mds.msg_qnum <= 0) {
                msgctl(msgid, IPC_RMID, 0);
                printf("消息队列%d已经删除\n", msgid);
            } else {
                printf("消息队列%d还有%d个msg\n", msgid, mds.msg_qnum);
            }
        }
        printf("消息队列设置恢复默认\n");
        printf("进程已退出\n");
        signal(SIGINT, SIG_DFL);
        exit(0);    //退出进程
    }
}
int main() {
    key_t key = ftok(".", 100);
    int msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget"), exit(-1);
    }
    struct Msg msg1, msg2;
    msg1.mtype = 1; strcpy(msg1.buf, "zhangfei");
    msg2.mtype = 2; strcpy(msg2.buf, "guanyu");
    /* 注意发送的时候,大小只是数据的大小,不包括数据类型 */
    msgsnd(msgid, &msg1, sizeof(msg1.buf), 0);
    msgsnd(msgid, &msg2, sizeof(msg2.buf), 0);
    printf("pid = %d\n", getpid());
    signal(SIGINT, fa);
    while (1);    //为了回收消息队列的资源(删除);
    return 0;
//在msgtypea中写用信号2删除消息队列的代码
}


/*
 * 消息队列,按类型接收消息b
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct Msg {
    long mtype;        //消息类型
    char buf[20];    //存储有效数据;
};
int main() {
    key_t key = ftok(".", 100);
    int msgid = msgget(key, 0);
    if (msgid == -1) {
        perror("msgget"), exit(-1);
    }
    struct Msg msg;
    int res, num;
    for (num = 2; num >= 0; num--) {
        /* 接收类型为num的消息 */
        res = msgrcv(msgid, &msg, sizeof(msg.buf), num, 0);
        printf("接收了%d字节数据,类型:%d,内容:%s\n",
                   res, msg.mtype, msg.buf);
    }
    return 0;
}


在使用有类型的消息时,第2个参数地址是整个结构的地址,但第3个参数大小只是数据区的大小(不包括消息类型msgtype):

    资源的管理一般遵循谁创建谁回收的原则    



信号量集(semaphore arrays)
    信号量集和信号(signal)没有关系,是一个信号量的数组;
    信号量集就是信号量数组,信号量就是一个计数器;
    //如同马与马虎马上看起来很相似,其实没关系
    信号量是一个计数器,用于控制访问共享资源的最大并行(同时运行)进程/线程总数;
    假如信号量是3,有5个进程:1先进,结束;2 3 4同时上,2结束,5再上
    计数器有两种工作方式:
        1.自增型;开始时计数0,来一个自增1,走一个自减1,到计数最大值后不允许再来;
        2.自减型;开始计数就是最大值,来一个自减1,走一个自增1,计数到0就阻塞,直到有其他进程结束,计数不再是0;//便于编程维护
    如果进程需要访问多个共享资源,需要多个计数器,而多个计数器就使用信号量集(信号量数组);
    信号量集其实是进程间的调度,并不能真正的互发数据;

信号量集的编程步骤
    1.使用ftok()或者头文件生成key;
    2.使用semget(key,...)创建/获取信号量集;
        int semget(key_t key, int nsems/*数组大小*/, int semflg);
    3.使用semctl(semid,...)给每个信号量集中的每个信号量赋值;//给数组中的每个元素赋值;
    4.使用信号量集semop();
    5.如果不再使用,可以用semctl(semid,0,IPC_RMID)删除;//与之前略有不同;

函数注解
    semctl()初始化信号量集中的每个信号量,格式:
    semctl(semid, int index, int cmd, ...)
        如果cmd是SETVAL,可以给信号量集中的一个成员赋值,index是信号量集的下标,第四个参数就是初始值;比如:
        setctl(semid, 0, SETVAL, 5)//给第1个信号量最大计数设置为5;
        失败返回-1;可以使用errno;
        
    semop()用于计数的+1和-1;
    int semop(int semid, struct sembuf semoparray[], size_t nops);
    参数nops为信号量数组的大小;
    参数semoparray是一个指针,它指向一个信号量数组,信号量操作由sembuf结构表示:
    struct sembuf{
        unsigned short sem_num;/* 操作信号量的下标 */
        short         sem_op; /* 对信号量的操作方式:负数/0/正数 */
        short         sem_flg;/* 计数是否阻塞:0阻塞,IPC_NOWAIT不阻塞 */
    };
    如果sem_op为正,则对应于进程释放占用的资源数,sem_op值加到信号量上;如果指定了undo标志(sem_flg成员设置了SEM_UNDO位),则也从该进程的此信号量中减去sem_op;
    若sem_op为负,则表示要获取该信号量的控制资源;

/*
 * 信号量集操作演示
 * sema.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <signal.h>
int semid;
void sem_free(int signo) {
    printf("捕获到信号%d\n", signo);
    printf("正在回收资源\n");
    if (signo == 2) {
        semctl(semid, 0, IPC_RMID);
        printf("信号量集已回收\n");
        exit(0);
    }
}
int main() {
    printf("PID = %d\n", getpid());
    signal(SIGINT, sem_free);
    printf("按Ctrl+C退出\n");
    key_t key = ftok(".", 100);
    semid = semget(key, 1, 0666 | IPC_CREAT); //第二个参数是数组长度
    if (semid == -1) {
        perror("semget"), exit(-1);
    }
    int res = semctl(semid, 0, SETVAL, 5);    //初始化最大计数5
    if (res == -1) {
        perror("semctl"), exit(-1);
    }
    printf("成功创建并初始化信号量集\n");
    //练习:用信号Ctrl+C实现信号量集的回收;
    while (1) ;
    return 0;
}

/*
 * 信号量集操作演示
 * semb.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
int main() {
    printf("pid = %d\n", getpid());
    key_t key = ftok(".", 100);
    int semid = semget(key, 0, 0);    //获取时给0
    if (semid == -1) {
        perror("semget"), exit(-1);
    }
    int i;
    for (i = 0; i <= 9; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            printf("进程%d开始启动;申请访问资源\n", i + 1);
            struct sembuf op;
            op.sem_num = 0;    //操作下标为0的信号量
            op.sem_op = -1;    //申请访问,计数-1
            op.sem_flg = 0;    //0阻塞
            /* op.sem_flg = IPC_NOWAIT; //不阻塞 */
            semop(semid, &op, 1); //执行-1操作;
                //数组的地址==首元素的地址;
            printf("进程%d访问资源\n", i + 1);
            sleep(10);
            op.sem_op = 1; //释放资源,计数+1
            semop(semid, &op, 1);    //执行计数+1
            printf("进程%d释放资源\n", i + 1);
            exit(0);
        }
    }
    printf("父进程已经结束pid = %d\n", getpid());
    return 0;
}



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值