信号的共性:
简单,不能够携带大量的信息,满足某个特定条件,优先级高
使用信号的目的:
1.让进程知道已经发生了一个特定的事情
2.强迫进程执行它自己代码中的信号处理程序(中断机制)
信号的来源可以有很多种试,按照产生条件的不同可以分为硬件和软件两种。
当用户在终端上按下某键时,将产生信号。如按下组合键后将产生一个SIGINT信号。
终端按键产生信号
ctrl+c ==> 2)SIGINT用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
ctrl+z ==> 20) SIGTSTP 停止终端交互进程的运行。默认动作为暂停进程。
ctrl+\ ==> 3) SIGQUIT 用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
硬件异常产生信号:除数据、无效的存储访问等。这些事件通常由硬件(如:CPU)检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时正在进行的程序。
异常程序操作(eg:除0操作) ==>8) SIGFPE 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
非法访问内存 ==>20) SIGTSTP:停止终端交互进程的运行。默认动作为暂停进程。
总线错误 ==>7) SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
用户在终端下调用kill命令向进程发送任务信号。
kill -15 pid (15:信号编号 向进程发送SIGTERM信号)
进程调用kill或sigqueue函数发送信号。
当检测到某种软件条件已经具备时发出信号,如由alarm或settimer设置的定时器超时时将生成SIGALRM信号。
每一进程收到的所有信号,都是由内核负责发送的,内核处理
A给B发送信号。B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,立即去处理信号(信号的优先级高)。与硬件中断类似,但信号是软件层实现的中断,又成为“软中断”。
在Shell下输入kill –l 可显示Linux 系统支持的全部依赖,信号列表如下:
在signal.h中,在Linux中没有16和32这两个信号。上面信号的含义如下:
(1) SIGHUP:当用户退出Shell时,由该Shell启的发所有进程都退接收到这个信号,默认动作为终止进程。
(2) SIGINT:用户按下组合键时,用户端时向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
(3) SIGQUIT:当用户按下组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程并产生core文件。
(4) SIGILL :CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件。
(5) SIGTRAP:该信号由断点指令或其他trap指令产生。默认动作为终止进程并产生core文件。
(6) SIGABRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
(7) SIGBUS:非法访问内存地址,包括内存地址对齐(alignment)出错,默认动作为终止进程并产生core文件。
(8) SIGFPE:在发生致命的算术错误时产生。不仅包括浮点运行错误,还包括溢出及除数为0等所有的算术错误。默认动作为终止进程并产生core文件。
(9) SIGKILL:无条件终止进程。本信号不能被忽略、处理和阻塞。默认动作为终止进程。它向系统管理员提供了一种可以杀死任何进程的方法。
(10) SIGUSR1:用户定义的信号,即程序可以在程序中定义并使用该信号。默认动作为终止进程。
(11) SIGSEGV:指示进程进行了无效的内存访问。默认动作为终止进程并使用该信号。默认动作为终止进程。
(12) SIGUSR2:这是另外一个用户定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
(13) SIGPIPE:Broken pipe:向一个没有读端的管道写数据。默认动作为终止进程。
(14) SIGALRM:定时器超时,超时的时间由系统调用alarm设置。默认动作为终止进程。
(15) SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是,该信号可以被阻塞和处理。通常用来要求程序正常退出。执行Shell命令kill时,缺少产生这个信号。默认动作为终止进程。
(16) SIGCHLD:子程序结束时,父进程会收到这个信号。默认动作为忽略该信号。
(17) SIGCONT:让一个暂停的进程继续执行。
(18) SIGSTOP:停止(stopped)进程的执行。注意它和SIGTERM以及SIGINT的区别:该进程还未结束,只是暂停执行。本信号不能被忽略、处理和阻塞。默认作为暂停进程。
(19) SIGTSTP:停止进程的动作,但该信号可以被处理和忽略。按下组合键时发出该信号。默认动作为暂停进程。
(20) SIGTTIN:当后台进程要从用户终端读数据时,该终端中的所有进程会收到SIGTTIN信号。默认动作为暂停进程。
(21) SIGTTOU:该信号类似于SIGTIN,在后台进程要向终端输出数据时产生。默认动作为暂停进程。
(22) SIGURG:套接字(socket)上有紧急数据时,向当前正在运行的进程发出此信号,报告有紧急数据到达。默认动作为忽略该信号。
(23) SIGXCPU:进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程。默认动作为终止进程。
(24) SIGXFSZ:超过文件最大长度的限制。默认动作为yl终止进程并产生core文件。
(25) SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是它只计算该进程占有用的CPU时间。默认动作为终止进程。
(26) SIGPROF:类似于SIGVTALRM,它不仅包括该进程占用的CPU时间还抱括执行系统调用的时间。默认动作为终止进程。
(27) SIGWINCH:窗口大小改变时发出。默认动作为忽略该信号。
(28) SIGIO:此信号向进程指示发出一个异步IO事件。默认动作为忽略。
(29) SIGPWR:关机。默认动作为终止进程。
(30) SIGRTMIN~SIGRTMAX:Linux的实时信号,它没有固定的含义(或者说可以由用户自由使用)。注意,Linux线程机制使用了前3个实时信号。所有的实时信号的默认动作都是终止进程。
执行命令:man 7 signal
默认处理动作
Term:终止进程
Core:终止进程,生成Core文件(查验进程死亡原因,用于gdb调试) ulimit -a/ulimit -c 1024
Ing:忽略信号
Stop:暂停信号
Cont:继续运行进程
9)SIGKILL和19)SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
只要产生信号发送的事件发送,信号一定会产生并且递送,但是由于阻塞信号集的原因,该信号不一定会被递达,信号的产生和处理都是内核做到事情。信号的处理方式是信号递达之后的处理方式
当信号发生时,用户可以要求进程以下列3种方式之一对信号做出响应。
对于要捕捉的信号,可以为其指定信号处理函数,信号发生时该函数自动被调用,在该函数内部实现对该信号的处理。
大多数信号都可使用这种方式进行处理,但是SIGKILL和SIGSTOP这两个信号不能被忽略,同时这两个信号也不能被捕获和阻塞。此外,如果忽略某某些由硬件异常产生的信号(如非法存储访问或除以0),则进程的行为是不可预测的。
大部分信号的默认操作是终止进程,且所有的实时信号的默认动作都是终止进程。
注册一个信号捕捉函数
void (*signal(int signum, void (*handler))(int)))(int);
如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。
函数用于改变进程接收到特定信号后的行为。
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
该函数的第一个参数为 信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。
第二个参数是指 向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;
第三个 参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查 信号的有效性。
示例:
// 自定义信号捕捉函数
void sigShutdownHandler(int sig)
{
switch (sig)
{
case SIGINT:
break;
case SIGTERM:
break;
default:
break;
}
exit(0);
}
int initsignalHandle()
{
struct sigaction act;
signal(SIGHUP,SIG_IGN);
signal(SIGPIPE,SIG_IGN);
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sigShutdownHandler;
sigaction(SIGTERM,&act,NULL);
sigaction(SIGINT,&act,NULL);
return 0;
}
信号的产生和处理都是由内核进行的
频繁的在用户态和内核态进行切换是很浪费时间的
int raise(int sig);
对当前进程发送指定信号
int pause(void);
将进程挂起等待信号
int kill(pid_t pid,int sig);
通过进程编号发送信号
unsigned int alarm(unsigned int seconds);
指定时间(秒)发送SIGALRM信号。 seconds 为0 时取消所有已设置的alarm请求;
int sigqueue(pid_t pid,int sig,const union sigval val);
类似于kill函数,多了附带共用体 union sigval形数,将共用体中的成员 int sival_int 或 void *sival_ptr 的值传递给 信号处理函数中的定义类型 siginfo_t 中的 int si_int 或 void *si_ptr;
int setitimer(int which,const struct itimerval *value,struct itimerval *oldvalue);
可定时发送信号,根据which可指定三种信号类型:SIGALRM、SIGVTALRM 和 SIGPROF;作用时间也因which值不同而不同;struct itimerval 的成员 it_interval定义间隔时间,it_value 为0时,使计时器失效;
void abort(void) ;
将造成进程终止;除非捕获SIGABORT信号;
sigfillset(sigset_t *set); 设置所有的信号到set信号集中;
sigemptyset(sigset_t *set); 从set信号集中清空所有信号;
sigaddset(sigset_t *set,int sig);在set信号集中加入sig信号;
sigdelset(sigset_t *set,int sig);在set信号集中删除sig信号;
int sigprocmask(int how,const sigset_t *set,sigset_t *set);
根据how值,设置阻塞信号集,或释放阻塞的信号集
int sigpending(sigset_t *set); 获取在阻塞中的所有信号;
int sigsuspend(const sigset_t *set); 类似于 pause()函数!
可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽字”与“挂起等待信号”这个两个操作间隙失去CPU。除非将这两个步骤合成原子操作。sigsuspend可以实现。
int sigsuspend(const sigset_t *mask) 挂起等待信号
sigsuspend函数调用期间,进程信号屏蔽字由参数mask决定
可将某个信号从临时信号屏蔽集种删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,信号屏蔽字恢复原来的值。
#include <iostream>
using namespace std;
void myheadler(int signo)
{
cout<<"aa"<<endl;
}
int main(void)
{
signal(SIGALARM,myheadler);
sigset_t mask;
sigfillset(&mask);
sigdelset(&mask,SIGALARM);
alarm(1);
sigsuspend(&mask);
while(1);
return 0;
}
时序问题总结
竞态条件,跟系统负载有很紧密的关系,体现除信号的不可靠性。系统负载越严重,信号不可靠性越强。不可靠由其实现原理所致。信号是通过软件方式实现,每次系统调用结束后或中断处理结束后,需通过扫描PCB中的未决信号集来判断是否应处理某个信号。当系统负载过重时,会出现混乱
一个函数在被调用执行期间(尚未调用结束),由于某时序又被重复调用,称之为"重入"。
1.定义可重入函数,函数内不能包含全局变量集static变量,不能使用malloc、free
2.信号捕捉函数应设计为可重入函数
3.不可重入的原理:使用静态数据结构 调用了malloc和new (不是栈结构) 是标准I/O
当子进程停止或者结束时,会向父进程发送SIGCHLD信号,该信号默认处理动作是忽略,所有产生僵尸进程。
我们之前的程序是,在父进程中调用 wait或者waitpid来回收子进程,此时父进程要么阻塞等待,要么非阻塞而采用轮询的方式。那么将会导致父进程不能做其他工作,只能等着回收子进程。
我们可以利用SIGCHLD信号来捕捉,当有SIGCHLD信号产生时,父进程执行信号捕捉函数。
void sigchldHeadler(int signo)
{
pid_t pid;
int status;
while((pid=waitpid(-1,&status,WNOHANG))>0)
{
printf("回收成功:%d ok\n",pid);
if(WIFEXITED(status))
{
printf("退出状态:%d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("退出状态(信号):%d\n",WTERMSIG(status));
}
}
}
int main(void)
{
//创建10个子进程
int i;
for(i=0;i<10;i++)
{
pid_t pid = fork();
if(pid == 0)
{
break;
}
}
if(i < 10)
{
printf("I am child:%d\n",getpid());
sleep(1);
}
else if(i == 10)
{
struct sigaction act;
act.sa_handler = sigchldHeadler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,NULL);
while(1)
{
printf("I am parent:%d\n",getpid());
sleep(1);
}
}
return 0;
}
注:
当父进程在执行信号捕捉函数时,又有子进程死亡。或者有多个子进程死亡。我们知道信号集是位图机制,是不支持排队的。
while((pid=waitpid(-1,&status,WNOHANG))>0)
当父进程的信号捕捉函数还没有注册,就已经有子进程结束了。导致僵尸进程…
if(i < 10)
{
printf("I am child:%d\n",getpid());
sleep(1);
}
前面讲到信号是不能携带大量数据的,一般通过 kill 来发送信号
但是信号可以携带数据,可以携带少量。
通过sigqueue函数,可在向指定进程发送信号的同时携带参数
int sigqueue(pid_t pid,int sig,const union sigval value);
成功返回0,失败-1,设置error
注意事项:
向指定进程发送指定信号的同时携带数据。不能够传地址,不能进程之间虚拟地址空间各自独立,当前进程地址传递给另一进程没有实际意义
捕捉函数:
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
成功:0 失败:-1,并设置error
act:新的处理方式
oldact:传出参数,旧的处理方式
struct sigaction{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa mask;
int sa_flags;
void (*sa restorer)(void):
};
不使用sa_handler,而使用sa_sigaction。sa_flags必须指定为SA_SIGINFO
说明:设置定时器(闹钟)。在指定多少秒后,内核会给当前进程发送14)SIGALRM信号,进程收到该信号,默认动作终止进程。无论进程处于某种状态都会记时
原型:
unsigned int alarm(unsigned int seconds);
返回0或剩余的秒数
取消定时器:alarm(0)
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main(void)
{
int i=0;
alarm(1); //定时1秒,14)SIGALRM 终止进程
while(1)
{
++i;
//cout<<i<<endl;
printf("%d\n",i); //printf比cout更快一点点
}
return 0;
}
说明:设置定时器(闹钟)。可替代alarm函数,精度微妙us,可实现周期定时
原型:
int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
成功返回 0 失败-1,并设置error
#include <iostream>
#include <sys/time.h>
using namespace std;
void myalarm(unsigned long sec)
{
itimerval it,oldit;
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 0;
it.it_value.tv_sec = 1;
it.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL,&it,&oldit);
if(ret == -1)
{
perror("setitimer err");
exit(-1);
}
return;
}
int main(void)
{
int i;
myalarm(1);
while(1)
{
printf("%d\n",i);
++i;
}
return 0;
}