Linux信号

文章详细阐述了Linux系统中的信号机制,包括信号的定义、产生、处理方式以及常见信号类型。同时,讨论了中断的基本概念,如外部中断和内部中断,以及它们在操作系统中的作用。文章通过实例展示了如何使用signal函数自定义信号处理,并提及了核心转储(CoreDump)的情况及其在调试中的应用。
摘要由CSDN通过智能技术生成

红绿灯闹钟,王者荣耀,信号转向灯,狼烟..........

生活中的信号

1.你为什么认识这些信号呢?记住了对应场景下的信号+后续是有”动作“要你执行的

2.我们在我们的大脑中,能够识别这个信号的

3.如果特定信号没有产生,但是我们依旧知道应该如何处理这个信号

4.我在收到这个信号的时候,可能不会立即处理这个信号!

5.信号本身,在我们无法立即被处理的时候,也一定要先被临时的记住!

识别 = 记住了对应场景下的信号+后续是有”动作“要你执行的。


1.什么是Linux信号?

本质是一种通知机制,用户 or操作系统通过发送一定的信号,通知进程,某些事件已经发生,进程你可以在后续进行处理

2.结合进程,信号结论

a.进程要处理信号,必须具备信号“识别”的能力(看到+处理动作)

b.凭什么进程能够“识别”信号呢?程序员!进行编写代码,提前预处理一些可能出现的问题和状况。

c.信号产生是随机的,进程可能正在忙自己的事情,所以,信号的后续处理,可能不是立即处理的!

d.信号会临时的记录下对应的信号,方便后续进行处理

e.在什么时候处理呢?合适的时候

g.一般而言,信号的产生相对于进程而言是异步的

2.信号如何产生

我们通过Ctrl + c 的方式来理解。

ctrl+c.本质就是通过键盘组合键向目标进程发送2号信号,致使进程退出了

信号处理的常见方式:

a.默认(进程自带的,程序员写好的逻辑)

b.忽略(也是信号处理的一种方式)

c.自定义动作(捕捉信号)

4.如何理解组合键变成信号呢?

键盘的工作方式是通过:中断方式进行的

当然也能够识别组合键,ctrl+c

OS解释组合键->OS查找进程列表->前台运行的进程->OS写入对应的信号到进程内部的位图结构中!

5.如何理解信号被进程保存呢?

a.什么是信号?----->>>unisgned int

b.是否产生?----->>>进程必须具有保存信号的相关数据结构(位图,unisgned int,)

PCB内部保存了信号位图字段

信号位图是在task_struct -> task_struct内核数据结构->OS

信号发送的本质:OS向目标进程写信号,OS直接修改pcb中的指定的位图结构,完成“发送”信号的过程


关于中断的理解

每个设备都有中断号,其实就是数组下标。那么它是怎样与我们的设备相对应的呢???

其实是通过函数指针(数组),当我们的一个设备发生了中断,通过硬件的方式,向我们的计算机(CPU)发送我们的中断号,从而写入到特定寄存器中,我们的操作系统从而根据我们的中断号,查找我们内置的中断处理方法,完成硬件到软件的区别。

网卡中断,接受消息

时钟中断,刷新缓冲区,一个话痨

中断,英文名为Interrupt,计算机的世界里处处都有中断,任何工作都离不开中断,可以说整个计算机系统就是由中断来驱动的。那么什么是中断?简单来说就是CPU停下当前的工作任务,去处理其他事情,处理完后回来继续执行刚才的任务,这一过程便是中断。

中断分类

1外部中断

1、可屏蔽中断:通过INTR线向CPU请求的中断,主要来自外部设备如硬盘,打印机,网卡等。此类中断并不会影响系统运行,可随时处理,甚至不处理,所以名为可屏蔽中断。

2、不可屏蔽中断:通过NMI线向CPU请求的中断,如电源掉电,硬件线路故障等。这里不可屏蔽的意思不是不可以屏蔽,不建议屏蔽,而是问题太大,屏蔽不了,不能屏蔽的意思。

注:INTR和NMI都是CPU的引脚

2内部中断(软中断,异常)

1、陷阱:是一种有意的,预先安排的异常事件,一般是在编写程序时故意设下的陷阱指令,而后执行到陷阱指令后,CPU将会调用特定程序进行相应的处理,处理结束后返回到陷阱指令的下一条指令。如系统调用,程序调试功能等。

尽管我们平时写程序时似乎并没有设下陷阱,那是因为平常所用的高级语言对底层的指令进行了太多层的抽象封装,已看不到底层的实现,但其实是存在的。例如printf函数,最底层的实现中会有一条int 0x80指令,这就是一条陷阱指令,使用0x80号中断进行系统调用。

