linux c信号(2)-处理函数

2018-06-08 12:50:19

  一般而言信号处理函数设计的越简单越好。其中的一个重要原因在于,这将降低引发竞争条件的风险。下面是信号处理函数的两种常见设计。

1.信号处理器函数设置全局性标记变量并退出。主程序周期性检查,一旦变化随机采取相应动作。
2.信号处理器函数执行某种类型的清理动作,接着终止进程或者使用非本地跳转将栈解开并将控制返回到主程序中的预定位置。

可重入函数和非可重入函数

如果同一个进程的多条线程可以同时安全的调用某一函数,那么该函数就是可重入的。此处 安全 意味着,无论其他线程调用该函数的执行状态如何,函数都能产生预期的结果。

1.更新全局变量和静态数据结构一般为非可重入。
2.比如主程序调用malloc()(非可重入函数)一半时被信号中断执行handler,而handler函数中也调用malloc则会破坏之前的内存链。
3.一般像gmtime(), localtime()返回的都是静态分配内存而当handler也调用时则会覆盖之前的。
4.一般stdio库像printf,scanf都是非可重入函数,缓冲区结构可能被破坏。
5.读取变量这个动作是非原子性的(可能发生在读取变量之中被信号打断)而int或比int短如short的操作是原子的,如 sig_atomic_t 就是个int

异步信号安全函数

可重入函数或不能被信号打断的函数为异步信号安全函数。

SUSv3强调,上图以外的所有函数对于信号而言都是不安全的,所谓不安全,指的是当信号处理器函数中断了不安全函数的执行,同时处理器函数也调用了这个不安全函数时,才算不安全。所以编写信号处理函数时有几种选择:

1.确保信号处理函数本省是可重入的,且只调用异步信号安全函数。
2.当主程序执行不安全函数或是去操作信号处理函数也能更新的全局数据结构时,阻塞信号的传递。

第2种方法在复杂的程序中想要确保主程序对不安全函数的调用不被信号打断有些困难,所以上面一般简化为信号处理函数绝不调用不安全的函数。

信号处理器函数内部对errno的使用

上图中的函数依然可能会导致处理器函数不可重入,因为它们如果报错之类的可能会覆盖掉主程序的errno值。下面是一种变通方法。

void
handler(int sig)
{
int savedErrno;
savedErrno = errno;
/* 执行一些可能会改变errno的函数 */

//复原
errno = savedErrno;
}

全局变量和sig_atomic_t数据结构

尽管存在可重入问题,有时还是需要在主程序和信号处理器函数之间共享全局变量,只要处理得当,也是安全的。

所有在主程序与信号处理器函数之间共享的全局变量都应声明如下:

volatile sig_atomic_t flag;

来保证读写的原子性,对flag的++和--在某些硬件架构上可能不是原子操作。

 在信号处理器函数中执行非本地跳转

使用longjmp() 来退出信号处理函数时,system V一脉中不会恢复信号掩码(在调用信号处理器函数时会自动屏蔽掉触发该处理器函数的信号),而BSD一脉则会恢复。

鉴于两派之间的差异,POSIX定义了一对新函数来显示的控制。

#include <setjmp.h>

int sigsetjmp(sigjmp_buf env, int savesigs);

//初始化返回0, 通过 siglongjmp()返回非0

void siglongjmp(sigjmp_buf env, int val);

他们与setjmp()和 longjmp()类似,唯一的区别是env类型变了。savesigs 如果为非0,则会把当前信号掩码保存于env中,之后相同env 参数的siglongjmp()调用进行恢复。savesigs 为0则不恢复。

异常终止进程:abort()

函数abort() 通过产生 SIGABRT信号来终止程序,并生成核心转储(coredump)文件来供调试器调试。SUSV3要求,无论阻塞或者忽略SIGABRT信号,abort()调用均不受影响,abort()成功终止了进程,还将刷新stdio流并将其关闭。

SA_SIGINFO 标志

如果创建处理器函数时设置了 SA_SIGINFO 标志,那么在收到信号时处理器函数将会收到额外信息,需要将处理器函数声明如下:

void handler(int sig, siginfo_t *siginfo, void *ucontext);

其实struct sigaction的完整定义为这样

#define sa_handler	__sigaction_handler.sa_handler
#define sa_sigaction	__sigaction_handler.sa_sigaction
struct sigaction {
    union{
        void (*sa_handler)(int); /* Address of handler 或者 SIG_IGN , SIG_DFL */
        void (*sa_sigaction) (int, siginfo_t *, void *);
    } __sigaction_handler;
    
    sigset_t sa_mask; /* Signals blocked during handler invocation */
    
    int sa_flags; /* Flags controlling handler invocation */
    void (*sa_restorer)(void); /* Not for application use */
};

下面是使用SA_SIGINFO创建信号处理器函数的例子

struct sigaction act;

sigemptyset(&act.sa_mask);
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;

if (sigaction(SIGINT, &act, NULL) == -1)
    perror("sigaction");

siginfo_t结构体格式如下

typedef struct {
    int si_signo; /* Signal number */
    int si_code; /* Signal code */
    int si_trapno; /* Trap number for hardware-generated signal
(unused on most architectures) */
    union sigval si_value; /* Accompanying data from sigqueue() */
    pid_t si_pid; /* Process ID of sending process */
    uid_t si_uid; /* Real user ID of sender */
    int si_errno; /* Error number (generally unused) */
    void *si_addr; /* Address that generated signal
(hardware-generated signals only) */
    int si_overrun; /* Overrun count (Linux 2.6, POSIX timers) */
    int si_timerid; /* (Kernel-internal) Timer ID
(Linux 2.6, POSIX timers) */
    long si_band; /* Band event (SIGPOLL/SIGIO) */
    int si_fd; /* File descriptor (SIGPOLL/SIGIO) */
    int si_status; /* Exit status or signal (SIGCHLD) */
    clock_t si_utime; /* User CPU time (SIGCHLD) */
    clock_t si_stime; /* System CPU time (SIGCHLD) */
} siginfo_t;

系统调用的中断和重启

比如当系统调用read阻塞时,信号传递过来了,信号处理器返回后,read则调用失败,并将errno 置为EINTR。

可以利用如下代码来重启系统调用。

while ((cnt = read(fd, buf, BUF_SIZE)) == -1 && errno == EINTR)
    continue; /* Do nothing loop body */
if (cnt == -1) /* read() failed with other than EINTR */
    perror("read");

SA_RESTART 标志可以针对每个信号设置自动重启一些系统调用。换言之,允许某些信号处理函数中断阻塞的系统调用,而其他系统调用则可以自动重启。

#include <signal.h>

int siginterrupt(int sig, int flag);

//Returns 0 on success, or –1 on error

siginterrupt()是用来改变信号的SA_RESTART设置。若flag为1,sig的信号处理器函数会中断阻塞的系统调用,flag为0,会重启阻塞的系统调用。

工作原理是:调用sigaction()获取当前的信号处置副本,调整SA_RESTART标志,接着再次调用sigaction()来更新信号设置。SUSv4标记废止。


总结

下一篇继续,如果有疑问可以给我留言。


备注
1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/4e

©著作权归作者所有
收藏
推荐阅读
简介
天降大任于斯人也,必先苦其心志。