【Linux】信号(第十五篇)

目录

信号

1.信号的分类:

2.信号产生的原因以及发送信号的方式

1.终端组合按键产生信号

2.命令产生信号

3.函数产生信号

4.系统产生信号(硬件异常产生信号)

5.系统产生信号【软条件产生信号】 满足条件才发信号,否则不发

3.Linux中让信号失效的几种方式:

4.信号的三种行为及其五种默认处理动作

5.信号的传递过程

6.时序竟态问题

7.signal进程间通信


信号

A 给 B 发送信号,B 收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。 信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。 每个进程收到的所有信号,都是由内核负责发送的,内核处理。

概述:linux和unix 系统中进程间利用信号来传递不同的消息,每个信号都对应了固定的事件,可以利用信号机制完成进程间通信,也可以终止进程或者挂起进程

kill -l  //该命令查看系统支持的所有信号
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX    

信号的详细解释:

SIGHUP: 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程 SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。

SIGQUIT:当用户按下<ctrl+\ >组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。

SIGILL:CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件

SIGTRAP:该信号由断点指令或其他 trap 指令产生。默认动作为终止里程 并产生 core 文件。 SIGABRT: 调用 abort 函数时产生该信号。默认动作为终止进程并产生core 文件。

SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。 SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误。默认动作为终止进程并产生 core 文件。 SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。 SIGUSR1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。 SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。 SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。 SIGPIPE:Broken pipe 向一个没有读端的管道写数据。默认动作为终止进程。

SIGALRM: 定时器超时,超时的时间 由系统调用 alarm 设置。默认动作为终止进程。 SIGTERM:程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell 命令 Kill 时,缺省产生这个信号。默认动作为终止进程。

SIGSTKFLT:Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。 SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号。 SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。

SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。

SIGTSTP:停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。

SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。

SIGTTOU: 该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。

SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。

SIGXCPU:进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。

SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。

SIGVTALRM:虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间。默认动作为终止进程。

SGIPROF:类似于 SIGVTALRM,它不公包括该进程占用 CPU 时间还包括执行系统调用时间。默认动作为终止进程。

SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。

SIGIO:此信号向进程指示发出了一个异步 IO 事件。默认动作为忽略。

SIGPWR:关机。默认动作为终止进程。

SIGSYS:无效的系统调用。默认动作为终止进程并产生 core 文件。

34)SIGRTMIN ~

(64) SIGRTMAX:LINUX 的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

1.信号的分类:

unix经典信号(1-31):软件开发者经常使用。

自定义信号(34-64)/实时信号:驱动开发工程师常使用

前32种称为不可靠信号,一般用于软件不支持排队,后32种称为可靠信号 一般用于硬件支持排队。

linux用户可以通过man 7 signal查看各个信号的作用

32、33 这两个信号是存在的,被系统隐藏。(linux 系统中只支持进程,不支持线程,一般都是第三方库)。留给NPTL线程库使用。

Native POSIX Thread Library(NPTL): 因为没有内核支持LinuxThread的线程实现的诸多缺陷,所以要想实现完全跟POSIX线程标准兼容的线程库,重写线程库是必然的,内核的修改也势在必行。有关NPTL实现也从线程创建,同步互斥及信号处理及线程管理几个方面来说明。

2.信号产生的原因以及发送信号的方式

1.终端组合按键产生信号

ctrl+ c 终止进程 触发2号信号 SIGINT

ctrl+\ 终止进程(核心已转储) 触发3号信号SIGQUIT

ctrl+ z 挂起进程 触发20号信号SIGTSTP(通过ctrl+z 挂起进程,可以通过查看挂起进程编号jobs, 而后通过fg 1 则将1号作业唤醒到前台 或者bg 1 将作业唤醒到后台)

注意:ps aux 查看过程中 进程状态带有“+”前台允许 没有“+” 后台运行

2.命令产生信号

kill -信号编号 进程id (例:kill -2 3000) //向任意进程发送任意信号,信号发送者要注意权限

3.函数产生信号

int kill(pid_t pid, int sig); //可以通过该函数向任意进程发送任意信号(注意权限)

示例:

#include <sys/types.h>
#include <signal.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char** argv)
{
   
   if(argc <3)
   {
       perror("paramer not enough");
       exit(0);
   }
   kill(atoi(argv[2]),atoi(argv[1]));
​
   return 0;
}

使用方法:

./mykill 3 3294

其他发信号函数:

raise(int signo) //向当前进程发任意信号

abort(void) //向当前进程发SIGABRT 信号(即6号信号)

int sigqueue(pid_t pid, int sig, const union sigval value);//可以向任意进程发送任意信号,也支持进程间通信,信号携带数据

