1.可重入函数、异步信号安全、线程安全
重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。,常见的情况是,程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理函数,而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的。
要确保函数可重入,需满足一下几个条件:
- 不在函数内部使用静态或全局数据 .
- 不返回静态或全局数据,所有数据都由函数的调用者提供。
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
- 不调用不可重入函数。
APUE 10.6:
在信号处理程序中保证调用安全的函数,是可重入的并被称为异步信号安全的。
可以被信号控制器安全调用的函数被称为"异步信号安全"函数。信号就像硬件中断一样,会打断正在执行的指令序列。信号处理函数无法判断捕获到信号的时候,进程在何处运行。如果信号处理函数中的操作与打断的函数的操作相同,而且这个操作中有静态数据结构等,当信号处理函数返回的时候(当然这里讨论的是信号处理函数可以返回),恢复原先的执行序列,可能会导致信号处理函数中的操作覆盖了之前正常操作中的数据。
大多数函数是不可重入的,因为:
- 已知它们使用静态(static)数据结构;
- 它们调用malloc或free;
- 它们是标准I/O函数。标准I/O库很多实现都以不可重入方式使用全局数据结构。
12.5:
如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。
很多函数并不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区。通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安全。
如果一个函数对多个线程来说是可重入的,就说这个函数是线程安全的。但这并不能说明对信号处理程序来说是该函数也是可重入的。
如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。
CSAPP 12.7.2:
有一类重要的线程安全函数,叫做可重入函数,其特点在于:当它们被多个线程调用时,不会引起任何共享数据。
可重入函数集合是线程安全函数的一个真子集,可重入的函数一定是线程安全的,但线程安全不一定是可重入函数。
总结:
- 判断一个函数是不是可重入函数,在于判断其能否可以被打断,打断后恢复运行能够得到正确的结果。(打断执行的指令序列并不改变函数的数据)
- 判断一个函数是不是线程安全的,在于判断其能否在多个线程同时执行其指令序列的时候,保证每个线程都能够得到正确的结果。
- 如果一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来说该函数也是可重入的。
- 如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是"异步-信号安全"的。
参考:https://www.cnblogs.com/wolfrickwang/p/3729465.html
2.子进程继承
fork()后,子进程获得父进程数据空间、堆和栈的副本,父进程和子进程共享正文段(只读)。
由于fork之后经常跟随者exec,所以很多实现并不执行数据段、栈和堆的完全复制,而是使用 写时复制 技术。这些区域有父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子程序中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。
exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
文件描述符继承
文件描述符表位于内核态,fork和exec后仍然继承,不过可以通过设置文件描述符标志:执行时关闭(close-on-exec标志),在exec后自动关闭文件描述符。
fork函数
继承
父、子进程区别
exec函数
3.线程、子进程的信号处理
信号处理函数:
- 当一个进程调用fork时,因为子进程在开始时复制父进程的存储映像,信号捕捉函数的地址在子进程中是有意义的,所以子进程继承父进程的信号处理方式。
- 当子进程调用exec后,因为exec运行新的程序后会覆盖从父进程继承来的存储映像,那么信号捕捉函数在新程序中已无意义,所以exec会将原先设置为要捕捉的信号都更改为默认动作。
- 多线程:信号处理函数对所有线程有效。进程中的信号是递送到单个线程的。由硬件故障有关的信号,一般被递送到引起该事件的线程中去,其他的信号则被发送到任意一个线程。
信号掩码有以下规则:
- 创建线程时继承主线程的信号掩码,每个线程可以有自己信号掩码;
- fork出来的子进程会继承父进程的信号掩码,exec后信号掩码保持不变;
- 针对进程发送的信号,会被任意的没有屏蔽该信号的线程接收,注意只有一个线程会随机收到;
- fork之后子进程里pending的信号集初始化为空,exec会保持pending信号集。
线程通常如何处理信号?(APUE 12.8)
- 由主线程设置信号掩码,创建的线程继承该掩码。
- 单独设置一个信号处理线程,调用sigwait函数等待信号,处理想要接收的信号。
sigwait(const sigset_t set, int* signo)
set参数指定了线程等待的信号集,如果其中某个信号在sigwait调用时处于挂起状态,那么sigwait将无阻塞的返回,并使signo指向接收到的信号编号,并在返回前从进程中移除那些处于挂起等待状态的信号(并没有调用信号处理函数)。
线程在调用sigwait之前,必须阻塞那些它正在等待的信号。