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)可重入函数

一个函数可以被多个执行流同时进行访问,这种情况叫做重入

在这里插入图片描述
如上图重入后发生问题的称为不可重入函数


如果一个函数符合以下条件之一则是不可重入的:

  1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的
  2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

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

在gcc早期系列中,编译器会自动优化,例如以下代码

int flag=0;
void handler(int signo)
{
       flag=1;
       cout<<"flag is changed to 1"<<endl;
}
int main(int argc, char *argv[])
{
       signal(2, handler);
       while(!flag)
       {
               sleep(1);
               cout<<"flag is not changed still 0"<<endl;
       }
       return 0;

}

程序本来会接收到2号信号直接退出,但是由于main函数while(!flag)上下文没有与flag相关的操作,编译器会将这里的flag优化 为寄存器变量,之后判断的时候都是去ebx寄存器去取值,即一直死循环
gcc中-O选项代表优化级别,-O0优化级别最低,其次-O1 -O2...,将makefile中的编译代码加上-O0选项程序才会正常结束
$(CXX) -o $@ $^ -O0 $(LDFLAGS)


如果不想更改编译优化级别可以使用volatile
volatile关键字:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
volatile int flag=0;

7)SIGCHILD信号

父进程调用sigaction/signal将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用

void handler(int signo)
{
   cout << "fahter process ... " << getpid() << " " << getppid() << " signo: "<< signo << endl;
}
int main()
{
   signal(SIGCHLD, SIG_IGN);
   //signal(SIGCHLD, handler);
   if(fork() == 0){
       //child
       int count = 7;
       while(count){
           cout << "child process ... " << getpid() << " " << getppid() << " count: "<< count << endl;
           count--;
           sleep(1);
       }
       cout << "child quit ...!" <<endl;
       exit(0);
   }
   int ret = sleep(10);

   cout << "ret: " << ret << endl;
   //>0,parent
}

是否等待子进程:

  1. 需要子进程的退出码:wait->status
  2. 不需要,调用signal(SIGCHLD, SIG_IGN)忽略
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值