进程信号

注意:信号不是信号量

用户输入命令,在shell(bash)下启动一个前台程序。
用户按下“Ctrl+c”,这时 键盘输入 产生一个硬件中断,被操作系统获取,并解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
注意:
1、“Ctrl+c”产生的信号只能发给前台进程,一个命令后面加个&就可以放到后台运行(fg:把后台运行的进程放到前台来执行),这样bash不必等待进程结束就可以接受新的命令,启动新的进程了。
2、bash可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像“Ctrl+c”这种控制键产生的信号
3、前台进程在运行过程中用户随时可能按下“Ctrl+c”而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的

信号:针对于进程。信号是一个软件中断(软中断),用于通知一个事件的发生。信号会打断当前的操作去处理该事件,但是还得有一个前提,就是当前进程必须得识别这个信号。

信号种类很多,每个都代表不同事件。
kill -l:信号共有62个,分两大类型,1—31借鉴于UNIX操作系统,其中10和12表示用户自定义的事件,给用户自己来使用的。而且是非可靠/非实时信号,也就是信号有可能会丢失。随着操作系统和网络的发展,Linux发现用户自定义的信号不够用。34—64,可靠/实时信号(肚子叫两次,那就吃两顿饭)。由于这些信号并没有对应一个特殊的事件,所以命名很随意。
信号是有生命周期的,信号的产生 》 注册在进程pcb中 》 在进程中注销 》 信号的处理
阻塞:阻止信号被处理

SIGFPE:两数相除,分母为0。
SIGPIPE:管道的情况下,无论是匿名还是命名管道,把所有写/读的文件描述符关闭,注意是所有,你再尝试读/写(这个操作没意义,操作系统会直接认为这是一个不合理的操作,则write会触发异常(SIGPIPE信号),进程退出),肯定读不到东西,但此时read不会阻塞,这时候读会返回EOF,也就是read的返回值为0。
SIGSEGV:发生了内存访问错误,段错误。

#include<stdio.h>
#include<string.h>

int main()
{
	char *ptr = NULL;
	memcpy(ptr, “nihao”, 5);// 段错误
	return 0;
}//访问内存不合法是操作系统检测出来的,然后通过SIGSEGV信号通知进程,进程收到这个信号了解到是发生访问内存错误,然后自行退出。

信号的产生:

1、由硬件产生,那硬件是如何产生一个软件信号?例如,Ctrl+c产生SIGINT中断信号。键盘产生一个电信号交给cpu,cpu检测到电信号,然后传递给操作系统,操作系统定义好当用户按下Ctrl+c就表示用户想结束一个前台进程,就会将电信号解释成一个软件信号然后发送给进程。
bash进程是在前台的,此时当你把一个进程运行起来时,bash就会运行到后台,此时的前台程序就是你刚刚运行的。当程序退出后,bash又回到前台运行,当然调度过程都是操作系统完成的。我们在键盘的输入,只有前台进程能收到。
Ctrl+\:SIGQUIT退出信号

SIGINT和SIGQUIT两者之间的区别

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
Core Dump
首先解释什么是Core Dump?当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的, 因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。
首先用ulimit命令改变bash进程的Resource Limit,允许core文件最大为1024K,命令ulimit -c 1024
在这里插入图片描述
然后写一个死循环程序在这里插入图片描述
前台运行这个程序,然后在终端输入Ctrl-c(貌似不行)或Ctrl-\(介个阔以)
在这里插入图片描述
ulimit命令改变了bash进程的Resource Limit,./a.out进程的task_struct由bash进程复制而来,所以也具有和bash进程相同的Resource Limit值,这样就可以产生Core Dump了。
}
Ctrl+z:vim Ctrl+z-> SIGTSTP停止信号后直接退出(通过fg命令将在后台运行的进程放到前台来继续执行),vim非正常退出,这个交换文件(vim在打开之后会产生一个临时的交换文件)就不会被删除。

