Linux高性能服务器-第十章-信号

116 阅读8分钟

信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。

Linux信号可由下列条件产生:

  • 系统异常,如浮点异常或非法内存段访问;
  • 系统状态变化,如alarm定时器到期引发SIGALRM信号。
  • 运行kill命令或kill函数;
  • 对于前台进程,用户输入特殊字符引发信号,如输入Ctrl+C通常给进程发送一个中断信号。

服务器程序需处理(或至少忽略)一些常见信号,以避免异常终止。

10.1 发送信号

Linux下,进程通过kill函数给其他进程发送信号。

#include <sys/types.h>
#include <signal.h>

/*
	pid: 目标进程号
		pid = 0 发送给本进程组内的其他进程
		pid > 0 发送给进程pid
        pid = -1 发送给除init进程外的所有进程,但发送者需要有对目标进程发送信号的权限
        pid < -1 发送给进程组ID为-pid的所有成员
	sig: 信号
		sig = 0 不发送任何信号,但可检测目标进程或进程组是否存在,不可靠
		sig > 0 发送对应信号
	成功返回0, 失败返回-1并设置errno, 可能的errno见下表
*/
int kill( pid_t pid, int sig );

kill出错errno取值:

killerrno.png

10.2 处理信号

目标进程在收到信号时,需要定义一个接收函数来处理它,信号处理函数原型如下;

#include <signal.h>

typedef void (*__sighandler_t) (int);

信号处理函数只带有一个整型参数,用于指示信号类型。此外,这个函数应该是可重入的,因此禁止在该函数内调用不安全的函数。

bits/signum.h头文件中还定义了两种信号处理方式SIG_IGN和SIG_DEL。

#include <bits/signum.h>
/* 
	使用信号的默认处理方式,有如下几种:
	Term 	结束进程
	Ign 	忽略信号
	Core	结束进程并生成核心转储文件
    Stop 	暂停进程
    Cont 	继续进程
*/
#define SIG_DFL ((__sighandler_t) 0)	
// 忽略信号目标
#define SIG_IGN ((__sighandler_t) 1)			

10.3 Linux信号

Linux可用信号定义在bits/signum.h中,其中包括标准信号和POSIX实时信号。本书仅讨论标准信号。

sig1.png

sig2.png

如果程序在执行处于阻塞状态的系统调用时收到信号,并该信号有对应的信号处理函数,则默认情况下系统调用将被中断,errno被设置为EINTR。我们可使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。

对于默认行为是暂停进程的信号(SIGSTOP,SIGTTIN等),即使没有定义对应的信号处理函数,它们也可中断某些系统调用(如connect, epoll_wait)。

10.4 信号处理函数

10.4.1 signal系统调用

可使用signal为一个信号设置处理函数。

#include <signal.h>
/*
	sig: 要捕获的信号类型
	_handler:函数指针,指向sig的处理函数
	成功时返回一个函数指针,指向上一次调用signal函数时传入的函数指针,或sig对应的默认处理函数指针SIG_DEF(首次调用),失败返回SIG_ERR, 并设置errno
*/
_sighandler_t signal( int sig, _sighandler_t _handler );
10.4.2 sigaction系统调用

设置信号处理函数更健壮的接口是sigaction系统调用。

#include <signal.h>

/*
	sig:要捕获的信号类型
	act:指定新的信号处理方式
	oact:输出信号之前的处理方式(为什么要获取之前的处理方式?)
	成功返回0,失败返回-1并设置errno
*/
int sigaction( int sig, const struct sigaction* act, struct sigaction* oact );

struct sigaction
{
#ifdef __USE_POSIX199309
    union
    {
        _sighandler_t sa_handler;	// 函数指针 指定信号处理函数
        void (*sa_sigaction) ( int, siginfo_t*, void* );
    }
    _sigaction_handler;
#define sa_handler		__sigaction_handler.sa_handler
#define sa_sigaction	__sigaction_handler.sa_sigaction
#else
    _sighandler_t sa_handler;		// 函数指针 指定信号处理函数
#endif
    _sigset_t sa_mask;				// 设置进程的信号掩码,指定本进程屏蔽的信号, _sigset_t定义见下节
    int sa_flags;					// 设置程序收到信号的行为,可选值见下表
    void (*sa_restorer) (void);
};

sa_flags选项:

sa_flags.png

10.5 信号集

10.5.1 信号集函数

上节提到,sigaction结构体使用_sigset_t来表示一组信号。

#include <bits/sigset.h>

/*
	sigset_t实际是一个无符号长整型数组,每个位表示一个信号,共能表示1024个信号
*/
#define _SIGSET_NWORDS (1024 / 8*sizeof(unsigned long int))
typedef struct
{
    unsigned long int __val(_SIGSET_NWORDS);
} __sigset_t;

Linux提供如下函数来控制信号集。

#include <signal.h>

int sigemptyset( sigset_t* _set );					// 清空信号集
int sigfillset( sigset_t* _set );					// 设置所有信号
int sigaddset( sigset_t* _set, int _signo );		// 将信号_signo添加至信号集 
int sigdelset( sigset_t* _set );					// 将信号_signo从信号集删除
int sigismemset( sigset_t* _set );					// 信号_signo是否在信号集中
10.5.2 进程信号掩码

上节提到,sigaction结构体使用sa_mask来设置进程的信号掩码。

#include <signal.h>

