信号

    技术2026-06-16  7

    目录

    信号概念core 文件(调试用) 信号的使用信号的处理signal函数用来通知内核如何处理某个特定信号(忽略、捕捉、默认处理)。 信号的继承 信号集sigaction声明:参数:参数结构体定义:信号阻塞 **可重入与不可重入函数**发送信号信号屏蔽字sigprocmasksigpending

    信号

    概念

    信号的产生:

    当引发信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。信号产生时,内核会在进程表中设置一位标识。

    信号的递送(delivery):

    当进程对信号采取动作(执行信号处理函数或忽略)时称为递送。

    信号产生和递送之间的时间间隔内称信号是未决的(pending)。

    信号递送阻塞(block):

    进程可指定对某个信号采用递送阻塞,如果此时信号的处理是系统默认动作或者捕捉该信号,则该信号就会处于未决的状态,直到进程解除对该信号的递送阻塞或者处理方式改为忽略。

    来了一个信号,信号产生标记变为1,此时检测信号屏蔽字是否为0;为0可以递送,递送走之后,信号产生标记变为0,信号屏蔽字变为1;当捕捉函数接收递送的信号并结束,信号屏蔽字变为0。(类似于接电话,接通第一个电话的时候,接不了第二个电话) 如果信号的处理方式是忽略该信号,那么该信号永远不会处于递送或者递送未决状态


    非实时信号: 1~31(不支持排队,信号同一时间太多,会丢失,不可靠信号)

    信号名称编号信号说明默认处理SIGHUP1终端关闭时产生这个信号进程终止SIGINT2终端输入了中断字符ctrl+c进程终止SIGKILL9用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。进程终止SIGSEGV11试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据,默认打印出segment fault。进程终止并且产生core文件SIGPIPE13往管道写时,读者已经不在了,或者往一个已断开数据流socket写数据。发送的进程会收到进程终止SIGALRM14时钟定时信号, 计算的是实际的时间或时钟时间。alarm 函数使用该信号。进程终止SIGTERM15有kill函数调用产生。进程终止SIGCHLD17子进程停止或者终止时,父进程会收到该信号。忽略该信号SIGSTOP19用来停止一个进程。本信号不能忽略,也不能被捕捉进程暂停执行

    实时信号: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);
    Processed: 0.008, SQL: 9