文章目录
原视频来自于B站里昂,总长22min
视频链接
进程通信方法:管道,共享内存,消息队列,信号和信号量
2.信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。比如,如果当进程在前台运行时,你键入Ctrl+C(也就是同时按下Ctrl键和 C键),那么内核就会发送一个SIGINT信号给这个前台进程。
在linux系统中使用 kill -l 命令查看系统上支持的不同类型的信号。
每种信号类型都对应于某种系统事件,用不同的整数表示,例如SIGINT信号用号码2表示
1~31号为非实时信号(不可靠信号)处于就绪队列多个相同的非实时信号只会被响应一次。其余的被丢掉。
34~64号信号为实时信号(可靠信号),处于就绪队列多个相同的实时信号全部会被响应。
比如,如果一个进程试图除以0,那么内核就发送给它一个SIGFPE信号(号码8 Floating Point Exception浮点异常)。
如果一个进程执行一条非法指令,那么内核就发送给它一个SIGILL信号(号码4)。
sigill(ill生病,有病的,不健康的;不良的;不良的;〈美俚〉(因嫌疑而)被捕的,被拘留的;坏的,邪恶的,有害的;不舒服;)
kill -l
#查看系统上支持的不同类型的信号
3.信号的实现
在操作系统中,每个进程都有一个进程控制块(Process Control Block,PCB),它包含了进程的管理和控制信息.。
【例】Linux下,用一个名为task_struct的结构体类型来描述PCB,包括很多字段,如进程的状态进程的标识、进程的优先级等。每一种信息都用一个字段来实现。
struct task struct
{
...
volatile long state; /进程状态
pid_t pid; //进程ID,每个进程都有一个唯一的PID,用于区分不同进程,相当于身份证号
unsigned long rt_priority;//进程优先级
stuct mm_struct*mm,*active_mm;//与内存管理有关的数据结构,包含进程内存使用的信息
...
}
因此,我们可以将信号记录在进程的task_struct(PCB)结构体中。
例如,我们可以用位图来表示某个信号是否产生,每个比特位代表了某个特定的信号,比特位为0代表没有收到了信号,为1则代表收到了信号。进程收到信号,本质是位图被修改
4.信号的处理
通常有以下三种操作处理信号:
①忽略信号
不采取任何操作。但是有两种信号不能被忽略:SIGKILL(9)和SIGSTOP(19)
这样做的原因是系统管理员需要能够杀死或停止进程,如果进程能够选择忽略SIGKILL(使进程不能被杀死)或SIGSTOP(使进程不能被停止)将破坏这一权力。
②执行信号的默认操作
例如,进程收到SIGINT信号后会终止,本质上是向进程发送了一个编号为2的SIGINT信号,只不过这个信号是通过键盘输入的,然后经过操作系统处理后再发送给进程。
③捕获井处理信号
内核会暂停该进程正在执行的代码,并跳转到用户注册的函数。
SIGINT原本是用来结束进程的,但用signal自定义它的功能后就可以使它对进程的操作改变,
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
//handle的函数实现的功能是打印can't stop!"
void handle(int sigNum){
printf("can't stop! sigNum:%d\n",sigNum);
}
int main(){
signal(SIGINT,handle);
//将SIGINT号信号的功能改成handle
while(1){
printf("a\n");
sleep(1);
}
return 0;
}
可以看到,每次按下Ctrl+C,都会打印对应内容(实现自定义的功能而不是像原来一样终止进程),而sigNum也证明Ctrl+C对应的信号值确实是2号(SIGINT)。
经常捕获的两种信号是 SIGINT 和 SIGTERM。
SIGKILL和SIGSTOP 不能被捕获,即无法通过自定义handle函数来修改其信号操作。
exp1
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
//handle的函数实现的功能是打印can't stop!"
void handle(int sigNum){
printf("can't stop! sigNum:%d\n",sigNum);
}
int main(){
signal(SIGKILL,handle);
//将SIGINT号信号的功能改成handle
while(1){
printf("still alive\n");
sleep(1);
}
return 0;
}
exp2
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
int count=0;
//handle的函数实现的功能是打印"count=%d, still alive"
void handle(int sigNum){
printf("count=%d, still alive sigNum:%d\n",sigNum);
}
int main(){
signal(SIGKILL,handle);
signal(SIGTERM,handle);
signal(SIGINT,handle);
//将SIGINT号信号的功能改成handle
while(1){
count++;
sleep(1);
}
return 0;
}
2:sigint
15:sigterm
9:sigkill
几个Linux支持的典型信号:
SIGCHLD
当进程终止或停止时,内核会给进程的父进程发送此信号。在默认的情况下SIGCHLD是被忽略的,如果进程对它们的子进程是否存在感兴趣,那么进程必须显式地捕获并处理该信号。
SIGFPE
不考虑它的名字,该信号代表所有的算术异常,而不仅仅指浮点数运算相关的异常,异常包括溢出、下溢和除以0.
默认的操作是终止进程并形成内存转储文件,但进程可以捕获并处理该信号。
SIGILL
当进程试图执行一条非法机器指令时,内核会发送该信号。默认操作是终止进程并进行内存转储进程可以选择捕获并处理SIGILL。
SIGINT
当用户输入中断符(通常是Ctrl-C)时,该信号被发送给所有前台进程组中的进程默认的操作是终止进程。进程可以选择捕获并处理该信号,通常是为了在终止前进行清理工作
5.信号的产生
信号通常由以下方式产生:
① 通过终端按键(键盘)产生信号例如,Ctrl+C
发送2号信号SIGINT、Ctrl+\
发送3号信号SIGQUIT
② 程序异常时操作系统会向程序发送信号来终止进程。
③ 调用函数
- kill系统调用:
kil()调用会从一个进程向另一个进程发送信号:
int kill(pid t pid,int signo);//调用kill给pid代表的进程发送信号signo。
kill命令本质上是通过系统调用实现的。
- raise系统调用
【例子】创建一个raise程序,将其2号信号的处理函数改为自定义函数,该信号每隔一段时间便会给自己发送信号
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
void handle(int signo){
printf("get a signal : %d\n", signo);
}
int main(){
signal(2,handle);
//将2号信号自定义。
while(1){
printf("I'm a process, pid = %d\n",getpid());
sleep(1);
raise(2);
//进程自己给自己发送2号信号。
}
return 0;
}
abort 系统调用
abort 使当前进程接受到信号而异常终止
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
void handle(int signo){
printf("get a signal : %d\n", signo);
}
int main(){
signal(6,handle);
//将SIGINT号信号的功能改成handle
while(1){
printf("I am a process, pid : %d\n",getpid());
sleep(1);
abort();
}
return 0;
}
④由于软件条件产生信号
例如alarm()函数可以设置定时器,当定时器倒计时结束,就会向进程发送一个SIGALARM信号。
exp1
#include<singal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
void handle_alarm(int signo){
printf("Alarm clock!\n");
}
int main(){
//设置信号处理函数
signal(SIGALRM,handle_alarm);
//做其他事情或者简单地等待
while(1){
printf("running\n");
alarm(2);
sleep(2);
}
return 0;
}
exp2
pause()函数的作用是使当前进程进入睡眠状态,直到接收到一个信号为止。
int pause(void);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main( ) {
pid_t child_pid = fork();
//fork一个子进程。 子进程的pid不一样。
if(child_pid < 0) {//创建新进程失败。
perror("Fork failed");
exit(1);
}
if (child pid == 0){ // 子进程
while (1){
pause(); // 等待信号
}else { // 父进程
while (1) {
sleep(2);//每2秒发送一次信号
kill(child_pid, SIGUSR1);// 向子进程发送信号
printf("Sent SlGUSR1 to child process (PID: %d)\n", child_pid);
}
return 0;
}
⑤ 硬件异常产生信号
发生硬件异常时,它被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除零的指令CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号,并将该信号发送给进程。
总之,使用信号的两个主要目的:
① 让进程知道已经发生了一个特定的事件。
② 强迫进程执行它自己代码中的信号处理程序,
注意,并不是系统中所有进程都可以向其他进程发送信号,只有核心和超级用户可以。
普通进程只可以向拥有相同uid(用户标识号)和gid(组标识号)或者在相同进程组中的进程发送信号。
当信号产生时,内核将进程taskstruct中的信号相应标志位设置为1,表明产生了该信号。
系统对置位之前该标志位已经为1的情况不进行处理,这说明进程只处理最近接收的信号。
信号产生后并不马上送给进程,它必须等待直到进程再一次运行时才交给它。
每当进程从系统调用中退出时,内核会检查它的signal和blocked字段(位图),查看是否有需要发送的非屏蔽信号,若有则立即发送信号。
如果信号的处理被设置为缺省,则系统内核将会处理该信号,否则会执行用户提供的信号处理程序。
【练习1】信号是用户按下Ctrl+C时默认发送给前台进程组的信号?
A. ‘SIGKILL’
B.'SIGSTOP’
C.‘SIGINT’
D.‘SIGTERM’
答案:C
【练习2】哪个信号用于终止进程,并且不能被进程捕获或忽路?
A. 'SIGKILL`
B.“SIGBUS”
C. 'SIGINT
D.'SIGTERM
答案:A
【练习3】在信号的进程通信机制中,以下哪个说法是正确的?
A.对于任意的信号都可以忽略
B.对于任意的信号都可以捕获
C.系统中所有进程都可以向其他进程发送信号
D.系统对置位之前该标志位已经为1的情况不进行处理,这说明进程只处理最近接收的信号
答案:D