简析Linux IPC

IPC进程间通信

进程间通信方式:

  1. 早期通信:匿名管道、有名管道、信号
  2. system V IPC:共享内存、信号灯集、消息队列
  3. BSD: socket

这里简要分析进程间通信的六种方式,分别为:匿名管道、有名管道、信号、共享内存、信号灯集、消息队列。

匿名管道

特点

  1. 只能用于具有亲缘关系的进程间通信。
  2. 半双工通信方式,具有固定的读端和写端
  3. 无名管道可以看作一种特殊的文件。对他的读写操作采用文件IO的读写方式read、write
  4. 管道是一种基于文件描述符的通信方式,当一个管道建立,会自动的创建两个文件描述符fd[0]、fd[1]其中fd[0]是固定的读端,fd[1]是固定的写端

函数接口

int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端  fd[1]:写端
返回值:成功 0
              失败 -1

代码实现:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char const *argv[])
{
    int fd[2];
    pid_t pid;
    if (pipe(fd) < 0)
    {
        perror("pipe err");
        return -1;
    }
    pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        char arr[32] = {0};
        int n;
        close(fd[1]);
        while (1)
        {
            n = read(fd[0], arr, 32);
            if (n == 0)
            {
                close(fd[0]);
                break;
            }
            printf("%s\n", arr);
        }
        exit(0);
    }
    else
    {
        char arr[32] = {0};
        close(fd[0]);
        while (1)
        {
            fgets(arr, 32, stdin);
            if (strcmp(arr, "quit\n") == 0)
            {
                close(fd[1]);
                break;
            }
            write(fd[1], arr, 32);
        }
        wait(NULL);
        exit(0);
    }
    return 0;
}

有名管道

有名管道也叫FIFO,是一种文件类型。

特点

  1. 可以用于两个互不相干进程通信,是半双工通信,可过文件IO进行操作。
  2. 有名管道可以通过路径名指出,在文件系统中可见,但内容存放在内存里。
  3. 有名管道遵循先进先出原则,不支持lseek操作。

函数接口

int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
              mode:权限,与open函数中mode相同
返回值:成功:0
       失败:-1,并设置errno号

当管道中无数据时,读操作会阻塞;管道中无数据,将写端关闭,读操作会立即返回

管道中装满(管道大小64K)数据写阻塞,一旦有4k空间,写继续,直到写满为止

只有在管道的读端存在时,向管道中写入数据才有意义,否则,会导致管道破裂。

代码实现:

infifo.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("usage:%s <file1>", argv[0]);
        return -1;
    }
    int fd1 = open(argv[1], O_RDONLY);
    if (fd1 < 0)
    {
        perror("open file1 err");
        return -1;
    }
    if (mkfifo("./fifo", 0666) < 0)
    {
        if (errno == EEXIST)
            printf("fifo file exist\n");
        else
        {
            perror("fifo err");
            return -1;
        }
    }
    printf("mkfifo succ\n");
    int fd= open("./fifo", O_WRONLY);
    if (fd  < 0)
    {
        perror("open fifo");
        return -1;
    }
    printf("open1 success %d\n",fd);
    char arr[32];
    int n;

    while (n = read(fd1, arr, 32))
    {
        write(fd, arr, n);
    }

    close(fd);
    close(fd1);
    return 0;
}

outfifo.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    if (argc != 2)
    {
        printf("usage:%s <file1>", argv[0]);
        return -1;
    }
    int fd2 = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (fd2 < 0)
    {
        perror("open file1 err");
        return -1;
    }
    if (mkfifo("./fifo", 0666) < 0)
    {
        if (errno == EEXIST)
            printf("fifo file exist\n");
        else
        {
            perror("fifo err");
            return -1;
        }
    }
    printf("mkfifo succ\n");
    int fd;
    if ((fd = open("./fifo", O_RDONLY)) < 0)
    {
        perror("open fifo");
        return -1;
    }
    printf("open2 success %d\n",fd);
    char arr[32];
    int n;
    while (1)
    {
        n = read(fd, arr, 32);
        if (n == 0)
            break;
        write(fd2, arr, n);
    }


    close(fd);
    close(fd2);
    return 0;
}

信号

对Linux来说,信号是一次软中断,是一种异步通信方式。

信号的种类

每个信号都有一个名字和编号以及相对应的功能,这些名字都以“SIG”开头。我们可以通过命令行命令kill -l来查看信号的名字以及序号。

信号响应方式

信号的处理有三种方法,分别是:忽略、捕捉和默认。

  1. 忽略信号:对信号不做任何处理,但是有两个信号不能被忽略:SIGKILL SIGSTOP
  2. 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。不能被捕捉:SIGKILL SIGSTOP
  3. 执行默认缺省操作:linux对每种信号都规定了默认操作