2、故障:故障是在引起故障的指令被执行,但还没有执行结束时,CPU检测到的一类的意外事件。出错时交由故障处理程序处理,如果能处理修正这个错误,就将控制返回到引起故障的指令即CPU重新执这条指令。如果不能处理就报错。

常见的故障为缺页,当CPU引用的虚拟地址对应的物理页不存在时就会发生故障。缺页异常是能够修正的,有着专门的缺页处理程序,它会将缺失的物理页从磁盘中重新调进主存。而后再次执行引起故障的指令时便能够顺利执行了。

3、终止:执行指令的过程中发生了致命错误,不可修复,程序无法继续运行,只能终止,通常会是一些硬件的错误。终止处理程序不会将控制返回给原程序,而是直接终止原程序


信号

这是linux下的命令

Signal Value Action Comment ────────────────────────────────────────────────────────────────────── SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process SIGINT 2 Term Interrupt from keyboard SIGQUIT 3 Core Quit from keyboard SIGILL 4 Core Illegal Instruction SIGABRT 6 Core Abort signal from abort(3) SIGFPE 8 Core Floating point exception SIGKILL 9 Term Kill signal SIGSEGV 11 Core Invalid memory reference SIGPIPE 13 Term Broken pipe: write to pipe with no readers SIGALRM 14 Term Timer signal from alarm(2) SIGTERM 15 Term Termination signal SIGUSR1 30,10,16 Term User-defined signal 1 SIGUSR2 31,12,17 Term User-defined signal 2 SIGCHLD 20,17,18 Ign Child stopped or terminated SIGCONT 19,18,25 Cont Continue if stopped SIGSTOP 17,19,23 Stop Stop process SIGTSTP 18,20,24 Stop Stop typed at terminal SIGTTIN 21,21,26 Stop Terminal input for background process SIGTTOU 22,22,27 Stop Terminal output for background process The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

或者以这样的一个方式查看,其实我们需要注意的是

下面总共有62个信号,32,33这两个信号是没有的,前31个信号是普通信号,后31个是实时信号。

[hang@VM-4-12-centos day18]$ 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

这里可以扩展一些小知识,比如实时操作系统,分时操作系统。

我们计算机通常使用的是分时操作系统,通过采用时间片轮转的形式进行调度,确保多个进程在一定的时间段可以保持推进。分时操作系统的特点是可有效增加资源的使用率。 例如UNIX系统就采用剥夺式动态优先的CPU调度,有力地支持分时操作。 操作系统(Operating System,OS)是管理计算机硬件与软件资源的 计算机程序 ,同时也是计算机系统的内核与基石。

这个也导致了一个问题,如果我们下发了一个任务,而由于CPU可能正在调度其他进程,或者有优先级更高的进程就可能导致了我们新下发的任务不能及时被调度。

实时操作系统通常是车载系统,我们新能源车辆呢,通常可能会有智能避让系统,这个过程中肯定伴随着刹车。证明了我们平时的刹车是经过系统的,如果采用分时操作系统,我们刹车时这个进车可能无法立即调度,导致事故的发生。


产生信号

通过键盘

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。

第一个函数

signal

SYNOPSIS #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);

这个函数是可以自定义捕获信号的(修改对应的信号捕捉方法),通过函数指针的方式来传递新的处理方法。

第二个参数是一个回调函数,函数返回值是void,并且有一个int参数。它的返回值是旧的处理方法。

