信号知识总结

发布时间:2023年12月26日


信号(signal)是一种软件中断,它提供了一种处理异步事件的方法,也是进程间惟一的异步通信方式。在Linux系统中,根据POSIX标准扩展以后的信号机制,不仅可以用来通知某种程序发生了什么事件,还可以给进程传递数据。

一、信号的概念

信号的共性:
简单,不能够携带大量的信息,满足某个特定条件,优先级高
使用信号的目的:
1.让进程知道已经发生了一个特定的事情
2.强迫进程执行它自己代码中的信号处理程序(中断机制)

二、信号的来源

信号的来源可以有很多种试,按照产生条件的不同可以分为硬件和软件两种。

1、 硬件方式

当用户在终端上按下某键时,将产生信号。如按下组合键后将产生一个SIGINT信号。
终端按键产生信号
ctrl+c ==> 2)SIGINT用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
ctrl+z ==> 20) SIGTSTP 停止终端交互进程的运行。默认动作为暂停进程。
ctrl+\ ==> 3) SIGQUIT 用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
硬件异常产生信号:除数据、无效的存储访问等。这些事件通常由硬件(如:CPU)检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时正在进行的程序。
异常程序操作(eg:除0操作) ==>8) SIGFPE 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
非法访问内存 ==>20) SIGTSTP:停止终端交互进程的运行。默认动作为暂停进程。
总线错误 ==>7) SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。

2、 软件方式

用户在终端下调用kill命令向进程发送任务信号。
kill -15 pid (15:信号编号 向进程发送SIGTERM信号)
进程调用kill或sigqueue函数发送信号。
当检测到某种软件条件已经具备时发出信号,如由alarm或settimer设置的定时器超时时将生成SIGALRM信号。

三、信号的机制

每一进程收到的所有信号,都是由内核负责发送的,内核处理
A给B发送信号。B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,立即去处理信号(信号的优先级高)。与硬件中断类似,但信号是软件层实现的中断,又成为“软中断”。

四、信号的种类

1> Linux 系统信号列表

在Shell下输入kill –l 可显示Linux 系统支持的全部依赖,信号列表如下:

在这里插入图片描述

2>信号的值定义

在signal.h中,在Linux中没有16和32这两个信号。上面信号的含义如下:
(1) SIGHUP:当用户退出Shell时,由该Shell启的发所有进程都退接收到这个信号,默认动作为终止进程。
(2) SIGINT:用户按下组合键时,用户端时向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
(3) SIGQUIT:当用户按下组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程并产生core文件。
(4) SIGILL :CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件。
(5) SIGTRAP:该信号由断点指令或其他trap指令产生。默认动作为终止进程并产生core文件。
(6) SIGABRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
(7) SIGBUS:非法访问内存地址,包括内存地址对齐(alignment)出错,默认动作为终止进程并产生core文件。
(8) SIGFPE:在发生致命的算术错误时产生。不仅包括浮点运行错误,还包括溢出及除数为0等所有的算术错误。默认动作为终止进程并产生core文件。
(9) SIGKILL:无条件终止进程。本信号不能被忽略、处理和阻塞。默认动作为终止进程。它向系统管理员提供了一种可以杀死任何进程的方法。
(10) SIGUSR1:用户定义的信号,即程序可以在程序中定义并使用该信号。默认动作为终止进程。
(11) SIGSEGV:指示进程进行了无效的内存访问。默认动作为终止进程并使用该信号。默认动作为终止进程。
(12) SIGUSR2:这是另外一个用户定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
(13) SIGPIPE:Broken pipe:向一个没有读端的管道写数据。默认动作为终止进程。
(14) SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。
(15) SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是,该信号可以被阻塞和处理。通常用来要求程序正常退出。执行Shell命令kill时,缺少产生这个信号。默认动作为终止进程。
(16) SIGCHLD:子程序结束时,父进程会收到这个信号。默认动作为忽略该信号。
(17) SIGCONT:让一个暂停的进程继续执行。
(18) SIGSTOP:停止(stopped)进程的执行。注意它和SIGTERM以及SIGINT的区别:该进程还未结束,只是暂停执行。本信号不能被忽略、处理和阻塞。默认作为暂停进程。
(19) SIGTSTP:停止进程的动作,但该信号可以被处理和忽略。按下组合键时发出该信号。默认动作为暂停进程。
(20) SIGTTIN:当后台进程要从用户终端读数据时,该终端中的所有进程会收到SIGTTIN信号。默认动作为暂停进程。
(21) SIGTTOU:该信号类似于SIGTIN,在后台进程要向终端输出数据时产生。默认动作为暂停进程。
(22) SIGURG:套接字(socket)上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达。默认动作为忽略该信号。
(23) SIGXCPU:进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程。
(24) SIGXFSZ:超过文件最大长度的限制。默认动作为yl终止进程并产生core文件。
(25) SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是它只计算该进程占有用的CPU时间。默认动作为终止进程。
(26) SIGPROF:类似于SIGVTALRM,它不仅包括该进程占用的CPU时间还抱括执行系统调用的时间。默认动作为终止进程。
(27) SIGWINCH:窗口大小改变时发出。默认动作为忽略该信号。
(28) SIGIO:此信号向进程指示发出一个异步IO事件。默认动作为忽略。
(29) SIGPWR:关机。默认动作为终止进程。
(30) SIGRTMIN~SIGRTMAX:Linux的实时信号,它没有固定的含义(或者说可以由用户自由使用)。注意,Linux线程机制使用了前3个实时信号。所有的实时信号的默认动作都是终止进程。

