进程通信

1.进程通信简介

进程间的通信又叫IPC通信,包括:管道、信号、信号量集、共享内存、消息队列。

都遵循:打开创建PIC、读写IPC、关闭IPC

2.管道

管道分为有名管道和无名管道,无论是什么管道,都是半双工通信-即同一时刻使能收或者发,所以在进程通信的时候要创建两条管道。且管道遵循先入先出的原则。

注意:创建管道需要在创建进程之前。因为你fork以后子进程会拷贝除了父进程pid以外的所有东西(包括虚拟地址空间等),相当于进程运行了两次.

1.无名管道:

无名管道是一种独立的文件系统,它不存在磁盘,也不存在于的文件系统中(没有文件节点)。无名管道只存在于内存中,只能用于父子进程或者兄弟进程之间的通信

相关API:b

int pipe(int pipefd[2]);

函数描述:创建无名管道,pipefd[0]是读端,pipefd[1]是写端-------------文件描述符-----通过文件IO操作无名管道

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

int main(int argv,char *argc[])
{	
	int pipefd[2] = {0};
	int pipe_value = 1;
	char buff[32]={0};
	char buff1[32]={0};
	
	pipe_value = pipe(pipefd);
	pid_t pid = fork();
	if(pid > 0)
	{
		printf("父进程\n");
		if(pipe_value == 0)
		{
			printf("创建无名管道成功\n");
			ssize_t num = write(pipefd[1],"hello",5);
			if(num != 5)
			{
				perror("write");
			}
		}
	}
	else if(pid == 0)
	{
		sleep(2);//保证先想管道写
		printf("子进程\n");
//		execl("./exe.out","exe.out",NULL);
		read(pipefd[0],buff1,sizeof(buff1));
		printf("buff1 = %s\n",buff1);
	}
	else 
	{
		return 0;
	}
	
    return 0;
}

 

2.有名管道:

有名管道是建立在实际磁盘或文件系统中实际存在的。有名管道就是fifo文件,任何进程通过文件名才对管道进行读写。

相关API:

int mkfifo(const char * pathname,mode_t mode);

函数描述:mkfifo()会依参数 pathname 建立特殊的 FIFO 文件,该文件必须不存在,而参数 mode 为该文件的权限(mode%~umask),因此 umask值也会影响到 FIFO 文件的权限。Mkfifo()建立的 FIFO 文件其他进程都可以用读写一般文件的方式存取。当使用 open()来打开。

int unlink(const char *pathname);

函数描述:删除一个有名管道。

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

int main(int argv,char *argc[])
{	
	int pipefd[2] = {0};
	int pipe_value = 1;
	char buff[32]={0};
	char buff1[32]={0};
	
	pipe_value = mkfifo("./fifo",0777);
	pid_t pid = fork();
	if(pid > 0)
	{
		printf("父进程\n");
		if(pipe_value == 0)
		{
			printf("创建有名管道成功\n");
			int w_fd = open("./fifo",O_WRONLY);
			ssize_t num = write(w_fd,"hello",5);
			if(num != 5)
			{
				perror("write");
			}
		}
	}
	else if(pid == 0)
	{
		sleep(2);//保证先想管道写
		printf("子进程\n");
//		execl("./exe.out","exe.out",NULL);
		int r_fd = open("./fifo",O_RDONLY);
		read(r_fd,buff1,sizeof(buff1));
		printf("buff1 = %s\n",buff1);
	}
	else 
	{
		return 0;
	}
	
    return 0;
}

管道注意:

1 .  只有在管道的读端存在时,向管道写入数据才有意义,否则向管道写入数据的进程将收到内核传来的SIGPIPE信号。

2.无名管道在进行读写操作时,如果没有数据可读,read函数会导致进程阻塞。如果写端不存在,read函数将返回0。

3.如果读进程不读取管道缓冲区中的数据,数据会一直存在,当缓冲区(64K)被填满了,写进程执行写操作就会阻塞。

 

3.信号

当响应的事件产生时,产生对应的信号。

查看信号:kill -l

信号宏存放在系统头文件signal.h里边

常用:(2)SIGINT信号中断(ctrl+c) (3)SIGQUIT信号退出(ctrl+\) (9)SIGKILL信号杀死  (10)SIGUSR1 用户预留信号  (12)SIGUSR2 用户预留信号    (14)SIGALRM  闹钟信号

            (18)SIGCONT(继续运行)      (19)SIGTOP(暂停信号)

处理思路:信号的处理跟中断类似,内核发送信号(产生中断)-----------用户进程接收信号(接收中断)-----------------------信号服务函数(中断服务函数)

