关于代码的可重入性,设计开发人员一般只考虑到线程安全,异步信号处理函数的安全却往往被忽略。本文首先介绍如何编写安全的异步信号处理函数;然后举例说明在多线程应用中如何构建模型让异步信号在指定的线程中以同步的方式处理。

应用中编写安全的信号处理函数

  在开发多线程应用时,开发人员一般都会考虑线程安全,会使用 pthread_mutex 去保护全局变量。如果应用中使用了信号,而且信号的产生不是因为程序运行出错,而是程序逻辑需要,譬如 SIGUSR1、SIGRTMIN 等,信号在被处理后应用程序还将正常运行。在编写这类信号处理函数时,应用层面的开发人员却往往忽略了信号处理函数执行的上下文背景,没有考虑编写安全的信号处理函数的一些规则。本文首先介绍编写信号处理函数时需要考虑的一些规则;然后举例说明在多线程应用中如何构建模型让因为程序逻辑需要而产生的异步信号在指定的线程中以同步的方式处理。

线程和信号

  Linux 多线程应用中,每个线程可以通过调用 pthread_sigmask() 设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如 SIGSEGV;另外不能被忽略处理的信号 SIGKILL 和 SIGSTOP 也无法被阻塞。

当一个线程调用 pthread_create() 创建新的线程时,此线程的信号掩码会被新创建的线程继承。

POSIX.1 标准定义了一系列线程函数的接口,即 POSIX threads(Pthreads)。Linux C 库提供了两种关于线程的实现:LinuxThreads 和 NPTL(Native POSIX Threads Library)。LinuxThreads 已经过时,一些函数的实现不遵循POSIX.1 规范。NPTL 依赖 Linux 2.6 内核,更加遵循 POSIX..1 规范,但也不是完全遵循。

  基于 NPTL 的线程库,多线程应用中的每个线程有自己独特的线程 ID,并共享同一个进程ID。应用程序可以通过调用 kill(getpid(),signo)将信号发送到进程,如果进程中当前正在执行的线程没有阻碍此信号,则会被中断,线号处理函数会在此线程的上下文背景中执行。应用程序也可以通过调用 pthread_kill(pthread_t thread, int sig) 将信号发送给指定的线程,则线号处理函数会在此指定线程的上下文背景中执行。

基于 LinuxThreads 的线程库,多线程应用中的每个线程拥有自己独特的进程 ID,getpid() 在不同的线程中调用会返回不同的值,所以无法通过调用 kill(getpid(),signo) 将信号发送到整个进程。

下文介绍的在指定的线程中以同步的方式处理异步信号是基于使用了 NPTL 的 Linux C 库。请参考“Linux 线程模型的比较:LinuxThreads 和 NPTL”和“pthreads(7) - Linux man page”进一步了解 Linux 的线程模型,以及不同版本的 Linux C 库对 NPTL 的支持。

编写安全的异步信号处理函数

信号的产生可以是:

  • 用户从控制终端终止程序运行,如 Ctrk + C 产生 SIGINT;
  • 程序运行出错时由硬件产生信号,如访问非法地址产生 SIGSEGV;
  • 程序运行逻辑需要,如调用 killraise 产生信号。

因为信号是异步事件,即信号处理函数执行的上下文背景是不确定的,譬如一个线程在调用某个库函数时可能会被信号中断,库函数提前出错返回,转而去执行信号处理函数。对于上述第三种信号的产生,信号在产生、处理后,应用程序不会终止,还是会继续正常运行,在编写此类信号处理函数时尤其需要小心,以免破坏应用程序的正常运行。

关于编写安全的信号处理函数主要有以下一些规则:

  • 信号处理函数尽量只执行简单的操作,譬如只是设置一个外部变量,其它复杂的操作留在信号处理函数之外执行;
  • errno 是线程安全,即每个线程有自己的 errno,但不是异步信号安全。如果信号处理函数比较复杂,且调用了可能会改变 errno 值的库函数,必须考虑在信号处理函数开始时保存、结束的时候恢复被中断线程的 errno 值;
  • 信号处理函数只能调用可以重入的 C 库函数;譬如不能调用 malloc(),free()以及标准 I/O 库函数等;
  • 信号处理函数如果需要访问全局变量,在定义此全局变量时须将其声明为 volatile,以避免编译器不恰当的优化。

