system call, signal, exception in linux

System call and signal handler are running on behalf of the process

They are called soft exception or interrupt.

Hard exception or interrupt runs on behalf of itself, without the context of process.

So one question:
1. Can system call be interrupted by other signal, process and interrupt?
Yes
2.Can signal handler be interrupted by other signal, process and interrupt?
Can be by signal or interrupt, none by process

3. Is the system call be reentrant necessarily?
YES

4. Is the signal handler be reentrant necessarily?
No

进入signal handler以后,并没有什么信息留在内核栈上。被信号中断的那个用户态的上下文是被保存在用户栈上的。而sigreturn要做的事情就是,让调用者 指定一个sig上下文信息,然后内核负责把用户恢复到那个上下文去。(还有就是去掉相应的sig mask,这一点siglongjmp也会做。)
在此过程中,内核不需要保存任何中间状态。

进入signal handle的时候,用户态的栈应该是这样的:
=> setjmp时的栈 => ... => 被信号中断时的栈 => 内核放置的sigframe => signal handle的返回地址(去调用sigreturn) => signal handle的栈

如果signal handle正常返回,那么sigreturn被调用,栈上的sigframe被作为参数传递。然后经过sigreturn的折腾,返回用户态时,就回到了“被信号中断的栈”。
而如果signal handle里面longjmp了,那么用户态已经回到了setjmp时的栈上。后面的一系列信息都失效掉了。

当然,对于用户态多线程的情况,并不是这样。因为不同的用户态线程会有不同的栈空间,这时的longjmp会jmp到其他的栈空间,而不是自己栈空间里面靠前的栈。
------------------------------------------------------------------------------------------------------------------------------------------
Linux实时信号程序中锁的探索
此时程序进入死锁状态,需要用 kill或者 Ctrl+C强制使程序退出。该死锁发生的机制可以进行如下解释:如图 1 所示,当主程序请求并持有锁(sem_lock),开始做一些工作时。如果此时有信号的发生,主程序会被中断执行并跳转至信号函数 signal_test_func执行。进入信号函数之后,由于信号函数也需要访问共享资源,进而请求锁,由于锁仍然被主程序持有,信号函数就会一直等待 锁的释放。然而,因为主程序的运行已经被信号函数抢占,在信号函数完成之前无法运行,也就无法继续执行解锁动作,于是信号函数在请求锁时变成了死等待。

图 1. 在信号函数与主函数间加锁


图 1. 在信号函数与主函数间加锁

  方案 1,使用测试加锁

  如何解决这种问题呢?第一种思路可以将信号函数的加锁动作替换为测试加锁动作,例如使用:

  int sem_trywait(sem_t *sem);

  int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict abs_timeout);

  该函数是 sem_wait的非阻塞版本,如果加锁失败或超时则返回 -1。使用 sem_trywait修改函数 signal_test_func如下:

  清单 2. 使用 sem_trywait 代替 sem_wait

  void signal_test_func( int signo, siginfo_t * siginfo, void * ptr )

  {

  …

  if ( sem_trywait( &semlock ) != 0 )

  {

  msg = "exit signal handler. Lock failedn";

  write( 1, msg, strlen(msg));

  return;

  }

  // do something here.

  crit_value = 0;

  printf( "signal handled, crit_value = %d. n", crit_value );

  sem_post( &semlock );

  …

  }

  修改后的程序在运行时,无论信号发生在任何时刻都不会导致程序的死锁,信号函数在请求锁时一旦失败立即返回,而并不是死等在锁的请求上。当主程序有机会继续运行时,将锁进行释放,从而避免了死锁。执行结果如下:

  xxx@xxx-desktop:~$ ./test &

  …

  xxx@xxx-desktop:~$ kill -35 18748

  enter signal handler. If there is no exit message, a dead lock happened.

  exit signal handler. Lock failed

  main thread, crit_value = 1.

  xxx@xxx-desktop:~$ kill -35 18748

  …

  signal handled, crit_value = 0.

  …

  前后两次发送信号发生在不同的阶段,第一个信号发生于主函数持有锁的过程中,信号函数由于未能获得锁而退出;第二个信号发生在主函数释放锁之 后,信号函数成功获得锁。虽然这种方法避免了死锁,但却是以丢弃信号作为代价,因此这并不是一个好的解决办法,是否有更好的办法避免这个问题呢,下面引入 第二节,来探讨这个问题。

  方案 2,使用双线程处理信号与锁

  参考 Linux 系统、NPTL 对实时信号的实现,当系统派发一个信号给一个进程时,会选择该进程的某个线程进行处理,前提是这个线程未屏蔽该信号。而被选中的线程将先中断自己的执行并 跳转至信号函数执行,当信号函数执行完毕后,信号就被处理完毕,并进行释放,最后被中断的线程返回到中断处继续运行。但是,在被选中线程处理信号的过程 中,其他线程并不会停止运行,而是和信号处理线程处于平行关系的执行顺序,与线程间的执行关系完全相同,即信号函数的执行空间是在线程内的。如图 2