函数接口

int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
   sig:要发送的信号
返回值:成功 0     
       失败 -1

sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
      handler:信号处理方式
            SIG_IGN:忽略信号
            SIG_DFL:执行默认操作
           handler:捕捉信号  void handler(int sig)
返回值:成功:设置之前的信号处理方式
              失败:-1

代码实现司机与售票员问题:

1.售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)

2.售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)

3.司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)

4.司机等待售票员下车,之后司机再下车。

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
pid_t pid;

void handler1(int sig)
{
    switch (sig)
    {
    case SIGINT:
        kill(getppid(), SIGUSR1);
        break;
    case SIGQUIT:
        kill(getppid(), SIGUSR2);
        break;
    case SIGUSR1:
        printf("please get off the bus\n");
        raise(SIGKILL);
        break;
    default:
        break;
    }
}
void handler2(int sig)
{
    switch (sig)
    {
    case SIGUSR1:
        printf("let's gogogo\n");
        break;
    case SIGUSR2:
        printf("stop the bus\n");
        break;
    case SIGTSTP:
        kill(pid, SIGUSR1);
        break;
    case SIGCHLD:
        raise(SIGKILL);
        break;
    default:
        break;
    }
}

int main(int argc, char const *argv[])
{
    pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        signal(SIGINT, handler1);
        signal(SIGQUIT, handler1);
        signal(SIGUSR1, handler1);
        signal(SIGTSTP, SIG_IGN);
        while (1)
             pause();
    }
    else
    {

        signal(SIGUSR1, handler2);
        signal(SIGUSR2, handler2);
        signal(SIGTSTP, handler2);
        signal(SIGINT, SIG_IGN);
        signal(SIGQUIT, SIG_IGN);
        signal(SIGCHLD, handler2);
        while (1)
             pause();
    }

    return 0;
}

共享内存

共享内存是一种最为高效的进程间通信方式,为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。

使用:ipcs -m: 查看系统中的共享内存,ipcrm -m shmid:删除共享内存

函数接口

key_t ftok(const char *pathname, int proj_id);
功能:产生一个独一无二的key值
参数:
    Pathname:已经存在的可访问文件的名字
    Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
      失败:-1
key值组成:
前两位是字符的ascii值
中间两位是系统编号
后四位是文件对应inode号的后四位。

int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
    key  键值
    size   共享内存的大小
    shmflg   IPC_CREAT|IPC_EXCL(判错)|0666
返回值:成功   shmid
      出错    -1

void  *shmat(int  shmid,const  void  *shmaddr,int  shmflg);
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
    shmid   共享内存的id号
    shmaddr   一般为NULL,表示由系统自动完成映射
              如果不为NULL,那么由用户指定
    shmflg:SHM_RDONLY就是对该共享内存只进行读操作
                0     可读可写
返回值:成功:完成映射后的地址,
      失败:-1的地址

int shmdt(const void *shmaddr);
功能:取消映射
参数:要取消的地址
返回值:成功0  
      失败的-1

int  shmctl(int  shmid,int  cmd,struct  shmid_ds   *buf);
功能:对共享内存进行各种操作(可删除共享内存)
参数:
    shmid   共享内存的id号
    cmd     IPC_STAT 获得shmid属性信息,存放在第三参数
            IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
            IPC_RMID:删除共享内存,此时第三个参数为NULL即可
返回:成功0 
     失败-1
用法:shmctl(shmid,IPC_RMID,NULL)

操作步骤

  1. 创建key值,ftok
  2. 创建或打开共享内存 shmget
  3. 映射共享内存到地址空间 shmat
  4. 撤销映射 shmdt
  5. 删除共享内存 shmctl

代码实现:

inputshm.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
typedef struct DATA
{
    int flag;
    char arr[64];
} * data;

int main(int argc, char const *argv[])
{
    key_t key = ftok(".", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0600);
    if (shmid < 0)
    {
        if (errno == EEXIST)
            shmid = shmget(key, 128, 0600);
        else
        {
            perror("shmget err");
            return -1;
        }
    }
//使用共享内存地址空间首地址作为输入输出标志位
    // char *p = shmat(shmid, NULL, 0);
    // while (1)
    // {
    //     if (*p == 0)
    //     {
    //         fgets(p + 1, 127, stdin);
    //         *p = 'A';
    //     }
    //     if (strcmp(p + 1, "quit\n") == 0)
    //         break;
    // }
//使用附带标志位的结构体传送数据
    data p = shmat(shmid, NULL, 0);
    while (1)
    {
        fgets(p->arr, 64, stdin);
        p->flag=1;
        if (strcmp(p + 1, "quit\n") == 0)
            break;
    }

    shmdt(p);
    shmctl(shmid, IPC_RMID, NULL); //映射连接数为0时删除
    return 0;
}