从整个 Linux 应用的角度出发,因为应用中使用了异步信号,程序中一些库函数在调用时可能被异步信号中断,此时必须根据errno 的值考虑这些库函数调用被信号中断后的出错恢复处理,譬如socket 编程中的读操作:

  1. rlen = recv(sock_fd, buf, len, MSG_WAITALL);
  2. if ((rlen == -1) && (errno == EINTR)){
  3. // this kind of error is recoverable, we can set the offset change
  4. //‘rlen’ as 0 and continue to recv
  5. }
 

回页首

在指定的线程中以同步的方式处理异步信号

如上文所述,不仅编写安全的异步信号处理函数本身有很多的规则束缚;应用中其它地方在调用可被信号中断的库函数时还需考虑被中断后的出错恢复处理。这让程序的编写变得复杂,幸运的是,POSIX.1 规范定义了sigwait()、 sigwaitinfo() 和 pthread_sigmask() 等接口,可以实现:

  • 以同步的方式处理异步信号;
  • 在指定的线程中处理信号。

这种在指定的线程中以同步方式处理信号的模型可以避免因为处理异步信号而给程序运行带来的不确定性和潜在危险。

sigwait

sigwait() 提供了一种等待信号的到来,以串行的方式从信号队列中取出信号进行处理的机制。sigwait()只等待函数参数中指定的信号集,即如果新产生的信号不在指定的信号集内,则 sigwait()继续等待。对于一个稳定可靠的程序,我们一般会有一些疑问:

  • 多个相同的信号可不可以在信号队列中排队?
  • 如果信号队列中有多个信号在等待,在信号处理时有没有优先级规则?
  • 实时信号和非实时信号在处理时有没有什么区别?

笔者写了一小段测试程序来测试 sigwait 在信号处理时的一些规则。

清单 1. sigwait_test.c
  1. #include <signal.h>
  2. #include <errno.h>
  3. #include <pthread.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6.  
  7. void sig_handler(int signum)
  8. {
  9. printf("Receive signal. %d\n", signum);
  10. }
  11.  
  12. void* sigmgr_thread()
  13. {
  14. sigset_t waitset, oset;
  15. int sig;
  16. int rc;
  17. pthread_t ppid = pthread_self();
  18.  
  19. pthread_detach(ppid);
  20.  
  21. sigemptyset(&waitset);
  22. sigaddset(&waitset, SIGRTMIN);
  23. sigaddset(&waitset, SIGRTMIN+2);
  24. sigaddset(&waitset, SIGRTMAX);
  25. sigaddset(&waitset, SIGUSR1);
  26. sigaddset(&waitset, SIGUSR2);
  27.  
  28. while (1) {
  29. rc = sigwait(&waitset, &sig);
  30. if (rc != -1) {
  31. sig_handler(sig);
  32. } else {
  33. printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
  34. }
  35. }
  36. }
  37.  
  38. int main()
  39. {
  40. sigset_t bset, oset;
  41. int i;
  42. pid_t pid = getpid();
  43. pthread_t ppid;
  44.  
  45. sigemptyset(&bset);
  46. sigaddset(&bset, SIGRTMIN);
  47. sigaddset(&bset, SIGRTMIN+2);
  48. sigaddset(&bset, SIGRTMAX);
  49. sigaddset(&bset, SIGUSR1);
  50. sigaddset(&bset, SIGUSR2);
  51.  
  52. if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
  53. printf("!! Set pthread mask failed\n");
  54.  
  55. kill(pid, SIGRTMAX);
  56. kill(pid, SIGRTMAX);
  57. kill(pid, SIGRTMIN+2);
  58. kill(pid, SIGRTMIN);
  59. kill(pid, SIGRTMIN+2);
  60. kill(pid, SIGRTMIN);
  61. kill(pid, SIGUSR2);
  62. kill(pid, SIGUSR2);
  63. kill(pid, SIGUSR1);
  64. kill(pid, SIGUSR1);
  65.  
  66. // Create the dedicated thread sigmgr_thread() which will handle signals synchronously
  67. pthread_create(&ppid, NULL, sigmgr_thread, NULL);
  68.  
  69. sleep(10);
  70.  
  71. exit (0);
  72. }

