写在前面:

我的Linux的学习之路非常坎坷。第一次学习Linux是在大一下的开学没多久,结果因为不会安装VMware就无疾而终了,可以说是没开始就失败了。第二次学习Linux是在大一下快放暑假(那个时候刚刚过完考试周),我没什么事做就又重拾Linux,不服输的我选择再战Linux,这一次学习还算顺利,虽然中间有些小插曲但是不影响整体学习进度, 我看着B站上的视频一点点学习Linux,基本上把Linux的基础指令学完了。学完之后我又遇到问题了,视频基本上到这就结束了,而我却不知道下一步该学什么,于是就没怎么碰Linux,结果没过多长时间我就把学的Linux指令忘的一干二净。现在是我第三次学习Linux,我决定重新开始学Linux,同时为了让自己学习的效果更好,我选择以写blog的形式逼迫自己每天把学习到的Linux知识整理下来。这也就是我写这个系列blog的原因。


信号的相关的概念

信号的共性

  • 简单
  • 不能携带大量信息
  • 满足条件才能 发送

信号的特质

信号是软件层面的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止,处理信号,处理结束后,再继续执行后续指令。

所有信号的产生及处理全部都是由内核完成的

信号的产生

  1. 按键产生。如CTRL+c,CTRL+z,CTRL+\.
  2. 系统调用产生。如kill,raise,abort
  3. 软件条件产生。如定时器alarm,setitimer,sleep
  4. 硬件异常产生。如非法访问内存(段错误),内存对齐错误(总线错误)
  5. 命令产生。如kill命令

信号的两种状态

  • 未决:产生与递达之间的状态。
  • 递决:产生并且送达到进程,直接被内核处理掉。

信号的处理方式

  • 执行默认动作
  • 忽略
  • 捕捉(自定义)

信号屏蔽字和未决信号集

1

  • 阻塞信号集(信号屏蔽字):本质是位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
  • 未决信号集:本质是位图。用来记录信号的处理状态。该信号集中的信号,表示,已经产生,但尚未被处理。

信号四要素

  • 编号
  • 名称
  • 对应事件
  • 默认处理动作

信号使用之前,应先确定其4要素,而后再用!!!

常规信号

编号名称对应事件默认处理动作
1SIGHUP本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。挂断
2SIGINT在用户键入INTR字符(通常是Ctrl+C)时发出通知前台进程组终止进程
3SIGQUIT由QUIT字符(通常是Ctrl+)来控制进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
4SIGILL通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出也有可能产生这个信号。终止程序
5SIGTRAP由断点指令或其它陷阱(trap)指令产生
6SIGABRT调用abort函数生成的信号。
7SIGBUS访问非法地址终止程序
8SIGFPE在发生致命的算术运算错误时发出终止程序
9SIGKILL发现某个进程终止不了立即结束程序的运行
10SIGUSR1留给用户自定义留给用户自定义
11SIGSEGV试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据终止程序
12SIGUSR2留给用户自定义留给用户自定义
13SIGPIPE通常在进程间通信产生终止程序
14SIGALRM时钟定时信号终止程序
15SIGTERM程序结束(terminate)信号终止程序
16SIGCHLD子进程(child)状态变化时, 父进程会收到这个信号忽略
17SIGCONT让一个停止(stopped)的进程继续执行
18SIGSTOP需要暂停某一程序暂停(stopped)进程的执行,注意它和terminate以及interrupt的区别:该进程还未结束
19SIGTSTP用户键入SUSP字符时(通常是Ctrl+Z)发出这个信号终止程序
20SIGTTIN当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.终止程序
21SIGTTOU类似于SIGTTIN, 但在写终端(或修改终端模式)时收到终止程序
22SIGURG有”紧急”数据或out-of-band数据到达socket时产生.终止程序
23SIGXCPU超过CPU时间资源限制终止程序
24SIGXFSZ当进程企图扩大文件以至于超过文件大小资源限制。终止程序
25SIGVTALRM虚拟时钟信号终止程序
26SIGPROF类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.终止程序
27SIGWINCH窗口大小改变时发出.终止程序
28SIGIO文件描述符准备就绪, 可以开始进行输入/输出操作.终止程序
29SIGPWRPower failure终止程序
30SIGSYS非法的系统调用。终止程序

