信号和信号量半毛钱关系都没有!
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
查看信号的机制,如默认处理动作man 7 signal
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。
进程信号
kill命令可以给进程发送信号。kill -l
查看信号列表,共61个信号,其中信号1-31号:普通信号,34-64号:实时信号。我们只需了解普通信号。信号是给进程发的,kill -信号编号 pid
。
进程本身是被程序员编写的属性和逻辑的集合—程序员编码完成的,当进程收到信号时,不一定立即处理。
信号是进程之间事件异步通知的一种方式,属于软中断。
[yyq@VM-8-13-centos 2023_03_01_ProcessSignal]$ 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
ctrl+c
:是一个组合键,OS会识别为kill -2 pid
,是2号信号,用于终止前台进程。SIGINT的默认处理动作是终止进程。
实际执行信号的处理动作称为信号递达(Delivery)。
信号从产生到递达之间的状态,称为信号未决(Pending)。(信号产生了,但是没有被执行)
信号可以被进程选择性阻塞。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
首先进程需要认识信号,知道信号的属性和对应要做的动作
发送信号的本质就是更改对应进程PCB中的信号位图。PCB的管理者是OS,故信号也是由OS发送,因为只有OS有权利修改PCB里的内容。
无论有多少种发送信号的方式,本质都是通过OS向目标进程发送信号,故OS必须提供发送信号/处理信号的相关系统调用。
当信号来临时,进程不一定马上处理这个信号。异步:各自做着不一样的事情,进程继续执行自己的代码,而不执行信号。同步:进程先执行信号,停下自己的代码。由于信号可以随时产生,所以进程对信号是异步的。
ctrl+c
==> SIGINT-终止前台进程 ;ctrl+\
==> SIGQUIT-终止进程
kill() 给别人发任意信号、raise() 给自己发任意信号、abort() 给自己发SIGABRT信号
#include
#include
原型
int kill(pid_t pid, int sig);
参数
pid:
pid>0,发送给指定进程;
pid==0,发送给调用进程的进程组中的每个进程;
pid==-1,发送给所有调用进程有权限发送信号的进程,除了进程1 (init);
pid<-1, 发送给进程组中ID为-pid的所有进程
sig:信号编号
返回值
成功返回0;失败返回-1
#include
原型
int raise(int sig);// 相当于kill(getpid(), sig);
参数
sig:信号编号
返回值
成功返回0;失败返回非0
#include
原型
void abort(void);// 相当于kill(getpid(), SIGABRT);
大多数信号的默认处理动作都是终止进程,信号的意义:信号不同代表不同的事件,对事件发生之后的处理动作可以一样。
1、如除0异常的8号信号SIGFPE
那操作系统如何得知下面这个程序在执行除0操作?
答:通过硬件-cpu内的状态寄存器。状态寄存器有个溢出标记位,当除0溢出标记位就变成1,此次运算结果无意义,CPU发生运算异常,OS作为软硬件资源的管理者,就知道是当前的进程导致的硬件异常,于是OS修改该进程的信号标记位。
#include
#include
#include
#include
#include
#include
void handler(int signo)
{
std::cout << "捕捉到一个信号,编号是" << signo << std::endl;
}
int main()
{
signal(SIGFPE, handler);// 只是注册了该信号对应的动作
int a = 10;
a /= 0;
while(true)
{
std::cout << "我是一个进程:" << getpid() << std::endl;
sleep(1);
}
return 0;
}
在执行这个程序后,会一直输出
捕捉到一个信号,编号是8
,不应该只输出1次,程序就被终止了么?
答:首先我们自定义了收到8号信号的动作,就不会执行默认动作(退出)。其次,1、收到信号不一定会引起进程退出,2、进程没有退出就有可能还会被调度,3、cpu内部对应的寄存器只有一个,但是状态寄存器中的内容属于当前进程的上下文,但是用户没有能力去修改寄存器中的内容,4、当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程,所以每一次恢复的时候就会让OS识别到状态寄存器的溢出标志位,所以一直给进程发8号信号。
2、如访问野指针 11号信号SIGSEGV
int* p = nullptr;
*p = 100;//段错误
那操作系统如何得知下面这个程序访问野指针了?
答:咱们在程序里写的指针,本质是就是虚拟地址,而虚拟地址空间的0号地址,经过页表和MMU(在cou里))的映射,发现当前进程不允许访问0号地址,MMU这个硬件因为越界/野指针,进而发生异常,OS识别到了MMU异常,就将对应进程的信号位图的11位置为1.
1、管道读端关闭,写端一直写==>OS直接终止当前进程,13号信号SIGPIPE
2、定时器alarm()
函数 14号信号SIGALRM
#include
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
可以通过alarm()
的使用,我们可以明显观察到IO速度很慢
int main()
{
alarm(1); // 1s内计算机能够将数据累加到144250
int cnt = 0;
while(true)
{
std::cout << cnt++ << std::endl;//这里要进行cnt次IO,速度很慢
cnt++;
}
return 0;
}
---------------------------------------------
int cnt = 0;
void handler(int signo)
{
std::cout << "cnt: " << cnt << std::endl;//只需要进行1次IO
}
int main()
{
alarm(1); // 1s内计算机能够将数据累加到435882026
signal(SIGALRM, handler);
while(true)
{
cnt++;
}
return 0;
}
闹钟信号只会通知1次即只有1次输出,不会像硬件异常那样疯狂输出
int cnt = 0;
void handler(int signo)
{
std::cout << "cnt: " << cnt << std::endl;//只需要进行1次IO
alarm(1);//这样就可以实现每隔1s打印一次,简单的sleep(1)
}
int main()
{
alarm(1); // 1s内计算机能够将数据累加到435882026
signal(SIGALRM, handler);
while(true)
{
cnt++;
}
return 0;
}
alarm(0)
表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
任何一个进程都可以通过alarm系统调用在内核中设置闹钟,那么OS就要对其进行管理,先描述在组织。
struct alarm {
uint64_t when; //未来的时间 即时间戳+闹钟设定的时间
int type; //闹钟类型一次性或周期性
task_struct *p; //对应的进程
struct alarm* next; //下一个闹钟
};
比如用最小堆来管理,那么堆顶的闹钟就是将来第一个要超时的闹钟,OS就可以周期性的检查堆顶,当操作系统的当前时间>alarm.when,就给对应的进程发送信号。
在信号到来到信号被处理的这段时间,称为时间窗口,必须要保存这个信号。会被保存在进程PCBtask_struct{ unsigned int signal}
这个结构体里,unsigned int
有32个比特位,用来表示1-31号信号【比特位的位图结构】。比特位的位置,代表信号编号,比特位的内容,代表是否收到该信号,对应的值为0/1,收到对应的信号就置为1。
被阻塞的信号将保持在未决(pending)状态,直到进程解除对信号的阻塞(block),才执行递达动作。普通信号在递达之前产生多次只计一次,因为对应二进制位非0即1。
task_struct {
unsigned int pending = 0; //0000 0000 0000 0000 0000 0000 0000 0000 // 从右向左第1个比特位表示1号信号
unsigned int block = 0; //0000 0000 0000 0000 0000 0000 0000 0000
}
if ((1 << (signo - 1)) & pcb->block)
{
//被阻塞,那就不用递达了
}
else
{
if((1 << (signo - 1)) & pcb->pending)
{
//未被阻塞,且信号未决,那就递达
}
}
①PCB进程控制块中有信号屏蔽状态字(block),信号未决状态字(pending),还有是否忽略标识
②信号屏蔽状态字(block):1代表阻塞,0代表不阻塞;信号未决状态字(pending):1代表未决,0代表信号递达
③向进程发送SIGINT,内核首先判断信号屏蔽状态字是否阻塞,如果信号屏蔽状态字阻塞,信号未决状态字(pengding)相应位置1;
若阻塞解除,信号未决状态字(pending)相应位置0,表示信号可以递达了。
④block状态字,pending状态都是64bit,分别代表Linux系统中的64个信号。例如SIGINT是2号信号,对应block状态字中的第二位
⑤block状态字用户可以读写,pending状态字用户只能读,这是信号的设计机制。
注意:即使一个信号没有产生,它仍然可以先被设置为阻塞状态。进程能识别信号,是因为PCB结构中有相应的数据结构(block
位图、pending位图、handler_t表)
typedef void(*handler_t)(int signo);//函数指针-表示操作方法
handler_thandler[32] = 0;//函数指针数组,存储32个信号对应的处理方法 数组的下标+1表示信号编号,数组下标对应的内容表示对应信号的处理方法
收到信号后一般会有3个动作。
大多数信号的默认处理动作都是终止进程,信号的意义:信号不同代表不同的事件,对事件发生之后的处理动作可以一样。term,core,ign,stop,cont
当进程出现异常的时候,我们将进程对应时刻在内存中的有效数据转储到磁盘
First the signals described in the original POSIX.1-1990 standard.
//表中第一列是各信号的宏定义名称,第二列是各信号的编号,第三列是默认处理动作
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
Term表示终止当前进程,OS正常结束该进程
Core表示终止当前进程并且Core Dump(Core Dump 用于gdb调试)。在命令行输入ulimit -a
查看core file size
核心转储功能,云服务器默认是0(关闭该功能),命令行输入ulimit -c 1024
,打开云服务器的核心转储功能并设置大小为1024字节,此时再执行会发生段错误的程序,就会输出Segmentation fault (core dumped)
,且当前目录下会生成一个文件core.引起core问题的进程ID
,给该文件在编译时带上-g
选项,在进行调试的时候,输入core-file core.pid
就可以把对应的核心转储文件打开,就可以看到详细的报错信息。
Ign表示忽略该信号
Stop表示停止当前进程
Cont表示继续执行先前停止的进程
用户自定义信号处理函数称为捕捉。9号信号无法捕捉。信号产生不一定被立即处理,而是在合适的时候(由内核态返回用户态的时候)处理。如果是默认动作或忽略动作的信号则可以直接在内核态执行,不用等到回用户态再执行。
内核态:由用户/进程发起的,但实际执行系统调用的身份是内核
用户态:平时自己写的代码是运行在用户态的,而我们的代码难免访问2种资源【OS资源(getpid, waitpid),硬件资源(printf, write, read)】,用户为了访问内核和硬件资源,必须通过系统调用来完成对应的访问。
往往系统调用比较费时间,要进行角色转换。CPU内有寄存器,有个current寄存器指向当前正在运行进程的PCB的起始地址,CR3寄存器表征当前进程的运行级别(0表示内核态,3表示用户态)
合适的时候:从内核态返回用户态的时候,要返回的话,曾经肯定进入了内核态(系统调用、进程切换等),此时要从内核态切换回去,OS会先通过PCB看看每个信号的block表是否被阻塞,没被阻塞的再看对应的pending表是否未决,未决的信号让它递达。【block1,不管;block0&&pending0,不管;block0&&pending==1,查handler表对应的方法并执行】,默认和忽略这两个动作很方便,而自定义动作需要切换回用户态才能执行【不可以在内核态直接执行】,然后再回到内核态获取当前进程的上下文,才能回到用户态跳转的位置继续执行后续代码。
普通信号的自定义捕捉流程:用户态–>内核态【执行系统调用,以及信号检测】–>用户态【执行自定义动作】–>内核态【获取进程上下文】–>用户态。
#include
功能 信号处理器,即可以收到特定信号时,执行自定义动作
原型
typedef void (*sighandler_t)(int); // 函数指针,传入的int表示信号的编号
sighandler_t signal(int signum, sighandler_t handler);//其实就是拿着signum去操作方法表里修改对应信号的操作方法
// 设置了收到2号信号的自定义动作
void handler(int signo)
{
std::cout << "捕捉到2号信号" << signo << std::endl;
}
// 这里是对signal函数的调用,而不是对handler的调用
// handler对应的方法一般不会执行,除非收到对应的信号
signal(2, handler);// 只是注册了该信号对应的动作
//要想触发该动作,需要按ctrl+c组合键 SIGINT的默认处理动作是终止进程
//2号的默认动作是退出进程,当我们给这个进程的2号信号设置了自定义动作,就会执行自定义动作
#include
功能 信号处理器,即可以收到特定信号时,执行自定义动作
原型
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数
signo:信号编号
act输入型参数:
非nullptr:根据act修改对应信号的处理动作
oact输出型参数:
nullptr:不保存原来的操作方法
非nullptr:保存原来的操作方法
返回值
调用成功则返回0,出错则返回-1
struct sigaction {
void (*sa_handler)(int);//sa_handler==SIG_IGN,表示忽略;sa_handler==SIG_DFL,表示执行系统默认动作;sa_handler==用户自定义,表示捕捉
void (*sa_sigaction)(int, siginfo_t *, void *);//实时信号的处理函数
sigset_t sa_mask;//表示当前进程正在处理某一种信号时,额外需要屏蔽的信号集【正在递达的信号自动放进当前进程的信号屏蔽字里】。当信号处理函数返回时,自动恢复原来的信号屏蔽字【不会再屏蔽额外添加的这个信号集】 sigaddset(&act.sa_mask, signo);
int sa_flags;//设为0即可
void (*sa_restorer)(void);//已不再使用
};
sa_handler和sa_sigaction两个参数不共存!
SIGALRM:定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程
SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell命令Kill时,缺省产生这个信号。默认动作为终止进程
SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程
SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号
SIGCONT:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为终止进程
SIGTTIN:后台进程读终端控制台。默认动作为暂停进程
SIGTSTP:停止进程的运行。按下组合键时发出这个信号。默认动作为暂停进程
SIGTTOU:该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程
SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号
SIGXFSZ:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程
SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程
SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程
SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程
SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号
SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略
SIGPWR:关机。默认动作为终止进程
SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件
31号SIGRTMIN~64号SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。