2.1.4 进程之间的通信(共享通信、消息传递、管道通信)

1 思维导图

在这里插入图片描述

 进程通信分为共享内存、管道通信、消息传递

(1)共享内存:要互斥地访问共享空间;

(2)管道通信:

        一个管道只能实现半双工通信;

        写满时,不能再写,读空时,不能再读;

        没写满,不能读,没读空,不能写。

(3)消息传递

2 进程通信

  • 图中我们可以知道什么是进程通信,以及进程通信的低级和高级方式;
  • 我们还可以知道为什么要引入进程通信方式,以及它的意义

在这里插入图片描述

 进程通信就是进程之间的信息交换。

2.1 共享存储

共享一块大家都可以访问的空间,一次只能有一个进程进行读或写操作

在这里插入图片描述

 两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统的工具实现)

2.2 管道通信

在这里插入图片描述

(1)管道只能采用半双工通信,某一时间段只能单向传输;如果要实现双向同时通信,则要设置两个管道;

(2)各进程要互斥地访问管道;

(3)数据以字符流的方式写入管道,当管道写满时,写进程的write系统调用将被阻塞,等待读进程将数据取走。当写进程将数据全部取走后,管道为空,此时,读进程read系统调用将被阻塞;

(4)如果没有写满,就不允许读。如果没读空,就不允许写;

(5)数据一旦被读出,就从管道中被抛弃,意味着读进程最多只能有一个。

2.3 消息传递

发送信息的进程将消息头写好,接受信息进程根据消息头读取信息或寻找信封是哪一个

在这里插入图片描述

3 linux下常见进程通信

管道:简单
信号:开销小
mmap映射:非血缘关系进程间
socket(本地套接字):稳定

3.1 管道

 管道:实现原理: 内核借助环形队列机制,使用内核缓冲区实现。

特质

1. 伪文件
2. 管道中的数据只能一次读取。
3. 数据在管道中,只能单向流动。

局限性

1. 自己写,不能自己读。
2. 数据不可以反复读。
3. 半双工通信。
4. 血缘关系进程间可用。

3.1.1 管道的基本用法

pipe函数

// pipe函数: 创建,并打开管道。
int pipe(int fd[2]);
// 参数: fd[0]: 读端。
// fd[1]: 写端。
// 返回值: 成功: 0
// 失败: -1 errno

管道通信原理

创建一个管道,可读可写,再创建子进程,同样可读可写。

 关闭父进程从管道的读操作,关闭子进程对管道的操作,即对管道的操作是单向的。

一个管道通信的示例,父进程往管道里写,子进程从管道读,然后打印读取的内容