outputshm.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
typedef struct DATA
{
    int flag;
    char arr[64];
} * data;

int main(int argc, char const *argv[])
{
    key_t key = ftok(".", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0600);
    if (shmid < 0)
    {
        if (errno == EEXIST)
            shmid = shmget(key, 128, 0600);
        else
        {
            perror("shmget err");
            return -1;
        }
    }
//使用共享内存地址空间首地址作为输入输出的标志位
    // char *p = shmat(shmid, NULL, 0);
    // while (1)
    // {
    //     if (*p == 0)
    //         continue;
    //     if (*p == 'A')
    //     {
    //         if (strcmp(p + 1, "quit\n") == 0)
    //         {
    //             // *p = *(p+1)=0;//不删除共享内存,前两位清空
    //             break;
    //         }
    //         fputs(p + 1, stdout);
    //         *p = 0;
    //     }
    // }
//使用附带标志位的结构体传送数据
    data p = shmat(shmid, NULL, 0);
    while (1)
    {
        if (p->flag = 1)
        {
            if (strcmp(p + 1, "quit\n") == 0)
                break;
            fputs(p->arr,stdout);
            p->flag=0;
        }
    }

    shmdt(p);
    return 0;
}

信号灯集

概念

        信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制,并不存储进程间通信数据;System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。而Posix信号灯指的是单个计数信号灯。

函数接口

int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
    nsems:信号灯集中包含的信号灯数目
    semflg:信号灯集的访问权限,通常为IPC_CREAT |0666
返回值:成功:信号灯集ID
       失败:-1

