|
人们很容易高估某个决定性时刻的重要性,也很容易低估每天进行微小改进的价值。以前我也以为大规模的成功需要大规模的行动,现在我不这么认为了。长期来看,由于复利效果,一点小小的改进就能产生惊人的变化。 还有一点值得注意的情况,大多数人有了家庭和子女后,并且现在国内盛行加班文化,很难再集中精力能抽出大块的时间进行学习了,部分还能坚持学习的人几乎都是以牺牲睡眠时间为代价的,我个人不太认为这种做法,我始终认为有更合理健康的方法能形成一个工作、生活、学习、娱乐的有效循环,或许认识到 微进步 的重要性就是一个很好的开始吧。 本文就是我的微进步,欢迎阅读。 一、概述信号有时被称为提供处理异步事件机制的软件中断,与硬件中断的相似之处在于打断了程序执行的正常流程,很多比较重要的应用程序都需处理信号。事件可以来自于系统外部,例如用户按下 Ctrl+C,或者来自程序或者内核的某些操作。作为一种进程间通信 (IPC) 的基本形式,进行可以给另一个进程发送信号。 信号很早就是 Unix 的一部分。随着时间的推移,信号有了很大的改进。比如在可靠性方面,之前的信号可能会出现丢失的情况。在功能方面,现在信号可以携带用户定义的附加信息。最初,不同的 Unix 系统对信号的修改,后来,POSIX 标准的到来挽救并且标准化了信号机制。
信号类型: $ man 7 signalDESCRIPTION Standard signals First the signals described in the original POSIX.1-1990 standard. Signal Value Action Comment ────────────────────────────────────────────────────────────────────── SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process SIGINT 2 Term Interrupt from keyboard SIGQUIT 3 Core Quit from keyboard SIGILL 4 Core Illegal Instruction SIGABRT 6 Core Abort signal from abort(3) SIGFPE 8 Core Floating point exception SIGKILL 9 Term Kill signal SIGSEGV 11 Core Invalid memory reference SIGPIPE 13 Term Broken pipe: write to pipe with no readers SIGALRM 14 Term Timer signal from alarm(2) SIGTERM 15 Term Termination signal SIGUSR1 30,10,16 Term User-defined signal 1 SIGUSR2 31,12,17 Term User-defined signal 2 SIGCHLD 20,17,18 Ign Child stopped or terminated SIGCONT 19,18,25 Cont Continue if stopped SIGSTOP 17,19,23 Stop Stop process SIGTSTP 18,20,24 Stop Stop typed at terminal SIGTTIN 21,21,26 Stop Terminal input for background process SIGTTOU 22,22,27 Stop Terminal output for background process The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. Next the signals not in the POSIX.1-1990 standard but described in SUSv2 and POSIX.1-2001. Signal Value Action Comment ──────────────────────────────────────────────────────────────────── SIGBUS 10,7,10 Core Bus error (bad memory access) SIGPOLL Term Pollable event (Sys V). Synonym for SIGIO SIGPROF 27,27,29 Term Profiling timer expired SIGSYS 12,31,12 Core Bad argument to routine (SVr4) SIGTRAP 5 Core Trace/breakpoint trap SIGURG 16,23,21 Ign Urgent condition on socket (4.2BSD) SIGVTALRM 26,26,28 Term Virtual alarm clock (4.2BSD) SIGXCPU 24,24,30 Core CPU time limit exceeded (4.2BSD) SIGXFSZ 25,25,31 Core File size limit exceeded (4.2BSD) ... Next various other signals. Signal Value Action Comment ──────────────────────────────────────────────────────────────────── SIGIOT 6 Core IOT trap. A synonym for SIGABRT SIGEMT 7,-,7 Term SIGSTKFLT -,16,- Term Stack fault on coprocessor (unused) SIGIO 23,29,22 Term I/O now possible (4.2BSD) SIGCLD -,-,18 Ign A synonym for SIGCHLD SIGPWR 29,30,19 Term Power failure (System V) SIGINFO 29,-,- A synonym for SIGPWR SIGLOST -,-,- Term File lock lost (unused) SIGWINCH 28,28,20 Ign Window resize signal (4.3BSD, Sun) SIGUNUSED -,31,- Core Synonymous with SIGSYS (Signal 29 is SIGINFO / SIGPWR on an alpha but SIGLOST on a sparc.) 发送信号:
处理信号: Unix 系统提供了两种方法来改变信号处置:signal() 和 sigaction()。signal()系统调用是设置信号处置的原始 API,所提供的接口比sigaction() 简单。另一方面,sigaction() 提供了 signal() 所不具备的功能。进一步而言,signal() 的行为在不同 Unix 实现间存在差异,这意味着对可移植性有所追求的程序绝不能使用此调用来建立信号处理函数 (signal handler)。故此,sigaction()是建立信号处理器的首选API。 由于可能会在许多老程序中看到 signal() 的应用,我们先了解如何用 signal() 函数来处理信号。 signal() 的定义: $ man 2 signal#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
简单试用 signal()。 分解代码: static void ouch(int sig) {printf("OUCH! - I got signal %d\n", sig); (void) signal(SIGINT, SIG_DFL); } int main() { (void) signal(SIGINT, ouch); while(1) { printf("Hello World!\n"); sleep(1); } } 运行效果: $ ./ctrlc1 Hello World! Hello World! ^COUCH! - I got signal 2 Hello World! Hello World! 相关要点:
进程可以通过调用 kill 函数向包括它本身在内的其他进程发送一个信号。 kill(): $ man 2 kill#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); 把参数 sig 给指定的信号发送给由参数 pid 指定的进程号所指定的进程。 kill 调用会在失败时返回 -1 并设置 errno 变量,失败的原因:
关于权限: 要想发送一个信号,发送进程必须拥有相应的权限,包括2种情况:
进程可以通过调用 alarm() 函数在经过预定时间后发送一个 SIGALRM 信号。 alarm(): $ man 2 alarm#include <unistd.h> unsigned int alarm(unsigned int seconds);
相关要点:
用 kill() 模拟闹钟。 分解代码: int main()设置 signal handler: { pid_t pid; printf("alarm application starting\n"); pid = fork(); switch(pid) { case -1: /* Failure */ perror("fork failed"); exit(1); case 0: /* child */ sleep(5); kill(getppid(), SIGALRM); exit(0); } /* parent */ printf("waiting for alarm to go off\n"); (void) signal(SIGALRM, ding); pause(); if (alarm_fired) printf("Ding!\n"); printf("done\n"); exit(0); } 定义 signal handler: static int alarm_fired = 0;static void ding(int sig) { alarm_fired = 1; } 通过 fork 调用启动新的进程:子进程休眠 5 秒后向其父进程发送一个 SIGALRM 信号。父进程在安排好捕获 SIGALRM 信号后暂停运行,直到接收到一个信号为止。 运行效果: $ ./alarm alarm application starting waiting for alarm to go off <等待5 秒钟> Ding! done 相关要点:
三、信号集 (Signal Set) 多个信号可使用一个称之为信号集的数据结构来表示,POSIX.1 定义了数据类型 sigset_t 以表示一个信号集,并且定义了下列 5 个处理信号集的函数: $ man 3 sigemptysetNAME sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations SYNOPSIS #include <signal.h> int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset(sigset_t *set, int signum); int sigdelset(sigset_t *set, int signum); int sigismember(const sigset_t *set, int signum);
每个进程都有一个信号屏蔽字(或称信号掩码,signal mask),它规定了当前要阻塞递送到该进程的信号集。对于每种信号,屏蔽字中都有一位与之对应。对于某种信号,若其对应位被设置,则它当前是被阻塞的。进程可以调用 sigprocmask() 检测或更改,或同时进行检测和更改进程的信号屏蔽字。 向信号屏蔽字中添加信号的3种方式:
先来了解 sigprocmask(): $ man 2 sigprocmask#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 相关知识点:
sigemptyset(&blockSet); /* 1. Block SIGINT, save previous signal mask */ sigaddset(&blockSet, SIGINT); if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1) errExit("sigprocmask1"); /* 2. Code that should not be interrupted by SIGINT */ /* 3. Restore previous signal mask, unblocking SIGINT */ if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1) errExit("sigprocmask2"); 4.2 实验 demo main() 函数: 1> 为所有信号注册同一个信号处理函数,用于验证信号集是否被成功屏蔽: static void handler(int sig){ if (sig == SIGINT) gotSigint = 1; else sigCnt[sig]++; } int main(int argc, char *argv[]) { int n, numSecs; sigset_t fullMask, emptyMask; printf("%s: PID is %ld\n", argv[0], (long) getpid()); for (n = 1; n < NSIG; n++) (void) signal(n, handler); // UNSAFE ... } 注意:siganl() 是不可靠的,这里为了简化程序而采用该接口。 2> 初始化信号集,然后屏蔽所有信号: sigfillset(&fullMask);if (sigprocmask(SIG_SETMASK, &fullMask, NULL) == -1) { perror("sigprocmask"); exit(EXIT_FAILURE); } printf("%s: sleeping for %d seconds\n", argv[0], numSecs); sleep(numSecs); 先屏蔽所有的信号,然后睡眠。睡眠期间,进程无法响应除 SIGSTOP 和 SIGKILL 之外的任何信号。 3> 睡眠结束后,用空信号集来解除所有的信号屏蔽: sigemptyset(&emptyMask); /* Unblock all signals */if (sigprocmask(SIG_SETMASK, &emptyMask, NULL) == -1) { perror("sigprocmask"); exit(EXIT_FAILURE); } while (!gotSigint) /* Loop until SIGINT caught */ continue; for (n = 1; n < NSIG; n++) if (sigCnt[n] != 0) printf("%s: signal %d caught %d time%s\n", argv[0], n, sigCnt[n], (sigCnt[n] == 1) ? "" : "s"); exit(EXIT_SUCCESS); } 解除了对某个等待信号的屏蔽后,系统会立刻将该信号传递一次给进程。 打印信号集 printSigset(): void printSigset(FILE *of, const char *prefix, const sigset_t *sigset){ int sig, cnt; cnt = 0; for (sig = 1; sig < NSIG; sig++) { if (sigismember(sigset, sig)) { cnt++; fprintf(of, "%s%d (%s)\n", prefix, sig, strsignal(sig)); } } if (cnt == 0) fprintf(of, "%s<empty signal set>\n", prefix); } 3. 运行效果: $ ./signal_set 5屏蔽期间多次按下 ctrl + c (发送 SIGINT): ./signal_set: PID is 18375 blocked:1 (Hangup) blocked:2 (Interrupt) blocked:3 (Quit) ... blocked:64 (Real-time signal 30) ./signal_set: sleeping for 5 seconds ^C^C^Cblocked:<empty signal set> ./signal_set: signal 2 caught 1 time 在信号被屏蔽的 5 秒期间,连续按下 3 次 ctrl + c,所有信号都不会被处理。当过了 5 秒后,解除信号屏蔽,仅仅有一次 SIGINT 信号被成功地传递并处理。 五、等待中的信号 (Pending Signals)如果某进程接受了一个该进程正在阻塞的信号,那么会将该信号填加到进程的等待信号集中。当解除对该信号的锁定时,会随之将信号传递给此进程。为了确定进程中处于等待状态的是哪些信号,可以使用 sigpending()。 $ man 2 sigpendingNAME sigpending, rt_sigpending - examine pending signals SYNOPSIS #include <signal.h> int sigpending(sigset_t *set); DESCRIPTION sigpending() returns the set of signals that are pending for delivery to the calling thread (i.e., the signals which have been raised while blocked). The mask of pending signals is returned in set. sigpending() 为调用进程返回处于等待状态的信号集,并将其置于 set 指向的sigset_t 中。 相关知识点:
如果某进程接受了一个该进程正在阻塞 (blocking) 的信号,那么会将该信号填加到进程的 等待信号集 (set of pending signals) 中。当解除对该信号的阻塞时,会随之将信号传递给此进程。可以使用 sigpending() 确定进程中处于等待状态的是哪些信号。 $ man 2 sigpending#include <signal.h> int sigpending(sigset_t *set); sigpending() 为调用进程返回处于等待状态的信号集,并将其置于参数 set 指向的 sigset_t 中。 1. 一个简单的例子 (sig_pending.c)1) 分解代码: int main(void)1> main(): { sigset_t newmask, oldmask, pendmask; if (signal(SIGQUIT, sig_quit) == SIG_ERR) err_sys("can't catch SIGQUIT"); /* Block SIGQUIT and save current signal mask. */ sigemptyset(&newmask); sigaddset(&newmask, SIGQUIT); if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) err_sys("SIG_BLOCK error"); /* SIGQUIT here will remain pending */ sleep(5); if (sigpending(&pendmask) < 0) err_sys("sigpending error"); if (sigismember(&pendmask, SIGQUIT)) printf("\nSIGQUIT pending\n"); /* Restore signal mask which unblocks SIGQUIT. */ if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) err_sys("SIG_SETMASK error"); printf("SIGQUIT unblocked\n"); /* SIGQUIT here will terminate with core file */ sleep(5); exit(0); } main() 做了 5 件事:
注意:在设置 SIGQUIT 为阻塞时,我们保存了老的屏蔽字。为了解除对该信号的阻塞,用老的屏蔽字重新设置了进程信号屏蔽字。另一种方法是用 SIG_UNBLOCK 使阻塞的信号不再阻塞。如果编写一个可能由其他人使用的函数,而且需要在函数中阻塞一个信号,则不能用 SIG_UNBLOCK 简单地解除对此信号的阻塞,这是因为此函数的调用者在调用本函数之前可能也阻塞了此信号。 2> 信号处理函数 sig_quit(): static void sig_quit(int signo){ printf("caught SIGQUIT\n"); if (signal(SIGQUIT, SIG_DFL) == SIG_ERR) err_sys("can't reset SIGQUIT"); } 2) 运行效果: $ ./sig_pending ^\ // 按下 1 次 ctrl + \ (在5s之内) SIGQUIT pending // 从 sleep(5) 返回后 caught SIGQUIT // 在信号处理程序中 SIGQUIT unblocked // 从sigprocmask() 返回 ^\Quit (core dumped) 2 个值得注意的点:
七、不对待处理的信号进行排队处理 等待信号集只是一个掩码,仅表明一个信号是否发生,而未表明其发生的次数。换言之,如果同一信号在阻塞状态下产生多次,那么会将该信号记录在等待信号集中,并在稍后仅传递一次。后面会介绍实时信号,对实时信号所采取的是队列化管理。如果将某一实时信号的多个实例发送给一进程,那么将会多次传递该实时信号,暂不做深入介绍。 1. 仍是那个简单的例子 (sig_pending.c)为了降低学习难度,跟前面的 Pending Signals 章节使用同一个例子,修改一下测试步骤: $ ./sig_pending ^\^\^\ // 按下 3 次 ctrl + \ (在5s之内) SIGQUIT pending // 从 sleep(5) 返回后 caught SIGQUIT // 只调用了一次信号处理程序 SIGQUIT unblocked // 从sigprocmask() 返回 ^\Quit (core dumped) 第二次运行该程序时,在进程休眠期间产生了 3 次 SIGQUIT 信号,但是取消对该信号的阻塞后,系统只向进程传送了一次 SIGQUIT,从中可以看出在 Linux 系统上没有对信号进行排队处理。 2. 查看 Linux 内核里 Signal Pending 相关的实现 (非重点)1) 相关数据结构 <sched.h>内核用 struct task_struct 来描述一个进程,struct task_struct 中信号相关的成员 (Linux-4.14): struct task_struct { ... /* Signal handlers: */ struct signal_struct *signal; struct sighand_struct *sighand; sigset_t blocked; sigset_t real_blocked; /* Restored if set_restore_sigmask() was used: */ sigset_t saved_sigmask; struct sigpending pending; unsigned long sas_ss_sp; size_t sas_ss_size; unsigned int sas_ss_flags; ... }; 我们将注意力集中中 struct sigpending pending 上。struct sigpending pending 建立了一个链表,该链表包含了所有已经产生、且有待内核处理的信号,其定义如下: struct sigpending {struct list_head list; sigset_t signal; };
unsigned long sig[_NSIG_WORDS]; } sigset_t;
struct list_head list; int flags; siginfo_t info; ... };
注意:在 struct sigpending 链表中,struct sigqueue 对应的是一种类型的待处理信号,而不是某一个具体的信号。 示意图:
2) 信号的产生 当给进程发送一个信号时,这个信号可能来自内核,也可能来自另外一个进程。 内核里有多个 API 能产生信号,这些 API 最终都会调用 send_signal()。我们重点关注信号是何时被设置为 pending 状态的。 linux/kernel/signal.c: send_signal()__send_signal() struct sigqueue *q = __sigqueue_alloc(); list_add_tail(&q->list, &pending->list); // 将待处理信号添加到 pending 链表中 sigaddset(&pending->signal, sig); // 在位图中将信号对应的 bit 置 1 complete_signal(sig, t, group); signal_wake_up(); send_signal() 会分配一个新的 struct sigqueue 实例,然后为其填充信号的额外信息,并添加到目标进程的 sigpending 链表且设置位图。 如果信号成功发送,没有被阻塞,就可以用 signal_wake_up() 唤醒目标进程,使得调度器可以选择目标进程运行。 3) 信号的传递: 这些知识放在这篇文章里已经完全超纲了,如果将所有的细节都暴露出来会让初学者感到极度的困惑。 所以,我们只迈出一小步,将仅剩的一点注意力集中在内核在执行信号处理函数前是如何处理 pending 信号的。 在每次由内核态切换到用户态时,内核都会进行信号处理,最终的效果就是调用 do_signal() 函数。 linux/kernel/signal.c: do_signal()get_signal() dequeue_signal(current, ¤t->blocked, &ksig->info); handle_signal() signal_setup_done(); signal_delivered();
int sig = next_signal(pending, mask); collect_signal(sig, pending, info, resched_timer); sigdelset(&list->signal, sig); // 取消信号的 pending 状态 list_del_init(&first->list); // 删除 pending 链表中的 struct sigqueue 节点 copy_siginfo(info, &first->info);
|
微信公众号
手机版