进程间通信(5) - 命名管道(FIFO)

目录

1.前言

2.介绍

3.mknod函数

4.mkfifo函数

5.mknod与mkfifo区别

6.管道打开规则

7.管道读写规则

8.通信模式

9.管道和FIFO的限制


1.前言

本篇文章的所有例子,基于RHEL6.5平台。前一篇文章介绍了匿名管道。点此链接

2.介绍

管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。
FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。
需要注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

POSIX标准中的FIFO又名有名管道或命名管道。我们知道前面讲述的POSIX标准中管道是没有名称的,所以它的最大劣势是只能用于具有亲缘关系的进程间的通信。FIFO最大的特性就是每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。

FIFO包含下面两个特性:
  **和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流。
  **和管道不同的是,FIFO可以支持任意两个进程间的通信。

3.mknod函数

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int mknod(const char *pathname, mode_t mode, dev_t dev);

使用方法:  mknod 管道名称 p

4.mkfifo函数

#include<sys/types.h>
#include<sys/stat.h>
//成功则返回0,失败返回-1  
int mkfifo(const char * pathname,mode_t mode);

使用方法:
  mkfifo -m 权限 管道名称

参数说明:
  pathname:一个Linux路径名,它是FIFO的名字。即每个FIFO与一个路径名相对应。
  第二个参数与打开普通文件的open()函数中的mode 参数相同,指定的文件权限位。即创建该FIFO时,指定用户的访问权限,有以下值:S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。

如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。

mkfifo函数默认指定O_CREAT | O_EXECL方式创建FIFO,如果创建成功,直接返回0。如果FIFO已经存在,则创建失败,会返回-1并且errno置为EEXIST。对于其他错误,则置响应的errno值;

当创建一个FIFO后,它必须以只读方式打开或者只写方式打开,所以可以用open函数,当然也可以使用标准的文件I/O打开函数,例如fopen来打开。由于FIFO是半双工的,所以不能够同时打开来读和写。

其实一般的文件I/O函数,如read,write,close,unlink都可用于FIFO。对于管道和FIFO的write操作总是会向末尾添加数据,而对他们的read则总是会从开头数据,所以不能对管道和FIFO中间的数据进行操作,因此对管道和FIFO使用lseek函数,是错误的,会返回ESPIPE错误。

mkfifo的一般使用方式是:通过mkfifo创建FIFO,然后调用open,以读或者写的方式之一打开FIFO,然后进行数据通信。
下面是FIFO的一个简单的测试代码:

#include <iostream>  

#include <stdlib.h>
#include <unistd.h>  
#include <fcntl.h>  
#include <errno.h>  
#include <string.h>

#include <sys/stat.h>  
#include <sys/types.h>  

using namespace std;

#define FIFO_PATH "/root/fifo"  

int main() {
    if (mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST) {
        cout << "create fifo failed." << endl;
        return -1;
    }

    if (fork() == 0) {
        int readfd = open(FIFO_PATH, O_RDONLY);
        cout << "child open fifo success." << endl;

        char buf[256];
        read(readfd, buf, sizeof(buf));
        cout << "receive message from pipe: " << buf << endl;

        close(readfd);

        exit(0);
    }

    sleep(3);
    int writefd = open(FIFO_PATH, O_WRONLY);
    cout << "parent open fifo success." << endl;

    char* temp = "hello world";
    write(writefd, temp, strlen(temp) + 1);

    close(writefd);
}

输出:
[root@MiWiFi-R1CM csdnblog]# ./a.out
parent open fifo success.
child open fifo success.
receive message from pipe: hello world

由上面的运行结果可以看到,子进程以读方式open的操作会阻塞到父进程以写方式open;关于这一点以及read和write的操作会在后面管道和FIFO的属性部分进行介绍;
POSIX标准不仅规定了对mkfifo IPC的支持,还包括了对mkfifo shell命令的支持,所以符合POSIX标准的UNIX中都含有mkfifo命令来创建有名管道。