/*
	设置或查看进程的信号掩码
	_how:指定设置信号掩码的方式,可取值见下表
	_set:指定新的信号掩码,若为NULL,则进程信号掩码不变
	_oset:输出原信号掩码
	成功返回0,失败返回-1并设置errno
*/
int sigprocmask( int _how, _const sigset_t* _set, sigset_t* _oset );

_how参数取值:

_how-1709861713270.png

10.5.3 信号挂起

设置进程信号掩码后,被屏蔽的信号将不能被进程接收,若给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的被挂起信号。

若我们取消对该信号的屏蔽,则它能立即被进程接收到。

#include <signal.h>
/*
	获取当前进程被挂起的信号集
	set:保存被挂起的信号集,进程即使多次接收到同一个屏蔽信号,set中也只保存一次,当使用sigprocmask使能挂起信号时,该信号的处理函数也只触发一次
	成功返回0,失败返回-1并设置errno
*/
int sigpending( sigset_t* set );

10.6 统一事件源

信号是一种异步事件,信号处理函数与程序的主循环是两条不同的执行路线。同时,信号处理函数应尽可能快的执行完毕,以避免该信号被屏蔽太久。

一种典型处理方式是:将信号的主要处理逻辑放在程序的主循环中,而信号处理函数只负责通知主循环接收到信号,并将信号值传递给主循环。

实现方式是:创建一个管道,信号处理函数被触发后,通过管道将信号值发送给主循环,主循环使用IO复用监听管道读端上的可读事件。如此,信号事件就可与其他IO事件一样被处理,即统一事件源。

#define MAX_EVENT_NUMBER 1024
static int pipefd[2];

int setnonblocking( int fd )
{...}

// 往epoll内核事件表中注册文件描述fd上的事件
void addfd( int epollfd, int fd )
{ ... }

// 定义信号处理函数
void sig_handler( int sig )
{
    int save_errno = errno;
    int msg = sig;
    // 通过管道传递信号值给主循环
    send( pipefd[1], ( char* )&msg, 1, 0 );		
    errno = save_errno;
}

void addsig( int sig )
{
    struct sigaction sa;
    memset( &sa, '\0', sizeof( sa ) );
    sa.sa_handler = sig_handler;		// 设置信号处理函数
    sa.sa_flags |= SA_RESTART;			// 设置信号处理方法
    sigfillset( &sa.sa_mask );
    assert( sigaction( sig, &sa, NULL ) != -1 );
}

int main( int argc, char* argv[] )
{
	...		// socket创建,绑定,监听
    epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
    addfd( epollfd, listenfd );
	// 创建一个双向管道
    ret = socketpair( PF_UNIX, SOCK_STREAM, 0, pipefd );
    assert( ret != -1 );
    setnonblocking( pipefd[1] );
    addfd( epollfd, pipefd[0] );		// 注册pipefd[0]上的可读事件

    // 设置信号处理函数
    addsig( SIGHUP );
    addsig( SIGCHLD );
    addsig( SIGTERM );
    addsig( SIGINT );
    bool stop_server = false;

    while( !stop_server )
    {
        int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ( number < 0 ) && ( errno != EINTR ) )
        {
            printf( "epoll failure\n" );
            break;
        }
    
        for ( int i = 0; i < number; i++ )
        {
            int sockfd = events[i].data.fd;
            if( sockfd == listenfd )				// 处理新连接
            {
                ...
            }
            else if( ( sockfd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )		
            {
                // 如果就绪文件描述符是pipefd[0],则处理信号
                int sig;
                char signals[1024];
                ret = recv( pipefd[0], signals, sizeof( signals ), 0 );
                if( ret == -1 || ret == 0)
                {
                    continue;
                }              
                else
                {
					// 每个信号占1字节,故按字节来逐个接收信号
                    for( int i = 0; i < ret; ++i )
                    {
                        switch( signals[i] )
                        {
                            case SIGCHLD:
                            case SIGHUP:
                            {
                                // 信号处理逻辑
                                ...
                            }
                            case SIGTERM:
                            case SIGINT:
                            {
                                stop_server = true;
                            }
                        }
                    }
                }
            }
            else
            {
            }
        }
    }

    printf( "close fds\n" );
    close( listenfd );
    close( pipefd[1] );
    close( pipefd[0] );
    return 0;
}

10.7 网络编程相关信号

10.7.1 SIGHUP

当挂起进程的控制终端时,SIGHUP信号将被触发,对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。

10.7.2 SIGPIPE

默认情况下,往一个读端关闭的管道或者socket连接中写数据将引发SIGPEPE信号。我们需要在代码中捕获并处理该信号,或至少忽略它,因此SIGPIPE的默认处理方式是结束进程。引起SIGPIPE信号的写操作将返回errno为EPIPE。

第五章提到,我们可设置send函数的flags值为MSG_NOSIGNAL来禁止写操作触发SIGPIPE信号。在这种情况下,我们应根据返回的errno来判断管道或socket的读端是否已关闭。

此外,我们也可利用IO复用系统调用来判断管道或socket的读端是否已关闭。以poll为例,若管道读端关闭,写端文件描述符上的POLLHUP事件将被触发;当socket连接被对方关闭,socket上的POLLRDHUP事件将被触发。

10.7.3 SIGURG

Linux下内核通知应用程序有带外数据到达有两种方法:

一是通过IO复用,select等系统调用在接收到带外数据将立即返回,并向应用程序报告socket上的异常事件。

二是使用SIGURG信号。