【操作系统】信号Signal超详解|捕捉函数

2024-06-24 13:44

本文主要是介绍【操作系统】信号Signal超详解|捕捉函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

🔥博客主页: 我要成为C++领域大神
🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】
❤️感谢大家点赞👍收藏⭐评论✍️

本博客致力于知识分享,与更多的人进行学习交流

如何触发信号

信号是Linux下的经典技术,一般操作系统利用信号杀死违规进程,典型进程干预手段,信号除了杀死进程外也可以挂起进程

kill -l 查看系统支持的信号

32,33号信号预留给线程库NTPL

两种信号,经典信号/实时信号

1-31是unix经典信号,软件开发工程师使用,例如进程通信,信号捕捉等。

34-64是自定义信号,一般驱动开发使用,偏底层。

1、终端组合按键触发信号

Ctrl+/(SIGQUIT/3)系统向唯一的前台进程发送2号信号,目标进程被杀死

Ctrl+C(SIGINT/2)系统向唯一的前台进程发送2号信号,目标进程被杀死

Ctrl+Z(SIGTSTP/20)系统向唯一的前台进程发送20号信号,目标进程被挂起

终端组合按键触发的信号会发给唯一的前台进程

写一个无法退出的死循环,使用组合键杀死这个进程

按下Ctrl+C成功杀死这个死循环进程

2、命令发送信号

kill -signo pid

此命令可以向任意进程发送任意信号

kill命令成功杀死进程

3、函数发送信号

使用信号函数需要包含头文件 signal.h

常见信号函数:

kill(pid_t,int signo) 向任意进程发送任意信号

raise(int signo) 向自身进程发送任意信号

void abort(void) 向自身发送SIGABRT/6信号

关于kill函数的使用:

kill函数的第一个参数是进程ID,第二个参数是信号编号。这与kill命令正好相反,所以为了符合使用逻辑,在传入命令行参数时,将命令行第二个参数作为kill函数的第一个参数


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>int main(int argc,char **argv)
{if(argc<3){printf("参数数量错误\n");exit(0);}kill(atoi(argv[2]),atoi(argv[1]));return 0;
}

自定义kill命令杀死进程

4、硬件异常产生信号

1)对只读内存进行写操作,属于违规操作硬件,系统向违规进程发送SIGSEGV(11)信号,段错误,杀死违规进程。

例如:当我们尝试修改常量区内存时:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>int main()
{char *str="this is const";str[1]='d';return 0;
}

2)SIGBUS(总线错误),越界访问,无效访问内存,系统向违规进程发送SIGBUS(7)信号,杀死违规进程。

3)SIGFPE(浮点数例外),CPU违规运算,运算异常,系统向违规进程发送SIGFPE(8),杀死进程。

5、软条件触发信号

软条件触发信号(Soft Condition Trigger Signal),指的是某些条件满足时触发相应的处理操作。例如,文件描述符变为可读时触发读操作;设置一个定时程序,定时器结束后触发操作;管道读端结束,写端向管道写数据(触发软条件),系统向写端进程发送SIGPIPE13信号杀死写端进程。

信号的三大行为,与五种默认处理动作

默认行为

信号处置进程后,可以通过结果分析信号的默认动作。

默认处理动作:

TERM:直接杀死目标进程,SIGKILL,SIGINT

CORE:直接杀死进程,但是转储核心处理文件(dump core),SIGQUIT,SIGSEGV SIGFPE,SIGBUS

IGN:通知回收信号 SIGCHLD(唯一的忽略信号),忽略信号,发到进程不会影响进程

STOP:挂起进程SIGSTP,SIGTSTP

CONT:唤醒进程,SIGCONT

忽略行为

忽略行为没有处理动作,直接丢弃,不会影响进程。忽略行为的优先级比动作要高。

捕捉行为

捕捉行为可以实现,信号绑定自定义任务。信号触发,执行捕捉函数,执行自定义任务。捕捉技术在开发中普遍使用,例如Qt的信号与槽机制。

 Dump Core

如果进程因为硬件异常被系统杀死,那么会(Dump Core),错误原因 xxx(核心已转储)

例如:对常量空间的非法访问。

Dumb Cor中存放的是错误信息,但是系统不会生产core文件,我们可以进行修改:

ulimit -a查看系统限制

ulimit -c 4096 生成一个块大小的core,让进程产生的错误生成core文件