[root@MiWiFi-R1CM csdnblog]# mkfifo fifotest
[root@MiWiFi-R1CM csdnblog]# echo "hello world" > fifotest &
[1] 2726
[root@MiWiFi-R1CM csdnblog]# cat < fifotest
hello world
[1]+ Done echo "hello world" > fifotest

这里在第二行最后加上‘&’使进程转到后台运行,是因为FIFO以只写方式打开需要阻塞到FIFO以只读方式打开为止,所以必须要作为后台程序运行,否则进程会阻塞在前端,无法再进行相关输入。

5.mknod与mkfifo区别

mknod系统调用会产生由参数path所指定的文件,生成文件类型和访问权限由参数mode决定。
在很多unix的版本中有一个C库函数mkfifo,与mknod不同的是多数情况下mkfifo不要求用户有超级用户的权限。
利用命令创建命名管道p1.
#mkfifo -m 0644 p1
#mknod p2 p
#ll

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
main()
{
    if(mkfifo("p1",0644) < 0)
    {
        perror("mkfifo");
        exit(-1);
    }
    return;
}

6.管道打开规则

有名管道比管道多了一个打开操作:open。
由于在POSIX标准中,管道和FIFO都是通过文件描述符来进行操作的,默认的情况下,对他们的操作都是阻塞的,当然也可以通过设置来使对他们的操作变成非阻塞的。我们都知道可以有两种方式来设置一个文件描述符为O_NONBLOCK非阻塞的:
    --调用open时,指定O_NONBLOCK标志。例如:
       int fd = open(FILE_NAME, O_RDONLY | O_NONBLOCK);  
    --通过fcntl文件描述符控制操作函数,对一个已经打开的描述符启用O_NONBLOCK标志。其中对于管道必须使用这种方式。示例如下:
        int flag;  
        flag = fcntl(fd, F_GETFL, 0);  
        flag |= O_NONBLOCK;  
        fcntl(fd, F_SETFL, flag); 

下图主要说明了对管道和FIFO的各种操作在阻塞和非阻塞状态下的不同,这张图对对于理解和使用管道和FIFO是非常重要的。

从上图我们看到关于管道和FIFO的读出和写入的若干规则,主要需要注意的有以下几点:
    · 以只读方式open FIFO时,如果FIFO还没有以只写方式open,那么在阻塞模式下,该操作会阻塞到FIFO以只写方式open为止。
    · 以只写方式open FIFO时,如果FIFO还没有以只读方式open,那么在阻塞模式下,该操作会阻塞到FIFO以只读方式open为止。
    · 从空管道或空FIFOread,如果管道和FIFO已打开来写,在阻塞模式下,那么该操作会阻塞到管道或FIFO有数据为止,或管道或FIFO不再以写方式打开。如果管道和FIFO没有打开来写,那么该操作会返回0
    · 向管道或FIFOwrite,如果管道或FIFO没有打开来读,那么内核会产生SIGPIPE信号,默认情况下,该信号会终止该进程。

另外对于管道和FIFO还需要说明的若干规则如下:
    · 如果请求write的数据的字节数小于等于PIPE_BUFPOSIX关于管道和FIFO大小的限制值),那么write操作可以保证是原子的,如果大于PIPE_BUF,那么就不能保证了。

那么由此可知write的原子性是由写入数据的字节数是否小于等于PIPE_BUF决定的,和是不是O_NONBLOCK没有关系。下面是在阻塞和非阻塞情况下,write不同大小的数据的操作结果:

在阻塞的情况下:
    · 如果write的字节数小于等于PIPE_BUF,那么write会阻塞到写入所有数据,并且 写入操作是原子的。
    ·  如果write的字节数大于PIPE_BUF,那么write会阻塞到写入所有数据,但写入操作不是原子的,即write会根据当前缓冲区剩余的大小,写入相应的字节数,然后等待下一次有空余的缓冲区,这中间可能会有其他进程进行write操作。

在非阻塞的情况下:
    · 如果write的字节数小于等于PIPE_BUF,且管道或FIFO有足以存放要写入数据大小的空间,那么就写入所有数据;
    ·  如果write的字节数小于等于PIPE_BUF,且管道或FIFO没有足够存放要写入数据大小的空间,那么就会立即返回EAGAIN错误。
    · 如果write的字节数大于PIPE_BUF,且管道或FIFO有至少1B的空间,那么就内核就会写入相应的字节数,然后返回已写入的字节数;
    · 如果write的字节数大于PIPE_BUF,且管道或FIFO无任何的空间,那么就会立即返回EAGAIN错误。

