文件I/O编程范例解析

1    文件I/O编程

详细请关注本人新书发布!—Linux环境应用编程 作者:刘洋 胡鹏磊

1.1  系统调用学习——三部曲

在我们编程中用的最多的是函数,也就是如何调用函数。那我们如何以一种通用的方式快速掌握函数的使用方法呢?

1.     我们必须要知道函数的功能是什么——从抽象到感性认识

2.     再看这个函数需要哪些参数——多根据参数写测试代码上升到理性认识

3.     最后看返回值是什么——关心她你才能知道函数执行的状态

当我们面对一个函数时,既不知道函数的功能也不知道参数以及返回值时,我们该如何下手呢?必须得动手查询呗,可以使用man函数手册,终端,以及书本资料等一切可以利用的资源。

1.2  open函数

头文件

#include<sys/types.h> /*提供类型pid_t,size_t的定义*/

#include<sys/stat.h>

#include<fcntl.h>

函数原型

int open(const char *path, int oflags,mode_t mode);

函数说明

   open建立了一条到文件或设备的访问路径。

open函数一般用于打开或者创建文件,在打开或创建文件时可以制定文件的属性及用户的权限等各种参数。

    

第一个参数path表示:路径名或者文件名。路径名为绝对路径名(如C:/cpp/a.cpp,文件则是在当前工作目录下的。

第二个参数oflags表示:打开文件所采取的动作。

   可能值:必须指定下面某一种:

   O_RDONLY(只读),

   O_WRONLY(只写),

   O_RDWR(可读可写)

打开/创建文件时,至少得使用上述三个常量中的一个,以下常量是选用的:

 O_APPEND      每次写操作都写入文件的末尾

O_CREAT       如果指定文件不存在,则创建这个文件

O_EXCL        如果要创建的文件已存在,则返回 -1,并且修改 errno的值

O_TRUNC       如果文件存在,并且以只写/读写方式打开,则清空文件全部内容

O_NOCTTY      如果路径名指向终端设备,不要把这个设备用作控制终端。

O_NONBLOCK    如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O

                      设置为非阻塞模式(nonblocking mode

第三个参数mode表示:设置文件访问权限的初始值。(与用户掩码umask变量有关,实际的访问权限有mode &~umask确定)

       S_IRUSR,S_IWUSER,S_IXUSR,S_IRGRP,S_IWGRP,S_IXGRP,S_IROTH,S_IWOTH,S_IXOTH.其中R:读,W:写,X:执行,USR:文件所属的用户,GRP:文件所属的组,OTH:其他用户。

 

注:第三个参数是在第二个参数中有O_CREAT时才用作用。若没有,则第三个参数可以忽略。

返回值:如果操作成功,它将返回一个文件描述符,如果失败,返回-1

范例

显然这是用open函数对字符串数组进行操作,对filename设置了可读动作。

执行结果:(test.txt open success !

 

有关文件操作范例

 

#include<stdio.h>

#include<string.h>

  #include<Syd/types.h>

#include<sys/stat.h>

#include<faintly.h>

#include<unistd.h>

int main()

{

char temp[]="hello!";

int fd;

 

 

if((fd=open("fileopen.txt",O_WRONLY|O_CREAT,0640))==-1)

 

{

 

printf("creat file wrong!");

 

}

 

Int  len=strlen(temp)+1;

 

write(fd,temp,len);//fd0,或这里直接写0,则会输出到屏幕上而不写入文件中

 

close(fd);

 

}

 

1.3  close函数

头文件

#include<unistd.h>

函数原型

intclose(int fd)

函数说明

close函数用于关闭一个打开文件。

参数fd是要关闭的文件描述符。

需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。

返回值

成功返回0,失败返回-1

 

范例

 

#include<stdio.h>

 

#include<stdlib.h>

 

#include<sys/types.h>

 

#include<sys/stat.h>

 

#include<fcntl.h>

 

#include<unistd.h>

 

 

int main()

 

{

 

int fd;

 

fd = open("/tmp/hello.c",O_CREAT| O_TRUNC | O_WRONLY, 0600);

 

/*调用open函数*/

 

if(fd<0)

 

{

 

perror("open:");

 

exit(1);

 

}

 

else printf("open file: hello.c %d\n",fd);

 

 

close(fd);         /*调用close函数*/

 

exit(0);

 

}

1.4  read函数

头文件

      #include<unistd.h>

函数原型

ssize_t read(int fd,void *buf,int count)

函数说明

函数从打开的设备或文件中读取数据。

fd 将要读取数据的文件描述词。
buf
:指缓冲区,即读取的数据会被放到这个缓冲区中去。
count
表示调用一次read操作,应该读多少数量的字符。

以下几种情况会导致读取到的字节数小于 count

    1.读取普通文件时,读到文件末尾还不够 count字节。例如:如果文件只有10字节,而我们想读取 100
字节,那么实际读到的只有10字节,read函数返回10。此时再使用 read函数作用于这个文件会导致 read返回 0

    2.
从终端设备读取时,一般情况下每次只能读取一行。
    3.
从网络读取时,根据不同的传输层协议和内核缓存机制,网络缓存可能导致读取的字节数小于 count字节。后面socket编程部分会详细讲解。
    4.
读取 pipe时,pipe里的字节数可能小于 count
    5.
从面向记录的设备读取时,某些面向记录的设备(如磁带)每次最多只能返回一个记录。
    6.
在读取了部分数据时被信号中断。返回值会小于count。为-1,且设置errorEINTP

注意:读取到的字节存放在buf缓冲区中,必须最后加上一个字节'\0'才能组成一个字符

 

现在明确一下阻塞这个概念当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。

读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

返回值

若读取失败则返回-1.读取成功则返回实际读取到的字节数

范例

有关read函数对文件操作如下3.5write函数3.6 lseek函数示例里有介绍。

 

 

下面介绍一下网络中read函数接收数据实例

#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>         /*
包含头文件。*/

#define PORT 6677        //定义一个端口号

main()
{
int sockfd,newsockfd,fd;       //
定义相关的变量
struct sockaddr_in addr;
int addr_len = sizeof(struct sockaddr_in);
fd_set myreadfds;
char msgbuffer[256];
char msg[] ="This is the message from server.Connected.\n";

if ((sockfd = socket(AF_INET,SOCK_STREAM,0))<0) //
建立一个socket
{
perror("socket");
exit(1);
}
else
{
printf("socket created .\n");  //socket
建立成功。
printf("socked id: %d \n",sockfd);
}

  bzero(&addr,sizeof(addr));    //清空zero所在的内存
addr.sin_family =AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);

  if(bind(sockfd,&addr,sizeof(addr))<0) //绑定IP端口
{
perror("connect");
exit(1);
}
else
{
printf("connected.\n");

printf("local port:%d\n",PORT) ;

}

   if(listen(sockfd,3)<0)    //监听一个端口号
{
perror("listen");
exit(1);
}
else
{
printf("listenning......\n");
}

   if((newsockfd =accept (sockfd,&addr,&addr_len))<0) //接受一个连接
{
perror("accept");
}
else           //
输出结果
{
printf("cnnect from %s\n",inet_ntoa(addr.sin_addr));
if(read(newsockfd,msgbuffer,sizeof(msgbuffer))<=0)
         //
接收信息
{
perror("accept");
}
else
{   
printf("message:\n%s \n",msgbuffer);  //
输出接收到的信息
}
}
}

输入下面的命令,编译这个程序。

gcc 17.24.c

 

输入下面的命令,运行这个程序。

 ./a.out

程序的运行结果如下所示。结果表明这个程序正在监听本地计算机的6677号端口。

 

socket created .

socked id: 3

connected.

local port:6677

listenning......

打开浏览器,在浏览器中输入下面的网址,然后按“Enter”键,使浏览器访问本地计算机的6677号端口。

浏览器显示无法打开网页。在终端中显示了下面的代码,这些代码是浏览器向本机的6677号端口请求打开网页的数据。


message:

GET / HTTP/1.1

Host: 127.0.0.1:6677

User-Agent: Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.0.18)Gecko/2010021718 CentOS/3.0.18-1.el5.centos Firefox/3.0.18

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

Accept-Language: zh-

 

1.5  write函数

 

头文件

#include<unistd.h>

函数原型

size_t write(int fd,const void *buf,size_t count)

函数说明

writebuf中写入 count字节到文件fd中,数据来源为 buf。返回值一般总是等于 count,否则就是出错了。常见的出错原因是磁盘空间满了或者超过了文件大小限制。

对于普通文件,写操作始于 cfo。如果打开文件时使用了 O_APPEND,则每次写操作都将数据写入文件末尾。成功写入后,cfo增加,增量为实际写入的字节数

返回值

执行成功则返回写入的字节数 ;执行失败则返回-1.

 

范例rw.c

#include <stdio.h>   

#include <stdlib.h>   

#include <string.h>  

#include <unistd.h>   

#include <sys/types.h>  

#include <fcntl.h>

Int  main(void)
{
int fd, size;
char  buf1[]="Hello, world";
char  buf2[50];

if((fd=open("/home/sam/helloworld",O_CREAT|O_TRUNC|O_RDWR,0666))==-1)
{
printf("
Open file named \"helloworld\"failed\n");
exit(1);
}
write(fd,buf1,sizeof(buf1));//
buf1中写sizeof(buf1)字节数到文件fd
close(fd);

if((fd=open("/home/sam/helloworld",O_RDONLY))==-1)
{
printf
("Open file named \"helloworld\" failed!\n");
exit(1);
}

size=read(fd,buf2,sizeof(buf2));

if(-1==size)

{

 printf("Read failure!\n")

exit(0);

}
close(fd);
printf("%s\n",buf2);

if((fd=open("/home/sam/helloworld",O_RDONLY))==-1)
{
printf("Open  file named\"helloworld\" failed.\n");
exit(1);
}
lseek(fd,6,SEEK_SET);// lseek
函数下面有详解,将读写位置移到文件的第6个字节位置


size=read(fd,buf2,sizeof(buf2));

if(-1==size)

{

 printf("Read failure!\n")

exit(0);

}
printf("%s\n",buf2);//
close(fd);
return 0;
}

编译如下:

[root@localhost opt]#gcc rw.c –o rw

[root@localhost opt]# ./rw

 Hello, world

world

1.6  lseek函数

头文件

#include <sys/types.h>

   #include<unistd.h>

函数原型

  off_tlseek(int  f i l e d e s, off_t  o f f s e t, int    w h en c e) ;

函数说明

每个打开文件都有一个与其相关联的“当前文件位移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。(本节稍后将对“非负”这一修饰词的某些例外进行说明。)通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数。按系统默认,当打开一个文件时,除非指定O A P P E N D选择项,否则该位移量被设置为0

可以调用l s e e k显式地定位一个打开文件。

 

 参数whence为下列其中一种:SEEK_SET,SEEK_CURSEEK_END和依次为012.  

SEEK_SET将读写位置指向文件头后再增加offset个位移量。 

SEEK_CUR以目前的读写位置往后增加offset个位移量。  

 SEEK_END将读写位置指向文件尾后再增加offset个位移量。 

 当whence值为SEEK_CURSEEK_END时,参数offet允许负值的出现。  下列是教特别的使用方式:  

   1)欲将读写位置移到文件开头时:  lseekint fildes,0,SEEK_SET); 

  2)欲将读写位置移到文件尾时:  lseekint fildes0,SEEK_END); 

  3)想要取得目前文件位置时:  lseekint fildes0,SEEK_CUR);

 