后续调试可以通过gdp ./可执行文件名 core快速定位错误位置,无需逐步调试,节省编译时间

让信号失效的三种方式:屏蔽、忽略、捕捉

屏蔽(Block):延迟处理信号,直到信号解除屏蔽。

忽略(Ignore):完全忽略信号,不对信号作出任何响应。

捕捉(Catch):使用自定义的信号处理程序响应信号。

1. 屏蔽(Block)

屏蔽信号指的是将信号加入到进程的信号屏蔽集(blocked signal set)中。当信号被屏蔽时,即使信号被发送给进程,它也不会立即处理该信号,而是将其放入未决信号集(pending signal set),直到信号被解除屏蔽时才会处理。

2. 忽略(Ignore)

忽略信号指的是将信号处理程序设置为SIG_IGN,使得进程完全忽略某个特定的信号。当信号被忽略时,进程不会对该信号作出任何响应。

3. 捕捉(Catch)

捕捉信号指的是将信号处理程序设置为自定义函数,当信号到达时执行该函数。捕捉信号允许进程在信号到达时执行特定的操作而不会执行本来的功能。

如果所有的信号都被屏蔽、忽略、捕捉,那么不就会导致病毒无处可寻吗?

系统保留高权级信号,这类信号无法被屏蔽,捕捉和忽略,服务于内核,只要发出必然抵达。

SIGKILL(9),无法被屏蔽、捕捉、忽略,只要发出必然杀死

SIGSTOP(19),无法被屏蔽,捕捉忽略,只要发出必然挂起

信号的传递过程

当某个事件发生(例如用户按下Ctrl+C),内核会生成一个SIGINT信号,传递给目标进程虚拟内存中3-4G内核层PCB,PCB中存有信号处理信息。同时更新目标进程的PCB中的未决信号集。如果发送信号的行为是屏蔽,那么也会更新目标进程PCB中的屏蔽信号集,将对应信号位设置1, 可以实现阻塞信号的效果。当屏蔽解除或者信号不被屏蔽时,未决信号集中的信号将被处理。处理过程是由信号处理程序(Handler)来执行的。

未决信号集:是一个位图,每一位代表一个特定的信号。位值为1表示该信号是未决的。

当内核发送一个信号给进程时,它会将对应信号的位设置为1,表示此信号正在传递,还未处理。

屏蔽信号集:用于表示哪些信号当前被屏蔽(阻塞)。每个信号对应屏蔽信号集中的一位,位值为1表示该信号被屏蔽。

未决信号集和屏蔽信号集处理UNIX经典信号不支持信号排队处理。(因为一个信号即可达到目的)

自定义信号可以实现排队序列,多个相同信号触发也可以排队依次处理。(例如各种家电的遥控器,当连续按下多个按键时,可以连续执行。还有音乐播放器的切歌功能)

信号屏蔽的实现

#include <signal.h>
sigset_t 是一个数据类型,用于表示信号集。

sigemptyset(sigset_t set); 初始化信号集 set,将其设置为空,即不包含任何信号。

sigfillset(sigset_t set); 初始化信号集 set,将其设置为满,即包含所有信号。

sigaddset(sigset_t set, int signo); 将信号 signo 添加到信号集 set 中。

sigdelset(sigset_t set, int signo); 从信号集 set 中删除信号 signo

int sigismember(const sigset_t set, int signo); 检查信号 signo 是否是信号集 set 的成员。如果是,返回 1;否则,返回 0。

sigprocmask(int how, const sigset_t newset, sigset_t oldset); 检查并更改当前线程的信号屏蔽字。

how 可以是:

