目录
信号
概念
-
信号的产生:
当引发信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。信号产生时,内核会在进程表中设置一位标识。
-
信号的递送(delivery):
当进程对信号采取动作(执行信号处理函数或忽略)时称为递送。
信号产生和递送之间的时间间隔内称信号是未决的(pending)。
-
信号递送阻塞(block):
进程可指定对某个信号采用递送阻塞,如果此时信号的处理是系统默认动作或者捕捉该信号,则该信号就会处于未决的状态,直到进程解除对该信号的递送阻塞或者处理方式改为忽略。
来了一个信号,信号产生标记变为1,此时检测信号屏蔽字是否为0;为0可以递送,递送走之后,信号产生标记变为0,信号屏蔽字变为1;当捕捉函数接收递送的信号并结束,信号屏蔽字变为0。(类似于接电话,接通第一个电话的时候,接不了第二个电话)
如果信号的处理方式是忽略该信号,那么该信号永远不会处于递送或者递送未决状态
-
非实时信号: 1~31(不支持排队,信号同一时间太多,会丢失,不可靠信号)
信号名称 编号 信号说明 默认处理 SIGHUP 1 终端关闭时产生这个信号 进程终止 SIGINT 2 终端输入了中断字符ctrl+c 进程终止 SIGKILL 9 用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。 进程终止 SIGSEGV 11 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据,默认打印出segment fault。 进程终止并且产生core文件 SIGPIPE 13 往管道写时,读者已经不在了,或者往一个已断开数据流socket写数据。发送的进程会收到 进程终止 SIGALRM 14 时钟定时信号, 计算的是实际的时间或时钟时间。alarm 函数使用该信号。 进程终止 SIGTERM 15 有kill函数调用产生。 进程终止 SIGCHLD 17 子进程停止或者终止时,父进程会收到该信号。 忽略该信号 SIGSTOP 19 用来停止一个进程。本信号不能忽略,也不能被捕捉 进程暂停执行 -
实时信号:34~64(支持排队,可靠信号)(克服了信号丢失的问题)
core 文件(调试用)
- 限制产生的core文件的大小不能超过1024kb: ulimit -c 1024
- 禁止产生core文件:ulimit -c 0
- 调试找错误命令: gdb -c core.5032 a.out
信号的使用
进程可以从三个方面使用信号:
-
指定当收到信号时进程的处理函数(信号处理)。
-
阻塞一个信号(也就是推迟它的递送),比如处于一段临界代码,执行完临界代码后在启用这个信号。
-
向另外一个进程发送信号(进程间通讯)。
信号的处理
signal函数用来通知内核如何处理某个特定信号(忽略、捕捉、默认处理)。
#include <signal.h>
//typedef简化函数声明
typedef void (*sighandler_t)(int);//定义一个新函数指针类型sighandler_t
sighandler_t signal(int signum, sighandler_t handler);//函数返回值是函数指针
void (*signal(int signo, void (*handler)(int signo)))(int);
//signo:要处理的信号
//handler:处理的方式(是系统默认还是忽略还是捕获)。
//signal把信号做为参数传递给handler信号处理函数,并开始执行它,接着 signal函数返回函数指针,指向信号处理函数。
/*int (*p)();
这是一个函数指针, p所指向的函数是一个不带任何参数, 并且返回值为int的一个函数
int (*fun())();
这个式子与上面式子的区别在于用fun()代替了p,而fun()是一个函数,所以说就可以看成是fun()这个函数执行之后,它的返回值是一个函数指针,这个函数指针(其实就是上面的p)所指向的函数是一个不带任何参数,并且返回值为int的一个函数.
void (*signal(int signo, void (*handler)(int)))(int);就可以看成是signal()函数(它自己是带两个参数,一个为整型,一个为函数指针的函数),而这个signal()函数的返回值也为一个函数指针,这个函数指针指向一个带一个整型参数,并且返回值为void的一个函数.
*/
进程在接收到一个信号时,通常可以进行三种处理:
-
忽略此信号:大多数信号都可使用这种方式进行处理.
但有两种信号不能被忽略:SIGKILL和SIGSTOP。(它们向超级用户提供一种进程终止或或停止的可靠方法)
signal(SIGINT, SIG_IGN)//忽略SIGINT信号
- 捕捉信号:为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。
#include <stdio.h>
#include <signal.h>
void fun(int sig)
{
printf("sig:%", sig);//输出SIGINT的编号2
}
int main()
{
signal(SIGINT, fun);
while(1);
}
- 执行系统默认动作:对大多数信号的系统默认动作是终止该进程。
signal(SIGINT, SIG_DFL)//系统默认方式处理SIGINT
信号的继承
- 子进程会继承父进程的信号处理方式,直到子进程调用exec函数。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler1()
{
printf("hello world");
}
void handler2()
{
printf("nice time");
}
int main()
{
signal(SIGINT, handler1);
pid_t pid = fork();
if (pid == 0)
{
//子进程会继承父进程的信号处理方式
//收到SIGINT子进程输出hello world
getchar();
}
//收到SIGINT父进程输出hello world
getchar();
return 0;
}
int main()
{
signal(SIGINT, handler1);
pid_t pid = fork();
if (pid == 0)
{
signal(SIGINT, handler2);
//收到SIGINT信号子进程输出nice time
getchar();
}
//收到SIGINT父进程输出hello world
getchar();
return 0;
}
-
子进程调用exec函数后,exec将父进程中设置为捕捉的信号变为默认处理方式,其余不变(SIG_IGN)。例如在父进程中把SIGTERM设置为捕捉,SIGINT设置为忽略。子进程执行exec和不执行exec的区别。
int main() { signal(SIGINT, handler1); pid_t pid = fork(); if (pid == 0) { //收到SIGINT信号子进程结束(变为默认处理方式) execl("b.out"); } //收到SIGINT父进程输出hello world getchar(); return 0; }
int main() { signal(SIGINT, SIG_IGN); pid_t pid = fork(); if (pid == 0) { //收到SIGINT信号子进程忽略信号 //因为b.out里面没有handler自定义函数 execl("b.out"); } //收到SIGINT父进程忽略信号 getchar(); return 0; }
信号集
-
目前Linux系统定义了64个信号,以后也可能进一步扩展。所以用类型sigset_t来表示所有的信号。
-
一般情况下,sigset_t中的一个比特位表示一种信号。
-
对信号集的操作需要专用的操作函数。
-
信号0是系统保留,不被使用的。
-
信号集操作函数有以下5个
#include <signal.h>
//把所有信号集的比特位置为0
int sigemptyset(sigset_t *set);
//把所有信号集的比特位设为1
int sigfillset(sigset_t *set);
//把信号signum加入信号集set中
int sigaddset(sigset_t *set, int signum);sigaddset(&set, SIGINT);
//把信号signum从信号集set中清除
int sigdelset(sigset_t *set, int signum);
//判断某个信号是否在信号集set中
int sigismember(const sigset_t *set, int signum);
- 使用例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main()
{
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, 2);
sigaddset(&sigset, 5);
sigaddset(&sigset, 17);
sigaddset(&sigset, 22);
int i;
for (i = 1; i <= 64; i++)
{
if (i == 32 || i == 33)
continue;
if (sigismember(&sigset, i))
{
printf("i:%d\n", i);//打印出信号集里的所有信号
}
}
}
sigaction
Linux中signal注册函数最终也是调用sigaction来实现,保留signal主要是为了兼容。
声明:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
-
signum:指定需要处理的信号(除SIGKILL和SIGSTOP)。
-
act:设定信号的处理方式,act可以为NULL。
-
oldact:之前设定的信号处理方式会保存到第三个参数oldact,oldact为NULL时不保存。
返回0成功,返回-1失败。
参数结构体定义:
struct sigaction {
void (*sa_handler)(int); //不可靠信号信号处理方式
/*sa_handler和signal函数的第二个参数类型(函数指针)一样,当信号递送给进程时会调
用这个sa_handler.
*/
void (*sa_sigaction)(int, siginfo_t *, void *); //可靠信号的信号处理方式
/*sa_sigaction也是信号处理的函数指针,它只会在sa_flags包含SA_SIGINFO时才会被
调用。否则linux内核默认调用sa_handler。siginfo_t 包含了信号产生的原因。不用同
时赋值给sa_handler和sa_sigaction,因为它们可能是一个union。
*/
sigset_t sa_mask;//在处理信号的过程中,要屏蔽掉的信号
/*
信号屏蔽字,当执行sa_handler信号处理函数时,sa_mask指定的信号会被阻塞,直到该信
号处理函数返回。当前信号处理函数对应的信号会被自动加入到sa_mask中。
*/
int sa_flags;
/*
用来改变信号处理时的行为。当sa_flags包含SA_RESTART时,被中断的系统调用在信号处
理完后会被自动启动。当值包含SA_RESETHAND时,对此信号的处理方式在此信号捕捉函数的
入口处重置为SIG_DFL,对应于早期不可靠信号。
Act.sa_flags = SA_RESTART;
*/
void (*sa_restorer)(void); /* obsolete */
}
信号阻塞
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void fun()
{
sleep(10);
printf("I woke up");
}
int main()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = fun;
//把信号集act.sa_mask比特位全部置为1;屏蔽掉所有的信号,信号处理函数在运行时,就不会被打断
sigfillset(&act.sa_mask);
act.sa_flags = SA_RESTART;
sigaction(SIGHUP, &act, NULL);
while (1);
return 0;
}
可重入与不可重入函数
-
由于信号处理函数可被中断,所以在处理函数中调用的函数都必须是可重入的
-
可重入函数:指一个可以被多个任务调用的过程(函数),任务在调用时不必担心数据是否会出错。因为进程在收到信号后,就将跳转到信号处理函数去接着执行。如果信号处理函数中使用了不可重入函数,那么信号处理函数可能会修改原来进程中不应该被修改的数据,这样进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果。不可再入函数在信号处理函数中被视为不安全函数
-
满足下列条件的函数多数是不可重入的:
(1)使用静态的数据结构或者变量或者使用了全局的对象或变量的函数,如strtok(),gmtime(),getgrgid(),gethostbyname(),localtime()以及getpwnam()等等;
(2)实现时使用了标准I/O函数的。标准I / O库的很多实现都以不可再入方式使用全局数据结构。
tips:fun_r()可重入函数
发送信号
- kill-------------kill函数可以向某个进程或者进程组发送特定的信号
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- raise----------函数向调用进程发送一个信号(给自己发送信号),相当于kill(getpid(),signum)
#include <signal.h>
int raise(int sig);
信号屏蔽字
sigprocmask
-
每个进程都会有一个信号屏蔽字,它规定了当前那些信号可以递送,哪些信号需要阻塞。
-
当程序执行敏感任务时(比如更新数据库),不希望外界信号中断程序的运行。在这个阶段并不是简单地忽略信号,而是阻塞这些信号,当进程处理完关键任务后,还会处理这些信号。
-
sigprocmask可以检测和修改进程的信号屏蔽字。oldset非空时,当前信号屏蔽字会保存到oldset中。如果set非空,那么参数how指示如何修改当前屏蔽字。
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); /*how有三个值: SIG_BLOCK 被阻塞的信号是当前屏蔽字加上第二个参数包含的信号 SIG_UNBLOCK 第二个参数包含的信号从当前屏蔽字中移除 SIG_SETMASK 当前屏蔽字被完全设置为第二个参数的值。 */
sigpending
#include <signal.h>
int sigpending(sigset_t *set);
-
sigpending返回当前处于阻塞递送的信号,即信号已经产生,但还没有递送给进程处理。
-
成功返回0,失败返回-1
-
在sigpending中,如果我们在解除某个信号调用sleep函数,那么未被递送的信号会首先被处理。导致pause无法被唤醒。
-
需要把解除信号和等待信号递送做成一个原子操作,这样就不会产生上述问题。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
/*how有三个值:
SIG_BLOCK 被阻塞的信号是当前屏蔽字加上第二个参数包含的信号
SIG_UNBLOCK 第二个参数包含的信号从当前屏蔽字中移除
SIG_SETMASK 当前屏蔽字被完全设置为第二个参数的值。
*/
eg:
sigset_t set
sigset_t set2
//所有信号加到信号集set
sigfillset(&set);
//设置屏障
sigprocmask(SIG_SETMASK, &set, NULL);
//敏感任务
meeting();
//返回挡在门外的信号集set2
sigpending(&set2);
//找出挡在门外的信号
for 1 to 64
sigismember();
//解除屏障
sigemptyset(set); //信号集清空
sigprocmask(SIG_SETMASK, &set, NULL);