返回值

   若成功为新的文件位移,若出错为- 1

范例

   #include<stdio.h>  

    #include <stdlib.h>  

    #include <string.h> 

   #include <unistd.h>  

    #include <sys/types.h>

#include<sys/stat.h>  

   #include <fcntl.h> 

   int main(void)  

     {   

      int handle; 

     char msg[] = "This is a test"; 

     char ch[50];  

     handle = open("TEST.txt", O_CREAT| O_RDWR, S_IREAD | S_IWRITE);               

     write(handle, msg, strlen(msg));   

    lseek(handle, 0, SEEK_SET); //将读写位置移到文件开头   

 do  {  

read(handle, ch, sizeof(ch)); 

 printf("%s", ch);  

} while (!eof(handle)); 

 close(handle);  

return 0;  

}

1.7  rewind函数

头文件

   #include"stdio.h"

函数原型

  voidrewind(FILE *fp)

函数说明

 每当进行一次读写后,该指针自动指向下一次读写的位置

当文件刚打开或创建时,该指针指向文件的开始位置。

可以用函数ftell()获得当前的位置指针,也可以用rewind()fseek()函数改变位置指针,使其指向需要读写的位置。

文件指针FILE *fp中,包含一个读写位置指针char *_nextc,它指向下一次文件读写的位置。

 

  typedef struct

   {

    int _fd;   /*文件号 */

    int _cleft;  /*缓冲区中剩下的字节数 */

    int _mode;  /*文件操作模式 */

    char * _nextc; /*下一个字节的位置 */

    char * _buff; /*文件缓冲区位置 */

   }FILE;

