一.知识点梳理
1. 信号概念及特点
1.1 信号的概念
- 信号首先是一种软件中断
- 当做进程之间的通信的一种机制
比如在编译一段程序时遇到段错误,就会通知控制台进行中断,这里是进程间的一种通信
1.2 信号的特点
- 信号是异步的
进程不知道信号是什么时候到达
- 进程既可以处理信号,也可以发指定的信号给进程
- 每个信号都有一个名字,都以SIG开头
1.3 信号的来源
- 硬件异常产生信号
除数为0、无效的内存访问等等
- 软件产生异常信号
可以用kil、raise、alarm、setitimer产生信号
1.4 信号的分类
- 不可靠信号
信号值小于SIGRTMIN(34之前)
每次信号处理后,该信号对应的处理函数会恢复到默认值
信号可能丢失
- 可靠信号
大于SIGRTMIN(34之后)的信号均为可靠信号
- 非实时信号
前32种为非实时信号,不支持排队
- 实时信号
支持排队,是实时信号
1.5 信号的产生
当引发信号的事件发生时,为进程产生一个信号,信号产生时,内核会在进程表中设置一位标识
1.6 信号的递送
当进程对信号采取动作时称为递送,信号产生和递送之间的时间间隔内次你好是未决的
1.7 信号递送阻塞
进程可指定对某个信号采用递送阻塞,如果此信号的处理是系统默认动作或者捕捉该信号,则该信号就会处于未决的状态,直到进程接触对该信号的递送阻塞或者处理方式改为忽略
如果信号的处理方式是忽略该信号,那么该信号永远不会处于递送挥着递送未决状态
2. Linux系统信号
2.1 Linux系统信号的介绍【常用】
信号名称 | 信号说明 | 默认处理 |
---|---|---|
SIGABRT | 程序调用abort时产生该信号,程序异常结束 | 进程终止并产生core文件 |
SIGALEM | timer到期,有alarm(8)或者setimer | 进程终止 |
SIGBUS | 总线错误,地址没对齐等,取决于具体硬件 | 进程终止并生成core文件 |
SIGCHLD | 子进程停止或者终止时,父进程会受到该信号 | 忽略该信号 |
SIGCONT | 让停止的程序继续运行 | 继续执行或者忽略 |
SIGFPE | 算数运算异常,除0等 | 进程终止并产生core文件 |
SIGAHUP | 终端关闭时产生该信号 | 进程终止 |
SIGILL | 代码中有非法指令 | 进程终止并产生core文件 |
SIGINT | 终端输入了中断字符Ctrl+c | 进程终止 |
SIGIO | 异步IO,与SIGOLL一样 | 进程终止 |
SIGIOT | 执行IO时产生硬件错误 | 进程终止并产生core文件 |
SIGKILL | 这个信号用户不能去捕捉它 | 进程终止 |
SIGPIPE | 往管道写时,读者已经不在了,或者往一个已断开数据流socket写数据,发送的进程会收到 | 进程终止 |
SIGPOLL | 异步IO,与SIGIO一样 | 进程终止 |
SIGPROF | 有steitimer设置的timer到期引发 | 进程终止 |
SIGPWR | Ups电源切换时 | 进程终止 |
SIGQUIT | Ctrl+\,不同于SIGINT,这个会产生core dump文件 | 进程终止并生成core文件 |
SIGSEGV | 内存非法访问,默认打出segment fault(段错误) | 进程终止并生成core文件 |
SIGSTOP | 让某个进程停止执行,该信号不能被用户捕捉 | 进程暂停执行 |
2.2 core文件
- core文件介绍
程序崩溃时留下的记录文件,仅仅是一个内存映像,主要用来调试
- 开启或关闭core文件的生成
- 1.关闭core文件的生成
ulimit -c 0
- 2.检查core文件是否打开
ulimit -a
- 3.用户指定生成core文件(生成一个小于1024kb的core文件)
ulimit -c 1024
- 4.使用core文件,在core所在目录通过gdb查看
gdb -c core
2.3 信号的处理
- 忽略此信号:
大多数信号都可以采取忽略的方式,但是SIGKILL,SIGSTOP不能忽略:防止中毒不能关闭提供了可靠的关闭信号
- 捕捉信号
调用一个用户函数,提供用户先要对该事件的处理方式。
- 执行系统默认动作
对大多数信号的默认动作是终止该进程
3. 信号相关的函数
3.1 信号注册函数signal
- 函数功能
用来通知内核如何处理某个特定的信号(忽略。捕捉、默认处理)
- 函数原型
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
void (*signal)(int signo, void (*func)(int)))(int);
- 参数
signum:信号名称, 可以通过kill -l查看
handler:信号处理方式
SIG_IGN :忽略此信号
SIG_DEL:系统默认方式处理该信号
用户自定义函数地址
- 返回值
注册成功:返回前一次注册的信号处理函数
注册失败:返回SIG_ERR
3.2 pause函数
- 函数功能
会是调用进程进入睡眠状态,直到有信号递送,如果你对某个信号采取了SIG_IGN,pause不会被唤醒
#include <unistd.h>
int pause(void);
- 返回值
pause返回值-1, erron设置为EINTR
3.3 信号发送函数kill和raise
- 函数功能
可以向某个进程或进程组发送特定的信号
- 函数原型
#include <>sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
inr raise(int sig);
- 函数参数
pid
pid > 0 将信号发送给进程ID为pid的进程;
pid == 0 将信号发送给起进程组ID等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程;
pid < 0 将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送信号的所有进程。
pid == -1 将信号发送给除进程0以外的所有进程,但发送进程必须有许可权。
- 函数返回值
成功返回0, 失败返回-1
- raise函数相当于kill(getpid(), signum);
3.4 信号屏蔽字函数sigprocmask
- 函数功能
每个进程都有一个信号屏蔽字,它规定了哪些信号可以递送,哪些信号需要阻塞
当程序执行敏感任务时,不希望其他程序来打断,这个时候不能简单地忽略这些信号,而是阻塞这些信号,当进程处理完这些关键任务后,在处理这些信号
- 函数原型
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 函数参数
how有三个值:
SIG_BLOCK:被阻塞的信号是当前屏蔽字加上第二个参数包含的信号
SIG_UNBLOCK:第二个参数包含的信号从当前屏蔽字移除
SIG_SETMASK:当前屏蔽字完全设置为第二个参数的值
4. 信号集
4.1 信号集概念
Linux系统定义了64个信号,以后也可能进一步扩展,所以用类型sigset_t来表示所有的信号
sigset_t中的一个比特位表示一个信号
对信号集的操作需要专用的操作函数
信号0是系统保留,不被使用
4.2 信号集操作函数
- 头文件
#include <signal.h>
- 把所有信号集的比特位置为0
int signemptyset(sigset_t *set);
- 把所有信号集的比特位置为1
int signfillset(sigset_t *set);
- 把信号signum加入信号集set中
int sigaddset(sigset_t *set, int signum);
sigaddset(&set, SIGINT);
- 把信号signum从信号中清除
int signdelset(sigset_t *set, int signum);
- 判断某个信号是否在信号集set中
int sigismember(const sigset_t *set, int signum);
4.3 sigaction函数
- 函数功能
注册函数也是通过sigcation
- 函数原型
#include <int signum, const struct sigaction *act, struct sigaction *oldact>
- 函数参数
signum:指定需要处理的信号
act:设定信号处理的方式,可为NULL
oldact:之前设定的信号处理方式会保存到第三个参数,为NULL时不保存 - sigaction结构体说明
struct sigaction
{
void (*sa_handler)(int); //不可靠信号的处理方式
void (*sa_sigaction)(int, siginfo_t *, void *) //可靠信号处理方式
sigset_t sa_mask; //处理信号的过程中需要屏蔽的信号
int sa_flags;
};
- 函数返回值
成功返回, 失败返回-1
5. 信号的继承
- 子进程会继承父进程处理信号的方式,直到子进程调用exec函数
- 子进程调用exec函数后,exec将父进程中设置为捕捉的信号变为默认处理方式,其余不变
二. 重难点总结
1. 如何验证子进程能继承父进程的信号处理方式?在子进程里设置子进程处理方式与父进程不同,此时观察父子进程收到同一信号时,处理方式是否不同?验证子进程在调用exec函数后,父进程里设置为捕捉的信号变为默认处理方式,其余不变?
- 1.验证子进程能继承父进程对信号的处理方式
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <signal.h>
void RecvSig(int sig)
{
printf("The sinal is receved!\n");
sleep(3);
}
int main(int argc, char const *argv[])
{
signal(SIGINT, RecvSig);
pid_t pid = fork();
if (fork < 0)
{
printf("fork failed\n");
exit(0);
}
if (pid == 0)
{
printf("This is son:\n");
printf("阻塞中,等待接收信号:\n");
getchar();
}
printf("This is parent:\n");
printf("阻塞中,等待接收信号:\n");
getchar();
return 0;
}
- 由结果可知,子进程能继承杜金城处理信号的方式
- 2.子进程里设置子进程处理方式与父进程不同,此时观察父子进程收到同一信号时,处理方式是否不同
- 由结果可知,处理方式不同
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <signal.h>
void RecvSig(int sig)
{
printf("The sinal is receved!\n");
sleep(3);
}
void RecvSig_son(int sig)
{
printf("The sinal is receved_son!\n");
sleep(3);
}
int main(int argc, char const *argv[])
{
signal(SIGINT, RecvSig);
pid_t pid = fork();
if (fork < 0)
{
printf("fork failed\n");
exit(0);
}
if (pid == 0)
{
signal(SIGINT, RecvSig_son);
printf("This is son:\n");
printf("阻塞中,等待接收信号:\n");
getchar();
}
printf("This is parent:\n");
printf("阻塞中,等待接收信号:\n");
getchar();
return 0;
}
- 3.子进程在调用exec函数后,父进程里设置为捕捉的信号变为默认处理方式
由于exec调用的是其它内容,所以捕捉的信号只能变为默认处理方式,无法按照用户自定义的输出进行信号捕捉;
2. 当在执行非常敏感的任务时,不希望其它程序来中断当前的程序,应该怎么做?
可以使用信号屏蔽字来实现
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<unistd.h>
int main(void)
{
sigset_t t;
sigfillset(&t);
sigprocmask(SIG_SETMASK, &t, NULL);
printf("正在屏蔽中...........\n");
getchar();
printf("解除屏蔽.............\n");
sigemptyset(&t);
sigprocmask(SIG_SETMASK, &t, NULL);
while(1);
return 0;
}
- 当此时接收到信号时:
- 当解除屏蔽后,收到的信号才会被处理
三. 自我总结
学习完信号之后,我对Linux之前的一些疑惑也明白了,就拿之前在想系统是怎么直到我在写一个程序时发生了段错误并且还行提醒我,现在明白了就是在程序进行非法访问的时候,系统的内核会发送一个指令给到进程,并且做出相应的处理。这节课的难度相对较小,但是需要将一些常用的信号记住,还有信号集、信号注册函数等熟练掌握。