由于初学,后面的一些信号我不敢保证正确性(后面的都是我查资料cv过来的,等我学明白之后我会来更新这个表格
上面列出的信号中,程序不可捕捉,阻塞,忽略的是SIGKILL,SIGSTOP


kill函数

int kill(pid_t pid, int signum

参数

  • pid:
    • >0,发送信号给指定的进程
    • =0,发送信号给跟调用kill函数的那个进程处于同一进程组的进程。
    • <-1,取绝对值,发送信号给该绝对值所对应的进程组的所有组员。
    • =-1,发送信号给,有权限发送的所有进程。
  • signum,待发送的信号

返回值

  • 成功,0
  • 失败,-1,errno被设置。

举个栗子

我们写一个程序,用子进程调用kill杀死父进程
源代码:

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

int main()
{
	pid_t pid;
	pid=fork();
	if(0==pid)
{
		printf("I am child,i will kill my parent\n");
		sleep(1);
		int res=kill(getppid(),SIGKILL);
		if(res==-1)
			perror("kill error");
	}
	else 
	{
		for(int i=0;;i++)	
			printf("%d-------I am parent, i will be killed by my child\n",i);
	}
	return 0;
}

效果:
1


alarm函数

每个进程都有唯一的闹钟
unsigned int alarm(unsigned int seconds)

参数

  • second,定时的秒数,单位是s

返回值

  • 上次定时剩余的时间

取消闹钟

alarm(0);

举个栗子

我们使用alarm来计时,统计计算机一秒钟能打印多少。
源代码

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

int main()
{
	alarm(1);
	int i;
	for(i=0;;i++)
		printf("%d\n",i);
}

效果:
1


setitimer函数

设置闹钟,可以替代alarm函数,精度微秒us,可以实现周期定时
int setitimer(int which,const struct itimerval * new_value,struct itimerval * old_val)

参数

  • which:选择计时方式
    • ITIMER_REAL:采用自然计时。(一般用这个)——>SIGALRM
    • ITIMER_VIRTUAL:采用用户空间计时——>SIGVTALRM
    • ITIMER_PROF:采用内核+用户空间计时——>SIGPROF
  • new_value:定时秒数
  • old_value:传出参数,上次定时剩余的时间.(如果不关心的话,可以直接传NULL)

返回值

  • 成功,0
  • 失败,-1,errno被设置

struct itimerval类型

struct itimerval
{
	struct timeval 
	{
		time_t tv_sec;			/*second*/
		suseconds_t tv_usec;	/*microsecond*/
	}it_interval;				/*用于设定两个任务之间的间隔时间*/
	struct timerval
	{
		time_t tv_sec;			/*second*/
		susecond_t tv_usec ;	/*microsecond*/
	}it_value;					/*第一次定时秒数*/
}

可以理解有两个定时器

  • 一个用于第一个闹钟什么时候触发
  • 第一个闹钟触发后间隔多少时间再次触发闹钟。

举个栗子

计数

功能和上面的alarm一样
源代码:

#include<unistd.h>
#include<stdio.h>
#include<sys/time.h>

int main()
{
	struct itimerval t;
	t.it_interval.tv_sec=0;
	t.it_interval.tv_usec=0;

	t.it_value.tv_sec=1;
	t.it_value.tv_usec=0;
	int res=setitimer(ITIMER_REAL,&t,NULL);
	for(int i=0;;i++)
		printf("%d\n",i);
	return 0;
}

效果:
1

向屏幕打印信息

源代码:

#include<unistd.h>
#include<stdio.h>
#include<sys/time.h>
#include<signal.h>

void fun(int signal)
{
	printf("hellow linux\n");
}

int main()
{
	signal(SIGALRM,fun);
	struct itimerval t;
	t.it_interval.tv_sec=2;
	t.it_interval.tv_usec=0;

	t.it_value.tv_sec=1;
	t.it_value.tv_usec=0;
	int res=setitimer(ITIMER_REAL,&t,NULL);
	while(1);
	return 0;
}

效果:
1


信号集操作函数

  • 信号集set函数
    • sigset_t set; :自定义信号集(这是一种类型)
    • sigemptyset(sigset_t* set);:清空信号集,全部置为0
    • sigfillset(sigset_t * set);:填满信号集,全部置为1
    • sigaddset(sigset_t* set);:将指定信号添加到信号集中,即将指定信号置为1
    • sigdelset(sigset_t* set);:将指定信号移除信号集,即将指定信号置为0
    • sigismember(const sigset_t* set,int signum);:判断指定信号是否在集合中。在—>1,不在—>0.
  • sigprocmask函数
    int sigprocmask(int how,const sigset_t * set,sigset* oldset);
    • how:
      • SIG_BLOCK:设置阻塞(位与)
      • SIG_UNBLOCK:取消阻塞(取反位与)
      • SIG_SETMASK:用自定义set填充mask (不推荐)
    • set:自定义的set\
    • oldset:旧有的mask(不用的话可以传NULL
  • sigpending函数
    int sigpending(sigset* set)
    读取未决信号集,参数是传出的未决信号集。

1

举个栗子

源代码:

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

void print_set(sigset_t* pset)
{
	for(int i=1;i<33;i++)
		if(sigismember(pset,i))
			printf("1");
		else 
			printf("0");
	printf("\n");
}

int main()
{
	sigset_t set,pset;
	sigemptyset(&set);
	sigaddset(&set,SIGINT);
	sigaddset(&set,SIGQUIT);
	sigaddset(&set,SIGKILL);
	sigaddset(&set,SIGHUP);
	sigprocmask(SIG_BLOCK,&set,NULL);
	while(1)
	{
		sigpending(&pset);
		print_set(&pset);
		sleep(1);
	}
	return 0;
}

效果
2


signal实现信号捕捉

注册一个信号捕捉函数,ANS设置,不同操作系统存在差异建议使用sigaction函数
sighandler_t signal(int signum,sighandler_t handler)

参数:

  • signum :捕捉信号
  • handler:捕捉信号后的操作函数

返回值

  • 成功,返回操作函数的返回值
  • 失败,返回SIG_ERR,errno被设置。

举个栗子

源代码:

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

void fun(int signal)
{
	printf("hellow linux,SIGQUIT has been not work\n");
}

int main()
{
	signal(SIGQUIT,fun);
	while(1);
}

效果:
1


sigaction实现信号捕捉

注册一个信号捕捉函数
int sigaction(int signum,const struct sigaction * act,struct sigaction * oldact)

struct sigaction类型

struct sigaction {
	void     (*sa_handler)(int);		//操作函数名
	void     (*sa_sigaction)(int, siginfo_t *, void *);		//一般不用管
	sigset_t   sa_mask;		//只作用于函数捕捉期间
	int        sa_flags;	//一般置为0,表示用默认的
	void     (*sa_restorer)(void);
};

举个栗子

源代码:

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

void fun(int signal)
{
	printf("catch you SIGQUIT\n");
}

int main()
{
	struct sigaction act;
	act.sa_handler=fun;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	sigaction(SIGQUIT,&act,NULL);
	while(1);
}

效果:
1


信号捕捉

信号捕捉的特性

  • 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask
  • 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs = 0).其他信号不屏蔽,如需屏蔽则调用sigsetadd函数修改
  • 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!

内核实现信号捕捉简析

3


综合练习

我们借助信号捕捉回收子进程
源代码:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>

void fun(int signal)
{
	while(waitpid(-1,NULL,0)!=-1)
		printf("wait successfully\n");

}

int main()
{
	int i;
	pid_t pid;
 	sigset_t set;
	sigemptyset(&set);
	sigaddset(&set,SIGCHLD);
	sigprocmask(SIG_BLOCK,&set,NULL);
	for(i=0;i<15;i++)
	{
		pid=fork();
		if(0==pid)
			break;
	}
	if(15==i)
	{
		struct sigaction act;
		act.sa_handler=fun;
		sigemptyset(&act.sa_mask);
//		sigdelset(&set,SIGCHLD);
		sigprocmask(SIG_UNBLOCK,&set,NULL);
		act.sa_flags=0;
		sigaction(SIGCHLD,&act,NULL);
		printf("i am parent,i wait my all child\n");
		while(1);
	}
	else
	{
		printf("i am %dth child,i will be waited\n",i+1);
	}
	return 0;
}

效果:
4


写在最后

个人亲身经验:我们学习的一系列Linux命令,一定要自己亲手去敲。不要只是看别人敲代码,不要只是停留在眼睛看,脑袋以为自己懂了,等你实际上手去敲会发现许许多多的这样那样的问题。正可谓“键盘敲烂,月薪过万


如果你觉得我写的题解还不错的,请各位王子公主移步到我的其他题解看看

  1. 数据结构与算法部分(还在更新中):
  1. Linux部分(还在更新中):

✨🎉总结

“种一颗树最好的是十年前,其次就是现在”
所以,
“让我们一起努力吧,去奔赴更高更远的山海”
在这里插入图片描述
如果有错误❌,欢迎指正哟😋

🎉如果觉得收获满满,可以动动小手,点点赞👍,支持一下哟🎉

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