返回值

范例

 

 把一个文件的内容显示在屏幕上,并同时复制到另一个文件。

 

#include "stdio.h"

 

void main()

{

FILE *fp1, *fp2;

fp1 = fopen("file1.c", "r"); /* 源文件 */

 

fp2 = fopen("file2.c", "w"); /* 复制到file2.c */

 

while(!feof(fp1)) putchar(fgetc(fp1)); /*显示到屏幕上 */

 

rewind(fp1); /* fp回到开始位置 */

 

while(!feof(fp1)) fputc(fgetc(fp1), fp2);

 

fclose(fp1);

 

fclose(fp2);

}

1.8  dup函数

头文件

 #include<unistd.h>

函数原型

 Int dup(int oldfd);

函数说明

  dup()函数用来创建一个文件描述符oldfd的备份。

 执行成功则返回新的文件描述符.旧的文件描述符和新的文件描述符共享文件的偏移量和文件的状态标志;例如,若其中一个文件描述符的文件偏移量被lseek()函数修改过的话则另外一个文件描述符的文件偏移量也被修改.但是两个文件描述符不共享文件描述符标志(close-on-exec标志),转存时该标志被关闭.

 dup()函数使用一个最小的未用的整数作为新文件描述符.

 

返回值

  执行成功则返回新的文件描述符,若发生错误则返回-1.