我们通常使用的ctrl + c 来终止进程其实是给进程发送的是2号信号SIGINT

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; void catchSig(int signum) { cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl; } int main() { //signal(2,catchSig); signal(SIGINT,catchSig); while(true) { cout<<"wo shi yi ge jin cheng,running.....,pid: "<<getpid()<<endl; sleep(1); } return 0; }

关于signal函数需要注意的点是:

signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作 如果后续没有任何SIGINT信号产生,catchSig会不会被调用??永远也不会被调用

相当于原来ctrl +c是终止进程,现在呢,ctrl + c 是调用新的处理动作。

特定信号的处理动作,一般只有一个。就以kill这个为例。

core dump

我们观察到

term:termination 终止。

那么 core 是???

Core Dump

首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁 盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许 产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的, 因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许 产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024

讲课:

核心转储,我们上面的Core Dump显示着我们是否发生核心转储???---->>>当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中!

一般而言,云服务器(生产环境)的核心转储功能是被关闭的!为什么呢??有可能产生的core文件过多,导致磁盘饱满。

由于我们的线上环境运行时间较长,为了避免产生差错。通常会有监视系统,一些系统甚至在服务器宕机的时候会自动重启,在这个过程中,core文件会逐渐增多。

ulimit -a查看我们的当前的一些资源配置

[hang@VM-4-12-centos day18]$ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 7908 max locked memory (kbytes, -l) unlimited max memory size (kbytes, -m) unlimited open files (-n) 100001 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 7908 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited

注意看第一行,大小为0.

如何修改

[hang@VM-4-12-centos day18]$ ulimit -c 10240 [hang@VM-4-12-centos day18]$ ulimit -a core file size (blocks, -c) 10240 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 7908 max locked memory (kbytes, -l) unlimited max memory size (kbytes, -m) unlimited open files (-n) 100001 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 7908 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited

接下来的代码是关于Core Dump的

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; void catchSig(int signum) { cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl; } int main() { //signal(2,catchSig); signal(SIGINT,catchSig); //signal(SIGFPE,catchSig); while(true) { cout<<"wo shi yi ge jin cheng,running.....,pid: "<<getpid()<<endl; sleep(1); } return 0; }

这个时候Core文件的值为0,运行结果为

如果设定Core文件的值为10240

ulimit -c 10240

则运行结果为

那么生成的core.pid的文件,可以在gdb调试时发挥作用

生成的core文件可以帮助我们在调试的时候直接定位到报错的地方。(事后调试)

(gdb) core-file core.1748 warning: exec file is newer than core file. [New LWP 1748] Core was generated by `./mykill'. Program terminated with signal 8, Arithmetic exception. #0 0x00007f817e3979e0 in __nanosleep_nocancel () from /lib64/libc.so.6 Missing separate debuginfos, use: debuginfo-install glibc-2.17-326.el7_9.x86_64 libgcc-4.8.5-44.el7.x86_64 (gdb)

验证进程等待中的core dump标记位

这段代码是发生除0错误

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; void catchSig(int signum) { cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl; } int main() { //signal(2,catchSig); signal(SIGINT,catchSig); //signal(SIGFPE,catchSig); pid_t id = fork(); if(id == 0) { sleep(1); int a = 100; a /= 0; exit(0); } int status = 0; waitpid(id, &status, 0); cout << "父进程:" << getpid() << " 子进程:" << id << \ " exit sig: " << (status & 0x7F) << " is core: " << ((status >> 7) & 1) << endl; return 0; }

运行结果

[hang@VM-4-12-centos day18]$ ./mykill 父进程:3668 子进程:3669 exit sig: 8 is core: 0

core dump 标记位意义:证明是否生成core文件。

kill

KILL(2) Linux Programmer's Manual KILL(2) NAME kill - send signal to a process SYNOPSIS #include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);

就是给一个进程传递对应的信号,可以写一个简单的mykill

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; void catchSig(int signum) { cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl; } static void Usage(string proc) { cout << "Usage:\r\n\t" << proc << " signumber processid" << endl; } int main(int argc, char *argv[]) { if(argc != 3) { Usage(argv[0]); exit(1); } int signumber = atoi(argv[1]); int procid = atoi(argv[2]); kill(procid, signumber); return 0; }

raise

NAME raise - send a signal to the caller SYNOPSIS #include <signal.h> int raise(int sig);

自己给自己发信号

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; int main() { cout<<"i ma running......... "<<endl; sleep(1); raise(8); return 0; }

abort

NAME abort - cause abnormal process termination SYNOPSIS #include <stdlib.h> void abort(void);

给自己发送一个确定的abort信号

int main() { cout<<"i ma running......... "<<endl; sleep(1); //raise(8); abort(); return 0; }

通常用来进行终止进程!


b.系统调用接口

如何理解?用户调用系统接口·>执行OS对应的系统调用代码-→OS提取参数。或者设置特定的数值->OS向目标进程写信号·修改对应进程的信号标记位->进程后续会处理信号->执行对应的处理动作!

c.由软件条件产生信号

管道,读端不光不读,而且还关闭了,写端一直在写,会发生什么问题?写没有意义! OS会自动终止对应的写端进程,通过发送信号的方式,SIGPIPE! 13)SIGPIPE

管道问题

1.创建匿名管道⒉让父进程进行读取,子进程进行写入(why)3.父子可以通信一段时间4.让父进程关闭读端&&8. waitpid(),子进程只要一直写入就行5.子进程退出,父进程waitpid拿到子进程的退出status 6.提取退出信号!

闹钟问题

alarm

NAME alarm - set an alarm clock for delivery of a signal SYNOPSIS #include <unistd.h> unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后 响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

利用闹钟验证1s之内,我们一共会进行多少次count++

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; int main() { alarm(1); int count = 0; while(true) { cout<<count++<<endl; } return 0; }

其实这个数据还是比较少的。

因为cout 需要时间,网络发送呢,又必须等待打印完毕(因为我们的网络发送通常是阻塞式的,打印不完,就无法发送)。cout + 网络发送 = IO,IO的效率其实非常低尤其是带上网络。

单纯计算算力,将IO的影响减少到最少。

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; uint64_t count = 0; void catchSig(int signum) { //cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl; cout<<"count : "<<count<<endl; alarm(1); } int main() { signal(SIGALRM,catchSig); alarm(1); while(true) { count++; } return 0; }

运行结果

[hang@VM-4-12-centos day18]$ ./mykill count : 505834475 count : 1011757279 count : 1517832513 count : 2024655509

上面的代码其实也就是实现定时器的功能。

如何理解软件条件给进程发送信号: a.OS先识别到某种软件条件触发或者不满足 b.OS构建信号,发送给指定的进程

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; typedef function<void ()> func; vector<func> callbacks; uint64_t count = 0; // void catchSig(int signum) // { // cout<<"count : "<<count<<endl; // alarm(1); // } // static void Usage(string proc) // { // cout << "Usage:\r\n\t" << proc << " signumber processid" << endl; // } void showCount() { cout << "final count : " << count << endl; } void showLog() { cout << "日志功能" << endl; } void logUser() { if(fork() == 0) { execl("/usr/bin/who", "who", nullptr); exit(1); } wait(nullptr); } void catchSig(int signum) { for(auto &f : callbacks) { f(); } alarm(1); } int main() { signal(SIGALRM,catchSig); alarm(1); callbacks.push_back(showCount); callbacks.push_back(showLog); callbacks.push_back(logUser); while(true) { count++; } return 0; }

运行结果

[hang@VM-4-12-centos day18]$ ./mykill final count : 507140157 日志功能 hang pts/8 2023-02-21 11:50 (111.52.6.133) final count : 1014240561 日志功能 hang pts/8 2023-02-21 11:50 (111.52.6.133) final count : 1519987385 日志功能 hang pts/8 2023-02-21 11:50 (111.52.6.133) final count : 2026289298 日志功能 hang pts/8 2023-02-21 11:50 (111.52.6.133) final count : 2532814700 日志功能 hang pts/8 2023-02-21 11:50 (111.52.6.133) final count : 3040053604 日志功能 hang pts/8 2023-02-21 11:50 (111.52.6.133) ^C


4. 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除 以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非 法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

除0错误

#include <iostream> #include <string> #include <vector> #include <functional> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <signal.h> #include <stdlib.h> using namespace std; void catchSig(int signum) { cout<<"获得了一个信号 :"<<signum<<endl; sleep(1); } int main() { signal(SIGFPE,catchSig); int a =1; a /= 0; while(true) { sleep(1); } return 0; }

运行结果

[hang@VM-4-12-centos day18]$ ./mykill 获得了一个信号 :8 获得了一个信号 :8 获得了一个信号 :8 获得了一个信号 :8 获得了一个信号 :8 获得了一个信号 :8 获得了一个信号 :8 获得了一个信号 :8 ^C [hang@VM-4-12-centos day18]$

我们发现这个进程一直在打印错误。错误原因后面讲。

如何理解除0呢?

1.进行计算的是CPU,这是个硬件

2.CPU内部是有寄存器的,状态寄存器(位图),有对应的状态标记位,溢出标记位,(因为我们CPU计算完毕是要写回的,这个写回操作肯定不是我们用户,是由操作系统帮我们写回内存的,但是操作系统并不会无脑写回,还是会进行一番检测的,就进行状态位的检测)OS会自动进行计算完毕之后的检测!如果溢出标记位是1,OS里面识别到有溢出问题,立即只要找到当前谁在运行提取PID,OS完成信号发送的过程,进程会在合适的时候,进行处理

其实各个硬件都是拥有寄存器的。

3.一旦出现硬件异常,进程一定会退出吗?不一定!一般默认是退出,但是我们即便不退出,我们也做不了什么

4.为什么会死循环?寄存器中的异常一直没有被解决!(由于我们的进程是一个死循环,进程的上下文数据一直都是异常,而每次被调度到CPU的时候,都会被重新捕获,只有进程退出,上下文数据被刷新,才停止)

如何理解野指针或者越界问题?

1.都必须通过地址,找到目标位置

2.我们语言上面的地址,全部都是虚拟地址

3.将虚拟地址转成物理地址(由于这个转化是最高频的,所以我们的转换方式和算法都是很重要的)

4.页表+MMU(Memory Manager Unit,硬件! ! )(故采用这种转化方式)

5.野指针,越界-》非法地址-》MMU转化的时候,一定会报错!

所有的信号,有他的来源,但最终全部都是被OS识别,解释,并发送的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值