muduo库学习之C++多线程系统编程精要07——多线程与 fork、多线程与signal

东阳的学习笔记

1. 多线程与 fork

多线程与 fork() 的协作性很差。这是 POSIX 系列操作系统的历史包袱。

1.1 fork 一般不能在多线程程序中使用

因为 Linux 的 fork() 只克隆当前线程的 thread of control,不克隆其他线程。fork() 之后,除了当前线程之外的所有线程都消失了。

forkall() 是很难办到的。因为其他线程可能等在 condition variable 上面,可能阻塞在系统调用上、可能等着 metux 以进入临界区

1.2 fork() 之后的子进程可能陷入无法调用的境地

  • fork()之后子进程中 只有一个线程,其他线程都消失了,这就造成一个危险的局面:

    • 其他线程可能正好位于临界区之内,持有了某个锁, 而它突然死亡,再也没有机会去解锁了
    • 如果子进程试图再对同一个mutex加锁,就会立刻死锁
  • 在fork()之后,子进程就相当于处于signal handler之中,你不能调用线程安全的函数(除非它是可重入的),而只能调用异步信号安全(async-signal-safe)的函数

  • 比方说,fork()之后,子进程不能调用:

    • malloc。因为malloc()在访问全局状态时几乎肯定会加锁
    • 任何可能分配或释放内存的函数,包括new、map::insert()、 snprintf(在浮点数转换为字符串时有可能需要动态分配内存)……等等
    • 任何Pthreads函数。你不能用pthread_cond_signal()去通知父进程, 只能通过读写pipe来同步(见http://github.com/chenshuo/muduo-protorpc中Zurg slave示例的Process::start())
    • printf()系列函数。因为其他线程可能恰好持有stdout/stderr的锁
    • 除了man 7 signal中明确列出的“signal安全”函数之外的任何函数

1.3 唯一安全的做法是 fork() 加 exec()

这样会彻底断绝子进程与父进程的联系

2. 多线程与 Singal

在多线程程序中,使用 singal 的第一原则就是不要使用 Singal

2.1 处理信号是一件棘手的事

  • 在单线程时代:
    • **编写信号处理函数就是一件棘手的事情,**由于signal打断了正在运行的thread of control
    • 在signal handler中只能调用async-signal-safe的函数(http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03),即所谓的“可重入(reentrant)”函数,就好比在DOS时代编写中断处理例程(ISR)(http://en.wikipedia.org/wiki/Interrupt_handler)一样。不是每个线程安全的函数都是可重入的(见上面“二”中举的例子)
    • 还有一点,如果signal handler中需要修改全局数据,那么被修改的变量必须是sig_atomic_t类型的(http://www.gnu.org/software/libc/manual/html_mono/libc.html#Atomic-Data-Access)。否则被打断的函数在恢复执行后很可 能不能立刻看到signal handler改动后的数据,因为编译器有可能假定这个变量不会被他处修改,从而优化了内存访问
  • 在多线程时代:
    • signal的语义更为复杂。信号分为两类:
      • 发送给某一线程(SIGSEGV)
      • 发送给进程中的任一线程(SIGTERM)
    • 还要考虑掩码(mask)对信号的屏蔽等
    • 特别是在signal handler中不能调用任何Pthreads函数,不能通过condition variable来通知其他线程

2.2 在多线程程序中,使用 singal 的第一原则就是不要使用 Singal

  • 不要用signal作为IPC的手段,包括不要用SIGUSR1等信号来触发服务端的行为。如果确实需要,可以用后面的“构建易于维护的分布式程序”文章中介绍的增加监听端口的方式来实现双向的、可远程访问的进程控制
  • **也不要使用基于signal实现的定时函数,**包括 alarm/ualarm/setitimer/timer_create、sleep/usleep等等
  • 不主动处理各种异常信号(SIGTERM、SIGINT等等),只用默认语义:结束进程
    • 有一个例外:SIGPIPE,服务器程序通常的做法是忽略此信号,否则如果对方断开连接,而本机继续write的话,会导致程序意外终止
    • 在命令行程序中,默认的SIGPIPE行为非常有用。例如查看日志中的前10条错误信息,可以用管道将命令穿起来:gunzip -c log.gz | grep ERROR | head,由于head关闭了管道的写入端,grep会遇到SIGPIPE而终止,同理gunzip也就不需要解压缩整个巨大的日志文件。这也可能是Unix默认使用阻塞IO的历史原因之一
  • 在没有别的替代方法的情况下(比方说需要处理SIGCHLD信 号),把异步信号转换为同步的文件描述符事件。传统的做法是在 signal handler里往一个特定的pipe写一个字节,在主程序中从这个pipe读取,从而纳入统一的IO事件处理框架中去。现代Linux的做法是采 用signalfd(2)把信号直接转换为文件描述符事件,从而从根本上避免使 用signal handler(例子见http://github.com/chenshuo/muduo-protorpc中Zurg slave示例的ChildManager class)

猜你喜欢

转载自blog.csdn.net/qq_22473333/article/details/113530156