信号概述:
信号是软件中断.提供了一种处理异步通信(随时接受发)的方式.
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件
每个信号都有一个名字,这些名字都以3个字符SIG开头,
例如:当进程调用abort函数时回产生SIGARBT信号 alarm函数会产生SIGALRM信号
在头文件<signal.h>中,信号名被定义为正整数常量(信号编号) 可以通过kill -l查看
不存在0号信号 其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。
很多条件下产生信号
- 当用户按某些终端键时,引发终端产生的信号 (按ctrl+c会产生中断SIGINT信号)
- 硬件异常产生信号:除数为0,无效的内存引用,有硬件检测并通知内核,并产生信号(对执行一个无效内存引用会产生SIGSEGV信号)
- 软件中断(SIGALRM,SIGPIPE)
产生信号的事件对进程而言是随机出现的.进程不能简单测试一个变量来判断是否发生一个信号,而是必须要告诉内核"在此信号发生,请执行以下操作"
信号的处理方式
- 忽略此信号(SIGKILL和SIGSTOP除外 原因:他们向内核和超级用户提供了使进程终止或停止的可靠方法)
- 捕捉信号:通知内核在某种信号发生时,调用一个用户函数(可执行用户希望对这种事件进行的处理)
- 执行默认动作
编号 | 信号 | 对应事件 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 用户退出shell时,由该shell启动的所有进程将收到这个信号 | 终止进程TREM |
2 | SIGINT | 当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 | 终止进程 |
3 | SIGQUIT | 用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号 | 终止进程 |
4 | SIGILL | CPU检测到某进程执行了非法指令 | 终止进程并产生core文件 |
5 | SIGTRAP | 该信号由断点指令或其他 trap指令产生 | 终止进程并产生core文件 |
6 | SIGABRT | 调用abort函数时产生该信号 | 终止进程并产生core文件 |
7 | SIGBUS | 非法访问内存地址,包括内存对齐出错 | 终止进程并产生core文件 |
8 | SIGFPE | 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 | 终止进程并产生core文件 |
9 | SIGKILL | 无条件终止进程。本信号不能被忽略,处理和阻塞 | 终止进程,可以杀死任何进程 |
10 | SIGUSE1 | 用户定义的信号。即程序员可以在程序中定义并使用该信号 | 终止进程 |
11 | SIGSEGV | 指示进程进行了无效内存访问(段错误) | 终止进程并产生core文件 core |
12 | SIGUSR2 | 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 | 终止进程 |
13 | SIGPIPE | Broken pipe向一个没有读端的管道写数据 | 终止进程 |
14 | SIGALRM | 定时器超时,超时的时间 由系统调用alarm设置 | 终止进程 |
15 | SIGTERM | 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 | 终止进程 |
16 | SIGSTKFLT | Linux早期版本出现的信号,现仍保留向后兼容 | 终止进程 |
17 | SIGCHLD | 子进程结束时,父进程会收到这个信号 | 忽略这个信号lgn |
18 | SIGCONT | 如果进程已停止,则使其继续运行 | 继续/忽略 |
19 | SIGSTOP | 停止进程的执行。信号不能被忽略,处理和阻塞 | 为终止进程 |
20 | SIGTSTP | 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 | 暂停进程 |
21 | SIGTTIN | 后台进程读终端控制台 | 暂停进程 |
22 | SIGTTOU | 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 | 暂停进程stop |
23 | SIGURG | 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 | 忽略该信号 |
24 | SIGXCPU | 进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程 | 终止进程 |
25 | SIGXFSZ | 超过文件的最大长度设置 | 终止进程 |
26 | SIGVTALRM | 虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 | 终止进程 |
27 | SGIPROF | 类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间 | 终止进程 |
28 | SIGWINCH | 窗口变化大小时发出 | 忽略该信号 |
29 | SIGIO | 此信号向进程指示发出了一个异步IO事件 | 忽略该信号 |
30 | SIGPWR | 关机 | 终止进程 |
31 | SIGSYS | 无效的系统调用 | 终止进程并产生core文件 |
34~64 | SIGRTMIN ~ SIGRTMAX | LINUX的实时信号,它们没有固定的含义(可以由用户自定义) | 终止进程 |
部分信号说明
SIGCHLD : 在一个进程终止和停止时,SIGCHLD信号被送给父进程,按系统默认将忽略此信号.
如果父进程希望被告知子进程的这种状态改变,则应捕捉信号.信号捕捉函数通常要调用wait函数取得子进程ID和其终止状态
信号产生函数
kill函数
给指定进程发送指定信号(不一定是杀死)
参数:
- pid取值的四种情况:
- pid>0:将信号传送给进程ID为pid的进程
- pid=0:当前进程所在进程组的所有进程
- pid=-1:系统所有进程
- pid<-1:指定进程组的所有进程,进程组号等于pid的绝对值
- sig:信号编号,信号的宏定义
返回值:
- 成功:0
- 失败:-1
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
int main()
{
pid_t pid=-1;
pid=fork();
if(pid==0)
{
for(int i=0;i<5;i++)
{
printf("do work.......%d\n",i);
sleep(1);
}
}
else{
printf("parent work...........\n");
sleep(2);
kill(pid,SIGINT);
}
return 0;
}
raise函数
给当前进程发送指定信号(自己给自己法),等价于kill(getpid(),sig);
返回值:
- 成功:0
- 失败:非0
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
int main()
{
int i=0;
for(i=0;i<5;i++)
{
printf("do work...............%d\n",i);
sleep(1);
if(i==3)
{
raise(SIGINT);
}
}
return 0;
}
功能:给自己发送异常终止信号 6) SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT);
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int i=0;
for(i=0;i<5;i++)
{
printf("do work...............%d\n",i);
sleep(1);
if(i==3)
{
abort();
}
}
return 0;
alarm函数
参数:
功能:
- 设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器
- 取消定时器alarm(0),返回旧闹钟余下秒数。
参数: seconds:指定的时间,以秒为单位
返回值:返回0或剩余的秒数
#include<stdio.h>
#include<unistd.h>
int main()
{
int second=0;
second= alarm(5);
printf(".........%d\n",second);
sleep(2);
second=alarm(5);
printf(".......%d\n",second);//返回上一个闹钟剩余的时间
return 0;
}
setitimer函数(定时器 替代闹钟 精确到us,实现周期性定时)
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:
设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
参数:
which:指定定时方式
a) 自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间
b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用cpu的时间
c) 运行时计时(用户 + 内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
new_value:struct itimerval, 负责设定timeout时间
struct itimerval {
struct timerval it_interval; // 闹钟触发周期
struct timerval it_value; // 闹钟触发时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
}
itimerval.it_value: 设定第一次执行function所延迟的秒数
itimerval.it_interval: 设定以后每几秒执行function
old_value: 存放旧的timeout值,一般指定为NULL
返回值:
成功:0
失败:-1
#include<sys/time.h>
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void func(int sig)
{
printf("Hello.........\n");
}
int main()
{
struct itimerval new_itimer;
//触发性周期
new_itimer.it_interval.tv_sec=2;
new_itimer.it_interval.tv_usec=0;
//触发时间
new_itimer.it_value.tv_sec=3;
new_itimer.it_value.tv_usec=0;
signal(SIGALRM,func);
setitimer(ITIMER_REAL,&new_itimer,NULL);
for(int i=0;i<10;i++)
{
printf("do work.......%d\n",i);
sleep(1);
}
return 0;
}
信号集
在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。
这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对其进行位操作。而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。
自定义函数集
信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。既然是一个集合,就需要对集合进行添加/删除等操作。
#include <signal.h>
int sigemptyset(sigset_t *set); //将set集合置空
int sigfillset(sigset_t *set); //将所有信号加入set集合
int sigaddset(sigset_t *set, int signo); //将signo信号加入到set集合
int sigdelset(sigset_t *set, int signo); //从set集合中移除signo信号
int sigismember(const sigset_t *set, int signo); //判断信号是否存在
#include<signal.h>
#include<stdio.h>
int main()
{
sigset_t set;
int ret=0;
sigemptyset(&set);//清空信号集
//判断sigint是否在信号集中
ret=sigismember(&set,SIGINT);
if(ret==0)
{
printf("SIGINT is not a member of set\n");
}
printf("add SIGINT to set\n");
sigaddset(&set,SIGINT);
ret=sigismember(&set,SIGINT);
if(ret==1)
{
printf("SIGINT is a member of set\n");
}
printf("delete SIGINT to set.......\n");
sigdelset(&set,SIGINT);
ret=sigismember(&set,SIGINT);
if(ret==0)
{
printf("已删除sigint\n");
}
return 0;
}
sigprocmask函数
信号阻塞集也称信号屏蔽集、信号掩码。每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。
所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。
我们可以通过 sigprocmask() 修改当前的信号掩码来改变信号的阻塞情况。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
参数:
how : 信号阻塞集合的修改方法,有 3 种情况:
SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。相当于 mask = mask|set。
SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。相当于 mask = mask & ~ set。
SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于mask = set。
set : 要操作的信号集地址。
若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。
oldset : 保存原先信号阻塞集地址
返回值:
成功:0,
失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。
#include<signal.h>
#include<stdio.h>
int main()
{
sigset_t set;
sigset_t old;
sigemptyset(&set);
sigaddset(&set,SIGINT);
sigprocmask(SIG_BLOCK,&set,&old);
while(1);
return 0;
}
sigpending函数
#include <signal.h>
int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:
set:未决信号集
返回值:
成功:0
失败:-1
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
sigset_t set;
sigset_t old;
sigemptyset(&set);
sigaddset(&set,SIGINT);
sigaddset(&set,SIGQUIT);
//自定义信号集设置到内核中的阻塞信号集
sigprocmask(SIG_BLOCK,&set,&old);
sigset_t pend;
int i=0;
int ret=0;
while(1)
{
//读内核的未决信号集的状态
sigpending(&pend);
for(i=0;i<32;i++)
{
if(sigismember(&pend,i))
{
printf("1");
}
else{
printf("0");
}
}
printf("\n");
sleep(1);
++ret;
if(ret==5)
{
sigprocmask(SIG_SETMASK,&old,NULL);
}
}
return 0;
}
信号捕捉
(通知内核在某种信号发生时,调用一个用户函数(可执行用户希望对这种事件进行的处理)
signal函数
该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:
注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。
参数:
signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill - l("l" 为字母)进行相应查看。
handler : 取值有 3 种情况:
SIG_IGN:忽略该信号
SIG_DFL:执行系统默认动作
信号处理函数名:自定义信号处理函数,如:func
回调函数的定义如下:
void func(int signo)
{
// signo 为触发的信号,为 signal() 第一个参数的值
}
返回值:
成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。
失败:返回 SIG_ERR
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
void fun(int sig)
{
if(sig==SIGINT)
printf("捕捉到sigint\n");
if(sig==SIGQUIT)
printf("捕捉到sigquit\n");
}
int main()
{
printf("wait...SIGINT SIGQUIT.....\n");
signal(SIGINT,fun);
signal(SIGQUIT,fun);
while(1);
return 0;
}
sigaction函数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:
检查或修改指定信号的设置(或同时执行这两种操作)。
参数:
signum:要操作的信号。
act: 要设置的对信号的新处理方式(传入参数)。
oldact:原来对信号的处理方式(传出参数)。
如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。
返回值:
成功:0
失败:-1
struct sigaction结构体
struct sigaction {
void(*sa_handler)(int); //旧的信号处理函数指针
void(*sa_sigaction)(int, siginfo_t *, void *); //新的信号处理函数指针
sigset_t sa_mask; //信号阻塞集
int sa_flags; //信号处理的方式
void(*sa_restorer)(void); //已弃用
};
1) sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,应根据情况给sa_sigaction、sa_handler 两者之一赋值,其取值如下:
- a) SIG_IGN:忽略该信号
- b) SIG_DFL:执行系统默认动作
- c) 处理函数名:自定义信号处理函数
2) sa_mask:信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号。
3) sa_flags:用于指定信号处理的行为,通常设置为0,表使用默认属性。它可以是一下值的“按位或”组合:
- Ø SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)
- Ø SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
- Ø SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
- Ø SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
- Ø SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
- Ø SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
信号处理函数
void(*sa_sigaction)(int signum, siginfo_t *info, void *context);
参数说明:
signum:信号的编号。
info:记录信号发送进程信息的结构体。
context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文
#include<signal.h>
#include<unistd.h>
#include<stdio.h>
void fun(int sig)
{
printf("捕捉到信号.....%d\n",sig);
sleep(5);
printf("do work...............\n");
}
int main()
{
struct sigaction act;
act.sa_flags=0;
act.sa_handler=fun;
//清空
sigemptyset(&act.sa_mask);
//添加
sigaddset(&act.sa_mask,SIGQUIT);
sigaction(SIGINT,&act,NULL);
while(1);
return 0;
}
::
不可重入,可重入函数
那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数。
满足以下条件为不可重入函数:
- 函数体内使用了静态的数据结构;
- 函数体内调用了malloc() 或者 free() 函数(谨慎使用堆);
- 函数体内调用了标准 I/O 函数
保证函数的可重入性的方法:
- 在写函数时候尽量使用局部变量(例如寄存器、栈中的变量);
- 对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。
SIGCHLD信号
产生条件:
- 子进程终止
- 子进程接收到SIGSTOP信号停止时
如何避免僵尸进程
1)最简单的方法,父进程通过 wait() 和 waitpid() 等函数等待子进程结束,但是,这会导致父进程挂起。
2) 如果父进程要处理的事情很多,不能够挂起,通过 signal() 函数人为处理信号 SIGCHLD , 只要有子进程退出自动调用指定好的回调函数,因为子进程结束后, 父进程会收到该信号 SIGCHLD ,可以在其回调函数里调用 wait() 或 waitpid() 回收。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
W>void sig_fun(int sig)
{
pid_t pid;
while((pid=waitpid(-1,NULL,WNOHANG))>0)
{
printf("child %d terminated\n",pid);
}
}
int main()
{
pid_t pid=-1 ;
//捕捉信号
signal(SIGCHLD,sig_fun);
pid=fork();
if(pid==0)
{
//child
printf("do..........work.........\n");
exit(0);
}
else{
sleep(5);
printf("i am father.............\n");
system("ps -ef | grep defunct");
}
return 0;
}
3)如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
int main()
{
pid_t pid;
// 忽略子进程退出信号的信号
// 那么子进程结束后,内核会回收, 并不再给父进程发送信号
signal(SIGCHLD, SIG_IGN);
pid = fork(); // 创建进程
if (pid < 0)
{ // 出错
perror("fork error:");
exit(1);
}
else if (pid == 0)
{ // 子进程
printf("I am child process,pid id %d.I am exiting.\n", getpid());
exit(0);
}
else if (pid > 0)
{ // 父进程
sleep(2); // 保证子进程先运行
printf("I am father, i am exited\n\n");
system("ps -ef | grep defunct"); // 查看有没有僵尸进程
}
return 0;