Linux----信号

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux----信号相关的知识,希望对你有一定的参考价值。

信号

1)引入

信号是进程之间事件异步通知的一种方式,属于软中断


kill -l查看所有信号
列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为34 ~ 64的信号是后来扩充的,称做可靠信号(实时信号),实时信号底层也有一个实时信号队列来维护
实时信号请参考Unix/Linux编程:实时信号
运行一个死循环程序
执行命令kil -2 [pid]或kill -SIGINT [pid]等价于crtl+c

解释:
用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程前台进程因为收到信号,进而引起进程退出

信号的产生方式:

  1. kill命令
  2. 硬件产生
  3. 程序异常(core dump)
    –进程角度程序崩溃
    –OS角度进程收到信号
    –… …

信号的识别:

  1. 进程收到信号,并不是马上处理,而是在合适的时候(进程在收到信号的时候可能在处理更重要的事情)

信号的处理:

  1. 默认(部分是终止进程,部分是特定的功能)
  2. 忽略信号
  3. 自定义(捕捉信号)

信号的本质:

  1. 保存在进程PCB,进程控制块的task_struct
  2. 通过一个位图unsigned int signals,比特位的内容(0或1)表示是否有信号,比特位的位置表示谁发送的信号
  3. 发送信号的本质就是写task_struct的位图,OS通过修改对应进程的信号位图发送信号

2)产生信号

①通过终端按键产生信号

CoreDump(核心转储)

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

命令 ulimit -a 显示当前的各种用户进程限制

可以看到core file size是unlimited(系统默认打开了core dump)
如果不生成core文件:
检查core产生路径是否正确 cat /proc/sys/kernel/core_pattern,如果路径不在当前目录下,则设置:echo "./core-%e-%p-%s" > /proc/sys/kernel/core_pattern


测试:

int a=10;
int b=0;
a/=b

生成一个core文件
gdb调试:core-file [core文件]

② 调用系统函数向进程发信号

#include <signal.h>
int raise(int sig);


功能:raise函数可以给当前进程发送指定的信号(自己给自己发信号)


代码如下:

void handler(int signo)

   cout<<"signo is "<<signo<<endl;
   exit(1);

for(int i=0;i<=31;i++)
	signal(i, handler);

sleep(3);
raise(2);

收到2号信号

#include <stdlib.h>
void abort(void);


功能:就像exit函数一样,abort函数总是会成功的,所以没有返回值
6号信号SIGABRT

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);


功能: kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号


实现自己的kill命令(需要用到命令行参数)

int main(int argc, char *argv[])

   if(argc!=3)
           cout<<"wrong usage"<<endl;
   
   kill(atoi(argv[2]), atoi(argv[1]));
   return 0;

③ 由软件条件产生信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);


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


下面是一秒内数数的次数代码,一秒后被SIGALRM信号终止

int count=0;
alarm(1)
for(;1;count++)
	printf("%d ",count);

④ 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号:

  1. 当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程
  2. 当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程
  3. … …

3)阻塞信号

① 信号的保存

引入(了解其他):

  1. 实际执行信号的处理动作称为信号递达(Delivery)
  2. 信号从产生到递达之间的状态,称为信号未决(Pending)
  3. 进程可以选择阻塞 (Block)某个信号
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  5. 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

在内核中的表现形式:

  1. pending位图:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否收到信号。OS发送信号本质是修改task_ struct pending位 图的内容
  2. block位图:比特位的位置代表信号的编号,比特位的内容(0 or 1) 代表是否阻塞该信号
  3. handler数组:用信号的编号,作为数组的索引,找到该信号对应的信号处理方式,然后指向对应的方法(递达)(信号自定义捕捉方法是用户提供的

注意内核首先判断信号屏蔽状态字是否阻塞,如果该信号被设为为了阻塞的,那么信号未决状态字(pending)相应位制成1;若该信号阻塞解除,信号未决状态字(pending)相应位制成0;表示信号此时可以抵达,也就是可以接收该信号

② sigset_t

信号集用来描述信号的集合,每个信号占用一位(64位),每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的,因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略


注意:不同平台,不同版本操作系统,不同位数的操作系统 在内核中的sigset_t实现是不同的,见下图:

有的可能通过一个结构体来存储,有的会用一个unsigned long来存储

③ 信号集操作函数

1.设置即判断信号集函数

在main函数内定义一个sigset_t set,是在栈上开辟空间,这里的栈是用户栈,而我们要设置的是OS的进程属性,同时,底层sigset_t实现可能是不同的,所以系统调用接口不可或缺

#include <signal.h>
int sigemptyset(sigset_t *set);


功能:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号(全置0)
返回值:成功为0,出错-1


set:信号集

#include <signal.h>
int sigfillset(sigset_t *set);


功能:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号(全置1)
返回值:成功为0,出错-1


set:信号集

#include <signal.h>
int sigaddset (sigset_t *set, int signo);


功能:在该信号集中添加某种有效信号(指定位置设置为1)
返回值:成功为0,出错-1


set:信号集
signo:信号

#include <signal.h>
int sigdelset(sigset_t *set, int signo);


功能:在该信号集中删除某种有效信号(指定位置设置为0)
返回值:成功为0,出错-1


set:信号集
signo:信号

#include <signal.h>
int sigismember(const sigset_t *set, int signo);


功能:sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1


set:信号集
signo:信号

2.sigprocmask和sigpending

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);


功能:读取或更改进程的信号屏蔽字(阻塞信号集)
返回值:成功为0,出错-1
注意:

  1. 如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出
  2. 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改
  3. 如果oldset和set都是非空指针,则先将原来的信号 屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字

