Linux 多线程环境中的信号处理不同于进程的信号处理。一方面线程间信号处理函数的共享性使得信号处理更为复杂,另一方面普通异步信号又可转换为同步方式来简化处理。
本文首先介绍信号处理在进程中和线程间的不同,然后描述相应的线程库函数,在此基础上给出一组示例代码,以讨论线程编程中信号处理的细节和注意事项。
本文通过 调用来“等待”信号,而通过 / 注册的信号处理函数来“捕获”信号,以体现其同步和异步的区别。
1.1 进程与信号
信号是向进程异步发送的软件通知,通知进程有事件发生。事件可为硬件异常(如除0)、软件条件(如闹钟超时)、控制终端发出的信号或调用 / 函数产生的用户逻辑信号。
当信号产生时,内核通常在进程表中设置一个某种形式的标志,即向进程递送一个信号。在信号产生(generation)和递送(delivery)之间(可能相当长)的时间间隔内,该信号处于未决(pending)状态。已经生成但未递送的信号称为挂起(suspending)的信号。
进程可选择阻塞(block)某个信号,此时若对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程(a)对此信号解除阻塞,或者(b)将对此信号的动作更改为忽略。内核为每个进程维护一个未决(未处理的)信号队列,信号产生时无论是否被阻塞,首先放入未决队列里。当时间片调度到当前进程时,内核检查未决队列中是否存在信号。若有信号且未被阻塞,则执行相应的操作并从队列中删除该信号;否则仍保留该信号。因此,进程在信号递送给它之前仍可改变对该信号的动作。进程调用 函数判定哪些信号设置为阻塞并处于未决状态。
若在进程解除对某信号的阻塞之前,该信号发生多次,则未决队列仅保留相同不可靠信号中的一个,而可靠信号(实时扩展)会保留并递送多次,称为按顺序排队。
每个进程都有一个信号屏蔽字(signal mask),规定当前要阻塞递送到该进程的信号集。对于每个可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则该信号当前被阻塞。
应用程序处理信号前,需要注册信号处理函数(signal handler)。当信号异步发生时,会调用处理函数来处理信号。因为无法预料信号会在进程的哪个执行点到来,故信号处理函数中只能简单设置一个外部变量或调用异步信号安全(async-signal-safe)的函数。此外,某些库函数(如read)可被信号中断,调用时必须考虑中断后出错恢复处理。这使得基于进程的信号处理变得复杂和困难。
1.2 线程与信号
内核也为每个线程维护未决信号队列。当调用 时,返回整个进程未决信号队列与调用线程未决信号队列的并集。进程内创建线程时,新线程将继承进程(主线程)的信号屏蔽字,但新线程的未决信号集被清空(以防同一信号被多个线程处理)。线程的信号屏蔽字是私有的(定义当前线程要求阻塞的信号集),即线程可独立地屏蔽某些信号。这样,应用程序可控制哪些线程响应哪些信号。
信号处理函数由进程内所有线程共享。这意味着尽管单个线程可阻止某些信号,但当线程修改某信号相关的处理行为后,所有线程都共享该处理行为的改变。这样,若某线程选择忽略某信号,而其他线程可恢复信号的默认处理行为或为信号设置新的处理函数,从而撤销原先的忽略行为。即对某个信号处理函数,以最后一次注册的处理函数为准,从而保证同一信号被任意线程处理时行为相同。此外,若某信号的默认动作是停止或终止,则不管该信号发往哪个线程,整个进程都会停止或终止。
若信号与硬件故障(如SIGBUS/SIGFPE/SIGILL/SIGSEGV)或定时器超时相关,该信号会发往引起该事件的线程。其它信号除非显式指定目标线程,否则通常发往主线程(哪怕信号处理函数由其他线程注册),仅当主线程屏蔽该信号时才发往某个具有处理能力的线程。
Linux 系统 C 标准库提供两种线程实现,即 LinuxThreads(已过时)和 NPTL(Native POSIX Threads Library)。NPTL 线程库依赖 Linux 2.6 内核,更加(但不完全)符合 POSIX.1 threads(Pthreads)规范。两者的详细区别可以通过 man 7 pthreads 命令查看。
NPTL 线程库中每个线程拥有自己独立的线程号,并共享同一进程号,故应用程序可调用 将信号发送到整个进程;而 LinuxThreads 线程库中每个线程拥有自己独立的进程号,不同线程调用 getpid() 会得到不同的进程号,故应用程序无法通过调用 kill() 将信号发送到整个进程,而只会将信号发送到主线程中去。
多线程中信号处理函数的共享性使得异步处理更为复杂,但通常可简化为同步处理。即创建一个专用线程来“同步等待”信号的到来,而其它线程则完全不会被该信号中断。这样就可确知信号的到来时机,必然是在专用线程中的那个等待点。
注意,线程库函数不是异步信号安全的,故信号处理函数中不应使用 pthread 相关函数。
2.1 pthread_sigmask
线程可调用 pthread_sigmask() 设置本线程的信号屏蔽字,以屏蔽该线程对某些信号的响应处理。
主线程调用 设置信号屏蔽字后,其创建的新线程将继承主线程的信号屏蔽字。然而,新线程对信号屏蔽字的更改不会影响创建者和其他线程。
通常,被阻塞的信号将不能中断本线程的执行,除非该信号指示致命的程序错误(如 SIGSEGV)。此外,不能被忽略处理的信号(SIGKILL 和 SIGSTOP)无法被阻塞。
注意, 与 函数功能类似。两者的区别在于, 是线程库函数,用于多线程进程,且失败时返回 errno;而 针对单线程的进程,其行为在多线程的进程中没有定义,且失败时设置 errno 并返回 -1。
2.2 sigwait
线程可通过调用sigwait()函数等待一个或多个信号发生。
参数 sigset 指定线程等待的信号集,signop 指向的整数表明接收到的信号值。该函数将调用线程挂起,直到信号集中的任何一个信号被递送。该函数接收递送的信号后,将其从未决队列中移除(以防返回时信号被 signal/sigaction 安装的处理函数捕获),然后唤醒线程并返回。该函数执行成功时返回 0,并将接收到的信号值存入 signop 所指向的内存空间;失败时返回错误编号(errno)。失败原因通常为 EINVAL(指定信号无效或不支持),但并不返回 EINTR 错误。
给定线程的未决信号集是整个进程未决信号集与该线程未决信号集的并集。若等待信号集中某个信号在 sigwait() 调用时处于未决状态,则该函数将无阻塞地返回。若同时有多个等待中的信号处于未决状态,则对这些信号的选择规则和顺序未定义。在返回之前,sigwait() 将从进程中原子性地移除所选定的未决信号。
若已阻塞等待信号集中的信号,则 sigwait() 会自动解除信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait() 将恢复线程的信号屏蔽字。因此,sigwait() 并不改变信号的阻塞状态。可见,sigwait() 的这种“解阻-等待-阻塞”特性,与条件变量非常相似。
为避免错误发生,调用 sigwait() 前必须阻塞那些它正在等待的信号。在单线程环境中,调用程序首先调用 sigprocmask() 阻塞等待信号集中的信号,以防这些信号在连续的 sigwait() 调用之间进入未决状态,从而触发默认动作或信号处理函数。在多线程程序中,所有线程(包括调用线程)都必须阻塞等待信号集中的信号,否则信号可能被递送到调用线程之外的其他线程。建议在创建线程前调用 pthread_sigmask() 阻塞这些信号(新线程继承信号屏蔽字),然后绝不显式解除阻塞(sigwait 会自动解除信号集的阻塞状态)。
若多个线程调用 sigwait() 等待同一信号,只有一个(但不确定哪个)线程可从 sigwait() 中返回。若信号被捕获(通过sigaction安装信号处理函数),且线程正在 sigwait() 调用中等待同一信号,则由系统实现来决定以何种方式递送信号。操作系统实现可让 sigwait 返回(通常优先级较高),也可激活信号处理程序,但不可能出现两者皆可的情况。
注意,sigwait() 与 sigwaitinfo() 函数功能类似。两者的区别在于,sigwait() 成功时返回0并传回信号值,且失败时返回 errno;而 sigwaitinfo() 成功时返回信号值并传回 siginfo_t 结构(信息更多),且失败时设置 errno 并返回 -1。此外, 当产生等待信号集以外的信号时,该信号的处理函数可中断 sigwaitinfo(),此时 errno 被设置为 EINTR。
对 SIGKILL(杀死进程)和 SIGSTOP(暂停进程)信号的等待将被系统忽略。
使用 sigwait() 可简化多线程环境中的信号处理,允许在指定线程中以同步方式等待并处理异步产生的信号。为了防止信号中断线程,可将信号加到每个线程的信号屏蔽字中,然后安排专用线程作信号处理。该专用线程可进行任何函数调用,而不必考虑函数的可重入性和异步信号安全性,因为这些函数调用来自正常的线程环境,能够知道在何处被中断并继续执行。这样,信号到来时就不会打断其他线程的工作。
这种采用专用线程同步处理信号的模型如下图所示:
其设计步骤如下:
1) 主线程设置信号屏蔽字,阻塞希望同步处理的信号;
2) 主线程创建一个信号处理线程,该线程将希望同步处理的信号集作为 sigwait()的参数;
3) 主线程创建若干工作线程。
主线程的信号屏蔽字会被其创建的新线程继承,故工作线程将不会收到信号。
2.3 pthread_kill
应用程序可调用 pthread_kill(),将信号发送给同一进程内指定的线程(包括自己)。
但应注意,系统在经过一段时间后会重新使用进程号,故当前拥有指定进程号的进程可能并非期望的进程。此外,进程存在性的测试并非原子操作。kill() 向调用者返回测试结果时,被测试进程可能已终止。
线程号仅在进程内可用且唯一,使用另一进程内的线程号时其行为未定义。当对线程调用 成功或已分离线程终止后,该线程生命周期结束,其线程号不再有效(可能已被新线程重用)。程序试图使用该无效线程号时,其行为未定义。标准并未限制具体实现中如何定义 pthread_t 类型,而该类型可能被定义为指针,当其指向的内存已被释放时,对线程号的访问将导致程序崩溃。因此,通过 测试已分离的线程时,也存在与 kill() 相似的局限性。仅当未分离线程退出但不被回收(join)时,才能期望 必然返回 ESRCH 错误。同理,通过 取消线程时也不安全。
若要避免无效线程号的问题,线程退出时就不应直接调用 pthread_kill(),而应按照如下步骤:
1) 为每个线程维护一个 Running 标志和相应的互斥量;
2) 创建线程时,在新线程启动例程 ThrdFunc 内设置 Running 标志为真;
3) 从新线程启动例程 ThrdFunc 返回(return)、退出(pthread_exit)前,或在响应取消请求时的清理函数内,获取互斥量并设置 Running 标志为假,再释放互斥量并继续;
4) 其他线程先获取目标线程的互斥量,若 Running 标志为真则调用 pthread_kill(),然后释放互斥量。
信号发送成功后,信号处理函数会在指定线程的上下文中执行。若该线程未注册信号处理函数,则该信号的默认处理动作将影响整个进程。当信号默认动作是终止进程时,将信号发送给某个线程仍然会杀掉整个进程。因此,信号值非0时必须实现线程的信号处理函数,否则调用pthread_kill()将毫无意义。
本节将通过一组基于 NPTL 线程库的代码示例,展示多线程环境中信号处理的若干细节。
首先定义两个信号处理函数:
其中,SigHandler() 用于同步处理,sighandler() 则用于同步处理。
3.1 示例1
本示例对比单线程中,sigwait() 和 sigwaitinfo() 函数的可中断性。
编译链接(加 -pthread 选项)后,执行结果如下:
对比可见,sigwaitinfo() 可被等待信号集以外的信号中断,而 sigwait() 不会被中断。
3.2 示例2
本示例测试多线程中,sigwait()和sigwaitinfo()函数对信号的同步等待。
注意,线程创建和启动之间存在时间窗口。因此创建线程时通过 pvArg 参数传递的某块内存空间值,在线程启动例程中读取该指针所指向的内存时,该内存值可能已被主线程或其他新线程修改。为安全起见,可为每个需要传值的线程分配堆内存,创建时传递该内存地址(线程私有),而在新线程内部释放该内存。
本节示例中,主线程仅向 SigMgrThread 线程传递信号屏蔽字,且主线程结束时进程退出。因此,尽管 SigMgrThread 线程已分离,但仍可直接使用创建线程时 pvArg 传递的信号屏蔽字。否则应使用全局屏蔽字变量,或在本函数内再次设置屏蔽字自动变量。
编译链接后,执行结果如下(无论是否定义 USE_SIGWAIT):
以下按行解释和分析上述执行结果:
【6~13行】相同的非实时信号(编号小于 SIGRTMIN)不会在信号队列中排队,只被递送一次;相同的实时信号(编号范围为 SIGRTMIN~SIGRTMAX)则会在信号队列中排队,并按照顺序全部递送。若信号队列中有多个非实时和实时信号排队,则先递送编号较小的信号,如 SIGUSR1(10)先于 SIGUSR2(12),SIGRTMIN(34)先于 SIGRTMAX(64)。但实际上,仅规定多个未决的实时信号中,优先递送编号最小者。而实时信号和非实时信号之间,或多个非实时信号之间,递送顺序未定义。
注意,SIGRTMIN/SIGRTMAX 在不同的类 Unix 系统中可能取值不同。NPTL 线程库的内部实现使用两个实时信号,而 LinuxThreads 线程库则使用三个实时信号。系统会根据线程库适当调整 SIGRTMIN 的取值,故应使用 SIGRTMIN+N/SIGRTMAX-N(N为常量表达式)来指代实时信号。用户空间不可将 SIGRTMIN/SIGRTMAX 视为常量,若用于 switch…case 语句会导致编译错误。
通过 kill –l 命令可查看系统支持的所有信号。
【6~13行】sigwait() 函数是线程安全(thread-safe)的。但当 tMgrThrdId 和 tMgrThrdId2 同时等待信号时,只有先创建的 tMgrThrdId(SigMgrThread) 线程等到信号。因此,不要使用多个线程等待同一信号。
【14行】调用 pthread_create() 返回后,新创建的线程可能还未启动;反之,该函数返回前新创建线程可能已经启动。
【15行】SIGQUIT 信号被主线程捕获,因此不会中断 SigMgrThread 中的 sigwaitinfo() 调用。虽然 SIGQUIT 安装(signal语句)在 SigMgrThread 内,由于主线程共享该处理行为,SIGQUIT 信号仍将被主线程捕获。
【16行】sleep() 函数使调用进程被挂起。当调用进程捕获某个信号时,sleep() 提前返回,其返回值为未睡够时间(所要求的时间减去实际休眠时间)。注意,sigwait() 等到的信号并不会导致 sleep() 提前返回。因此,示例中发送 SIGQUIT 信号可使 sleep() 提前返回,而 SIGINT 信号不行。
在线程中尽量避免使用 sleep() 或 usleep(),而应使用 nanosleep()。前者可能基于 SIGALARM 信号实现(易受干扰),后者则非常安全。此外,usleep() 在 POSIX 2008 中被废弃。
【17行】WorkerThread 线程启动后调用 pthread_detach() 进入分离状态,主线程将无法得知其何时终止。示例中 WorkerThread 线程一直运行,故可通过 ThreadKill() 检查其是否存在。但需注意,这种方法并不安全。
【18行】已注册信号处理捕获 SIGINT 信号,同时又调用 sigwait() 等待该信号。最终后者等到该信号,可见 sigwait() 优先级更高。
【19行】sigwait() 调用从未决队列中删除该信号,但并不改变信号屏蔽字。当 sigwait() 函数返回时,它所等待的信号仍旧被阻塞。因此,再次发送 SIGINT 信号时,仍被 sigwait() 函数等到。
【21行】通过 pthread_sigmask() 阻塞 SIGSEGV 信号后,sigwait() 并未等到该信号。系统输出 “Segmentation fault” 错误后,程序直接退出。因此,不要试图阻塞或等待 SIGSEGV 等硬件致命错误。若按照传统异步方式使用 signal()/sigaction() 注册信号处理函数进行处理,则需要跳过引发异常的指令(longjmp)或直接退出进程(exit)。注意,SIGSEGV 信号发送至引起该事件的线程中。例如,若在主线程内解除对该信号的阻塞并安装处理函数 sighandler(),则当 SigMgrThread 线程内发生段错误时,执行结果将显示该线程捕获 SIGSEGV 信号。
本示例剔除用于测试的干扰代码后,即为“主线程-信号处理线程-工作线程”的标准结构。
3.3 示例3
本示例结合信号的同步处理与条件变量,以通过信号安全地唤醒线程。为简化实现,未作错误处理。
3.4 示例4
本示例将 sigwait() 可用于主线程,即可正常捕捉信号,又不必考虑异步信号安全性。
该例中主要等待 SIGINT/SIGQUIT 等终端信号,然后退出程序。
Linux 线程编程中,需谨记两点:
1) 信号处理由进程中所有线程共享;
2) 一个信号只能被一个线程处理。
具体编程实践中,需注意以下事项:
- 不要在线程信号屏蔽字中阻塞、等待和捕获不可忽略的信号(不起作用),如 SIGKILL 和 SIGSTOP。
- 不要在线程中阻塞或等待 SIGFPE/SIGILL/SIGSEGV/SIGBUS 等硬件致命错误,而应捕获它们。
- 在创建线程前阻塞这些信号(新线程继承信号屏蔽字),然后仅在 sigwait() 中隐式地解除信号集的阻塞。
- 不要在多个线程中调用 sigwait() 等待同一信号,应设置一个调用该函数的专用线程。
- 闹钟定时器是进程资源,且进程内所有线程共享相同的 SIGALARM 信号处理,故它们不可能互不干扰地使用闹钟定时器。
- 当一个线程试图唤醒另一线程时,应使用条件变量,而不是信号。
Linux高级编程——线程信号处理_线程间信号-CSDN博客
到此这篇sem_wait返回值(wait_event_interruptible返回值)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/rfx/57391.html