线程间发送信号

pthread_kill(pthread_t tid,int signo) // 可以向任意线程发送任意信号

kill 函数和pthread 函数都具备其他功能

kill(3096,0) 如果信号参数填0,该函数可以帮助用户校验进程是否存在(存活) pthread_kill(0x1000,0) 如果信号参数填0,该函数可以帮助用户校验线程是否存在(存活)

4.系统产生信号(硬件异常产生信号)

1)用户非法操作内存 【11:SIGSEGV】段错误 char *sz = "444"; sz[0] ='9';kill -11 id 2)CPU运算异常 【8:SIGFPE 】浮点数例外 非法运算(比如说1/0) kill -8 id 3)总线错误 【7.SIGBUS】(堆栈溢出,调度错误)

5.系统产生信号【软条件产生信号】 满足条件才发信号,否则不发

1)使用定时器进行定时,定时到时后,系统会产出信号发送给定时进程 【14.SIGALRM】

2)匿名管道读端关闭,写端向管道写数据,内核发送13.SIGPIPE信号杀死写端进程

3.Linux中让信号失效的几种方式:

1)屏蔽阻塞信号 (拒之门外) ​ 2)忽略信号 (忽略掉信号的行为 ,例如工具卸掉了) ​ 3)信号捕捉(让信号为用户所用) SIGKILL SIGSTP 这两个信号除外 【9.SIGKILL】 直接为内核服务,较高的权限,只要发出必然杀死进程,无法被捕捉忽略等处理 【19.SIGSTP】 直接为内核服务,较高的权限,只要发出必然挂起进程,无法被捕捉忽略等处理

4.信号的三种行为及其五种默认处理动作

默认行为: SIG_DFL 五种默认的处理动作 TREM #杀死进程 CORE #杀死进程并转储运行核心(将进程的信息记录下来) IGN#信号失效,不会终止进程(默认即时对该种信号忽略操作) STOP #停止(暂停)进程 COUNT#继续运行进程

忽略行为: SIG_IGN 无处理动作 捕捉行为: SIG_ACTION 捕捉进程,自定义任务 //可以通过特殊方式,改变信号的行为,让其失效

Linux 内核的进程控制块 PCB 是一个结构体task_struct, 除了包含进程 id,状态,工作目录,用户 id,组 id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

阻塞信号集(信号屏蔽字):

将某些信号加入集合,对他们设置屏蔽,当屏蔽 某 信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)

未决信号集:

信号产生,未决信号集中描述该信号的位立刻翻转为 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂。 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。

5.信号的传递过程

CTRL+ C 终端组合按键信号,发送的特定目标为终端前台进程---->TTY PROCESS 终端进程

注释:规程,简单说就是规则+流程

.信号集为未决态信号,未决集对应码要置成1,当通过屏蔽字后,信号转为递达态,未决集中对应位置回0 .信号集每位对应一个信号,位码为0表示信号可传递, 1 表示不可传递 .信号尝试通过屏蔽字,但对应位码为1,那么信号被阻塞屏蔽,无法递达(即屏蔽行为) .某信号尝试通过未决信号集,但是对应的位码为1,那么该信号直接丢弃,不做处理 .信号通过未决信号集,没有被处理掉前,未决信号集对应位,置1 .未决信号集,由内核设置与翻转,用户只能读不能设置与修改, 屏蔽字用户可以自由设定,用于自定义阻塞屏蔽某个信号

由于信号传递的特性,经典信号不支持排队(1-32)但(34-62)实时信号/自定义信号 支持排队,触发多次信号都会放到队列中等待处理。

思考题:

为什么经典信号不支持排队?因为经典信号一般用于杀死进程,杀死一次就可以。 为什么实时信号支持排队?因为可以累积,比如点了好多次下一首歌

通过系统提供的类型及接口,实现信号屏蔽,让其失效

1)信号集类型 sigset_t 设置信号集的API sigemtpyset(sigset_t * set) #将信号集所有位码初始化为0 sigfillset(sigset_t *set) #将信号集所有的位码初始化为1 sigaddset(sigset_t *set,int signo) #将一个信号集中某一个信号的位码置为1 sigdelset(sigset_t set,int signo) #将一个信号集中某一个信号的位码置为0 sigismember(sigset_t set,int signo)#可以帮助使用者获取某个信号集中某信号的位码是0还是1,并且直接返回。

如何改变进程的信号集? sigprocmask(int how,sigset_t new,sigset_t old) #使用自定义信号集对进程默认信号集进行替换,替换成功可以传出进程默认信号集(old也可以不传) int how: SIG_SETMASK(直接覆盖替换)