set:进程当前的信号屏蔽字 (阻塞信号集)
oldset:输出型参数,备份set
how的选项:

  1. SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
  2. SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
  3. SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set

#include <signal.h>
int sigpending(sigset_t *set);


功能:读取当前进程的未决(pending)信号集,通过set参数传出
返回值:调用成功则返回0,出错则返回-1


set:输出型参数

④ 综合代码测试

实现打印阻塞信号集,并在发送2号信号后打印阻塞信号集,再捕捉信号观察恢复的阻塞信号集

  1. 设置进程的pending位图为阻塞2号信号
  2. 尝试发送2号信号,10秒后回复原来的pending,打印
void handler(int signo)

    cout<<"signo is "<<signo<<endl;
    //exit(1);

void showpending(sigset_t *pending)

    for(int i=1;i<32;i++)
        if(sigismember(pending, i))//检查此pending内是否有i号信号
            cout<<1;
        else
            cout<<0;
    
    cout<<endl;


int main(int argc, char *argv[])

	signal(2,handler);//捕捉2号信号后自定义处理
	
	sigset_t in,out;
	//0初始化
	sigemptyset(&in);
	sigemptyset(&out);

	sigaddset(&in, 2);//用户栈设置
	sigprocmask(SIG_SETMASK, &in, &out);//设置OS的pending位图
	
	int count=0;
	sigset_t pending;//用于获取未决状态的信号集
	while(1)
	    sigpending(&pending);//获取未决状态的信号集(信号产生和递达间的状态)
	    showpending(&pending);//打印pending
	
	    sleep(1);
	    count++;
	    if(count==10)
            sigprocmask(SIG_SETMASK, &out, &in);//相当于回复原来的状态
            cout<<"now sigset: ";
             showpending(&in);//打印
            cout<<"recover: ";
            showpending(&out);//打印
	    
	
	return 0;

分析:
开始设置阻塞信号集为0100…000, 打印的是pending信号集
当接受到ctrl+c也就是2号信号的时候,pending信号集会查看block信号集发现有阻塞,然后pendinig信号集被修改为0100…000

10秒后,回复pending信号集000…000

4)信号捕捉

① 内核信号捕捉

注意:

  1. 每个用户进程都有自己的用户级页表,但是OS只有一份,所以我们只需要.维护一份内核级页表
  2. CPU中会存在-一个权限相关的寄存器数据标识所处的状态,判断你使用的是哪个种类的页表
  3. 用户态和核心态的权限级别不同,决定看到的资源是不一样的
  4. 信号捕捉的时候,就必须由内核态切换为用户态操作系统不信任任何用户,当在内核态执行自定义捕捉方式的时候,可能会不安全,所以必须切换到用户态再执行

内核信号捕捉:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号,信号处理函数的代码在用户空间
处理过程:

  1. 用户程序注册了SIGQUIT信号的处理函数sighandler,当前正在执行main函数,这时发生中断或异常切换到内核态
  2. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达
  3. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程
  4. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态
  5. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行

② signal

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


功能:通过handler修改信号的处理方式


signum:信号(比如2:SIGINT)
注意:sighandler_t是一个函数指针类型,所以handler是一个函数指针
在signum-generic.h中有一个SIG_IGN宏定义,可供handler参数使用,表示忽略信号,定义如下(将1强转为sighandler_t类型):

代码测试

void handler(int signo)

	cout<<"signo is "<<signo<<endl;

int main()

	signal(SIGINT, handler);//收到2号信号就打印signo is [signo]
	pid_t pid=getpid();
	while(1)
		cout<<"process"<<pid<< "proceeding"<<endl;
	
	return 0;


使用其他信号终止进程,比如kill -3


注意可以对大部分信号进行捕捉或忽略,但是有少部分信号不能自定义(比如9号信号)
更改signal函数的signum参数为SIGKILL,直接kill进程
11号 segment fault 自行测试

③ sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);


功能: sigaction函数可以读取和修改与指定信号相关联的处理动作

  1. 若act指针非空,则根据act修改该信号的处理动作
  2. 若oldact指针非空,则通过oact传出该信号原来的处理动作,act和oldact指向sigaction结构体

返回值:调用成功则返回0,出错则返回- 1


sa_mask:一个调用信号捕捉函数之前要加到进程信号屏蔽字中的信号集
(如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字)

sa_flag:信号处理选项,一般默认设为0
参考:sigaction函数sa_flags各标志影响的实例讲解


代码如下:

void handler(int signo)

       while(1)
               cout<<"signo is "<<signo<<endl;
               sleep(1);
       //exit(1);


int main(int argc, char *argv[])

       struct sigaction act, oldact;
       act.sa_handler=handler;
       act.sa_flags=0;
       sigemptyset(&act.sa_mask);
       sigaddset(&act.sa_mask,3);
       sigaddset(&act.sa_mask,4);
       sigaction(SIGINT,&act,&oldact);
       while(1)
               cout<<"progress"<<endl;
               sleep(1);
       
       return 0;

2,3,4号信号均不可以将其终止

5)可重入函数

这里是引用

6)从信号角度重新理解关键字volatile

7)SIGCHILD信号

以上是关于Linux----信号的主要内容,如果未能解决你的问题,请参考以下文章

为啥 sin(0.2*x + pi) 是非周期函数? [关闭]

Linux的信号解释

Linux 结束进程

[linux] 详解linux进程信号

[linux] 详解linux进程信号

关于用FFT分析信号频谱的问题