图 2. 双线程时,信号的处理


图 2. 双线程时,信号的处理

  这就给我们以一个提示,如果我们用 MainThread线程作为主线程运行,并使其屏蔽该信号,Thread1线程作为信号处理线程执行,专门用于信号函数的处理,并对 MainThread和 SignalHandler之间临界区的访问进行加锁,这样既避免死锁的问题,又避免了临界区访问重入问题,同时也避免了信号丢失。其中,需要使用到线程 信号处理函数:

  int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask);

  该函数的作用与 sigprocmask 函数颇为类似,但它处理的范围仅限于调用线程。how指定了处理 signalmask的方法,可以为 SIG_SETMASK,SIG_BLOCK或者 SIG_UNBLOCK,顾名思义,SIG_SETMASK使用 newmask参数替换原有 signal mask;SIG_BLOCK将 newmask里面的 signal mask标志位置为 block状态;SIG_UNBLOCK与前者功能相反。oldmask里面用来存放替换前的 signal mask状态,如果程序不需要恢复原来的 signal mask状态,可将这个参数置为 NULL。

  按照思路修改程序,其中关键程序片段如下:

  清单 3. 使用 pthread_sigmask 屏蔽主线程接收信号

  int main()

  {

  …

  if ( pthread_create( &Thread1_pt, NULL, &thread1_func, NULL ) != 0)

  perror( "Creating Child thread failedn" );

  // Blocking RT_TEST_SIG in Main Thread, always using thread1.

  sigset_t sigmask;

  sigemptyset( &sigmask );

  sigaddset( &sigmask, RT_TEST_SIG );

  // Should not use sigprocmask, it will block signals over process instead thread.

  pthread_sigmask( SIG_BLOCK, &sigmask, NULL );

  …

  }

  在程序执行过程中,需要自己用 kill 随机触发 RTMIN+1(35)信号,每次触发该信号时,都会打印

  [signal] enter signal handler.

  从程序的运行结果可以看得出来,对于 Thread1而言,虽然表面上它不做任何事情,但实际上,它在运行过程中,会因为信号的到来跳转至信号函数运行;因为信号函数由 Thread1运行,处在与 MainThread平行的状态,所以如果 MainThread(即主函数)处于临界区访问时,信号函数会一直等待到 MainThread退出临界区才继续运行,这样它们都有被系统调度运行的机会,于是不会造成死锁的问题。

  似乎,到现在对于信号与锁的问题得到了圆满的解决,但是如果我们扩展一下思路,将 Linux 系统对实时信号的处理特性扩展到多线程信号处理的情况下,就是我下面的扩展。

  多线程信号并行处理机制的实现

  设想现在有一个这样的应用:由于需要处理的信号比较多,出于对效率的考虑,如果能够将信号的处理并行化,将有效的提高程序的效率。如何编写这样的模型呢?基于双线程的一些讨论,我们可以得到以下理论模型,如图 3:

图 3. 多线程信号处理模型


图 3. 多线程信号处理模型

  在这个模型中,程序中存在多个线程,当有多个信号同时抵达时,系统会随机的选择其中一个线程完成信号的处理,当一个线程正在处理这个信号时,新 抵达的信号并不会被屏蔽,而是被分配给下一个可以处理信号的线程,同时由于信号处理的过程是在线程运行空间内完成的,这样在各个线程处理信号的过程中就达 到了一个并行的目的,根据重入性的需要,在处理函数中使用锁来达到互斥的目的。因此这个模型可以大大提高信号的处理能力,其同时可处理信号的个数将等于线 程的数量,根据应用的需要,对于 SMP 系统应该根据 CPU 的数目确定线程的数目。

  对于这个模型的实现程序,请读者自行实践,本文限于篇幅不给出详细的代码。而最简单的实现方法即是在方案 2 提供的程序中加入一个新的空线程。

  总结

  本文针对信号函数、锁及重入性问题进行一些有益的探讨,通过单线程程序、信号函数之间重入性的矛盾引入,扩展到 Linux 对于多线程程序信号函数处理的行为对双线程程序与信号重入性的影响,最后引申到多线程程序并行信号处理模型。本文中提供了解决在信号函数中调用不可重入程 序片段矛盾的方法,希望为使用信号函数的开发人员提供有益的参考。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值