• 异步信号安全


    前言

    Linux产生信号中断,会打断当前正在执行程序,转而执行信号处理函数,由于执行信号处理函数时,正常执行程序被挂起,信号处理函数怎么操作才能保证程序再次被唤醒后能够正常执行,下面我们看两个案例

    案例一

    void signal_handler()
    {
    	printf("this is a test
    ");
    }
    

    如果我们程序执行中调用malloc, printf等函数时,产生信号后执行上面信号处理函数,由于这些函数中有全局变量或static变量,执行后会破坏全局数据结构,造成不可预测后果。

    案例二

    void signal_handler()
    {
         lock(&mutex);
         printf("this is a test
    ");
         unlock(&mutex);
    }
    
    void  main()
    {
         .....
         lock(&mutex);
         printf("this is a test
    ");
         unlock(&mutex);
    }
    

    如果我们加锁是否就信号安全了呢,不尽然,看上面代码,假如我们执行main中lock函数获取锁后,程序捕获到信号,转而调用signal_handler函数,会发现永远无法加锁成功,造成死锁。

    开源软件处理方式

    Nginx

    1.信号处理函数首先保存errno值,函数结束时恢复该值,由于errno是全局变量,信号处理函数中间可能会被函数修改。
    2.通过设置一组全局变量,根据不同信号,设置不同值,当程序被唤醒后,通过判断这些值的状态来进行相应的操作,这个也是常用的做法。
    3.程序启动时,从strerr获取所有错误的字符串描述,保存进一个全局数组中,保证信号处理函数调用的安全,因为strerr信号调用中并不安全。
    4.信号处理函数中会进行一个时间的更新,由于时间变量是全局缓存变量,因此用锁进行了同步,不过这里的锁是一个尝试锁,并不会阻塞,尝试如果加锁失败,会立即返回并不会造成死锁。

    void ngx_signal_handler(int signo)
    {
        char            *action;
        ngx_int_t        ignore;
        ngx_err_t        err;
        ngx_signal_t    *sig;
    
        ignore = 0;
    	
    	/*保存errno值*/
        err = ngx_errno;
    	
    	/*判断是否是忽略的信号*/
        for (sig = signals; sig->signo != 0; sig++) {
            if (sig->signo == signo) {
                break;
            }
        }
    	
    	/*时间更新函数,这里有个尝试锁保证全局时间变量正确性*/
        ngx_time_sigsafe_update();
    
        action = "";
    
    	... ... ...
    	/*不同的信号,给不同的全局变量赋值*/
    	switch (signo) {
    
    	case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
    		ngx_quit = 1;
    		action = ", shutting down";
    		break;
    
    	case ngx_signal_value(NGX_TERMINATE_SIGNAL):
    	case SIGINT:
    		ngx_terminate = 1;
    		action = ", exiting";
    		break;
    
    	case ngx_signal_value(NGX_NOACCEPT_SIGNAL):
    		if (ngx_daemonized) {
    			ngx_noaccept = 1;
    			action = ", stop accepting connections";
    		}
    		break;
    
    	case ngx_signal_value(NGX_RECONFIGURE_SIGNAL):
    		ngx_reconfigure = 1;
    		action = ", reconfiguring";
    		break;
    
    	case ngx_signal_value(NGX_REOPEN_SIGNAL):
    		ngx_reopen = 1;
    		action = ", reopening logs";
    		break;
    
    	case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
    		if (getppid() > 1 || ngx_new_binary > 0) {
    
    			/*
    			 * Ignore the signal in the new binary if its parent is
    			 * not the init process, i.e. the old binary's process
    			 * is still running.  Or ignore the signal in the old binary's
    			 * process if the new binary's process is already running.
    			 */
    
    			action = ", ignoring";
    			ignore = 1;
    			break;
    		}
    
    		ngx_change_binary = 1;
    		action = ", changing binary";
    		break;
    
    	case SIGALRM:
    		ngx_sigalrm = 1;
    		break;
    
    	case SIGIO:
    		ngx_sigio = 1;
    		break;
    
    	case SIGCHLD:
    		ngx_reap = 1;
    		break;
    	}
    	
    	... ... ...
    	
    	/*日志记录*/
        ngx_log_error(NGX_LOG_NOTICE, ngx_cycle->log, 0,
                      "signal %d (%s) received%s", signo, sig->signame, action);
    	
    	/*恢复errno值*/
        ngx_set_errno(err);
    }
    

    libevent

    1.信号处理函数首先保存errno值,函数结束时恢复该值,由于errno是全局变量,信号处理函数中间可能会被函数修改。
    2.通过socketpair创建一个socket对fd[2],然后从fd[0]发送信号内容,程序被唤醒后会触发fd[1]接收事件,接收fd[0]发送的信号,进行相应处理。

    static void evsignal_handler(int sig)
    {
    	/*保存errno值*/
    	int save_errno = errno;
    
    	if (evsignal_base == NULL) {
    		event_warn(
    			"%s: received signal %d, but have no base configured",
    			__func__, sig);
    		return;
    	}
    
    	evsignal_base->sig.evsigcaught[sig]++;
    	evsignal_base->sig.evsignal_caught = 1;
    
    #ifndef HAVE_SIGACTION
    	signal(sig, evsignal_handler);
    #endif
    
    	/*通过socketpair发送产生信号事件,唤醒处理事件线程进行处理*/
    	send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);
    	
    	/*恢复errno值*/
    	errno = save_errno;
    }
    

    异步信号处理怎么做

    从Nginx和libevent中可以看出,信号处理时只是记录了下产生了什么信号,并没有进行实际处理,处理过程还是交给主程序本身,避免调用一些非信号安全的函数。因此我们再编写信号处理函数的时候也只要记录下信号状态,对errno这种全局变量,进行保存,处理结束后恢复变量值,尽量避免使用锁。

    附注:可重入函数,信号安全函数,线程安全函数区别

    1.可重入函数是指在任何时候任何地方调用都能保证安全的函数,无论是线程还是信号处理函数,函数一般没有共享变量或者锁之类的东西,linux下系统函数只有80多个可重入函数,可以参考unix环境高级编程一书。
    2.线程安全函数是指多个线程同时调用此函数,能保证安全执行,显然可重入函数只是线程安全函数的一个子集。
    3.信号安全函数是在信号处理函数中可以安全调用的函数。

  • 相关阅读:
    json&display
    postgresql AutoVacuum系统自动清理进程
    postgresql vacuum操作
    C++ 在.h文件中包含头文件和在.cpp文件中包含头文件有什么区别
    ResetEvent、CreateEvent、SetEvent
    《转载》C语言的移位操作符
    《转载》如何使用M6117D看门狗定时器复位系统
    《转载》 Bit,Byte,WORD,DWORD区别和联系
    $.messager.alert
    对一个或多个实体的验证失败
  • 原文地址:https://www.cnblogs.com/ourroad/p/4834383.html
Copyright © 2020-2023  润新知