3> 查看singal信号四要素

执行命令:man 7 signal
在这里插入图片描述
默认处理动作
Term:终止进程
Core:终止进程,生成Core文件(查验进程死亡原因,用于gdb调试) ulimit -a/ulimit -c 1024
Ing:忽略信号
Stop:暂停信号
Cont:继续运行进程
在这里插入图片描述

9)SIGKILL和19)SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
只要产生信号发送的事件发送,信号一定会产生并且递送,但是由于阻塞信号集的原因,该信号不一定会被递达,信号的产生和处理都是内核做到事情。信号的处理方式是信号递达之后的处理方式

五、进程对信号的响应

当信号发生时,用户可以要求进程以下列3种方式之一对信号做出响应。

1、 捕捉信号

对于要捕捉的信号,可以为其指定信号处理函数,信号发生时该函数自动被调用,在该函数内部实现对该信号的处理。

2、 忽略信号

大多数信号都可使用这种方式进行处理,但是SIGKILL和SIGSTOP这两个信号不能被忽略,同时这两个信号也不能被捕获和阻塞。此外,如果忽略某某些由硬件异常产生的信号(如非法存储访问或除以0),则进程的行为是不可预测的。

3、 按照系统默认方式处理

大部分信号的默认操作是终止进程,且所有的实时信号的默认动作都是终止进程。

六、信号处理函数与相关结构

1、信号安装

(1)、signal()

注册一个信号捕捉函数

void (*signal(int signum, void (*handler))(int)))(int);

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

(2)、sigaction()

函数用于改变进程接收到特定信号后的行为。

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

该函数的第一个参数为 信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。
第二个参数是指 向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;
第三个 参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查 信号的有效性。
示例:

// 自定义信号捕捉函数
void sigShutdownHandler(int sig)
{
switch (sig)
{
case SIGINT:
break;
case SIGTERM:
break;
default:
break;
}
exit(0);
}

int initsignalHandle()
{
struct sigaction act;
signal(SIGHUP,SIG_IGN);
signal(SIGPIPE,SIG_IGN);
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sigShutdownHandler;
sigaction(SIGTERM,&act,NULL);
sigaction(SIGINT,&act,NULL);
return 0;
}

信号的产生和处理都是由内核进行的
频繁的在用户态和内核态进行切换是很浪费时间的
在这里插入图片描述

2、发送信号函数