对FIFO打开规则的验证(主要验证写打开对读打开的依赖性)

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

#define FIFO_SERVER "/tmp/fifoserver"
int handle_client(char*);

int main(int argc, char** argv) {
    int r_rd;
    int w_fd;
    pid_t pid;
    if ((mkfifo(FIFO_SERVER, O_CREAT | O_EXCL) < 0) && (errno != EEXIST))
        printf("cannot create fifoserver\n");
    handle_client(FIFO_SERVER);
    return 0;
}

int handle_client(char* arg) {
    int ret;
    ret = w_open(arg);
    switch (ret) {
        case 0: {
            printf("open %s error\n", arg);
            printf("no process has the fifo open for reading\n");
            return -1;
        }
        case -1: {
            printf("something wrong with open the fifo except for ENXIO");
            return -1;
        }
        case 1: {
            printf("open server ok\n");
            return 1;
        }
        default: {
            printf("w_no_r return ----\n");
            return 0;
        }
    }
    unlink(FIFO_SERVER);
}

//0  open error for no reading
//-1 open error for other reasons
//1  open ok
int w_open(char* arg)
{
    if (open(arg, O_WRONLY | O_NONBLOCK, 0) == -1) {
        if (errno == ENXIO) {
            return 0;
        }
        else
            return -1;
    }
    return 1;
}

输出:
[root@MiWiFi-R1CM csdnblog]# ./a.out 
open /tmp/fifoserver error
no process has the fifo open for reading

7.管道读写规则

通过open打开,默认是阻塞方式打开,如果open指定O_NONBLOCK则以非阻塞打开。
O_NONBLOCK和O_NDELAY所产生的结果都是使I/O变成非搁置模式(non-blocking),在读取不到数据或是写入缓冲区已满会马上return,而不会搁置程序动作,直到有数据或写入完成。
它们的差别在于设立O_NDELAY会使I/O函式马上回传0,但是又衍生出一个问题,因为读取到档案结尾时所回传的也是0,这样无法得知是哪种情况;因此,O_NONBLOCK就产生出来,它在读取不到数据时会回传-1,并且设置errno为EAGAIN。
不过需要注意的是,在GNU C中O_NDELAY只是为了与BSD的程序兼容,实际上是使用O_NONBLOCK作为宏定义,而且O_NONBLOCK除了在ioctl中使用,还可以在open时设定。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
main()
{
    int fd;
   
    if((fd = open("p1",O_RRONLY,0)) < 0)//只读打开管道
  // if((fd = open("p1",O_WRONLY,0)) < 0)//只写打开管道
    {
        perror("open");
        exit(-1);
    }
    printf("open fifo p1 for write success!\n");
    close(fd);
}

从FIFO中读取数据:
约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。
  ·如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
  ·对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。
  ·读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。
  ·如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。
注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

向FIFO中写入数据:
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。
对于设置了阻塞标志的写操作:
  ·当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
  ·当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

对于没有设置阻塞标志的写操作:
  ·当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
  ·当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;

对FIFO读写规则的验证:
下面提供了两个对FIFO的读写程序,适当调节程序中的很少地方或者程序的命令行参数就可以对各种FIFO读写规则进行验证。

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO_SERVER "/tmp/fifoserver"
int main(int argc, char** argv)
//参数为即将写入的字节数
{
    int fd;
    char w_buf[4096 * 2];
    int real_wnum;
    memset(w_buf, 0, 4096 * 2);
    if ((mkfifo(FIFO_SERVER, O_CREAT | O_EXCL) < 0) && (errno != EEXIST))
        printf("cannot create fifoserver\n");
    if (fd == -1) {
        if (errno == ENXIO)
            printf("open error; no reading process\n");
    }

    fd = open(FIFO_SERVER, O_WRONLY | O_NONBLOCK, 0);
    //设置非阻塞标志
    //fd=open(FIFO_SERVER,O_WRONLY,0);
    //设置阻塞标志
    real_wnum = write(fd, w_buf, 2048);
    if (real_wnum == -1) {
        if (errno == EAGAIN)
            printf("write to fifo error; try later\n");
    }
    else {
        printf("real write num is %d\n", real_wnum);
    }
    real_wnum = write(fd, w_buf, 5000);
    //5000用于测试写入字节大于4096时的非原子性
    //real_wnum=write(fd,w_buf,4096);
    //4096用于测试写入字节不大于4096时的原子性

    if (real_wnum == -1) {
        if (errno == EAGAIN)
            printf("try later\n");
    }
    return 0;
}