相关API函数:

1)传送信号给指定进程

函数原型:int kill(pid_t pid,int sig);

函数描述:kill()可以用来送参数 sig 指定的信号给参数 pid 指定的进程。参数 pid 有几种情况:

pid>0 将信号传给进程识别码为 pid 的进程。

pid=0 将信号传给和目前进程相同进程组的所有进程

pid=-1 将信号广播传送给系统内所有的进程

pid<0 将信号传给进程组识别码为 pid 绝对值的所有进程

2)signal设置信号处理方式(注册信号)

函数原型:void (*signal(int signum,void(* handler)(int)))(int);

函数描述:signal()会依参数 signum 指定的信号编号来设置该信号的处理函数。当指定的信号到达时就会跳转到参数 handler 指定的函数执行。如果参数 handler 不是函数指针,则必须是下列两个常数之一:SIG_IGN 忽略参数 signum指定的信号。

SIG_DFL 将参数 signum 指定的信号重设为核心预设的信号处理方式。

返回值:返回先前的信号处理函数指针,如果有错误则返回 SIG_ERR(-1)

需要用typedef来规则函数指针

3)alarm设置信号传送闹钟

函数原型:unsigned int alarm(unsigned int seconds);

函数功能:alarm()用来设置信号 SIGALRM 在经过参数 seconds 指定的秒数后传送给目前的进程。如果参数 seconds 为 0,则之前设置的闹钟会被取消,并将剩下的时间返回。

例:alarm(3)---delay(1)---alarm(4)----delay(3)----alarm(0)

首先定义alarm(3),经过1s延时以后,再次调用alarm(4),此时返回秒数是2,经过3s以后再次调用alarm(0),返回秒数是1,同时闹钟被取消

#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<signal.h>

typedef void(*signal_t)(int);

void fun(int signal)
{
	printf("进入信号服务函数%d\n",signal);
	alarm(5);
}

int main(int argv,char *argc[])
{
//	kill(-1,atoi(argc[1]));
	alarm(5);
	signal_t p = signal(SIGALRM,fun);	
	if(p == SIG_ERR)
	{
		perror("signal");
	}
	while(1);

	return 0;
}

 

4)setitimer设置信号传送闹钟(双结构体--一个延时--一个定时)

使用时须要引入的头文件:

#include <sys/time.h>

setitimer函数原型:int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);

参数:

int which:参数表示类型。可选的值有:

ITIMER_REAL:以系统真实的时间来计算,它送出SIGALRM信号。

ITIMER_VIRTUAL:以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号。

ITIMER_PROF:以该进程在用户态下和内核态下所费的时间来计算。它送出SIGPROF信号。

const struct itimerval *new_value紧接着的new_value和old_value均为itimerval结构体,先看一下itimerval结构体定义:

struct itimerval {

        struct timeval it_interval;   /* next value */计时间隔

        struct timeval it_value;     /* current value */延时时长

};

struct timeval {

        time_t      tv_sec;         /* seconds */

        suseconds_t tv_usec;        /* microseconds */(范围是10^6)

};

itimeval又是由两个timeval结构体组成,timeval包括tv_sec和tv_usec两部分。当中tv_se为秒。tv_usec为微秒(即1/1000000秒)中的new_value参数用来对计时器进行设置it_interval为计时间隔it_value为延时时长,以下样例中表示的是在setitimer方法调用成功后,延时1微秒便触发一次SIGALRM信号,以后每隔200毫秒触发一次SIGALRM信号。

settimer工作机制是,先对it_value倒计时,当it_value为零时触发信号。然后重置为it_interval。继续对it_value倒计时。一直这样循环下去。基于此机制。setitimer既能够用来延时运行,也可定时运行。

假如it_value为0是不会触发信号的,所以要能触发信号,it_value得大于0;假设it_interval为零,仅仅会延时。不会定时(也就是说仅仅会触发一次信号)。

old_value参数,通经常使用不上。设置为NULL,它是用来存储上一次setitimer调用时设置的new_value值。

#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<string.h>

typedef void(*signal_t)(int);

//struct itimerval
//{
//	struct timeval it_interval; /* Interval for periodic timer */
//	struct timeval it_value;    /* Time until next expiration */
//};
//struct timeval 
//{
//	time_t      tv_sec;         /* seconds */
//	suseconds_t tv_usec;        /* microseconds */
//};



void fun(int signal)
{
	printf("进入信号服务函数%d\n",signal);
//	alarm(5);
}