程序编译运行在 RHEL4 的结果如下:

图 1. sigwait 测试程序执行结果

从以上测试程序发现以下规则:

  • 对于非实时信号,相同信号不能在信号队列中排队;对于实时信号,相同信号可以在信号队列中排队。
  • 如果信号队列中有多个实时以及非实时信号排队,实时信号并不会先于非实时信号被取出,信号数字小的会先被取出:如 SIGUSR1(10)会先于 SIGUSR2 (12),SIGRTMIN(34)会先于 SIGRTMAX (64), 非实时信号因为其信号数字小而先于实时信号被取出。

sigwaitinfo() 以及 sigtimedwait() 也提供了与 sigwait() 函数相似的功能。

Linux 多线程应用中的信号处理模型

在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑调用 sigwait()使用同步模型进行处理。其程序流程如下:

  1. 主线程设置信号掩码,阻碍希望同步处理的信号;主线程的信号掩码会被其创建的线程继承;
  2. 主线程创建信号处理线程;信号处理线程将希望同步处理的信号集设为 sigwait()的第一个参数。
  3. 主线程创建工作线程。
图 2. 在指定的线程中以同步方式处理异步信号的模型

代码示例

以下为一个完整的在指定的线程中以同步的方式处理异步信号的程序。

主线程设置信号掩码阻碍 SIGUSR1 和 SIGRTMIN 两个信号,然后创建信号处理线程sigmgr_thread()和五个工作线程worker_thread()。主线程每隔10秒调用 kill() 对本进程发送 SIGUSR1 和 SIGTRMIN 信号。信号处理线程 sigmgr_thread()在接收到信号时会调用信号处理函数 sig_handler()

程序编译:gcc -o signal_sync signal_sync.c -lpthread

程序执行:./signal_sync

从程序执行输出结果可以看到主线程发出的所有信号都被指定的信号处理线程接收到,并以同步的方式处理。