没有任何东西输出:
[root@MiWiFi-R1CM csdnblog]# ./a.out 

下面的程序是与上面的程序一起测试写FIFO的规则,第一个命令行参数是请求从FIFO读出的字节数。

#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO_SERVER "/tmp/fifoserver"
main(int argc, char** argv) {
    char r_buf[4096 * 2];
    int  fd;
    int  r_size;
    int  ret_size;
    r_size = atoi(argv[1]);
    printf("requred real read bytes %d\n", r_size);
    memset(r_buf, 0, sizeof(r_buf));
    fd = open(FIFO_SERVER, O_RDONLY | O_NONBLOCK, 0);
    //fd=open(FIFO_SERVER,O_RDONLY,0);
    //在此处可以把读程序编译成两个不同版本:阻塞版本及非阻塞版本
    if (fd == -1) {
        printf("open %s for read error\n");
        exit();
    }
    while (1) {

        memset(r_buf, 0, sizeof(r_buf));
        ret_size = read(fd, r_buf, r_size);
        if (ret_size == -1)
            if (errno == EAGAIN)
                printf("no data avlaible\n");
        printf("real read bytes %d\n", ret_size);
        sleep(1);
    }
    pause();
    unlink(FIFO_SERVER);
}

程序应用说明:
把读程序编译成两个不同版本:
  ·阻塞读版本:br
  ·以及非阻塞读版本nbr

把写程序编译成两个四个版本:
  · 非阻塞且请求写的字节数大于PIPE_BUF版本:nbwg
  · 非阻塞且请求写的字节数不大于PIPE_BUF版本:版本nbw
  · 阻塞且请求写的字节数大于PIPE_BUF版本:bwg
  · 阻塞且请求写的字节数不大于PIPE_BUF版本:版本bw
下面将使用br、nbr、w代替相应程序中的阻塞读、非阻塞读。
验证阻塞写操作:
1 当请求写入的数据量大于PIPE_BUF时的非原子性:
  nbr 1000
  bwg

2 当请求写入的数据量不大于PIPE_BUF时的原子性:
  nbr 1000
  bw

验证非阻塞写操作:
3 当请求写入的数据量大于PIPE_BUF时的非原子性:
  nbr 1000
  nbwg
4 请求写入的数据量不大于PIPE_BUF时的原子性:
  nbr 1000
  nbw

不管写打开的阻塞标志是否设置,在请求写入的字节数大于4096时,都不保证写入的原子性。但二者有本质区别:
对于阻塞写来说,写操作在写满FIFO的空闲区域后,会一直等待,直到写完所有数据为止,请求写入的数据最终都会写入FIFO;
而非阻塞写则在写满FIFO的空闲区域后,就返回(实际写入的字节数),所以有些数据最终不能够写入。
对于读操作的验证则比较简单,不再讨论。

8.通信模式

命名管道提供了两种基本通信模式:字节模式和消息模式。在字节模式中,消息以一个连续的字节流的形式,在客户机与服务器之间流动。这意味着,对客户机应用和服务器应用来说,在任何一个特定的时间段内,它们不能准确知道有多少字节从管道中读入或者写入管道。因此,在一方写入某个数量的字节,并不表示在另一方会读出等量的字节。这样一来,客户机和服务器在传输数据的时候,便不必关心数据的内容。而在消息模式中,客户机和服务器则通过一系列不连续的数据单位,进行数据的收发。每次在管道上发出了一条消息后,它必须作为一条完整的消息读入。