int main(int argv,char *argc[])
{
	struct itimerval new_value,old_value;
	bzero(&new_value,sizeof(struct itimerval));
	bzero(&old_value,sizeof(struct itimerval));
	//设置没500ms触发一次信号
	new_value.it_interval.tv_sec = 0;
	new_value.it_interval.tv_usec = 500000;
	//设置延时时间
	new_value.it_value.tv_sec = 1;//秒数
	new_value.it_value.tv_usec = 0;//微秒数
	
	int set_value = setitimer(ITIMER_REAL,&new_value,&old_value);
	if(set_value != 0)
	{
		perror("setitimer");
	}
	signal_t p = signal(SIGALRM,fun);
	if(p == SIG_ERR)
	{
		perror("signal");
	}
	while(1);

	return 0;
}

5)sigaction查询或设置信号处理方式

函数原型:int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);

函数功能:sigaction()会依参数 signum 指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。

int signum:信号值

const struct sigaction *act:

struct sigaction

{

void (*sa_handler) (int);      //信号处理函数

sigset_t sa_mask;           //掩码   sigemptyset(初始化)   sigaddset(加)   sigdelset(减)

int sa_flags;                //通常为0 

void (*sa_restorer) (void);     //此参数没有使用

}

sa_mask: 用来设置在处理该信号暂时将 sa_mask 指定的信号搁置(暂放)。(未决信号)。

 

#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<signal.h>
/*
struct sigaction
{
	void (*sa_handler) (int);      //信号处理函数
	sigset_t sa_mask;           //掩码   sigemptyset(初始化)   sigaddset(加)   sigdelset(减)
	int sa_flags;                //通常为0 
	void (*sa_restorer) (void);     //此参数没有使用
}*/

typedef void(*signal_t)(int);

void fun(int signal)
{
	printf("进入信号服务函数%d\n",signal);
	sleep(5);
	printf("信号函数退出\n");
}

int main(int argv,char *argc[])
{
	struct sigaction act,old;
	act.sa_handler = fun;
	sigemptyset(&act.sa_mask);//初始化信号集合
	sigaddset(&act.sa_mask,SIGQUIT);
	act.sa_flags = 0;

	sigaction(SIGINT,&act,&old);
	while(1);

	return 0;
}

发送2号信号以后就如中断服务函数,此时发送信号3就被搁置了(中断存在延时)注意:如果处理期间接收一种信号多次,默认处理一次。

4.共享内存

(1)含义:

共享内存就是在两个或者多个进程之间公用的一块内存区域,即同一快物理内存被映射到进程A,B地址空间,进程A会看到进程B数据的更新(反之也行)

共享内存经过了两次拷贝过程:                用户空间(进程)------>内存                 内存----->用户空间

管道、消息队列经过四次拷贝过程:         用户空间-------->内核           内核------->内存                  内存----------->内核                内核------->用户空间

特性:共享内存直接读写内存,传输数据效率高,覆盖写,使用前需要进行创建。

(2)内存命令

ipcs-m:查看共享内存

ipcs-q:查看消息队列

ipcs-s:查看信号灯

ipcrm -m id号:删除IPC对象

(3)API函数

1).创建共享内存

函数名称:int shmget(key_t key, size_t size, int shmflg);

函数描述:打开或者创建共享内存

函数参数:

key_t key:键值,用于生成shmid,如果是具有亲缘关系的进程中使用,可以填写0或者IPC_PRIVATE,如果是两个不同的进程之间通信,填写一个非0值或者用ftok()去随机生成一个数值

size_t size  :共享内存大小

int shmflg   :O_CREAT(没有就创建,有救打开)

                   O_EXCL    (检查是否存在)

返回值:成功返回shmid,失败-1

注意:共享内存与管道最主要区别在于,管道只能在两个进程之间通信,共享内存只要保证key值相同,可以在多个进程内通信.

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(int argc, char *argv[])
{
	printf("shmid= %d\n",shmget(0x512,512,IPC_CREAT|0777));
	return 0;
}

2)映射共享内存

函数名称:void *shmat(int shmid,const void *addr,int shmflg)
       函数描述:将共享内存映射到地址空间里

函数参数:

int shmid:标识符

const void *addr:映射进程空间首地址,可以手动映射也可以自动映射(NULL)

int shmflg:SHM_RDONLY(只读)  0(可读可写)


       返回值:成功返回映射后的首地址,失败-1

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(int argc, char *argv[])
{
	//创建共享内存
	int shmid = shmget(0x512,512,IPC_CREAT|0777);
	//进行映射
	char * p = (char *)shmat(shmid,NULL,0);
	//写数据
	*p=10;
	char * q = (char *)shmat(shmid,NULL,0);
	printf("%d\n",*q);
	return 0;
}