清单 2. signal_sync.c
  1. #include <signal.h>
  2. #include <errno.h>
  3. #include <pthread.h>
  4. #include <unistd.h>
  5. #include <sys/types.h>
  6.  
  7. void sig_handler(int signum)
  8. {
  9. static int j = 0;
  10. static int k = 0;
  11. pthread_t sig_ppid = pthread_self();
  12. // used to show which thread the signal is handled in.
  13.  
  14. if (signum == SIGUSR1) {
  15. printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
  16. j++;
  17. //SIGRTMIN should not be considered constants from userland,
  18. //there is compile error when use switch case
  19. } else if (signum == SIGRTMIN) {
  20. printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
  21. k++;
  22. }
  23. }
  24.  
  25. void* worker_thread()
  26. {
  27. pthread_t ppid = pthread_self();
  28. pthread_detach(ppid);
  29. while (1) {
  30. printf("I'm thread %d, I'm alive\n", ppid);
  31. sleep(10);
  32. }
  33. }
  34.  
  35. void* sigmgr_thread()
  36. {
  37. sigset_t waitset, oset;
  38. siginfo_t info;
  39. int rc;
  40. pthread_t ppid = pthread_self();
  41.  
  42. pthread_detach(ppid);
  43.  
  44. sigemptyset(&waitset);
  45. sigaddset(&waitset, SIGRTMIN);
  46. sigaddset(&waitset, SIGUSR1);
  47.  
  48. while (1) {
  49. rc = sigwaitinfo(&waitset, &info);
  50. if (rc != -1) {
  51. printf("sigwaitinfo() fetch the signal - %d\n", rc);
  52. sig_handler(info.si_signo);
  53. } else {
  54. printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
  55. }
  56. }
  57. }
  58.  
  59. int main()
  60. {
  61. sigset_t bset, oset;
  62. int i;
  63. pid_t pid = getpid();
  64. pthread_t ppid;
  65.  
  66. // Block SIGRTMIN and SIGUSR1 which will be handled in
  67. //dedicated thread sigmgr_thread()
  68. // Newly created threads will inherit the pthread mask from its creator
  69. sigemptyset(&bset);
  70. sigaddset(&bset, SIGRTMIN);
  71. sigaddset(&bset, SIGUSR1);
  72. if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
  73. printf("!! Set pthread mask failed\n");
  74.  
  75. // Create the dedicated thread sigmgr_thread() which will handle
  76. // SIGUSR1 and SIGRTMIN synchronously
  77. pthread_create(&ppid, NULL, sigmgr_thread, NULL);
  78.  
  79. // Create 5 worker threads, which will inherit the thread mask of
  80. // the creator main thread
  81. for (i = 0; i < 5; i++) {
  82. pthread_create(&ppid, NULL, worker_thread, NULL);
  83. }
  84.  
  85. // send out 50 SIGUSR1 and SIGRTMIN signals
  86. for (i = 0; i < 50; i++) {
  87. kill(pid, SIGUSR1);
  88. printf("main thread, send SIGUSR1 No. %d\n", i);
  89. kill(pid, SIGRTMIN);
  90. printf("main thread, send SIGRTMIN No. %d\n", i);
  91. sleep(10);
  92. }
  93. exit (0);
  94. }

注意事项

在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑使用同步模型进行处理;而对会导致程序运行终止的信号如 SIGSEGV 等,必须按照传统的异步方式使用 signal()、 sigaction()注册信号处理函数进行处理。这两种信号处理模型可根据所处理的信号的不同同时存在一个 Linux 应用中:

  • 不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL。
  • 不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
  • 确保 sigwait() 等待的信号集已经被进程中所有的线程阻塞。
  • 在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号。
  • 因为 sigwait()使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。

小结

在开发 Linux 多线程应用中, 如果因为程序逻辑需要引入信号, 在信号处理后程序仍将继续正常运行。在这种背景下,如果以异步方式处理信号,在编写信号处理函数一定要考虑异步信号处理函数的安全; 同时, 程序中一些库函数可能会被信号中断,错误返回,这时需要考虑对 EINTR 的处理。另一方面,也可考虑使用上文介绍的同步模型处理信号,简化信号处理函数的编写,避免因为信号处理函数执行上下文的不确定性而带来的风险。

