信号
正式开始步入Linux信号的学习中。。。。
一、信号的种类
**注:**Linux中的信号一共有64种,这里主要讨论前32种。(数字并不准确,习惯而已)
二、信号的产生种类
1、终端特殊按键
ctl + z SIGTSTP
ctl + \ SIGQUIT
ctl + c SIGINT
**注:**信号发送给当前shell里的前台进程
2、硬件异常
除0操作
#include<stdio.h>
int main(void)
{
int a = 0;
printf("%d\n",3/a);
return 0;
}
**注:**以上代码会发送一个“8”号信号。
访问非法内存
#include<stdio.h>
int main(void)
{
char *str = "world";
*str = 'H';
return 0;
}
**注:**以上代码会发送一个“11”号信号。
3、调用函数向进程发送信号
kill函数
函数说明:
NAME
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
/*
pid > 0
sig发送给ID为pid的进程
pid == 0
sig发送给与发送进程同组的所有进程
pid < 0
sig发送给组ID为|-pid|的进程,并且发送进程具有向其发送信号的权限
pid == -1
sig发送给发送进程有权限向他们发送信号的系统上的所有进程
sig为0时,用于检测,特定pid进程是否存在,如不存在,返回-1。
*/
RETURN VALUE
On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set appropriately.
示例:
//kill.c函数
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<sys/types.h>
int main(int argc,char *argv[])
{
if(argc < 3)
{
printf("./kill signal pid\n");
exit(-1);
}
if(kill((pid_t)atoi(argv[2]),atoi(argv[1]))< 0)
{
perror("kill");
exit(1);
}
return 0;
}
//while.c函数
#include<stdio.h>
int main()
{
while(1)
;
return 0;
}
**注:**作为普通用户只能向自己生成的进程发送信号。
raise函数
函数说明:
int raise(int sig)//成功返回1 ,失败返回0
**注:**自己向自己发送信号
abort函数
函数说明:
void abort(void)
**注:**自己给自己发送“6”号信号(终止进程)。
4、软件条件产生信号
定时器alarm
函数说明:
unsigned int alarm(unsigned int seconds)
/*返回值为“未睡够的秒数”*——定时100秒,由于某种原因打断这个定时器,在此之前已经定时40秒,此时返回60秒/
示例:
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int counter;
alarm(1);
for(counter=0; 1; counter++)
printf("counter=%d ", counter);
return 0;
}
**注:**每个进程只有一个定时器。
三、进程处理信号的行为
1、默认处理动作
Term 终止进程Ign 忽略信号Core 终止一个进程并且生成Core 文件(用于gdb调试)Stop 暂停一个进程Cont 继续一个进程执行
**注:**每个信号都有一个默认处理动作(可以通过“man 7 signal”查看)
2、忽略
3、捕捉(用户自定义信号处理函数-相当于中断函数)
四、信号集处理函数
1、信号集的概念
说明:在一个PCB中存在中存在两组信号集:未决信号集和阻塞信号集。未决信号集中的每一位对应一个信号(初始值为0,用户不可以设置,用户可以读),阻塞信号集中的值用户可以自己设定0或者1。当信号通过这两个信号集后对应一个handler(每一个handler对应三种处理动作)
例如:当对当前进程发送一个“2”号信号,此时未决信号集中的编号为2的信号位置1,说明“2”号信号产生,但此时并没有被当前进程响应。“2”号信号继续向阻塞信号集传递,如果阻塞信号集的编号为2号的位置的位为1,说明此时“2”号信号不能通过(处于阻塞状态),因此“2”号信号产生后不能执行相应的动作。这种状态称为未决态。若阻塞信号集编号为2的位置的位为0,则“2”号信号可以继续向上传递执行相应的动作(默认动作为Term)并且此时未决信号集中的编号为2的位置的位自动翻转为0,这种状态称为递达态。
**注:**前32个信号不支持“排队等候”,后32个信号支持“排队等候”。
2、信号集的设置
//sigset_t 为自己构造的信号集
int sigemptyset(sigset_t *set)//信号集置0
int sigfillset(sigset_t *set)//信号集置1
int sigaddset(sigset_t *set, int signo)//信号集某一个信号位置1
int sigdelset(sigset_t *set, int signo)//信号集某一个信号位置0
int sigismember(const sigset_t *set, int signo)//判断信号集某一个位是否置1
3、sigprocmask函数
函数说明:
/将自定义的信号集注册到阻塞信号集中*/
#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:成功返回0,出错返回-1
/*
how参数:
SIG_BLOCK set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCK set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK 设置当前信号屏蔽字为set所指向的值,相当于mask=set
set:新传进来的信号屏蔽字
oset:可以传出原来信号屏蔽字(阻塞信号集的位)
*/
4、sigpending函数
函数说明:
/*获取当前进程的未决信号集*/
#include <signal.h>
int sigpending(sigset_t *set);
返回值:成功返回0,出错返回-1
示例:
#include <signal.h>
#include <stdio.h>
void printsigset(const sigset_t *set)
{
int i;
for (i = 1; i < 32; i++)//没有0号信号,
if (sigismember(set, i) == 1)
putchar('1');
else
putchar('0');
puts("");//换行
}
int main(void)
{
sigset_t s, p; //sizeof(s) == 128Bytes,每个信号对应2个字节
sigemptyset(&s);//信号集清0
sigaddset(&s, SIGINT);//将s信号集中的SIGINT信号位置1
sigprocmask(SIG_BLOCK, &s, NULL);//NULL:表示原来的阻塞信号集不关心
while (1)
{
sigpending(&p);//获取当前进程的未决信号集
printsigset(&p);//打印当前进程的未决信号集
sleep(1);
}
return 0;
}
**注:**程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于 未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。
五、信号捕捉设定
1、sigaction函数
函数说明:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
signum:为几号信号设定捕捉函数
act:信号动作的设定
struct sigaction 定义:
struct sigaction {
void (*sa_handler)(int);//函数指针(相当于C++类中定义的方法)
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
/*
sa_handler : 早期的捕捉函数
sa_sigaction : 新添加的捕捉函数,可以传参 , 和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数
sa_mask : 在执行捕捉函数时,设置阻塞其它信号,sa_mask | 进程阻塞信号集,退出捕捉函数后,还原回原有的
阻塞信号集
sa_flags : SA_SIGINFO 或者 0
sa_restorer : 保留,已过时
*/
};
oldact:原有信号的动作(不关心设定为NULL)
示例:
#include<stdio.h>
#include<signal.h>
void do_sig(int num)
{
printf("I am do sig\n");
printf("num = %d\n",num);
}
int main(void)
{
struct sigaction act;
act.sa_handler = do_sig;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT,&act,NULL);
while(1)
{
printf("*****\n");
sleep(1);
}
return 0;
}
**注1:**act.sa_handler = SIG_DFL或者act.sa_handler = SIG_IGN都可以。
**注2:**当执行某个信号的捕捉函数时,内核会自动设置阻塞信号集所对应的这个信号的位,让其置1。以阻塞这个信号的再次产生。但此时未决信号集中这个信号所对应的位置1,当捕捉函数执行完成后,阻塞信号集将信号所对应的位从1变为0,这时这个信号所对应的捕捉函数会再次运行。(sa_mask来设定)
**注3:**信号并不是一产生进程就会响应。当主程序因为中断、异常或系统调用进入到内核时,当内核处理完异常准备回用户模式之前先处理完当前进程中可以传递的信号。
**注4:**定时中断:当一个进程正在执行某种操作,CPU为其分配一个时间片。在时间片中进程一直在处理某种操作,这时产生一个信号,这个信号并没有打断进程的操作,信号处于未决状态。当时间片用完后,内核剥夺当前进程使用CPU的权利(时间中断),此时内核发现有一个信号到来,但此进程所对应的时间片已经用完因此只能等待新的时间片。当新的时间片到来时,由内核空间到用户空间时此时才会执行这个信号所对应的操作。
**注5:**系统调用:比如read/write(本质是system read/system write)等函数的调用,这时才会检测当前进程是否有信号产生。
2、C标准库信号处理函数
函数说明:
/*1*/
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
/*2*/
NAME
system - execute a shell command
SYNOPSIS
#include <stdlib.h>
int system(const char *command);
示例:
#include<stdio.h>
#include<signal.h>
void do_sig(int n)
{
printf("hello\n");
}
int main(void)
{
signal(SIGINT,do_sig);
while(1)
{
printf("******\n");
sleep(1);
}
return 0;
}
六、可重入与不可重入函数
可重入函数:函数执行到一半可以被打断,并且打断之后重新可以执行且没有bug不可重入函数:函数执行到一半不可被打断
注1:在信号捕捉函数里应该使用可重入函数,禁止调用不可重入函数。(man 7 signal查看可重入函数)
注2:不含全局变量和静态变量是可重入函数的一个要素。
七、信号引起的竞态和异步I/O
1、时序竞态
int pause(void)
/*使调用进程挂起,直到有信号递达,如果递达信号是忽略,则继续挂起*/
int sigsuspend(const sigset_t *mask)
/*
1.以通过指定mask来临时解除对某个信号的屏蔽,
2.然后挂起等待,
3.当被信号唤醒sigsuspend返回时,进程的信号屏蔽字恢复为原来的值
*/
1.1 pause函数
示例1:
#include<stdio.h>
#include<signal.h>
void do_sig(int n)
{
}
int main(void)
{
struct sigaction act;
act.sa_handler = do_sig;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1,&act,NULL);
pause();
printf("helloworld");
return 0;
}
示例2-定时:
void do_sig(int n)
{
}
void mysleep(int n)
{
signal(SIGALRM,do_sig);
alarm(n);
pause();
}
注1:示例2的问题:假设alarm定时1秒,此时若CPU执行优先级更高的进程2秒,当2秒后当前进程获取这个CPU时此时alarm1秒钟已过使得SIGALRM信号已产生。因此执行到pause时会使其一直处于挂起状态。
注2:时序竞态:从示例2中如果正常执行则没有bug,然而当执行完alarm函数后别的进程经过竞争将CPU“夺走”,当过了n秒后此时alarm已经产生信号,此时再调用pause则会导致进程永久挂起。
1.2 sigsuspend函数
示例:
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
/* set our handler, save previous information */
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
/* block SIGALRM and save current signal mask */
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
alarm(nsecs);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM); /* make sure SIGALRM isn't blocked */
sigsuspend(&suspmask); /* wait for any signal to be caught */
/* some signal has been caught, SIGALRM is now blocked */
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL); /* reset previous action */
/* reset signal mask, which unblocks SIGALRM */
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return(unslept);
}
注:解决时序竞态的问题。
2、全局变量异步I/O
注:以一个8字节的全局变量n而言,对于一个32位的操作系统来说。当对其进行赋值操作时,此时需要两条汇编指令才能完成操作,假设在第一条汇编指令执行后有一个信号产生,信号产生后执行捕捉函数,在捕捉函数中如果再次对这个n进行操作则会导致bug会造成数据混乱。
解决方法:采用原子类型(在32位的处理器上能够一条指令执行完成的类型,这个类型所取的位宽为当前处理器的最大位宽)
sig_atomic_t
平台下的原子类型
volatile
防止编译器开启优化选项时,优化对内存的读写
八、SIGCHLD信号处理
1、SIGCHLD的产生条件
子进程终止时子进程接收到SIGSTOP信号终止时子进程处于停止态,接受到SIGCONT后唤醒时
2、status处理方式
pid_t waitpid(pid_t pid, int *status, int options)
options
WNOHANG
没有子进程结束,立即返回
WUNTRACED
如果子进程由于被停止产生的SIGCHLD, waitpid则立即返回
WCONTINUED
如果子进程由于被SIGCONT唤醒而产生的SIGCHLD, waitpid则立即返回
获取status
WIFEXITED(status)
子进程正常exit终止,返回真
WEXITSTATUS(status)返回子进程正常退出值
WIFSIGNALED(status)
子进程被信号终止,返回真
WTERMSIG(status)返回终止子进程的信号值
WIFSTOPPED(status)
子进程被停止,返回真
WSTOPSIG(status)返回停止子进程的信号值
WIFCONTINUED(status)
子进程由停止态转为就绪态,返回真