不同进程之间:

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	//创建共享内存
	int shmid = shmget(0x512,512,IPC_CREAT|0777);
	//进行映射
	char * p = (char *)shmat(shmid,NULL,0);
	//写数据
	*p=10;
	while(1);
	return 0;
}

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	//创建共享内存
	int shmid = shmget(0x512,512,IPC_CREAT|0777);
	char * q = (char *)shmat(shmid,NULL,0);
	printf("%d\n",*q);
	while(1);
	return 0;
}

3)解除共享内存映射(操作共享内存首地址)

           函数名称:int shmdt(const void *addr)

          函数描述:

          函数参数:

          返回值:成功0 失败-1

        4)删除共享内存(操作id)

    函数名称:int shmctl(int shmid,int cmd,struct shmid_ds *buf)

    函数描述:删除共享内存

    函数参数:   

    int cmd:共享内存控制命令。

                     IPC_STAT:查看共享内存的特性。

                    IPC_SET:设置共享内存的特性。

                    IPC_RMID:删除共享内存。

          struct shmid_ds *buf:共享内存特性结构体(命令附带参数),不需要则指定为NULL。

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

注意:删除共享内存之前必须先解除映射

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	//创建共享内存
	int shmid = shmget(0x512,512,IPC_CREAT|0777);
	//进行映射
	char * p = (char *)shmat(shmid,NULL,0);
	//写数据
	*p=10;
	//解除映射
	shmdt(p);
	while(1);
	return 0;
}

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	//创建共享内存
	int shmid = shmget(0x512,512,IPC_CREAT|0777);
	char * q = (char *)shmat(shmid,NULL,0);
	printf("%d\n",*q);
	shmdt(q);
	shmctl(shmid,IPC_RMID,NULL);
	while(1);
	return 0;
}

  5)获取共享内存键值

    函数名称: key_t ftok(const *patname, int proj_id)

    函数描述:让系统为共享内存分配一个键值

    函数参数:

    const *patnam:创建key值的路径(任意已存在的文件名),保证路径不容易被删除掉。

    proj_id:任意ascll 值(取值范围:0~255)

    返回值:得到的key键值(正整数)

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	//创建共享内存
	int shmid = shmget(ftok("./a.out",2),512,IPC_CREAT|0777);  
	char * q = (char *)shmat(shmid,NULL,0);
	printf("%d\n",*q);
	shmdt(q);
	shmctl(shmid,IPC_RMID,NULL);
	while(1);
	return 0;
}

5.消息队列

1).含义:是指通信双方发送接收操作均以消息(消息类型+消息内容)为单位,以消息缓冲区为中间介质

消息队列是消息的链表,存放在内核中并由消息队列标识符表示

消息队列与管道区别:管道先进先出,消息队列中同一消息类型先进先出,不同类型的消息读取不一定先进先出。

                                    管道是基于字节流,消息队列是基于消息。

2).API

创建消息队列:

函数原型:int msgget(key_t key, int msgflg);

参数说明:

Key:键值可以自己定义也可以用ftok函数进行分配。

Msgflg:它可以取下面的几个值:

IPC_CREAT :如果消息队列对象不存在,则创建之,否则则进行打开操作;

IPC_EXCL:和IPC_CREAT 一起使用(用”|”连接),如果消息对象不存在则创建之,否则产生一个错误并返回。int msgget(0x666, IPC_CREAT |  IPC_EXCL);

返回值:成功执行时,返回消息队列标识值。失败返回-1,errno被设为以下的某个值 ,有时也会返回0,这个时候也是可以正常使用的。

EACCES:指定的消息队列已存在,但调用进程没有权限访问它,而且不拥有CAP_IPC_OWNER权能。

EEXIST:key指定的消息队列已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志。

ENOENT:key指定的消息队列不存在同时msgflg中不指定IPC_CREAT标志

ENOMEM:需要建立消息队列,但内存不足。

ENOSPC:需要建立消息队列,但已达到系统的最大消息队列容量。

消息队列发送:

函数原型: int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数说明:

msqid: 是消息队列对象的标识符(由msgget()函数得到)

msgp: 指向要发送的消息所在的内存

*注意:发送消息的类型结构体,第一个参数必须是long类型,其他参数自己定义

mtype:代表消息类型

struct msgbuf