示例:使ctrl +c 失效

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    sigset_t newset ,oldset;
    sigemptyset(&newset);
    sigaddset(&newset,SIGINT);
    sigprocmask(SIG_SETMASK,&newset,&oldset);
    while(1);
    return 0;
}
​

注意:ctrl+c 结束不了进程,但是可以通过ctrl+\

定义信号的行为类型,通过系统API修改信号行为,使信号失效 struct sigaction new_act #信号行为结构 new_act.sa_handler=SIG_DFL SIG_IGN myjobs(捕捉函数,用户自定义)#可以通过handler成员,选择信号对应的行为 new_act.sa_flags = 0;#如果使用的是sa_handler行为设置,那么flags 就是填0 new_act.sa_mask = 临时屏蔽字,初始化需要使用sigemptyset

sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。 SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用 SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号

改变行为: sigaction(int signo,struct sigaction nact,struct sigaction oact) #替换信号行为结构体函数 示例:信号行为修改,将SIGINT 的默认行为改为忽略行为,使ctrl+c失效

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
​
​
void myjobs()
{
    printf("get ctrl +c \n");
}
​
int main()
{
    struct sigaction newaction,oldaction;
    newaction.sa_handler = SIG_IGN; //忽略行为
   // newaction.sa_handler = myjobs;//捕捉行为
    newaction.sa_flags= 0;
    sigemptyset(&newaction.sa_mask);
    sigaction(SIGINT,&newaction,&oldaction); 
    sigaction(SIGKILL,&newaction,NULL); //无法对高级的信号SIGKILL or SIGSTOP 进行忽略或者捕捉
​
    while(1);
    return 0;
}
 

思考题:如果在myjobs 代码更改为while() 3次,那么连续按ctrl + c,可能出现什么状况呢?

注意:信号未处理完成之前,内核设置临时屏蔽字,拒绝相同的信号递达.相同信号在处理阶段内核会设置临时屏蔽,所以某个信号最多支持一次屏蔽。

练习:

创建父子进程,当子进程退出,会向父进程发送SIGCHLD退出信号,父进程收到信号后回收僵尸进程。

6.时序竟态问题

概述:竞态是指设备或系统出现不恰当的执行时序,而得到不正确的结果,由于时间片,或其他因素,导致该到达并响应的信号没有被响应,这就是由信号引起的竞态。

示例:

自定义实现sleep

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myjobs()
{}
void mysleep(int nsecond)
{
   struct sigaction iact,oact;
   iact.sa_handler = myjobs;
   iact.sa_flags = 0;
   sigemptyset(&iact.sa_mask);
   sigaction(SIGALRM,&iact,&oact);
   alarm(nsecond);
    //sleep(3);
   pause();
}
​
int main()
{
  while(1)
   {
      mysleep(2);
      printf("two second......\n");
   }
   return 0;
}
​

int pause(void);   作用:使调用进程(线程)进入休眠状态(就是挂起);直到接收到信号且信号函数成功返回 pause函数才会返回   返回值:始终返回-1

注意:任意信号都可以唤醒pause

alarm(10)参数为定时得秒数,返回未定时够的秒数。 定时到时后,内核向定时的进程发送14SIGALRM信号通知定时进程( 默认情况下SIGALRM默认行为是终止进程)所以实现mysleep时,一定要让sigalrm信号失效

在实现过程中,多进程时序竟态很难模仿,所以采用在alram(图中功能A)和pause(图中功能B)之间添加sleep(); 这样的情况就会导致pause等不到信号而处于一直等待状态。

解决问题的方法:

可以采用原子操作,避免时序问题 【定时的同时挂起进程】

【挂起的同时解除信号屏蔽】

原子方法函数:sigsuspend #除挂起唤醒外设置进程的屏蔽字(临时替换) 屏蔽SIGALRM信号 #屏蔽信号避免挂起之前,信号被提前处理掉 执行定时alrm sigsuspend 挂起的同时解除屏蔽,如果信号已产生,那么唤醒,如果信号未产生继续等待信号

更改为:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
​
void myjobs()
{}
void mysleep(int nsecond)
{
   sigset_t newset,oldset;
   sigemptyset(&newset);
   sigaddset(&newset,SIGALRM);
   sigprocmask(SIG_SETMASK,&newset,&oldset);
   struct sigaction iact,oact;
   iact.sa_handler = myjobs;
   iact.sa_flags = 0;
   sigemptyset(&iact.sa_mask);
   sigaction(SIGALRM,&iact,&oact);
   alarm(nsecond);
   sigsuspend(&iact.sa_mask);//挂起当前进程,并且解除屏蔽字SIGALRM
   //sleep(3);
  //  pause();
}
​
int main()
{
  while(1)
   {
      mysleep(2);
      printf("two second......\n");
   }
   return 0;
}
​