/*************************************************************************
> File Name: pipe.c
> Author: Winter
> Created Time: 2021年10月14日 星期四 21时18分54秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
int main(int argc, char* argv[])
{
    int res;
    int fd[2];
    char* str = "hello pipe\n";
    char buff[1024];
    res = pipe(fd);
    if (res == -1) {
        perror("pipe error\n");
        exit(1);
    } 
    // 有管道了,父进程写,子进程读
    pid_t pid = fork();
    if (pid > 0 ){
        // 父进程
        close(fd[0]); // 关闭读端
        write(fd[1], str, strlen(str));
        sleep(3);
        close(fd[1]);
    } else if (pid == 0) {
        // 子进程
        close(fd[1]); // 关闭写端
        res = read(fd[0], buff, sizeof(buff));
        printf("child read res = %d\n",res);
        write(STDOUT_FILENO, buff, res);
        close(fd[0]);
    } else {
        perror("fork error");
        exit(1);
    } 
    return 0;
}

执行

3.1.2 管道读写行为

读管道

读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据:  
        1)无写端,read返回0 (类似读到文件尾)
        2)有写端,read阻塞等待。

写管道

1. 无读端, 异常终止。 (SIGPIPE导致的)
2. 有读端: 
        1) 管道已满, 阻塞等待
        2) 管道未满, 返回写出的字节个数。

3.1.3 父子进程通信练习

使用管道实现父子进程间通信,完成:ls | wc -l 假定父进程实现ls,子进程实现wc ls命令正常会将结果集写到stdout,但现在会写入管道写端wc -l命令正常应该从stdin读取数据,但此时会从管道的读端读。

/*************************************************************************
> File Name: pipe.c
> Author: Winter
> Created Time: 2021年10月14日 星期四 21时18分54秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
#include<fcntl.h>

void sys_err(const char* str) {
    perror(str);
    exit(1);
} 

int main(int argc, char* argv[])
{
    pid_t pid; // 进程用
    int res = 0; // 返回结果
    int fd[2]; // 管道读写
    // 创建管道
    if (pipe(fd) == -1) {
        sys_err("pipe error\n");
    } 
    pid = fork();
    if (pid < 0) {
        sys_err("fork error\n");
    } else if (pid > 0) {
        // 父进程执行
        close(fd[1]); // 关闭读端
        dup2(fd[0], STDIN_FILENO); // 将 参数2 重定向 参数1
        execlp("wc", "wc", "-l", NULL);
        sys_err("execlp wc error\n");
    } else {
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls", "ls", NULL);
        sys_err("execlp error\n");
    } 
    
    return 0;
}

执行

 3.1.4 兄弟间进程通信

练习题:兄弟进程间通信
兄:ls
弟:wc -l
父:等待回收子进程

要求,使用循环创建N个子进程模型创建兄弟进程,使用循环因子i标识,注意管道读写行为

 

/*************************************************************************
> File Name: pipe.c
> Author: Winter
> Created Time: 2021年10月14日 星期四 21时18分54秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
#include<sys/wait.h>
#include<fcntl.h>

void sys_err(const char* str) {
    perror(str);
    exit(1);
    } 

int main(int argc, char* argv[])
{
    pid_t pid; // 进程用
    int res = 0; // 返回结果
    int fd[2]; // 管道读写
    int i;
    // 创建管道
    if (pipe(fd) == -1) {
        sys_err("pipe error\n");
    } 

    // 循环创建2个子进程
    for (i = 0; i < 2; i++) {
        pid = fork(); // 创建进程
        if (pid < 0) {
            sys_err("fork error\n");
        } 
        // 子进程退出
        if (pid == 0) {
            break;
        }
    } 

    // 用i来标识子进程
    // 父进程
    if (i == 2) {
        // 父进程不使用进程,关掉
        close(fd[0]);
        close(fd[1]);
        wait(NULL);
        wait(NULL);
    } else if (i == 0) {
        // 兄进程
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls", "ls", NULL);
        sys_err("execlp error\n");
    } else if (i == 1) {
        // 弟进程
        close(fd[1]); // 关闭读端
        dup2(fd[0], STDIN_FILENO); // 将 参数2 重定向 参数1
        execlp("wc", "wc", "-l", NULL);
        sys_err("execlp wc error\n");
    } 
    
    return 0;
}

执行

 测试:

是否允许,一个pipe有一个写端多个读端 可以
是否允许,一个pipe有多个写端一个读端 可以

管道默认大小4096

3.1.5 多个写端操作管道

允许pipe中有一个写端,多个读端。

下面是一个父进程读,俩子进程写的例子,也就是一个读端多个写端。需要调控写入顺序才行。

/*************************************************************************
> File Name: pipe.c
> Author: Winter
> Created Time: 2021年10月14日 星期四 21时18分54秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<error.h>
#include<sys/wait.h>
#include<fcntl.h>

void sys_err(const char* str) {
    perror(str);
    exit(1);
} 

int main(int argc, char* argv[])
{
    pid_t pid; // 进程用
    int res = 0; // 返回结果
    int fd[2]; // 管道读写
    int i;
    char* buff[1024];
    // 创建管道
    if (pipe(fd) == -1) {
        sys_err("pipe error\n");
    } 

    // 循环创建2个子进程
    for (i = 0; i < 2; i++) {
        pid = fork(); // 创建进程
        if (pid < 0) {
            sys_err("fork error\n");
        } 
        // 子进程退出
        if (pid == 0) {
            break;
        }
    } 

    // 用i来标识子进程
    // 父进程
    if (i == 2) {
        // 父进程关闭写端,保留读端
        close(fd[1]);
        sleep(1);
        int n = 0;
        n = read(fd[0], buff, 1024);
        write(STDOUT_FILENO, buff, n);
        for (int k = 0; k < 2; k++) {
            wait(NULL);
        }
    } else if (i == 0) {
        // 兄进程
        close(fd[0]); // 关闭读端
        write(fd[1], "0 hello\n", strlen("0 hello\n"));
    } else if (i == 1) {
        // 弟进程
        close(fd[0]); // 关闭读端
        write(fd[1], "1 hello\n", strlen("1 hello\n"));
    } 

    return 0;
}

执行

 3.2 fifo

有名管道

优点:简单,相比信号,套接字实现进程通信,简单很多。
缺点:
    1.只能单向通信,双向通信需建立两个管道
    2.只能用于有血缘关系(父子,兄弟)的进程间通信。该问题后来使用fifo命名管道解决

fifo管道:可以用于无血缘关系的进程间通信。

命名管道: mkfifo

 无血缘关系进程间通信:

读端,open fifo O_RDONLY
写端,open fifo O_WRONLY

fifo操作起来像文件
下面的代码创建一个fifo:

/*************************************************************************
> File Name: testfifl.c
> Author: Winter
> Created Time: 2021年10月17日 星期日 21时02分59秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>

int main(int argc, char* argv[])
{
    int res = mkfifo("myTestFifo", 0664);
    if (res == -1) {
        perror("mkfifo error\n");
    } 

    return 0;
}

执行

如图,管道就通过程序创建出来了。

3.2.1 fifo实现非血缘关系进程间通信

下面这个例子,一个写fifo,一个读fifo,操作起来就像文件一样的:

fifo_w.c

/**************************************************************************
> File Name: fifo_w.c
> Author: Winter
> Created Time: 2021年10月17日 星期日 21时14分49秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>

int main(int argc, char* argv[])
{
    int fd;
    char buff[4096];
    if (argc < 2) {
        printf("Enter like this: ./a.out fifoname\n");
    } 
    
    fd = open(argv[1], O_WRONLY);
    if (fd < 0) {
        perror("fifo error\n");
        exit(1);
    } 

    int i = 0;
    while (1) {
        sprintf(buff, "hello %d\n",i++);
        write(fd, buff, strlen(buff));
        sleep(1);
    } 

    close(fd);
    return 0;
}

fifo_r.c

/*************************************************************************
> File Name: fifo_r.c
> Author: Winter
> Created Time: 2021年10月17日 星期日 21时19分15秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>

int main(int argc, char* argv[])
{
    int fd, len;
    char buff[4096];
    if (argc < 2) {
        printf("Enter like this: ./a.out fifoname\n");
    } 

    fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("fifo error\n");
        exit(1);
    }
    while (1) {
        len = read(fd, buff, sizeof(buff));
        write(STDOUT_FILENO, buff, len);
        sleep(1);
    } 

    close(fd);
    return 0;
}

如图

 编译执行,如图:

测试一个写端多个读端的时候,由于数据一旦被读走就没了,所以多个读端的并集才是写端的写入数据。

3.2.2 文件用于进程间通信

打开的文件是内核中的一块缓冲区。多个无血缘关系的进程,可以同时访问该文件。

文件通信这个,有没有血缘关系都行,只是有血缘关系的进程对于同一个文件,使用的同一个文件描述符,没有血缘关系的进程,对同一个文件使用的文件描述符可能不同。这些都不是问题,打开的是同一个文件就行。


3.3 mmap

3.3.1 函数原型

存储映射I/O(Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是从缓冲区中取数据,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使地址指针完成I/O操作。

使用这种方法,首先应该通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

mmap函数原型

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 
// 创建共享内存映射

// 参数:
// addr: 指定映射区的首地址。通常传NULL,表示让系统自动分配
// length:共享内存映射区的大小。(<= 文件的实际大小)
// prot: 共享内存映射区的读写属性。PROT_READ、PROT_WRITE、
PROT_READ|PROT_WRITE
// flags: 标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE
// fd: 用于创建共享内存映射区的那个文件的 文件描述符。
// offset:默认0,表示映射文件全部。偏移位置。需是 4k 的整数倍。
// 返回值:
// 成功:映射区的首地址。
// 失败:MAP_FAILED (void*(-1)), errno
// flags里面的shared意思是修改会反映到磁盘上,private表示修改不反映到磁盘上

munmap函数原型

int munmap(void *addr, size_t length); // 释放映射区。
// addr:mmap 的返回值
// length:大小

3.3.2 mmap建立映射区

下面这个示例代码,使用mmap创建一个映射区(共享内存),并往映射区里写入内容:

/*************************************************************************
> File Name: mmap.c
> Author: Winter
> Created Time: 2021年10月18日 星期一 20时30分00秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/mman.h>

int main(int argc, char* argv[])
{
    char* p = NULL;
    // 打开一个文件
    int fd = open("testmap", O_RDWR|O_CREAT|O_TRUNC, 0644);
    if (fd == -1) {
        perror("open error\n");
        exit(1);
    }
/*
    // 此时上面文件大小为0,拓展文件大小
    lseek(fd, 10, SEEK_END);
    write(fd, "1", 1); // 文件大小11
    上面这两个函数等于ftruncate()函数
*/

    ftruncate(fd, 20);
    int len = lseek(fd, 0, SEEK_END); // 有IO操作
    // mmap在这里
    p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED) {
    perror("mmap error\n");
        exit(1);
    } 

    // 使用p对文件进行读写操作
    strcpy(p, "hello mmap");
    // 读操作
    printf("-----%s\n",p);
    // 释放内存
    int res = munmap(p, len);
    if (res == -1) {
        perror("munmap error\n");
        exit(1);
    } 

    return 0;
}