范例

#include<unistd.h>

 #include<fcntl.h>

 #include<stdio.h>

 #include<stdlib.h>

 

 int main(void)

 {

 

   int fd;

    fd=open("test.txt",O_RDWR);

    if(fd==-1)

{

      perror("Fail to open");

      exit(1);

      }

else

{

      printf("Open Ok\n");

    }

 

   if(dup(fd)==-1) /*fd复制为1,也就是复制文件到标准输出位置*

{ 

      perror("fail to dup");

      exit(1);

}

else

{

      printf("Dup OK\n");

    }

  return 0;

 }

编译结果:

[root@localhostopt]#gcc dup.c –o dup

[root@localhost opt]#./dup

Open Ok

Dup OK

 

3.9 dup2函数

头文件

 #include<unistd.h>

函数原型

  int dup2(int oldfd,int newfd);

函数说明

复制文件的描述符,它经常用来重定向进程的stdinstdoutstderr

dup2赋值oldfdnewfd(也可以说是将newfd<已打开>重定向到oldfd),如果newfd以打开,则会关闭newfd,如果newfd等于oldfd,则直接返回newfd

返回值

范例

当个进程内使用dup()/dup2()实现重定向

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
int fd_file = -1;
int oldfd = -1;

fd_file = open("./test.txt", O_CREAT|O_RDWR);
if(fd_file == -1){
printf("open file error \n");
return -1;
}else{
printf("fd_file is %d\n", fd_file);
}

oldfd = dup(1);
printf("oldfd is %d\n", oldfd);
//重定向STDOUT定向到fd test.tx
dup2(fd_file, 1);
printf("
应该被写入到test . txt \n");
dup2(oldfd, 1);//restore fd for STDOUT
printf("
这写到终端\n");
close(oldfd);
close(fd_file);
return 0;
}


程序输出如下

./a.out
fd_file is 3
oldfd is 4
这写到终端
cattest.txt
应该被写入到test . txt

 

 

 

每个进程都有一个文件描述符表,每个表项中都有指向对应文件的指针,进程在获取描述符时总是获得"当前所有可用描述符中值最小的"那个.注意,在调用dup2,如果newfd指向的文件没有和其他描述符关联,那么该文件会被关闭.可以用下面的示意图加深对dup()/dup2()的理解

 

1.1  fcntl函数

头文件

 #include<unistd.h>

 #include<fcntl.h>

函数原型

  Int fcntl(int filedes,int cmd,);

函数说明

改变已经打开文件的性质

fcntl函数的

返回值

   若成功依赖于cmd(),出错为-1

范例

  #include<stdio.h>

  #include<fantl.h

int  main()

  {

   int Mymode,val;

   int fd=open("test.txt",O_RDWR);

    if(-1==fd)

    {

        perror("flie error!");        

    }

   printf("fd=%d\n",fd);

   //获得文件状态

   val=fcntl(fd,F_GETFL,0);

   Mymode=val & O_ACCMODE;

   if(Mymode==O_RDONLY)

       printf("raed only\n");

    else if(Mymode==O_WRONLY)

       printf("write only\n");

    else if(Mymode==O_RDWR)

       printf("read write\n");

    else printf("unknown mode");

    close(fd);

    return 0

   

ioctl函数

头文件

#include<unistd.h>

 

函数原型

int ioctl( int fd, int request, .../* void *arg */);

函数说明

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等

其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,有或没有是和cmd的意义相关的。

        ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道

 