2、由软件产生:kill +pid,默认kill是SIGTERM终止信号
kill -9 +pid:SIGKIL强杀信号,其实是进程接收到信号后自行退出的,而不是说被杀死的。
kill系统调用
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);//向指定进程发送指定信号,kill命令就是用这个接口实现的。
在程序中使用kill(getpid(), SIGKILL); 运行到kill函数的时候给自己发送一个信号,还没等调用返回呢,就被打断当前操作去处理信号。

raise库函数,发送一个信号给调用者。
#include <signal.h>
int raise(int sig);//raise(SIGTERM);
abort库函数,使进程终止。直接给自己发送SIGABRT信号。
#include <stdlib.h>
void abort(void);
ulimit -a //core dump默认是关闭的,有一个安全隐患,产生的core文件比较多的话会占满磁盘。
ulimit -c 1024 //1024指1024k,再运行此函数,生成一个core文件。直接看是不懂的。
gdb 可执行文件名(有没有./无所谓) +core文件名
bt:查看函数调用栈。main函数调用abort函数,abort里面调用raise函数。

alarm系统调用,n秒之后给自己发送SIGALRM信号。该信号的默认处理动作是终止当前进程。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm(3);
alarm(0);//取消上一个定时器,返回上一个闹钟的剩余时间。
//定时器只有一个,你设置第二个,就会取消上一个,前提是上一个还没有超时。而且后面设置的时间会覆盖之前的定时时间。

3、硬件异常产生信号
硬件异常被硬件以某种方式检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。
再比如当前进程访问了非法内存地址,MMU(MMU,该硬件设备专门负责地址映射的操作)会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

信号捕捉初识

#include<stdio.h>
#include<signal>

void handler(int sig) 
{ 
	printf("catch a sig : %d\n", sig); 
} 
int main() 
{ 
	signal(2, handler);//前文提到过,信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的,提前了解一下。 
	while(1); 
	return 0; 
}

在这里插入图片描述

信号在进程中的注册

在进程pcb中做标记,标记进程收到了哪些信号。

//task_struct中有一个struct sigpending peding;//还没处理的信号
struct sigpending
{
	struct list_head list;//双向链表
	sigset_t signal;//在此做标记,位图,64位。sigset_t称为信号集。
};
struct sigqueue//链表
{
	struct list_head list;
	int flags;
	siginfo_t info;//标记是几号信号
	struct user_struct *user;
};

非可靠信号注册:判断pcb中的peding中位图相应信号是否已经注册(位图相应位置是否已经置1);若未注册,则修改为1,向sigqueue链表中添加一个信号结点;若已经注册,则不做任何操作(表示事件丢失)。
可靠信号注册:不管信号已经注册,都会向链表中添加一个信的信号结点(事件不会丢失)。

信号的注销

非可靠信号:结点只有一个,删除结点,位图相应位置0。
可靠信号:结点有可能有多个,注销就是删除一个结点,判断链表中是否还有相同信号结点,若没有位图置零,否则位图依然是1.。

信号的处理

信号到来之后不是被立即处理的,而是选择一个合适的时机来处理(这个时机就是进程从内核态返回用户态的时候)。
进程如何从用户态切换到内核态?发起系统调用、程序异常、中断(如Ctrl+c、Ctrl+\、SIGALRM信号)。
信号处理有3种方式:
默认处理方式:即定义好的处理方式
忽略处理方式:依然会处理,只不过什么都没做。
自定义处理方式:用户自己确定信号如何处理。信号就是为了通知一个事件的发生,要求进程去处理这个事件,处理事件就是完成某个功能,在C语言中完成功能的最小模块就是函数,我们信号的处理方式说白了就是一个处理函数,用户自定义处理方式就是修改这个信号的默认处理函数。
如何修改信号处理方式?

1、signal系统调用

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

参时1:要修改的信号编号,替换这个信号的处理函数。
参数2:用户传入的处理函数,函数指针typedef void (*sighandler_t)(int);
SIG_DFL:信号的默认处理动作,每个信号的默认处理动作在系统内核中都是保存起来的,如果修改之后,下次想使用默认处理方式,得先还原回去。
SIG_IGN:信号的忽略处理动作
返回值是信号的原有动作,我们可以保存下来,以便还原回去。

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