(1) raise
int raise(int sig); 

对当前进程发送指定信号

(2)pause
 int pause(void); 

将进程挂起等待信号

(3)kill
 int kill(pid_t pid,int sig);

通过进程编号发送信号

(4) alarm
unsigned int alarm(unsigned int seconds); 

指定时间(秒)发送SIGALRM信号。 seconds 为0 时取消所有已设置的alarm请求;

(5) sigqueue

int sigqueue(pid_t pid,int sig,const union sigval val);
类似于kill函数,多了附带共用体 union sigval形数,将共用体中的成员 int sival_int 或 void *sival_ptr 的值传递给 信号处理函数中的定义类型 siginfo_t 中的 int si_int 或 void *si_ptr;

(6) setitimer
int setitimer(int which,const struct itimerval *value,struct itimerval *oldvalue); 

可定时发送信号,根据which可指定三种信号类型:SIGALRM、SIGVTALRM 和 SIGPROF;作用时间也因which值不同而不同;struct itimerval 的成员 it_interval定义间隔时间,it_value 为0时,使计时器失效;

(7) abort
 void abort(void) ;

将造成进程终止;除非捕获SIGABORT信号;

3、信号集及信号集操作

sigfillset(sigset_t *set); 设置所有的信号到set信号集中;
sigemptyset(sigset_t *set); 从set信号集中清空所有信号;
sigaddset(sigset_t *set,int sig);在set信号集中加入sig信号;
sigdelset(sigset_t *set,int sig);在set信号集中删除sig信号;

4、阻塞信号相关函数

int sigprocmask(int how,const sigset_t *set,sigset_t *set);
根据how值,设置阻塞信号集,或释放阻塞的信号集
int sigpending(sigset_t *set); 获取在阻塞中的所有信号;
int sigsuspend(const sigset_t *set); 类似于 pause()函数!

七、解决问题

1> 解决时序问题

可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽字”与“挂起等待信号”这个两个操作间隙失去CPU。除非将这两个步骤合成原子操作。sigsuspend可以实现。
int sigsuspend(const sigset_t *mask) 挂起等待信号
sigsuspend函数调用期间,进程信号屏蔽字由参数mask决定
可将某个信号从临时信号屏蔽集种删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,信号屏蔽字恢复原来的值。

#include <iostream>
using namespace std;
void myheadler(int signo)
{
    cout<<"aa"<<endl;
}
int main(void)
{
    signal(SIGALARM,myheadler);
    sigset_t mask;
    sigfillset(&mask);
    sigdelset(&mask,SIGALARM);
    alarm(1);
    sigsuspend(&mask);
    while(1);
    return 0;
}

时序问题总结
竞态条件,跟系统负载有很紧密的关系,体现除信号的不可靠性。系统负载越严重,信号不可靠性越强。不可靠由其实现原理所致。信号是通过软件方式实现,每次系统调用结束后或中断处理结束后,需通过扫描PCB中的未决信号集来判断是否应处理某个信号。当系统负载过重时,会出现混乱

2>可不可重入函数

一个函数在被调用执行期间(尚未调用结束),由于某时序又被重复调用,称之为"重入"。
1.定义可重入函数,函数内不能包含全局变量集static变量,不能使用malloc、free
2.信号捕捉函数应设计为可重入函数
3.不可重入的原理:使用静态数据结构 调用了malloc和new (不是栈结构) 是标准I/O

3>僵尸进程

当子进程停止或者结束时,会向父进程发送SIGCHLD信号,该信号默认处理动作是忽略,所有产生僵尸进程。
我们之前的程序是,在父进程中调用 wait或者waitpid来回收子进程,此时父进程要么阻塞等待,要么非阻塞而采用轮询的方式。那么将会导致父进程不能做其他工作,只能等着回收子进程。
我们可以利用SIGCHLD信号来捕捉,当有SIGCHLD信号产生时,父进程执行信号捕捉函数。

