Linux C编程实战:信号机制原理、安全处理与高级应用
1. 项目概述从信号到实战在Linux C编程的世界里信号Signal是一个既基础又核心的概念。它就像是操作系统与进程之间或者进程与进程之间的一种“紧急电报”或“中断通知”。想象一下你正在一个命令行终端里运行一个耗时很长的计算程序突然想让它停下来于是你按下了CtrlC。这个操作并没有直接去修改程序的内存或寄存器而是由终端捕获了键盘输入向你的计算程序发送了一个名为SIGINT的信号。程序收到这个信号后才执行了终止自身的动作。这就是信号最直观的体现——异步事件通知。我之所以花时间整理《Linux C编程实战》中关于信号发送与处理的笔记是因为在实际开发中信号处理的好坏直接关系到程序的健壮性、可维护性甚至是安全性。无论是编写一个需要优雅退出的守护进程Daemon还是实现一个能处理用户中断的交互式工具亦或是进行多进程间的协同与控制信号机制都是你必须熟练掌握的武器。很多初学者包括当年的我往往只停留在知道signal()函数和几个常见信号名的层面一旦遇到信号竞争、信号丢失、不可重入函数调用等深水区问题就容易束手无策。因此这个“实战笔记”的目的就是结合书中的理论融入我这些年踩过的坑和总结的经验把信号的发送、捕获、处理这一整套流程掰开揉碎让你不仅能看懂代码更能写出在生产环境中稳定运行的代码。2. 信号机制核心原理与常见信号解析2.1 信号的本质与生命周期信号本质上是一个整数编号在Linux系统中这些编号被定义在signal.h头文件中并赋予了有意义的宏名如SIGINT(2),SIGKILL(9),SIGTERM(15) 等。它的生命周期可以概括为“产生-》递送-》处理”三个阶段。产生Generation信号产生的原因多种多样。除了用户按键CtrlC产生SIGINT,Ctrl\产生SIGQUIT外还包括硬件异常例如进程访问了非法内存地址硬件会触发一个页面错误内核随后向该进程发送SIGSEGV段错误信号。软件条件例如子进程终止时内核会向其父进程发送SIGCHLD信号当进程向一个已经关闭的管道Pipe写入数据时会收到SIGPIPE信号。系统调用最典型的就是kill()系统调用一个进程可以主动向另一个进程或自身发送信号。Shell命令我们在终端里输入的kill -9 PID命令就是通过kill()系统调用实现的。递送Delivery信号产生后内核会在目标进程的进程控制块PCB中设置一个标志位表示该信号处于“未决”Pending状态。如果目标进程当前正在执行并且没有被阻塞Block该信号内核就会在合适的时机通常是从内核态返回用户态之前将信号递送给进程。处理Handling进程收到递送来的信号后会采取相应的动作。动作分为三类忽略Ignore除了SIGKILL和SIGSTOP这两个信号不能被忽略外进程可以忽略大多数信号。默认Default执行系统预定义的动作大多数信号的默认动作是终止进程有些会附带核心转储Core Dump如SIGSEGV。捕获Catch进程可以预先注册一个函数称为信号处理函数Signal Handler。当信号递送时内核会临时中断进程的正常执行流转而去执行这个处理函数。执行完毕后再通常返回到被中断的地方继续执行。这是我们编程中需要重点掌控的部分。注意信号是异步的。这意味着信号处理函数可能在程序执行的任何时间点被调用这给编程带来了复杂性我们后面会详细讨论如何编写安全的信号处理函数。2.2 你必须认识的几个关键信号在几十个标准信号中有几个出场率极高必须深刻理解SIGINT (2)中断信号。由终端中断键通常是CtrlC产生。默认动作是终止进程。我们常通过捕获它来实现程序的“优雅退出”例如保存当前工作状态后再退出。SIGTERM (15)终止信号。这是kill命令不加参数时发送的信号。它是请求程序“正常终止”的标准方式允许程序进行清理工作。与SIGKILL相比它更友好。SIGKILL (9)杀死信号。这个信号不能被捕获、阻塞或忽略。它提供了一种确保进程终止的终极手段。当你使用kill -9 PID时进程会立即被操作系统强制结束没有任何机会进行清理。在编写需要高可靠性的程序时应尽量避免依赖外部发送SIGKILL同时也要考虑自己的程序被SIGKILL时可能造成的状态不一致问题。SIGSEGV (11)段错误信号。当进程试图访问不属于它的内存区域如空指针解引用、数组越界访问栈或堆外空间时产生。默认动作是终止进程并产生核心转储文件core dump用于事后调试。SIGCHLD (17)子进程状态改变信号。当子进程终止或停止时内核会向父进程发送此信号。这是处理僵尸进程Zombie Process的关键。如果父进程不等待wait子进程也不处理SIGCHLD信号子进程在终止后会成为僵尸进程占用系统进程表资源。SIGPIPE (13)管道破裂信号。当进程向一个没有读端的管道或套接字Socket写入数据时产生。对于网络服务器如果不处理这个信号默认动作会导致服务器进程意外终止。通常的做法是忽略它signal(SIGPIPE, SIG_IGN)然后在write或send调用中检查返回值来处理错误。3. 信号的发送不止是kill命令发送信号是触发信号处理流程的起点。最广为人知的是kill()系统调用和kill命令但方式远不止于此。3.1 kill() 系统调用详解kill()的函数原型是int kill(pid_t pid, int sig);。它的核心在于第一个参数pid的取值这决定了信号发送给谁pid 0发送给进程ID为pid的特定进程。pid 0发送给与调用进程属于同一个进程组的所有进程。这在管理一组相关进程时非常有用。pid -1发送给调用进程有权限发送信号的所有进程除了init进程和自身。普通用户通常只能发给自己的进程。pid -1发送给进程组ID等于|pid|的所有进程。实操心得在编写多进程程序比如一个“Master-Worker”模型的服务器时Master进程需要管理多个Worker子进程。当Master需要优雅关闭所有Worker时它可以调用kill(0, SIGTERM)如果Master和Workers在同一进程组或者遍历自己创建的子进程PID列表逐一发送SIGTERM。相比之下前者更简洁但要求进程组设置正确。3.2 raise() 与 alarm()向自己发送信号raise(int sig)向调用进程自身发送信号sig。它等价于kill(getpid(), sig)。常用于触发自身的信号处理逻辑进行测试或者在检测到严重错误时主动终止自己例如raise(SIGTERM)。alarm(unsigned int seconds)设置一个定时器在指定的秒数seconds后内核会向调用进程发送SIGALRM信号。如果之前已经设置过定时器alarm()会返回旧定时器剩余的秒数并用新值替换旧值。将seconds设为0可以取消一个已设置的定时器。一个经典应用场景为可能阻塞的系统调用如read从终端、open一个FIFO、connect网络连接设置超时。#include unistd.h #include signal.h #include stdio.h #include errno.h void timeout_handler(int sig) { // 什么也不做只是为了中断阻塞的系统调用 } int read_with_timeout(int fd, void *buf, size_t count, int timeout_sec) { struct sigaction sa, old_sa; sa.sa_handler timeout_handler; sigemptyset(sa.sa_mask); sa.sa_flags 0; sigaction(SIGALRM, sa, old_sa); // 设置新的SIGALRM处理函数 alarm(timeout_sec); // 启动定时器 int n; do { n read(fd, buf, count); } while (n -1 errno EINTR); // 如果被信号中断重启read int saved_errno errno; alarm(0); // 取消定时器 sigaction(SIGALRM, old_sa, NULL); // 恢复旧的处理函数 errno saved_errno; return n; }注意上面的代码是一个简化示例实际使用中需要更精细地处理信号竞态条件例如alarm和read之间如果信号递送了怎么办。更现代、更推荐的做法是使用select、poll或epoll等I/O多路复用机制来实现超时它们更可靠且避免了信号处理带来的复杂性。3.3 终端与控制信号除了SIGINT和SIGQUIT终端还会产生其他控制信号SIGTSTP (20)终端挂起键CtrlZ产生。默认动作是停止进程Stopped进程被挂起到后台可以用fg或bg命令恢复。SIGCONT (18)这是一个“继续”信号。发给一个停止的进程使其恢复执行。它通常由Shell的fg/bg命令发送。4. 信号的处理从signal()到sigaction()注册信号处理函数是信号编程的核心。早期广泛使用signal()函数但在严肃的编程中强烈推荐使用sigaction()。4.1 signal()的简单与陷阱signal()用法简单void (*signal(int sig, void (*func)(int)))(int);。虽然原型看起来复杂但用起来直观#include signal.h #include stdio.h #include unistd.h void handler(int sig) { printf(“Caught signal %d\n”, sig); } int main() { signal(SIGINT, handler); // 捕获SIGINT while(1) { printf(“Running…\n”); sleep(1); } return 0; }运行这个程序按CtrlC会打印 “Caught signal 2”但程序不会退出因为默认行为被我们自定义的handler替换了。然而signal()的行为在历史上System V和BSD有差异且存在重大缺陷信号处理函数重置在某些实现中当信号处理函数被调用一次后对该信号的处理方式会被重置为默认行为SIG_DFL。这意味着如果你连续快速按两次CtrlC第一次被handler捕获第二次则直接以默认方式终止程序handler可能来不及完成清理工作。为了解决这个问题不得不在handler函数内部再次调用signal()重新注册自己但这又引入了新的竞态条件窗口。不可靠信号在信号处理函数执行期间如果同种信号再次产生行为是不确定的。可能被丢失也可能导致处理函数被重入递归调用引发不可预知的问题。正因为这些不可靠性sigaction()被引入以提供更精确、更可靠的控制。4.2 sigaction()可靠信号处理的基石sigaction()的函数原型是int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);。核心在于struct sigaction这个结构体它允许你指定处理函数和各种标志来控制信号行为。struct sigaction { void (*sa_handler)(int); // 简单的处理函数指针类似signal() void (*sa_sigaction)(int, siginfo_t *, void *); // 能获取更多信息的处理函数 sigset_t sa_mask; // 在执行处理函数期间需要额外阻塞的信号集 int sa_flags; // 修改信号行为的标志位 void (*sa_restorer)(void); // 已废弃不要使用 };关键字段解析sa_handler / sa_sigaction指定信号处理函数。如果sa_flags包含了SA_SIGINFO标志则使用sa_sigaction否则使用sa_handler。sa_sigaction能通过siginfo_t结构体获取信号的来源、发送者PID等信息功能更强大。sa_mask这是一个信号集signal set。在调用信号处理函数之前内核会自动将当前正在处理的信号加入进程的信号掩码即阻塞它防止在处理函数中重入。通过sa_mask你可以指定在处理函数执行期间还需要阻塞哪些其他信号。这是编写安全处理函数的关键。sa_flags一系列标志位用于精细控制信号行为。最重要的几个是SA_RESTART如果设置被此信号中断的某些“慢”系统调用如read,write,accept等会自动重启。这能简化编程但并非所有系统调用都支持。对于需要超时控制的场景通常不设置此标志。SA_SIGINFO使用sa_sigaction而非sa_handler作为处理函数。SA_RESETHAND模拟不可靠的signal()行为处理函数执行一次后重置为默认动作。应避免使用。SA_NODEFER/SA_NOMASK不自动阻塞当前正被处理的信号。这意味着处理函数可能被重入非常危险除非你非常清楚自己在做什么否则不要使用。一个使用sigaction的可靠SIGINT处理示例#include stdio.h #include stdlib.h #include signal.h #include unistd.h void graceful_shutdown(int sig) { // 注意在信号处理函数中能安全调用的函数非常有限后面会讲 write(STDOUT_FILENO, “\nInitiating graceful shutdown…\n”, 31); // 这里可以设置一个全局标志位通知主循环退出 // volatile sig_atomic_t shutdown_flag 1; _exit(0); // 使用_exit而非exit避免刷新标准I/O缓冲区等可能不安全的操作 } int main() { struct sigaction sa; sa.sa_handler graceful_shutdown; sigemptyset(sa.sa_mask); // 初始化sa_mask为空 // 在处理SIGINT期间我们还希望阻塞SIGTERM防止两个关闭信号交织 sigaddset(sa.sa_mask, SIGTERM); sa.sa_flags 0; // 不设置SA_RESTART因为我们可能希望某些系统调用能被中断 if (sigaction(SIGINT, sa, NULL) -1) { perror(“sigaction”); exit(1); } // 同样设置SIGTERM的处理 if (sigaction(SIGTERM, sa, NULL) -1) { perror(“sigaction”); exit(1); } while (1) { // 主循环工作 printf(“Main loop working…\n”); sleep(2); } return 0; }5. 编写安全的信号处理函数规则与禁区信号处理函数运行在一个特殊的“信号上下文”中它异步地中断了主程序的正常执行流。因此在其中能做的事情受到严格限制。违反这些规则是导致程序出现诡异、难以调试问题的常见根源。5.1 异步信号安全函数在信号处理函数中只能调用“异步信号安全”async-signal-safe的函数。所谓异步信号安全是指该函数要么是可重入的不依赖全局或静态数据要么在实现上已经防止了信号处理函数调用它时产生竞态条件。POSIX标准明确列出了异步信号安全的函数。常见的有write系统调用但printf,puts等标准I/O函数不安全_exitsignal/sigaction但要注意在信号处理函数中修改自身信号的处理方式需格外小心kill部分字符串函数如strlen只读但strtok绝对不安全。绝对禁止在信号处理函数中调用非异步信号安全的函数例如malloc,free堆管理函数通常使用全局锁在信号处理中调用极易导致死锁。printf,sprintf,fprintf标准I/O库使用全局缓冲区和非原子操作。任何可能修改全局数据结构或静态变量的函数。实操心得如果需要在信号处理中记录日志或输出信息最安全的方法是使用write系统调用直接向文件描述符如STDERR_FILENO写入字符串。或者更常见的做法是信号处理函数只做最少的工作——通常是设置一个全局的、类型为volatile sig_atomic_t的标志变量。主程序循环定期检查这个标志并在安全的主程序上下文中执行实际的清理和退出逻辑。#include signal.h #include stdbool.h #include unistd.h volatile sig_atomic_t g_shutdown_requested 0; void handle_signal(int sig) { // 只做这一件事设置标志位 g_shutdown_requested 1; } int main() { // … 设置信号处理 … while (!g_shutdown_requested) { // 主循环工作 // 可以安全地调用任何函数 if (some_condition) { printf(“This is safe here.\n”); // 在主循环中printf是安全的 } // 模拟工作 sleep(1); } // 清理工作在主循环退出后进行这里是安全上下文 printf(“Performing cleanup…\n”); // … 释放资源关闭文件等 … printf(“Exiting gracefully.\n”); return 0; }5.2 全局变量的正确使用如上例所示在信号处理函数和主程序之间通信通常使用全局变量。但必须注意使用volatile防止编译器优化确保每次读取都从内存中获取最新值。使用sig_atomic_t类型这是C标准定义的整数类型保证在该类型变量上的读写是原子的即不会被信号中断。对于简单的标志位这足够了。对于更复杂的数据结构需要借助其他同步机制如后面会提到的“自管道技巧”。5.3 避免竞态条件与信号丢失信号的异步特性使得竞态条件很容易发生。例如检查-然后-行动Check-Then-Act主程序检查g_shutdown_requested 0然后进入一个可能阻塞的系统调用如sleep,read。如果在检查和进入调用之间信号到来并设置了标志但程序已经进入阻塞那么对信号的响应就会被延迟直到阻塞调用结束。如果阻塞调用是pause()它可能永远不会返回。信号合并在标准信号1-31中如果某个信号在处于未决状态时再次产生它可能只被记录一次即丢失了后续的信号。对于实时信号SIGRTMIN到SIGRTMAX则不会丢失会排队。解决方案使用sigsuspend或“自管道技巧”来原子性地解除信号阻塞并进入等待状态。6. 高级话题与实战技巧6.1 信号集与进程信号掩码进程可以通过“信号掩码”来阻塞Block一组信号。被阻塞的信号在解除阻塞之前不会递送给进程。相关函数有sigemptyset,sigfillset,sigaddset,sigdelset,sigismember操作信号集sigset_t。sigprocmask用于检查和更改进程的信号掩码。一个典型应用在程序初始化或执行关键代码段时临时阻塞某些信号如SIGINT防止被意外中断。sigset_t newmask, oldmask; sigemptyset(newmask); sigaddset(newmask, SIGINT); // 阻塞SIGINT if (sigprocmask(SIG_BLOCK, newmask, oldmask) 0) { perror(“SIG_BLOCK error”); } // 执行关键代码不会被SIGINT中断 perform_critical_section(); // 恢复旧的信号掩码 if (sigprocmask(SIG_SETMASK, oldmask, NULL) 0) { perror(“SIG_SETMASK error”); }6.2 等待信号sigsuspend的正确用法sigsuspend(const sigset_t *sigmask)是一个非常重要的系统调用。它原子性地执行两个操作1) 将进程的信号掩码替换为sigmask2) 使进程休眠直到一个未被sigmask阻塞的信号到达。当信号处理函数返回后sigsuspend返回并恢复调用前的信号掩码。这解决了前面提到的“检查-然后-行动”竞态问题。一个经典的模式是等待一个全局标志被信号处理函数设置volatile sig_atomic_t quit_flag 0; sigset_t zeromask, newmask, oldmask; sigemptyset(zeromask); sigemptyset(newmask); sigaddset(newmask, SIGINT); // 先阻塞SIGINT防止在检查标志和调用pause之间信号到达 sigprocmask(SIG_BLOCK, newmask, oldmask); while (quit_flag 0) { // 原子性地解除对SIGINT的阻塞并进入休眠 sigsuspend(zeromask); // zeromask不阻塞任何信号 } // 恢复旧信号掩码 sigprocmask(SIG_SETMASK, oldmask, NULL);6.3 自管道技巧将信号事件转化为I/O事件在多线程程序或复杂的事件驱动程序中混合使用信号和传统的I/O多路复用如select,poll,epoll会很棘手。一个优雅的解决方案是“自管道技巧”Self-Pipe Trick。原理创建一个管道pipe或更现代的eventfd。在信号处理函数中不做复杂操作只向这个管道的写端写入一个字节的数据。主程序将这个管道的读端加入到select/poll/epoll的监听集合中。这样信号事件就被“转换”成了一个普通的可读I/O事件可以在主事件循环中统一处理。使用eventfd的示例更现代更简洁#include sys/eventfd.h #include signal.h #include unistd.h #include stdint.h static int signal_fd; static void signal_handler(int sig) { uint64_t one 1; write(signal_fd, one, sizeof(one)); // 写入一个8字节的1 } int main() { // 创建eventfd signal_fd eventfd(0, EFD_NONBLOCK); if (signal_fd -1) { /* 处理错误 */ } // 设置信号处理 struct sigaction sa; sa.sa_handler signal_handler; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART; // 可以设置因为write是异步信号安全的 sigaction(SIGINT, sa, NULL); sigaction(SIGTERM, sa, NULL); // 主事件循环 (例如使用epoll) int epoll_fd epoll_create1(0); struct epoll_event ev; ev.events EPOLLIN; ev.data.fd signal_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, signal_fd, ev); struct epoll_event events[10]; while (1) { int n epoll_wait(epoll_fd, events, 10, -1); for (int i 0; i n; i) { if (events[i].data.fd signal_fd) { uint64_t count; read(signal_fd, count, sizeof(count)); // 读出数据清空eventfd printf(“Received signal, count%lu\n”, (unsigned long)count); // 在这里安全地执行清理和退出逻辑 goto cleanup; } else { // 处理其他I/O事件 } } } cleanup: close(epoll_fd); close(signal_fd); return 0; }这种方法将异步的信号处理同步化到了主事件循环中极大地简化了程序逻辑避免了在信号处理函数中做任何复杂操作是处理信号的最佳实践之一。6.4 多线程与信号在多线程程序中信号的处理变得更加复杂。POSIX规定信号的处理sigaction设置是进程级别的即所有线程共享。但信号的递送目标可以是整个进程如果信号是由于硬件异常如SIGSEGV,SIGFPE或kill()发送给进程ID产生的内核会任选一个没有阻塞该信号的线程来递送。特定线程使用pthread_kill()可以向特定线程发送信号。该信号只会递送给那个线程。最佳实践在多线程程序中通常建议在主线程或某个专用信号处理线程中阻塞所有需要处理的信号使用pthread_sigmask然后由这个线程调用sigwait或sigwaitinfo来同步地等待并处理信号。这样信号处理就变成了一个普通的函数调用完全避免了异步信号安全的问题。// 在主线程中阻塞SIGINT和SIGTERM sigset_t set; sigemptyset(set); sigaddset(set, SIGINT); sigaddset(set, SIGTERM); pthread_sigmask(SIG_BLOCK, set, NULL); // 创建专用信号处理线程 void* signal_thread_func(void* arg) { int sig; while (1) { if (sigwait(set, sig) 0) { printf(“Signal thread received: %d\n”, sig); // 在这里安全地处理信号可以调用任何函数 if (sig SIGINT || sig SIGTERM) { // 通知其他线程优雅退出 break; } } } return NULL; } pthread_t signal_thread; pthread_create(signal_thread, NULL, signal_thread_func, NULL);7. 常见问题排查与调试技巧信号相关的Bug往往难以复现和调试。以下是一些常见问题和排查思路。7.1 程序不响应CtrlC原因1信号被阻塞。检查程序是否调用了sigprocmask或pthread_sigmask阻塞了SIGINT。原因2信号被忽略。检查是否调用了signal(SIGINT, SIG_IGN)或sigaction设置了忽略。原因3程序处于“停止”Stopped状态。比如收到了SIGTSTP(CtrlZ)。用ps aux | grep your_program查看进程状态如果是T可以用kill -CONT PID恢复。原因4程序在前台进程组对于后台作业以启动或通过bg放入后台终端产生的SIGINT默认只发送给前台进程组。用fg命令将作业切换到前台再试。7.2 程序意外终止没有核心转储核心转储Core Dump对于调试SIGSEGV,SIGABRT等错误至关重要。检查系统限制使用ulimit -c查看核心文件大小限制。如果是0则不会生成。可以用ulimit -c unlimited当前Shell有效或在程序启动脚本中设置。检查路径和权限核心文件通常生成在进程的当前工作目录。确保该目录有写权限。有时系统配置会指定核心文件路径如/proc/sys/kernel/core_pattern。信号处理函数的影响如果你为SIGSEGV注册了处理函数并且没有在函数中终止进程或重新抛出信号那么默认的终止并生成核心转储的动作就不会发生。7.3 僵尸进程的产生与处理僵尸进程是已经终止但其退出状态尚未被父进程收集通过wait或waitpid的子进程。它会占用一个进程表项。根本原因父进程没有正确处理SIGCHLD信号。解决方案显式等待父进程在创建子进程后在适当的地方调用waitpid(-1, status, WNOHANG)非阻塞方式或wait(status)阻塞方式来回收子进程。但阻塞等待可能影响父进程并发。使用SIGCHLD信号推荐为SIGCHLD设置处理函数在处理函数中循环调用waitpid直到没有已终止的子进程。void sigchld_handler(int sig) { int saved_errno errno; // waitpid可能会修改errno while (waitpid(-1, NULL, WNOHANG) 0) { // 成功回收一个子进程 } errno saved_errno; } // 设置处理函数时sa_flags最好加上SA_RESTART防止某些系统调用被中断 // 也可以加上SA_NOCLDSTOP这样只有子进程终止时才会收到SIGCHLD停止时不会 struct sigaction sa; sa.sa_handler sigchld_handler; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART | SA_NOCLDSTOP; sigaction(SIGCHLD, sa, NULL);注意在信号处理函数中调用waitpid是安全的因为waitpid是异步信号安全函数。7.4 使用strace进行信号调试strace是Linux下强大的调试工具可以跟踪程序执行的系统调用和信号。strace -e tracesignal ./your_program只跟踪与信号相关的系统调用kill,tgkill,rt_sigaction,rt_sigprocmask等。strace -e signalall ./your_program跟踪所有信号的递送。观察输出可以看到程序注册了哪些信号处理函数收到了哪些信号以及信号处理函数是如何被调用的。这对于理解复杂的信号交互非常有帮助。信号是Linux/Unix系统编程中一个深刻且微妙的领域。从简单的CtrlC处理到构建健壮的多进程、多线程服务都离不开对信号的正确理解和使用。掌握sigaction而非signal理解异步信号安全的约束善用信号集、sigsuspend和自管道技巧是迈向高级系统程序员的必经之路。在实践中最稳妥的策略永远是让信号处理函数尽可能简单只设置标志位在主程序的安全上下文中处理业务逻辑。多线程环境下则优先考虑使用专用线程通过sigwait同步处理信号。把这些原则和技巧融入你的编程习惯就能写出从容应对各种异步事件的可靠程序。