int main()
{
	signal(SIGINT, SIG_IGN);//修改2号中断信号

	while(1)
	{
		printf(“xiuxihui\n”);
		sleep(10);
	}
	return 0;
}
//运行
//按Crtl+c依旧运行,并没有中断进程。

2、sigaction()系统调用

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

参数1:
参数2:用户自己定义的新的处理结构替换信号原有的结构
参数3:信号原有的结构被保存到参数3中

struct sigaction
{
	void (*sa_handler)(int);//如果没什么特殊需求,选这个就行了,这个函数就是通知有什么信号到来了。
										//如果我们在给一个进程发送信号的时候还额外带有一些信息的话,就必须得用第二个。
	void (*sa_sigaction)(int, siginfo_t *, void *);//选择其中一个处理函数,因为它们俩是一个联合体。
	sigset_t sa_mask;//当我们在处理这个信号的时候,不希望受到其他信号的影响,可以临时阻塞一些信号。这是一个位图。
	int sa_flags;//通常给0,表示使用void (*sa_handler)(int)这个函数,如果是SA_SIGINFO,将使用第二个函数。
	void (*sa_restorer)(void);
};

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

int main()
{
	struct sigaction new_act, old_act;
	new_act.sa_flags = 0;
	new_act.sa_handler = SIG_IGN;
	//sigmptyset()清空信号集合,也就是清空位图,因为你并不知道这块内存有什么。
	sigmptyset(&new_act.sa_mask);
	sigaction(2, &new_act, &old_act);

	while(1)
	{
		printf(“xiuxihui\n”);
		sleep(10);
	}

	return 0;
}
//运行
//按Crtl+c依旧运行,并没有中断进程
//按Ctrl+\就退出了。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void sigcb(int signo)//回调函数,但不是被main函数调用,而是被系统所调用。
                               //sigcb和main函数使用不同的栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
{
	printf("recv a signo: %d\n", signo);
}

int main()
{
	struct sigaction new_act, old_act;
	new_act.sa_flags = 0;
	new_act.sa_handler = sigcb;
	//sigemptyset()清空信号集合,也就是清空位图。
	//位图所有位清零,表示该信号集合不包含任何有效信号。
	//而sigfillset()恰好与sigemptyset()作用相反,将位图所有位值1。
	sigemptyset(&new_act.sa_mask);
	sigaction(2, &new_act, &old_act);

	while(1)
	{
		printf(“xiuxihui\n”);
		sleep(10);
	}

	return 0;
}
//运行
//按Crtl+c依旧运行,只不过多打印一条语句,并没有中断进程。
//休眠10秒才打印xiuxihui\n,可是Crtl+c就会立即打印xiuxihui\n?
//在休眠的过程中,信号的到来会打断休眠操作,处理信号后回到主流程,休眠已经被打断,然后继续执行printf(“xiuxihui\n”); 信号会打断当前的阻塞操作。

sigcb不是被main函数调用,而是被系统所调用。
sigcb和main函数使用不同的栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
信号会打断当前的阻塞操作。

自定义处理方式信号的处理流程

用户态
系统调用/程序异常/中断,从用户态进入到内核态。以系统调用进入到内核态为例。
—————————————————————————————————————————————————————————————
内核态
系统调用完成之后,此时并不是直接返回用户态接着执行,而是在返回之前先判断一下是否有信号待处理。调用do_signal系统调用完成信号处理,这里面就有判断信号,就是判断位图相应位是否0。如果有,并且处理方式是默认/忽略,这两种都是内核定义好的,此时就在内核里直接完成,完成之后返回用户态。
但是对于用户自定义的,在判断是否有信号待处理的时候,如果发现这个信号的处理方式是自定义的,那么先切换到用户态完成相应的事件处理,在这个信号处理函数完成之后呢,它会调用接口sigreturn再次返回到内核态,再看有没有信号待处理,等到没有信号要处理了,调用sig_return接口返回用户态接着执行。

