signal信号机制是属于计算机异常处理机制中的一种。
signal信号属于一种异步处理异常的机制之一。
类似于我们平常在命令行上对于死循环的程序,按下ctrl-z暂时挂起,ctrl-c程序终止,这些挂起,终止信号都属于signal信号的一种,常见的几种signal信号如下图所示(供查询使用,平时并无用):
常见的信号如下:
通过/bin/kill程序发送信号:
/bin/kill -9 15213
发送信号给进程15213
/bin/kill -9 -15213
如果进程前面表示为负的,说明发送信号9给15213进程组的所有信号
还有一个同名函数也可以在程序中对指定进程发送信号:
#include
#include
int kill(pid_t pid, int sig);
从键盘发送信号
Unix用作业(job)这个抽象概念来表示对一个命令行求值而创建的进程。在任何时刻,至多只有一个前台进程和0个或多个后台进程,而ctrl-z或者ctrl-c则会向每一个前台进程发送信号。
通过alarm函数给自身进程发送信号
alarm的函数原型:
#include
unsigned int alarm(unsigned int secs);
//返回:前一次闹钟剩余的秒数,若以前没有闹钟,则设置为0
一个程序接受一个信号的时候似乎很好理解,且不会出现太大的问题,但是一旦一个进程接受了多个信号的时候,问题就出现了
以下是问题的说明:
该信号如果理解成现实生活中的信号,现实中的信号也永远是异步。例如,小张和小和正在说话,这是一个顺序执行的操作。如果,这时候上司(操作系统内核)突然说了声“小张,那个文件给我处理一下了”,这就是一个异步信号,小张在接受这个信号之后就会去处理这个文件,这个就是信号处理函数,如果上司说了“那个文件给我处理一下”,这时候另一个同事说“一起去吃饭”,这时候就有两个信号了,这时候小张就先去“处理文件”的信号,再去处理“一起去吃饭”的信号,这就是待处理信号被阻塞。如果上司一直说“那个文件给我处理一下”,“那个文件给我处理一下”,这就是相同类型的信号有很多个,这时候只有第一个有用,小张就去做第一个,这就是待处理信号不会排队等待的道理
待处理信号被阻塞:Unix信号函数会阻塞统一类型的待处理信号。就比如,一个程序接受了一个SIGCHLD的信号,那么在执行SIGCHLD的信号的时候,下一的SIGCHLD的信号就会被阻塞,成为待处理信号。
待处理信号不会排队等待。即针对同一类型的信号,只能有一个待处理信号。例如,一个进程接受了一个SIGCHLD的信号,在执行SIGCHLD的信号处理程序的时候,来了两个SIGCHLD信号,那么只有一个SIGCHLD会成为待处理信号。
系统调用可以被中断。像read,wait,accept这样的系统调用潜在地会阻塞进程一段时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号,被中断的系统调用在信号处理程序进行后不再返回,而是立即返回给用户一个错误条件,并将errno设置为EINTR.
为了说明以上几种情况,先来介绍以下signal函数
C 库函数 void (*signal(int sig, void (*func)(int)))(int) 设置一个函数来处理信号,即带有 sig 参数的信号处理程序。
下面是 signal() 函数的声明。
void (*signal(int sig, void (*func)(int)))(int)
等价于
void (int) * signal(int sig, void (int) * func);
参数
SIGABRT (Signal Abort) 程序异常终止。
SIGFPE (Signal Floating-Point Exception) 算术运算出错,如除数为 0 或溢出(不一定是浮点运算)。
SIGILL (Signal Illegal Instruction) 非法函数映象,如非法指令,通常是由于代码中的某个变体或者尝试执行数据导致的。
SIGINT (Signal Interrupt) 中断信号,如 ctrl-C,通常由用户生成。
SIGSEGV (Signal Segmentation Violation) 非法访问存储器,如访问不存在的内存单元。
SIGTERM (Signal Terminate) 发送给本程序的终止请求信号。
SIG_DFL 默认的信号处理程序。
SIG_IGN 忽视信号。
返回值
该函数返回信号处理程序之前的值,当发生错误时返回 SIG_ERR。
菜鸟教程上signal的例子:
#include
#include
#include
#include
void sighandler(int);
int main()
{
signal(SIGINT, sighandler);
while(1)
{
printf("开始休眠一秒钟...\n");
sleep(1);
}
return(0);
}
void sighandler(int signum)
{
printf("捕获信号 %d,跳出...\n", signum);
exit(1);
}
让我们编译并运行上面的程序,这将产生以下结果,且程序会进入无限循环,需使用 CTRL + C 键跳出程序。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
#define MAXBUF 100
void handler1(int sig) {
pid_t pid;
if ((pid = waitpid(-1, NULL, 0)) < 0)
printf("waitpid error");
printf("Handler reaped child %d\n", (int)pid);
sleep(2);
return;
}
int main() {
int i, n;
char buf[MAXBUF];
if (signal(SIGCHLD, handler1) == SIG_ERR)
printf("signal error"); /* Parent creates children */
for (i = 0; i < 3; i++) {
if (fork() == 0) {
printf("Hello from child %d\n", (int)getpid());
sleep(1);
exit(0);
}
}
/* Parent waits for terminal input and then processes it */
if((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
printf("read");
printf("Parent processing input\n");
while (1)
;
exit(0);
}
可以从结果明显的看出只回收了两个孩子进程,剩下一个孩子进程变成僵尸进程
导致这个结果的原因是:等待信号不会排队.
父进程创建了三个子进程,当三个子进程结束的时候,三个进程成为僵尸进程,向父进程传递三个SIGCHLD信号,一个正在处理,一个待处理,因为同一个信号只有一个待处理信号,剩下一个信号就被抛弃,所以有一个子进程的SIGCHLD信号不被处理,一个子进程成为僵尸进程。
说明了以上第一点和第二点。
针对这个问题,改进版的signal1
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
#define MAXBUF 100
void handler1(int sig) {
pid_t pid;
while((pid = waitpid(-1, NULL, 0)) > 0)
printf("Handler reaped child %d\n", (int)pid);
if(errno!=ECHILD)
printf("waitpid error");
sleep(2);
return;
}
int main() {
int i, n;
char buf[MAXBUF];
if (signal(SIGCHLD, handler1) == SIG_ERR)
printf("signal error"); /* Parent creates children */
for (i = 0; i < 3; i++) {
if (fork() == 0) {
printf("Hello from child %d\n", (int)getpid());
sleep(1);
exit(0);
}
}
/* Parent waits for terminal input and then processes it */
if((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
printf("read");
printf("Parent processing input\n");
while (1);
exit(0);
}
重点的改动在于这几行代码:
while((pid = waitpid(-1, NULL, 0)) > 0)
printf("Handler reaped child %d\n", (int)pid);
if(errno!=ECHILD)
printf("waitpid error");
用while循环来尽可能个获取未被回收的进程。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024 /* max line size */
#define MAXARGS 128 /* max args on a command line */
#define MAXJOBS 16 /* max jobs at any point in time */
#define MAXJID 1<<16 /* max job ID */
#define MAXBUF 100
void handler1(int sig) {
pid_t pid;
while((pid = waitpid(-1, NULL, 0)) > 0)
printf("Handler reaped child %d\n", (int)pid);
if(errno!=ECHILD)
printf("waitpid error");
sleep(2);
return;
}
int main() {
int i, n;
char buf[MAXBUF];
if (signal(SIGCHLD, handler1) == SIG_ERR)
printf("signal error"); /* Parent creates children */
for (i = 0; i < 3; i++) {
if (fork() == 0) {
printf("Hello from child %d\n", (int)getpid());
sleep(1);
exit(0);
}
}
/* Manually restart the read call if it is interrupted */
while ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
{
if (errno != EINTR)
printf("read error\n");
}
printf("Parent processing input\n");
while (1)
;
exit(0);
}
系统调用可以被中断。像read,wait,accept这样的系统调用潜在地会阻塞进程一段时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号,被中断的系统调用在信号处理程序进行后不再返回,而是立即返回给用户一个错误条件,并将errno设置为EINTR.
用while尽可能的执行read,实现对read系统调用进行自动重启。
以下程序给出了一个简单的信号处理程序,它捕捉两个用户定义的信号并打印信号编号。pause函数,它使调用进程在接到一信号前挂起。
#include
#include
#include
static void sig_usr(int);
int main()
{
if(signal(SIGUSR1,sig_usr)==SIG_ERR)
printf("can't catch SIGUSR1");
if(signal(SIGUSR2,sig_usr)==SIG_ERR)
printf("can't catch SIGUSR2");
for(; ;) pause();
}
static void sig_usr(int signo)
{
if(signo==SIGUSR1)
printf("received SIGUSR1\n");
else if(signo==SIGUSR2)
printf("received SIGUSR2\n");
else
{
printf("received signal %d\n",signo);
}
}
我们使该程序在后台运行,并且用kill(1)命令将信号发送给它。注意,在UNIX系统中,杀死(kill)这个术语是不恰当的,kill(1)命令和kill(2)函数只是 将一个信号发送给了一个进程或进程组。该信号是否终止进程取决于该信号的类型,以及进程是否安排好了捕捉信号。
当执行一个程序时,所有信号的状态都是系统默认或忽略。通常所有信号都被设置为它们的默认动作,除非调用 exec 的进程忽略该信号。确切的讲, exec 函数将原先设置为要捕捉的信号都更改为他们的默认动作,其他信号的状态则不变(对于一个进程原先要捕捉的信号,当其执行一个新程序后,就自然不能再捕捉它了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义了)。
很多捕捉这两个信号的交互式程序具有下列形式的代码:
void sig_int(int), sig_quit(int);
if (signal(SIGINT, SIG_IGN) != SIG_IGT)
signal(SIGINT, sig_int);
if (signal(SIGQUIT, SIG_IGN) != SIG_IGN)
signal(SIGQUIT, sig_quit);
这样处理后,仅当信号当前未被忽略时,进程才会捕捉它们。
在早期的UNIX系统中(如SVR4中),进程每次处理信号时,随即将信号动作复位为默认动作。因此需要重复设置信号函数,例如:
上述解决方法存在的问题是:
➢ 在信号发生之后到信号处理程序中调用signal函数之间有一个时间窗口。在此期间,可能发生另一次信号,而第二个信号有可能造成执行默认动作。(信号丢失,即不可靠)
进程所能做的仅仅是捕捉或忽略信号,而不能让内核阻塞信号
➢ 不要忽略信号
➢ 但是在发生信号时,记住它,在进程做好准备的时候传递它
可靠性是指信号是否会丢失,即该信号是否支持排队;
如果支持排队就是可靠的,不支持排队就是不可靠的。
通俗解释