linux 异步信号的同步处理方式的更多相关文章

  1. linux异步IO的两种方式【转】

    转自:https://blog.csdn.net/shixin_0125/article/details/78898146 知道异步IO已经很久了,但是直到最近,才真正用它来解决一下实际问题(在一个C ...

  2. Libev源码分析06:异步信号同步化--sigwait、sigwaitinfo、sigtimedwait和signalfd

    一:信号简述 信号是典型的异步事件.内核在某个信号出现时有三种处理方式: a:忽略信号,除了SIGKILL和SIGSTOP信号不能忽略外,其他大部分信号都可以被忽略: b:捕捉信号,也就是在信号发生时 ...

  3. linux可重入、异步信号安全和线程安全

    一 可重入函数 当一个被捕获的信号被一个进程处理时,进程执行的普通的指令序列会被一个信号处理器暂时地中断.它首先执行该信号处理程序中的指令.如果从信号处理程序返回(例如没有调用exit或longjmp ...

  4. IO模型浅析-阻塞、非阻塞、IO复用、信号驱动、异步IO、同步IO

    最近看到OVS用户态的代码,在接收内核态信息的时候,使用了Epoll多路复用机制,对其十分不解,于是从网上找了一些资料,学习了一下<UNIX网络变成卷1:套接字联网API>这本书对应的章节 ...

  5. js中ajax连接服务器open函数的另外两个默认参数get请求和默认异步(open的post方式send函数带参数)(post请求和get请求区别:get:快、简单 post:安全,量大,不缓存)(服务器同步和异步区别:同步:等待服务器响应当中浏览器不能做别的事情)(ajax和jquery一起用的)

    js中ajax连接服务器open函数的另外两个默认参数get请求和默认异步(open的post方式send函数带参数)(post请求和get请求区别:get:快.简单 post:安全,量大,不缓存)( ...

  6. Java异步调用转同步的5种方式

    1.异步和同步的概念 同步调用:调用方在调用过程中,持续等待返回结果. 异步调用:调用方在调用过程中,不直接等待返回结果,而是执行其他任务,结果返回形式通常为回调函数. 2 .异步转为同步的概率 需要 ...

  7. [每日一题]面试官问:Async/Await 如何通过同步的方式实现异步?

    关注「松宝写代码」,精选好文,每日一题 ​时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...

  8. Linux异步IO【转】

    转自:http://blog.chinaunix.net/uid-24567872-id-87676.html Linux® 中最常用的输入/输出(I/O)模型是同步 I/O.在这个模型中,当请求发出 ...

  9. linux异步IO--aio

    简述 linux下异步方式有两种:异步通知和异步IO(AIO),异步通知请参考:linux异步通知 Linux的I/O机制经历了一下几个阶段的演进: 1. 同步阻塞I/O: 用户进程进行I/O操作,一 ...

随机推荐

  1. code format using astyle

    script context : #! /bin/bash for f in $(find . -name '*.c' -or -name '*.cpp' -or -name '*.h' -type ...

  2. SQL Server 调优系列进阶篇 - 如何重建数据库索引

    随着数据的数据量的急剧增加,数据库的性能也会明显的有些缓慢这个时候你可以考虑下重建索引或是重新组织索引了. DBCC SHOWCONTIG('表名') 可以查看当前表的索引碎情况. 重建索引 方法一: ...

  3. 51nod1615

    题解: 首先,当1+2+...+n=x时,答案就是n 如果1+2+...+n不会等于x,那么找一个最小的n,让1+2+....+n>x并且(1+2+.....+n-x)%2=0 代码: #inc ...

  4. 【转载】maven用处

    http://bbs.csdn.net/topics/80014314 当您的项目逐渐变得庞大和复杂时,最好使用一种构建工具来自动构建您的项目.例如,一个典型的java项目,每次构建时都要经历编译ja ...

  5. IE11浏览器,按F12 检查元素,工具会出来,但是没法正常使用?

    微软网站上找到IE11的累计安全更新,安装后就能正常使用了.https://www.microsoft.com/zh-cn/download/confirmation.aspx?id=45154

  6. java maven 安装

    1.java环境安装 maven 想要安装 Apache Maven 在Windows 系统上, 需要下载 Maven 的 zip 文件,并将其解压到你想安装的目录,并配置 Windows 环境变量. ...

  7. 《Python》内置方法进阶和常用模块

    一:__new__ 在__init__之前,实例化对象的第一步是__new__创建了一个空间 class Foo: def __init__(self): # 初始化方法 print('执行了init ...

  8. windows内存体系结构 内存查询,读,写(附录源码)

    “进程内存管理器”这个程序实现的最基本功能也就是对内存的读写,之前的两篇文章也就是做的一个铺垫,介绍了内核模式切换和IoDeviceControl函数进行的应用程序与驱动程序通信的问题.接下来就进入正 ...

  9. 安装Windows Installer服务

    Windows Installer 5.0.810.500 下载地址: 电信:http://mdl1.mydown.yesky.com/soft/201303/WindowsInstaller.rar ...

  10. STL标准库-算法-常用算法

    技术在于交流.沟通,本文为博主原创文章转载请注明出处并保持作品的完整性 介绍11种STL标准库的算法,从这11种算法中总结一下算法的基本使用 1.accumulate() 累加 2.for_each( ...