信号的阻塞

含义:阻止信号被递达,就是阻止信号被处理。信号依然可以注册,只是暂时不处理。
在pcb中还有一个阻塞信号集合,标记哪些信号暂时不被处理。
信号到来的时候修改pending(未决)位图,处理信号之前先后看block(阻塞)位图有没有,如果有的话就不处理,没有的话就处理。
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

#include<signal>
int sigemptyset(sigset_t *set); 
int sigfillset(sigset_t *set); 
int sigaddset (sigset_t *set, int signum); 
int sigdelset(sigset_t *set, int signum); 
//这四个函数都是成功返回0,出错返回-1(没有设置errno)。
注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
int sigismember(const sigset_t *set, int signum);
//sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1(也没有设置对应的errno)。 

sigpending

#include<signal.h>
int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。成功则返回0,失败返回-1(no errno)。

在这里插入图片描述

sigprocmask

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

//返回值:若成功则为0,若出错则为-1,并设置errno。
//参数1:你到底要对这个阻塞进行什么样的操作
SIG_BLOCK:向阻塞集合中添加参数2定义好的信号,此时阻塞集合=原有的阻塞集合+参数2定义好的 block = mask | set
SIG_UNBLOCK:从阻塞集合中移除参数2定义好的信号,此时阻塞集合=原有的阻塞集合-参数2定义好的 block = mask &(~set)
SIG_SETMASK:将参数2定义好的信号设置为阻塞集合,block = set
//参数3:用于保存原有的信号
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

#include<stdio.h>
#include<signal.h>

void sigcb(int signo)
{
	printf(“recv signo : %d\n”, signo);
}

int main()
{
	signal(SIGINT, sigcb);
	//34-64都是可靠信号,前3个不推荐使用,怕有预留。
	signal(40, sigcb);
	sigset_t set, oldset;
	sigemptyset(&set);//清空集合
	sigfillset(&set);//将所有信号都填充到set中
	sigprocmask(SIG_BLOCK, &set, &oldset);//所有信号都被阻塞了

	getchar();//就是为了获取键盘上的回车,否则程序一直阻塞在此处。

	sigprocmask(SIG_UNBLOCK, &set, NULL);
	//或者sigprocmask(SIG_BLOCK, &oldset, NULL);
	//解除阻塞

	return 0;
}
//运行
//Ctrl+cCtrl+cCtrl+cCtrl+cCtrl+c没反应
//新窗口下
//kill -40 +pid
// kill -40 +pid
// kill -40 +pid
// kill -40 +pid
// kill -40 +pid
//回到原窗口,按下Enter,getchar得到回车符
//输出结果
// recv signo:40
// recv signo:40
// recv signo:40
// recv signo:40
// recv signo:40
// recv signo:2
//可靠信号与非可靠信号的区别
//重新运行
//Ctrl+\,Ctrl+z也不行,原来也都不需要按下Enter的。
//再开窗口,kill -10 +pid还是不行。
//但是kill -9/-19 +pid是可以的
//9号强杀信号和19号停止信号是不可以被阻塞的
//20号信号也是停止信号,但是是键盘停止信号,也就是Ctrl+z。它是可以被阻塞的。
//在所有信号中,9号信号SIGKILL和19号信号SIGSTOP,无法被阻塞,无法被自定义,无法被忽略。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

int main()
{
	signal(SIGINT, SIG_IGN);
	while(1)
	{
		printf(“------\n”);
		usleep(1000);
	}
	
	return 0;
}
//运行之后
//Ctrl+c不行,但还有Ctrl+\可以退出。
//但是./main &让这个进程在后台运行
// Ctrl+c不行,Ctrl+\也不行。
//在前台按的Ctrl+\在后台根本不生效
//如果你阻塞了所有信号,那这个进程就退不出去了。
//如果没有kill信号和stop信号,只能重启。 
//kill -9杀不死僵尸进程,因为僵尸进程已经死了。
//kill -19 僵尸进程的父进程,没作用。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值