SIG_SETMASK(将信号屏蔽字设置为 newset

SIG_BLOCK(将 newset 中的信号添加到当前信号屏蔽字)

SIG_UNBLOCK(从当前信号屏蔽字中移除 newset 中的信号)

下面通过一个demo程序屏蔽SIGINT信号:


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>int main(void)
{sigset_t set,oldset;sigemptyset(&set);//将信号屏蔽集全部置0sigaddset(&set,SIGINT);//将SIGINT加入屏蔽集sigprocmask(SIG_SETMASK,&set,&oldset);while(1) sleep(1);return 0;
}

结果:无论是哪种方式发送信号都无法实现

再添加SIGQUITSIGKILL为屏蔽信号

sigaddset(&set,SIGQUIT);//将SIGQUIT加入屏蔽集

sigaddset(&set,SIGKILL);//将SIGKILL加入屏蔽集(高优先级信号无法屏蔽)

SIGKILL无法被屏蔽,因为是高优先级信号

查看信号的屏蔽情况

信号已经被发出,抵达进程,进程中被屏蔽,要观察这种已触发被屏蔽的信号集只能查看未决信号集(只有读的权限)

获取进程的未决信号集,而后输出未决的每一位,0 or 1,查看信号屏蔽

sigpending(&pset)调用这个函数,系统会将进程的未决信号集传出到pset中

使用遍历循环结合sigismember,查看每一位的情况并输出。

查看未决信号集的demo程序:


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>//通过打印未决信号集,查看被屏蔽的信号
void print_sigpending(sigset_t pset){int i=1;//信号是从1开始的for(i;i<32;++i){if(sigismember(&pset,i)){putchar('1');}elseputchar('0');}putchar('\n');}
int main(void)
{sigset_t set,oldset,pset;sigemptyset(&set);//将信号屏蔽集全部置0sigaddset(&set,SIGINT);//将SIGINT加入屏蔽集sigaddset(&set,SIGQUIT);//将SIGQUIT加入屏蔽集sigprocmask(SIG_SETMASK,&set,&oldset);while(1){sigpending(&pset);print_sigpending(pset);sleep(1);}return 0;
}

运行结果:

当键盘按下组合键发送SIGINTSIGQUIT后,未决信号集被置1

信号行为修改

使用struct sigaction 结构体对行为进行操作:

结构体成员:

act.sa_handler:

可以设置为: SIG_DFL(默认信号处理方式)SIG_IGN(忽略信号)。也可以指向自定义信号处理函数的指针

act.sa_flags:

标志位,用于控制信号处理行为。如果使用 sa_sigaction 处理函数,则将 flags 设置为 SA_SIGINFO

常用值: 0(默认,不使用附加选项)

act.sa_mask:

类型为 sigset_t 的信号集。用于在信号处理期间临时阻塞的信号。通常使用 sigemptyset 初始化。

函数 sigaction:用 newact 替换进程的信号处理行为,并将原有的信号处理行为保存在 oldact 中。

sigaction(int signo, struct sigaction *newact, struct sigaction *oldact);

signo:要处理的信号编号。

newact:指向包含新信号处理方式的 struct sigaction 结构体的指针。

oldact:指向用于保存原有信号处理方式的 struct sigaction 结构体的指针(可以为 NULL)。

下面写一个设置自定义的信号处理demo程序:当收到 SIGINT 信号时,会调用自定义信号捕捉函数SIG_CATCH并打印收到的信号编号。


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>//act.sa_handler的类型是void (*fun)(int)
//是一个自定义函数指针类型,自定义的捕捉函数也要是这种类型
void SIG_CATCH(int signo){printf("SIGINT %d 捕捉信号\n",signo);
}int main()
{struct sigaction act,oldact;act.sa_handler=SIG_CATCH;//自定义的函数,用来输出捕捉到的信号编号act.sa_flags=0;sigemptyset(&act.sa_mask);//信号集类型sigaction(SIGINT,&act,&oldact);//替换信号行为成自定义的捕捉行为,并且将原来的行为保存到oldact中while(1)sleep(1);return 0;
}

当前进程收到SIGINT信号,触发捕捉函数

经典信号临时屏蔽

经典信号临时屏蔽是指在信号处理函数执行期间,临时阻塞某些信号,以避免这些信号在处理当前信号时再次被递送。 两个相同信号触发,可以最大排队一次。

在信号处理的上下文中,当信号被捕捉时,同一信号在默认情况下最多只能排队一次。

如果允许任意数量的信号排队,系统资源的消耗可能会显著增加。这种资源限制有助于系统避免因信号排队导致的资源枯竭。

信号排队行为

在这段代码中,如果按下 Ctrl+C 发送 SIGINT 信号,信号处理程序会执行,并在执行期间再次按下 Ctrl+C 发送的 SIGINT 信号将不会被排队处理,只能在当前信号处理完成后再处理一次。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>//act.sa_handler的类型是void (*fun)(int)
//是一个自定义函数指针类型,自定义的捕捉函数也要是这种类型
void SIG_CATCH(int signo){int flag=2;while(flag--){printf("                flag%d\n",flag);sleep(1);}
}int main()
{struct sigaction act,oldact;act.sa_handler=SIG_CATCH;//自定义的函数,用来输出捕捉到的信号编号act.sa_flags=0;sigemptyset(&act.sa_mask);//信号集类型sigaction(SIGINT,&act,&oldact);//替换信号行为成自定义的捕捉行为,并且将原来的行为保存到oldact中while(1)sleep(1);return 0;
}

不管我们发送多少次SIGINT信号,也最多触发两次

捕捉函数的冲突

为了避免不同信号绑定相同的捕捉函数,引发冲突。

在进程处理某一个信号时,可以使用sa_mask临时屏蔽其他信号,等信号处理完再解除屏蔽,避免不同信号调用相同的捕捉冲突。

使用信号回收僵尸进程

使用waitwaitpid回收僵尸进程的操作都是主动回收,无论是阻塞回收还是非阻塞回收都会花费大量的时间片和资源。

操作系统在每次子进程结束后,都会发送SIGCHLD信号给父进程,这个信号默认处理行为会忽略,起到通知父进程的作用。那么我们就可以使用捕捉技术,绑定SIGCHLD信号与回收函数。当发送SIGCHLD信号后,自动调用回收函数,杀死僵尸进程。

下面是实现这一机制的demo程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <string.h>
#include <sys/fcntl.h>
#include <signal.h>
#include <sys/wait.h>
void SIG_KILLZOMB(int signo){int zpid;while((zpid=waitpid(-1,NULL,WNOHANG))>0){printf("已成功回收一个进程\n");}
}int main()
{struct sigaction act,oldact;act.sa_handler=SIG_KILLZOMB;//自定义的回收捕捉函数act.sa_flags=0;sigemptyset(&act.sa_mask);//将信号集类型全部置0sigaction(SIGCHLD,&act,&oldact);//替换捕捉行为为自定义的函数,并且将原来的函数保存到oldset中pid_t pid;pid=fork();if(pid>0){printf("Parent PID:%d is working\n",getpid());while(1) sleep(1);}else if(pid==0){printf("Child PID:%d is Exiting\n",getpid());exit(0);}return 0;
}

捕捉函数实现的流程

main函数先执行,执行过程中产生信号,系统执行捕捉函数,捕捉函数执行完,稍后回到主函数继续执行

内核切换到用户层执行捕捉函数,使用进程本身资源(时间片或内存)

1、进程在用户层执行特定指令

2、系统发出信号到内核层(等待处理)

当系统中某个事件(如硬件中断、软件异常等)发生时,会产生一个信号,这个信号会被传递到内核层进行处理。

3、进程状态发生转换,从用户空间切换到内核空间

用户进程的执行发生中断,CPU 从用户空间切换到内核空间,以便处理信号。

这种转换通常是通过系统调用中断指令实现的。

4、切换到内核层后完成调用

内核捕获信号后,根据信号的类型和处理程序完成相应的处理。内核找到对应的信号处理函数,并准备调用它。

5、完成调用后,返回用户空间,检测是否有未处理的信号,有则处理

内核处理完信号后,恢复进程的执行上下文,并返回用户空间。返回用户空间前,内核会检查是否有未处理的信号,如果有则继续处理。

6、如果信号的处理行为为捕捉行为,调用用户层捕捉函数,携高权限切换到用户层

如果信号有用户定义的处理函数,内核会将信号处理行为转换为调用用户层的捕捉函数。内核会提升权限,切换到用户空间以执行用户层的捕捉函数。

7、捕捉到信号

捕捉函数执行具体的信号处理逻辑。

8、系统调用完毕,返回用户空间

捕捉函数执行完毕后,系统调用 sigreturn 指令返回内核空间,恢复进程的执行上下文。

9、从 main 被中断的位置继续执行

最后,进程从 main 函数中断的位置继续执行。

到底什么是内核层与用户层?

内核层与用户层就是不同级别的CPU访问权限。

可以参考 Intel单核处理器的特权级别(Privilege Levels),也称为保护环(Protection Rings)。

这种模式在操作系统中也存在,用于管理和限制不同层次的软件对硬件资源的访问权限。防止低权限的代码(如用户进程)直接访问高权限的资源(如操作系统内核),减少系统被恶意攻击。

Level 0(环0)

特权级别:最高级别的CPU权限,内核层(Kernel Mode)。

访问权限:可以访问所有的硬件资源和执行所有的CPU指令。

操作系统内核运行在这一层,包括设备驱动程序、硬件抽象层等关键系统组件。

Level 3(环3)

特权级别:最低级别的CPU权限,用户层(User Mode)。

访问权限:受限,无法直接访问大多数硬件资源,只能通过系统调用访问操作系统提供的服务。

应用程序和用户进程运行在这一层,以确保系统的稳定性和安全性。

捕捉函数的可重入和不可重入

捕捉函数的可重入性(reentrancy)和不可重入性(non-reentrancy)是指函数在被中断后重新进入执行时的行为特性。

可重入函数

可重入函数指的是能够在被中断后,安全地再次被调用的函数。即使在另一个调用还没有完成的情况下,也不会出现数据不一致或其他问题。

int add(int a, int b) {return a + b;
}

add 函数是可重入的,因为它不依赖于任何共享状态或资源。

不可重入函数

不可重入函数是指在被中断后,再次调用可能会导致数据不一致或程序崩溃,通常因为它们依赖于静态或全局状态。

char* get_message() {static char message[] = "Hello, World!";return message;
}

get_message 函数是不可重入的,因为它返回了一个指向静态变量的指针。如果该函数在中断过程中被重新调用,会导致多个调用共享同一个静态变量,可能会引发数据不一致的问题。

捕捉函数中的可重入和不可重入

在信号处理(捕捉函数)过程中,假设信号处理程序调用了一个不可重入函数,而这个函数在被信号中断之前已经在运行,这时重新进入这个函数可能会导致程序崩溃或数据损坏。

例如在链表中间插入节点,插入操作进行一般时,调用了捕捉函数,主函数中断。而在捕捉函数中插入了S1节点,捕捉函数处理完毕后,M1节点的next指向了end。

虽然这种情况不会影响对链表的遍历,但是会产生垃圾节点和数据。

在编写信号处理程序时,应尽量使用可重入函数,避免使用不可重入的函数或在信号处理程序中调用不可重入的函数。

这篇关于【操作系统】信号Signal超详解|捕捉函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1090312

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP

C++操作符重载实例(独立函数)

C++操作符重载实例,我们把坐标值CVector的加法进行重载,计算c3=c1+c2时,也就是计算x3=x1+x2,y3=y1+y2,今天我们以独立函数的方式重载操作符+(加号),以下是C++代码: c1802.cpp源代码: D:\YcjWork\CppTour>vim c1802.cpp #include <iostream>using namespace std;/*** 以独立函数

嵌入式Openharmony系统构建与启动详解

大家好,今天主要给大家分享一下,如何构建Openharmony子系统以及系统的启动过程分解。 第一:OpenHarmony系统构建      首先熟悉一下,构建系统是一种自动化处理工具的集合,通过将源代码文件进行一系列处理,最终生成和用户可以使用的目标文件。这里的目标文件包括静态链接库文件、动态链接库文件、可执行文件、脚本文件、配置文件等。      我们在编写hellowor

Linux操作系统 初识

在认识操作系统之前,我们首先来了解一下计算机的发展: 计算机的发展 世界上第一台计算机名叫埃尼阿克,诞生在1945年2月14日,用于军事用途。 后来因为计算机的优势和潜力巨大,计算机开始飞速发展,并产生了一个当时一直有效的定律:摩尔定律--当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。 那么相应的,计算机就会变得越来越快,越来越小型化。

LabVIEW FIFO详解

在LabVIEW的FPGA开发中,FIFO(先入先出队列)是常用的数据传输机制。通过配置FIFO的属性,工程师可以在FPGA和主机之间,或不同FPGA VIs之间进行高效的数据传输。根据具体需求,FIFO有多种类型与实现方式,包括目标范围内FIFO(Target-Scoped)、DMA FIFO以及点对点流(Peer-to-Peer)。 FIFO类型 **目标范围FIFO(Target-Sc

019、JOptionPane类的常用静态方法详解

目录 JOptionPane类的常用静态方法详解 1. showInputDialog()方法 1.1基本用法 1.2带有默认值的输入框 1.3带有选项的输入对话框 1.4自定义图标的输入对话框 2. showConfirmDialog()方法 2.1基本用法 2.2自定义按钮和图标 2.3带有自定义组件的确认对话框 3. showMessageDialog()方法 3.1