命名管道最大的特点便是建立一个简单的客户机/服务器程序设计体系。在这个体系结构中,在客户机与服务器之间,数据既可单向传递,亦可双向流动。这一点相当重要,因为我们可以自由地收发数据,无论应用程序是一个客户机还是一个服务器。对命名管道服务器和客户机来说,两者最大的区别在于:服务器是唯一一个有权创建命名管道的进程,也只有它才能接受管道客户机的连接请求。对一个客户机应用来说,它只能同一个现成的命名管道服务器建立连接。在客户机应用和服务器应用之间,一旦建好连接,两个进程都能对标准的Wi n 3 2函数,在管道上进行数据读取与写入。这些包括ReadFile和WriteFile等。

要想实现一个命名管道服务器,要求必须开发一个应用程序,通过它创建命名管道的一个或多个“实例”,再由客户机进行访问。对服务器来说,管道实例实际就是一个句柄,用于从本地或远程客户机应用接受一个连接请求。按下述步骤行事,便可写出一个最基本的服务器应用:
1) 使用API函数CreatNamedPipe,创建一个命名管道实例句柄。
2) 使用API函数ConnectNamedPipe,在命名管道实例上监听客户机连接请求。
3) 分别使用ReadFile和WriteFile这两个A P I函数,从客户机接收数据,或将数据发给客户机。
4) 使用API函数DisconnectNamedPipe,关闭命名管道连接。
5) 使用API函数CloseHandle,关闭命名管道实例句柄。

实现一个命名管道客户机时,要求开发一个应用程序,令其建立与某个命名管道服务器的连接。注意客户机不可创建命名管道实例。然而,客户机可打开来自服务器的、现成的实例。下述步骤讲解了如何编写一个基本的客户机应用:
1) 用API函数WaitNamedPipe,等候一个命名管道实例可供自己使用。
2) 用API函数CreatFile,建立与命名管道的连接。
3) 用API函数WriteFile和ReadFile,分别向服务器发送数据,或从中接收数据。
4) 用API函数CloseHandle,关闭打开的命名管道会话

9.管道和FIFO的限制

系统内核对于管道和FIFO的唯一限制为:OPEN_MAX和PIPE_BUF;
OPEN_MAX:一个进程在任意时刻可以打开的最大描述符数。
PIPE_BUF标识一个管道可以原子写入管道和FIFO的最大字节数,并不是管道或FIFO的容量。

关于这两个系统限制,POSIX标准中都有定义的不变最小值:POSIX_OPEN_MAX和_POSIX_PIPE_BUF,这两个宏是POSXI标准定义的编译时确定的值,他们是标准定义的且不会改变的,POSIX标准关于这两个值的限制为:
cout<<_POSIX_OPEN_MAX<<endl;  
cout<<_POSIX_PIPE_BUF<<endl;  
//运行结果为:  
20  
512  

我们都知道,关于POSIX的每个不变最小值都有一个具体的系统的实现值,这些是实现值由具体的系统决定,通过调用以下函数在运行时确定这个实现值:

#include <unistd.h>  
//成功返回具体的值,失败返回-1 
long sysconf(int name);  
long fpathconf(int filedes, int name);  
long pathconf(char *path, int name);  

其中sysconf是用于返回系统限制值,这些值是以_SC_开头的常量,pathconf和fpathconf是用于返回与文件和目录相关的运行时的限制值,这些值都是以_PC_开头的常量;
下面是在Linux 2.6.18下的测试代码:
cout<<sysconf(_SC_OPEN_MAX)<<endl;  
cout<<pathconf(FIFO_PATH, _PC_PATH_MAX)<<endl;  
//运行结果为:  
1024  
4096

当然上面两个系统限制值的具体实现值也可以通过ulimit命令来查看
[root@idcserver program]# ulimit -a  
open files                    (-n) 1024  
pipe size                     (512 bytes, -p) 8  

这两个值在Linux 2.6.18下都是不允许修改的,也是没有必要修改的。

  • 1
    点赞
  • 3
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

水草

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值