long mtype;        /* message type, must be > 0 */

char mtext[1];          /* message data */

char mtext1[1];          /* message data */

char mtext2[1];          /* message data */

};

msgsz: 是要发送信息的长度(字节数),可以用以下的公式计算:

msgsz = sizeof(struct mymsgbuf) - sizeof(long);

msgfl:是控制函数行为的标志,可以取以下的值:0堵塞,忽略标志位;IPC_NOWAIT(非堵塞),如果消息队列已满(在消息队列中,例如函数msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);这个函数调用的时候,msgsz最大只能为8192,也就是2的16次方8kb。可以看出这里的msgsz大小限制在一个short型。超过这个大小就会出错,),消息将不被写入队列,控制权返回调用函数的线程。如果不指定这个参数,线程将被阻塞直到消息被可以被写入。

返回值:错误时返回-1,可以打印错误信息;正确返回0

 

 

消息队列接收:

函数定义:int  msgrcv( int  msgid , struct   msgbuf*  msgp ,  int msgsz ,  long msgtyp, int msgflg);

参数:

函数的前三个参数和msgsnd()函数中对应的参数的含义是相同的。第四个参数mtype

指定了函数从队列中所取的消息的类型。函数将从队列中搜索类型与之匹配的消息并将 之返回。不过这里有一个例外。如果mtype 的值是零的话,函数将不做类型检查而自动返     回队列中的最旧的消息。第五个参数依然是是控制函数行为的标志,取值可以是:
0,表示忽略;
IPC_NOWAIT,如果消息队列为空,则返回一个ENOMSG,并将控制权交回调用函数
的进程。如果不指定这个参数,那么进程将被阻塞直到函数可以从队列中得到符合条件 的消息为止。如果一个client 正在等待消息的时候队列被删除,EIDRM 就会被返回。如果     进程在阻塞等待过程中收到了系统的中断信号,EINTR 就会被返回。
MSG_NOERROR,如果函数取得的消息长度大于msgsz,将只返回msgsz 长度的信息,
剩下的部分被丢弃了。如果不指定这个参数,E2BIG 将被返回,而消息则留在队列中不     被取出。
当消息从队列内取出后,相应的消息就从队列中删除了。

msgbuf:结构体,定义如下:

struct msgbuf

{

                      long  mtype ;  //信息种类

                       char   mtest[x];//信息内容   ,长度由msgsz指定

}

msgtyp:  信息类型。 取值如下:

 msgtyp = 0 ,不分类型,直接返回消息队列中的第一项

 msgtyp > 0 ,返回第一项 msgtyp与 msgbuf结构体中的mtype相同的信息

msgtyp <0 , 返回第一项 mtype小于等于msgtyp绝对值的信息

msgflg:取值如下:

IPC_NOWAIT ,不阻塞

IPC_NOERROR ,若信息长度超过参数msgsz,则截断信息而不报错

 

返回值:

成功时返回所获取信息的长度,失败返回-1,错误信息存于error

删除消息队列

 

函数原型: int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );

头文件:

参数:

msgqid:消息队列的识别码。

cmd:使用命令:

IPC_STAT读取消息队列的数据结构msqid_ds,并将其存储在b u f指定的地址中。

IPC_SET设置消息队列的数据结构msqid_ds中的ipc_perm元素的值。这个值取自buf参数。

IPC_RMID从系统内核中移走消息队列。(这时  struct msqid_ds *buf:NULL)

调用以后就进行删除

返回值:0成功 1失败 

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<stdio.h>
#include<string.h>
typedef struct msgbuf
{
	long mtype;
	char text[30];
}MSG;

int main(int argv,char *argc[])
{
	MSG msgbuff;
	MSG msgrec;
	int msgid = msgget(ftok("./1.out",55),IPC_CREAT|0777);
	if(msgid == -1)
	{
		perror("msgget");
		return 0;
	}
	msgbuff.mtype = 1;
	strcpy(msgbuff.text,"nihao");
	int flag_send =	msgsnd(msgid,(const void *)&msgbuff,sizeof(MSG)-sizeof(long),0);
	if(flag_send  != 0)
	{
		perror("msgsnd");
		return 0;
	}
	//是否强转
	int num = msgrcv(msgid,(MSG *)&msgrec,sizeof(MSG)-sizeof(long),1,0);
	printf("num = %d\n",num);
	printf("msgrcv = %s\n",msgrec.text);

	int flag_del = msgctl(msgid,IPC_RMID,NULL);
	if(flag_del == 0)
	{
		printf("删除成功\n");
	}


    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值