void sigchldHeadler(int signo)
{
    pid_t pid;
    int status;
    while((pid=waitpid(-1,&status,WNOHANG))>0)
    {
        printf("回收成功:%d   ok\n",pid);
        if(WIFEXITED(status))
        {
            printf("退出状态:%d\n",WEXITSTATUS(status));
        }
        if(WIFSIGNALED(status))
        {
            printf("退出状态(信号):%d\n",WTERMSIG(status));
        }
    }
}
 
int main(void)
{
    //创建10个子进程
    int i;
    for(i=0;i<10;i++)
    {
        pid_t pid = fork();
        if(pid == 0)
        {
            break;
        }
    }
    if(i < 10)
    {
        printf("I am child:%d\n",getpid());
        sleep(1);
    }
    else if(i == 10)
    {      
        struct sigaction act;
        act.sa_handler = sigchldHeadler;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD,&act,NULL);
        while(1)
        {
            printf("I am parent:%d\n",getpid());   
            sleep(1);
        }
    }
    return 0;
}

注:
当父进程在执行信号捕捉函数时,又有子进程死亡。或者有多个子进程死亡。我们知道信号集是位图机制,是不支持排队的。

 while((pid=waitpid(-1,&status,WNOHANG))>0)

当父进程的信号捕捉函数还没有注册,就已经有子进程结束了。导致僵尸进程…

if(i < 10)
    {
        printf("I am child:%d\n",getpid());
        sleep(1);
    }

4> 信号传参

(a) sigqueue

前面讲到信号是不能携带大量数据的,一般通过 kill 来发送信号
但是信号可以携带数据,可以携带少量。
通过sigqueue函数,可在向指定进程发送信号的同时携带参数

int sigqueue(pid_t pid,int sig,const union sigval value);

成功返回0,失败-1,设置error
注意事项:
向指定进程发送指定信号的同时携带数据。不能够传地址,不能进程之间虚拟地址空间各自独立,当前进程地址传递给另一进程没有实际意义
捕捉函数:

(b) sigaction
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
    成功:0    失败:-1,并设置error
    act:新的处理方式
    oldact:传出参数,旧的处理方式
      struct sigaction{
		void  (*sa_handler)(int);
		void  (*sa_sigaction)(int, siginfo_t *, void *);
		sigset_t sa mask;
		int  sa_flags;
		void  (*sa restorer)(void):
	};

不使用sa_handler,而使用sa_sigaction。sa_flags必须指定为SA_SIGINFO

? alarm函数
	说明:设置定时器(闹钟)。在指定多少秒后,内核会给当前进程发送14)SIGALRM信号,进程收到该信号,默认动作终止进程。无论进程处于某种状态都会记时
    原型:
    unsigned int alarm(unsigned int seconds);
    返回0或剩余的秒数

取消定时器:alarm(0)

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
 
int main(void)
{
    int i=0;
    alarm(1);   //定时1秒,14)SIGALRM  终止进程
   
    while(1)
    {
        ++i;
        //cout<<i<<endl;
        printf("%d\n",i);   //printf比cout更快一点点
    }
    return 0;
}
(d)setitimer函数

说明:设置定时器(闹钟)。可替代alarm函数,精度微妙us,可实现周期定时
原型:

int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);

成功返回 0 失败-1,并设置error

#include <iostream>
#include <sys/time.h>
using namespace std;
 
void myalarm(unsigned long sec)
{
    itimerval it,oldit;
 
    it.it_interval.tv_sec = 0;
    it.it_interval.tv_usec = 0;
    it.it_value.tv_sec = 1;
    it.it_value.tv_usec = 0;
    int ret = setitimer(ITIMER_REAL,&it,&oldit);
    if(ret == -1)
    {
        perror("setitimer err");
        exit(-1);
    }
    return;
}
 
int main(void)
{   
    int i;
    myalarm(1);
    while(1)
    {
        printf("%d\n",i);
        ++i;
    }
    return 0;
}
文章来源:https://blog.csdn.net/Bossking321/article/details/135156806
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。