执行

 查看testmap

1 od -tcx testmap

3.3.3 mmap使用注意事项

使用注意事项:

1 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出 “总线错误”。

int len = 20;
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

2 用于创建映射区的文件大小为 0,实际指定0大小创建映射区, 出 “无效参数”。

int len = 0;
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

3 用于创建映射区的文件读写属性为,只读(只写)。映射区属性为 读、写。 出 “无效参数(权限不允许)”。

4 创建映射区,需要read权限。当访问权限指定为 “共享”MAP_SHARED时, mmap的读写权限,应该<=文件的open权限。 只写不]行。两个都是只读【段错误】
5 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用 地址访问。
6 offset 必须是 4096的整数倍。(MMU 映射的最小单位 4k )
7 对申请的映射区内存,不能越界访问。
8 munmap用于释放的 地址,必须是mmap申请返回的地址。
9 映射区访问权限为 “私有”MAP_PRIVATE, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。
10 映射区访问权限为 “私有”MAP_PRIVATE, 只需要open文件时,有读权限,用于创建映射区即可。

mmap函数的保险调用方式:

fd = open("文件名", O_RDWR);
mmap(NULL, 有效文件大小, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

总结
1. 创建映射区的过程中,隐含着一次对映射文件的读操作
2. 当MAP_SHARED时,要求:映射区的权限应该<=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制
3. 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭
4. 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大
小!!mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。如,400字节大小的文件,在建立映射区时,offset4096字节,则会报出总线错误
5. munmap传入的地址一定是mmap返回的地址。坚决杜绝指针++操作
6. 文件偏移量必须为4K的整数倍
7. mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

3.3.4 父子进程间通信

父子进程使用 mmap 进程间通信:

父进程 先 创建映射区。 open( O_RDWR) mmap( MAP_SHARED );
指定 MAP_SHARED 权限
fork() 创建子进程。
一个进程读, 另外一个进程写。

下面这段代码,父子进程mmap通信,共享内存是一个int变量:

/*************************************************************************
> File Name: fork_mmap.c
> Author: Winter
> Created Time: 2021年10月20日 星期三 21时07分53秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/mman.h>
#include<sys/wait.h>
#include<fcntl.h>

int var = 10;
int main(int argc, char* argv[])
{
    int fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
    int* p = NULL;
    pid_t pid;
    if (fd < 0) {
        perror("open error\n");
        exit(1);
    } 

    ftruncate(fd, 4); // 将文件大小拓展为4
    p = (int*) mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED) {
        perror("mmap error\n");
        exit(1);
    } 

    close(fd); // 映射区创建完毕,关闭文件
    pid = fork(); // 创建子进程
    if (pid == 0) {
        // 子进程处理
        *p = 11000; // 写共享内存
        var = 20; // 修改全局变量
        printf("child *p = %d, var = %d\n",*p, var);
    } else if (pid > 0) {
        // 父进程处理
        sleep(1);
        printf("parent *p = %d, var = %d\n",*p, var); // 读共享内存
        wait(NULL); // 回收子进程
        // 回收
        int res = munmap(p, 4);
        if (res == -1) {
            perror("munmmap error\n");
            exit(1);
        }
    } else {
        perror("fork error\n");
        exit(1);
    } 

    return 0;
}

执行【读时共享,写时复制】

如图,子进程修改p的值,也反映到了父进程上,这是因为共享内存定义为shared的。
如果将共享内存定义为private,运行结果如下:

父子进程使用mmap进程间通信。
父进程先创建映射区,O_RDWR,指定MAP_SHARED,fork创建子进程,一个读一个写。

3.3.5 无血缘关系进程间mmap通信

两个进程 打开同一个文件,创建映射区。
指定flags 为 MAP_SHARED。
一个进程写入,另外一个进程读出。

【注意】:无血缘关系进程间通信。

mmap:数据可以重复读取。
fifo:数据只能一次读取。

下面是两个无血缘关系的通信代码,先是写进程:

/*************************************************************************
> File Name: mmap_w.c
> Author: Winter
> Created Time: 2021年10月22日 星期五 20时43分01秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/mman.h>

struct Stu{
    int id;
    char name[20];
    char gender;
};

int main(int argc, char* argv[])
{
    struct Stu student = {1, "xiaoming", 'M'};
    struct Stu* p = NULL;
/*  if (argc < 2) {
    printf("please input a.out sharedFile......\n");
    exit(1);
}*/

    int fd = open("stuFile", O_RDWR|O_CREAT|O_TRUNC, 0664);
    if (fd == -1) {
        perror("open error\n");
        exit(1);
    } 
    // 扩大内存
    ftruncate(fd, sizeof(student));
    p = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED) {
        perror("mmap error\n");
        exit(1);
    } 
    
    close(fd);
    while (1) {
        memcpy(p, &student, sizeof(student));
        student.id++;
        sleep(1);
    } 
    munmap(p, sizeof(student));
    return 0;
}

 然后是读进程:

/*************************************************************************
> File Name: mmap_w.c
> Author: Winter
> Created Time: 2021年10月22日 星期五 20时43分01秒
************************************************************************/

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/mman.h>

struct Stu{
    int id;
    char name[20];
    char gender;
};

int main(int argc, char* argv[])
{
    struct Stu student;
    struct Stu* p = NULL;
/*  if (argc < 2) {
        printf("please input a.out sharedFile......\n");
        exit(1);
    }
*/ 

    int fd = open("stuFile", O_RDONLY);
    if (fd == -1) {
        perror("open error\n");
        exit(1);
    } 

    p= mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED) {
        perror("mmap error\n");
        exit(1);
    } 
    
    close(fd);
    while (1) {
        printf("id = %d, name = %s, gender = %c\n",p->id, p->name, p->gender);
        sleep(1);
    } 
    munmap(p, sizeof(student));
    return 0;
}

执行

多个写端一个读端也没问题,打开多个写进程即可,完事儿读进程会读到所有写进程写入的内容。
这里要注意一个,内容被读走之后不会消失,所以如果读进程的读取时间间隔短,它会读到很多重复内容,就是因为写进程没来得及写入新内容

3.3.6 mmap匿名映射区

匿名映射:只能用于 血缘关系进程间通信。

p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

 较老的系统 类unix

/dev/zero------->聚宝盆
/dev/null------->文件黑洞

参考:https://blog.csdn.net/weixin_43914604/article/details/104882398

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值