int semctl ( int semid, int semnum,  int cmd…/*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
    semnum: 要操作的集合中的信号灯编号
     cmd:
        GETVAL:获取信号灯的值,返回值是获得值
        SETVAL:设置信号灯的值,需要用到第四个参数:共用体
        IPC_RMID:从系统中删除信号灯集合
返回值:成功 0
      失败 -1

int semop ( int semid, struct sembuf  *opsptr,  size_t  nops);
功能:对信号灯集合中的信号量进行PV操作
参数:semid:信号灯集ID
     opsptr:操作方式
     nops:  要操作的信号灯的个数 1个
返回值:成功 :0
      失败:-1
struct sembuf {
   short  sem_num; // 要操作的信号灯的编号
   short  sem_op;  //    0 :  等待,直到信号灯的值变成0
                   //   1  :  释放资源,V操作
                   //   -1 :  申请资源,P操作                    
    short  sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};

操作步骤

  1. 创建key值 ftok
  2. 创建或打开信号灯集 semget
  3. 初始化信号灯 semctl
  4. PV操作 semop
  5. 删除信号灯集 semctl

代码实现共享内存同步操作:

input.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/sem.h>
union semun {
    int val;
};
key_t getkey()
{
    key_t key = ftok(".", 'A');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    return key;
}
void initsem(int semid, int semnum, int val)
{
    union semun sem;
    sem.val = val;
    semctl(semid, semnum, SETVAL, sem);
}
int creatsem(key_t key, int n, int arr[])
{
    int semid = semget(key, n, IPC_CREAT | IPC_EXCL | 0666);
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, n, 0666);
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else
    {
        for (int i = 0; i < n; i++)
            initsem(semid, i, arr[i]);
    }
    return semid;
}

void Popt(int semid, int semnum)
{
    struct sembuf buf;
    buf.sem_num = semnum;
    buf.sem_op = -1;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}
void Vopt(int semid, int semnum)
{
    struct sembuf buf;
    buf.sem_num = semnum;
    buf.sem_op = 1;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}

int main(int argc, char const *argv[])
{
    key_t key = ftok(".", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0600);
    if (shmid < 0)
    {
        if (errno == EEXIST)
            shmid = shmget(key, 128, 0600);
        else
        {
            perror("shmget err");
            return -1;
        }
    }
    char *p = shmat(shmid, NULL, 0);
    if(p==(void *)-1)
    {
        perror("shmat err");
        return -1;
    }
    key = getkey();
    int arr[2] = {1, 0};
    int semid = creatsem(key, 2, arr);
    while (1)
    {
        Popt(semid, 0);
        fgets(p, 127, stdin);
        Vopt(semid, 1);
        if (strcmp(p, "quit\n") == 0)
            break;
    }

    shmdt(p);
    return 0;
}

output.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/sem.h>
union semun {
    int val;
};
key_t getkey()
{
    key_t key = ftok(".", 'A');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    return key;
}
void initsem(int semid, int semnum, int val)
{
    union semun sem;
    sem.val = val;
    semctl(semid, semnum, SETVAL, sem);
}
int creatsem(key_t key, int n, int arr[])
{
    int semid = semget(key, n, IPC_CREAT | IPC_EXCL | 0666);
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, n, 0666);
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else
    {
        for (int i = 0; i < n; i++)
            initsem(semid, i, arr[i]);
    }
    return semid;
}

void Popt(int semid, int semnum)
{
    struct sembuf buf;
    buf.sem_num = semnum;
    buf.sem_op = -1;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}
void Vopt(int semid, int semnum)
{
    struct sembuf buf;
    buf.sem_num = semnum;
    buf.sem_op = 1;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
    key_t key = ftok(".", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0600);
    if (shmid < 0)
    {
        if (errno == EEXIST)
            shmid = shmget(key, 128, 0600);
        else
        {
            perror("shmget err");
            return -1;
        }
    }
    char *p = shmat(shmid, NULL, 0);
    if (p == (void *)-1)
    {
        perror("shmat err");
        return -1;
    }
    key = getkey();
    int arr[2] = {1, 0};
    int semid = creatsem(key, 2, arr);
    while (1)
    {
        Popt(semid, 1);
        if (strcmp(p, "quit\n") == 0)
            break;
        fputs(p, stdout);
        Vopt(semid, 0);
    }

    shmdt(p);
    shmctl(shmid, IPC_RMID, NULL);
    semctl(semid, 0, IPC_RMID);
    return 0;
}

消息队列

概念及特点

  1. 消息队列是IPC对象的一种
  2. 由消息队列ID来唯一标识
  3. 就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等
  4. 可以按照类型来发送(添加)/接收(读取)消息

函数接口

int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数:  key值
       flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:成功:msgid
       失败:-1

int msgsnd(int msqid, const void *msgp, size_t size, int flag); 
功能:添加消息
参数:msqid:消息队列的ID
      msgp:指向消息的指针。常用消息结构msgbuf如下:
          struct msgbuf{
            long mtype;          //消息类型
            char mtext[N]};   //消息正文
   size:发送的消息正文的字节数
   flag:IPC_NOWAIT消息没有发送完成函数也会立即返回    
         0:直到发送完成函数才返回
返回值:成功:0
      失败:-1

int msgrcv(int msgid,  void* msgp,  size_t  size,  long msgtype,  int  flag);
功能:读取消息
参数:msgid:消息队列的ID
     msgp:存放读取消息的空间
     size:接受的消息正文的字节数
    msgtype:0:接收消息队列中第一个消息。
            大于0:接收消息队列中第一个类型为msgtyp的消息.
            小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
     flag:0:若无消息函数会一直阻塞
        IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:成功:接收到的消息的长度
      失败:-1

int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,如删除消息队列
参数:msqid:消息队列的队列ID
     cmd:
        IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
        IPC_SET:设置消息队列的属性。这个值取自buf参数。
        IPC_RMID:从系统中删除消息队列。
     buf:消息队列缓冲区
返回值:成功:0
      失败:-1
用法:msgctl(msgid, IPC_RMID, NULL)

操作步骤

  1. 创建key值 ftok
  2. 创建或打开消息队列 msgget
  3. 添加消息:按照类型将消息添加到以打开的消息队列末尾 msgsnd
  4. 读取消息:可以按照类型将消息从消息队列中读走 msgrcv
  5. 删除消息队列 msgctl

测试代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

struct msgbuf
{
    long mtype;
    char text[32];
};
int main(int argc, char const *argv[])
{
    key_t key = ftok(".", 66);
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    if (msgid <= 0)
    {
        if (errno == EEXIST)
            msgid = msgget(key, 0666);
        else
        {
            perror("msgget err");
            return -1;
        }
    }
    printf("msgid:%d\n", msgid);

    struct msgbuf buf;
    buf.mtype=123;
    fgets(buf.text,32,stdin);

    msgsnd(msgid,&buf,sizeof(buf)-4,0);
    struct msgbuf msg;
    msgrcv(msgid,&msg,sizeof(msg)-4,123,0);
    printf("%s",msg.text);
    
    msgctl(msgid,IPC_RMID,NULL);
    return 0;
}

IPC方式总结

  1. 无名管道:速度慢,容量有限,只有父子进程能通讯
  2. 有名管道:任何进程间都能通讯,速度慢
  3. 信号:实现异步的软中断
  4. 共享内存:能够很容易控制容量,速度快,但要注意同步问题
  5. 信号灯集:不能传递复杂消息,只能用来同步。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据
  6. 消息队列:容量受到系统限制,要注意读的时候,要考虑上一次没有读完数据的问题
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

史莱姆·张

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

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

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

打赏作者

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

抵扣说明:

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

余额充值