第三个参数总是一个指针,但指针的类型依赖于request参数。

我们可以把和网络相关的请求划分为6类:

套接口操作

文件操作

接口操作

ARP高速缓存操作

路由表操作

流系统

下表列出了网络相关ioctl请求的request参数以及arg地址必须指向的数据类型:

类别

Request

说明

数据类型

SIOCATMARK

SIOCSPGRP

SIOCGPGRP

是否位于带外标记

设置套接口的进程ID或进程组ID

获取套接口的进程ID或进程组ID

int

int

int

FIONBIN

FIOASYNC

FIONREAD

FIOSETOWN

FIOGETOWN

设置/清除非阻塞I/O标志

设置/清除信号驱动异步I/O标志

获取接收缓存区中的字节数

设置文件的进程ID或进程组ID

获取文件的进程ID或进程组ID

int

int

int

int

int

SIOCGIFCONF

SIOCSIFADDR

SIOCGIFADDR

SIOCSIFFLAGS

SIOCGIFFLAGS

SIOCSIFDSTADDR

SIOCGIFDSTADDR

SIOCGIFBRDADDR

SIOCSIFBRDADDR

SIOCGIFNETMASK

SIOCSIFNETMASK

SIOCGIFMETRIC

SIOCSIFMETRIC

SIOCGIFMTU

SIOCxxx

获取所有接口的清单

设置接口地址

获取接口地址

设置接口标志

获取接口标志

设置点到点地址

获取点到点地址

获取广播地址

设置广播地址

获取子网掩码

设置子网掩码

获取接口的测度

设置接口的测度

获取接口MTU

(还有很多取决于系统的实现)

struct ifconf

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

struct ifreq

ARP

SIOCSARP

SIOCGARP

SIOCDARP

创建/修改ARP表项

获取ARP表项

删除ARP表项

struct arpreq

struct arpreq

struct arpreq

SIOCADDRT

SIOCDELRT

增加路径

删除路径

struct rtentry

struct rtentry

I_xxx

   

 

返回值

返回0:成功 -1:出错

范例

文件操作:

以下5个请求都要求ioctl的第三个参数指向一个整数。

FIONBIO根据ioctl的第三个参数指向一个0或非0值分别清除或设置本套接口的非阻塞标志。本请求和O_NONBLOCK文件状态标志等效,而该标志通过fcntlF_SETFL命令清除或设置。

FIOASYNC根据iocl的第三个参数指向一个0值或非0值分别清除或设置针对本套接口的信号驱动异步I/O标志,它决定是否收取针对本套接口的异步I/O信号(SIGIO)。本请求和O_ASYNC文件状态标志等效,而该标志可以通过fcntlF_SETFL命令清除或设置。

FIONREAD通过由ioctl的第三个参数指向的整数返回当前在本套接口接收缓冲区中的字节数。本特性同样适用于文件,管道和终端。

FIOSETOWN对于套接口和SIOCSPGRP等效。

FIOGETOWN对于套接口和SIOCGPGRP等效。

接口配置:

得到系统中所有接口由SIOCGIFCONF请求完成,该请求使用ifconf结构,ifconf又使用ifreq

套接口操作:

  明确用于套接口操作的ioctl请求有三个,它们都要求ioctl的第三个参数是指向某个整数的一个指针。

  SIOCATMARK:如果本套接口的的度指针当前位于带外标记,那就通过由第三个参数指向的整数返回一个非0值;否则返回一个0值。POSIX以函数sockatmark替换本请求。

  SIOCGPGRP: 通过第三个参数指向的整数返回本套接口的进程ID或进程组ID,该ID指定针对本套接口的SIGIOSIGURG信号的接收进程。本请求和fcntlF_GETOWN 命令等效,POSIX 标准化的是fcntl函数。

  SIOCSPGRP: 把本套接口的进程ID或者进程组ID设置成第三个参数指向的整数,该ID指定针对本套接口的SIGIOSIGURG信号的接收进程,本请求和fcntl F_SETOWN 命令等效,POSIX标准化的是fcntl操作

范例

这个实例用来使用ioctl控制CDROM

#include<linux/cdrom.h>

#include<stdio.h>

#include<fcntl.h>

int main()

{

int fd  =open(“dev/cdrom”,O_RDONLY);

if(fd<0)

{

printf("打开CDROM失败\n");

return  -1;

}

if(!ioctl(fd,CDROMEJECT,NULL))

{

printf("成功弹出CDROM\n");

}

else

{

printf("弹出CDROM失败\n");

}

return 0

}

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值