信号捕捉设定中,信号处理是否是实时的? 信号处理是非实时的,不会第一时间处理信号

信号捕捉函数是内核帮助进程调用,何时调用?使用的是内核资源还是进程资源? 系统调用:权级转换

因中断,异常或系统调用,从用户层切换到内核层,才可以检测是否有未递送待处理的信号

详细过程:

1.产生中断

2.由用户层切换到内核层

3.恢复中断,处理异常,完成系统调用,完成后回到用户模式前检测一下是否有未递送的信号

4.处理该信号,如果信号行为是捕捉,捕捉函数位于用户空间,内核临时切换到用户空间,帮助开发者调用捕捉函数

5.内核携带自身的权限,从内核模式切换到用户模式,调用捕捉函数

6.调用结束后,通过特殊指令SIG_RETURN返回内核模式

7.内核执行SYSTEM_RETURN,从内核模式切换用户模式,main从被中断的位置继续执行

信号机制,导致的全局变量访问冲突(尽量避免信号捕捉函数与主函数共享访问全局资源)

7.signal进程间通信

kill 向任意进程发送信号

raise向自身进程发送任意信号

abort向自身进程发送SIGABRT信号

sigqueue可以向任意进程发送任意信号并且发送者可以自定义消息,与信号一起发送

sa_handler默认捕捉函数接口,可以完成基本的信号捕捉,但是无法利用它接收数据,因为handler的捕捉接口不足,无法保存union数据包

捕捉函数

void sa_sigaction(int n ,siginfo* info,char* ptr);可以接收union数据包,完成IPC_SIGNAL任务

内核鉴定出,信号携带数据,内核将数据保存在捕捉函数中siginfo_t *info

捕捉函数中只有sigaction版可以接收union数据包,完成IPC_SIGNAL任务

示例:

实现 两个进程相互发信号,完成数字累加

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
void my_sigaction(int signum,siginfo_t* info, void *ptr)
{
    printf("signal content : %d\n",info->si_int);
}
int main()
{
     //
     pid_t pid  = fork();
    if(pid >0)
    {
         union sigval value;
         value.sival_int = 10;
         sleep(1); //如果父进程先发送信号,子进程后执行捕捉则捕捉不到,解决方案一 父进程延迟发送
         sigqueue(pid,SIGINT,value);
         printf("parent send signal.....\n");
         while(1)sleep(1);
         
    }
    else if(pid ==0)
    {
         struct sigaction iact,oact;
         iact.sa_sigaction = my_sigaction;
         iact.sa_flags = SA_SIGINFO;
         sigaction(SIGINT,&iact,&oact);
         printf("child process %d\n",getpid());
         while(1)sleep(1);
    }
    else
    {
        perror("process fork failed");
        exit(0);
    }
     return 0;
}
​


#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
void my_sigusr1(int signum,siginfo_t* info, void *ptr)
{

    printf("child signal content : %d\n",info->si_int);
    union sigval value;
    value.sival_int = ++(info->si_int);
    sigqueue(getppid(),SIGUSR2,value);//子进程给父进程发送
    sleep(1);
}
void my_sigusr2(int signum,siginfo_t* info, void *ptr)
{

    printf("parent signal content : %d\n",info->si_int);
    union sigval value;
    value.sival_int = ++(info->si_int);
    sigqueue(getpid()+1,SIGUSR1,value);//父进程给子进程发送
    sleep(1);
}
int main()
{
     sigset_t iset,oset;
     sigemptyset(&iset);
     sigaddset(&iset,SIGUSR1);
     sigprocmask(SIG_SETMASK,&iset,&oset);
     pid_t pid  = fork();
    if(pid >0)
    {
         struct sigaction iact,oact;
         iact.sa_sigaction = my_sigusr2;
         iact.sa_flags = SA_SIGINFO;
         sigaction(SIGUSR2,&iact,&oact);
         union sigval value;
         value.sival_int = 10;
         //sleep(1);
         sigqueue(pid,SIGUSR1,value);
         printf("parent send signal.....\n");
         while(1)sleep(1);

    }
    else if(pid ==0)
       {
         struct sigaction iact,oact;
         iact.sa_sigaction = my_sigusr1;
         iact.sa_flags = SA_SIGINFO;
         sigaction(SIGUSR1,&iact,&oact);
         printf("child process SIGUSR1 %d\n",getpid());
         sigprocmask(SIG_SETMASK,&iact.sa_mask,NULL);
         while(1)sleep(1);
    }
    else
    {
        perror("process fork failed");
        exit(0);
    }
     return 0;
}
                                 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱编程的小猴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值