Linux高性能服务器编程|阅读笔记:第10章 - 信号

举报
海轰Pro 发表于 2023/07/29 23:10:50 2023/07/29
【摘要】 简介Hello!非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~ ଘ(੭ˊᵕˋ)੭昵称:海轰标签:程序猿|C++选手|学生简介:因C语言结识编程,随后转入计算机专业,获得过国家奖学金,有幸在竞赛中拿过一些国奖、省奖…已保研学习经验:扎实基础 + 多做笔记 + 多敲代码 + 多思考 + 学好英语! 唯有努力💪 本文仅记录自己感兴趣的内容 10.1 Linux信号概述信号是由用户...

简介

Hello!
非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~
 
ଘ(੭ˊᵕˋ)੭
昵称:海轰
标签:程序猿|C++选手|学生
简介:因C语言结识编程,随后转入计算机专业,获得过国家奖学金,有幸在竞赛中拿过一些国奖、省奖…已保研
学习经验:扎实基础 + 多做笔记 + 多敲代码 + 多思考 + 学好英语!
 
唯有努力💪
 
本文仅记录自己感兴趣的内容

10.1 Linux信号概述

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

linux信号可由如下条件产生:

  • 对于前台进程,用户可以通过输人特殊的终端字符来给它发送信号。比如输入Ctl+C 通常会给进程发送一个中断信号。
  • 系统异常。比如浮点异常和非法内存段访问。
  • 系统状态变化。比如alarm定时器到期将引起SIGALRM信号。
  • 运行kill命令或调用kil函数。

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

10.1.1 发送信号

Linux下,一个进程给其他进程发送信号的API是kill函数。其定义如下:

该函数把信号sig发送给目标进程;

目标进程由pid参数指定,其可能的取值及含义如表10-1所示

Linux定义的信号值都大于0,如果sig取值为0,则kill函数不发送任何信号。

但将sig设置为0可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行。不过这种检测方式是不可靠的

  • 一方面由于进程PD的回绕,可能导致被检测的PID不是我们期望的进程的PD
  • 另一方面,这种检测方法不是原子操作

10.1.2 信号处理方式

目标进程在收到信号时,需要定义一个接收函数来处理之。

信号处理函数的原型如下:

信号处理函数只带有一个整型参数,该参数用来指示信号类型

信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。

除了用户自定义信号处理函数外,bits/signum.h头文件中还定义了信号的两种其他处理
方式一SIG_IGN和SIG_DEL:

SIG_IGN表示忽略目标信号

SIG_DFL表示使用信号的默认处理方式。

信号的默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件 (Core)、暂停进程(Stop),以及继续进程(Cont)。

10.1.3 Linux信号

Linux的可用信号都定义在bits/signum.h头文件中,其中包括标准信号和POSIX实时信号。

10.1.4 中断系统调用

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

对于默认行为是暂停进程的信号(比如SIGSTOP、SIGTTIN),如果我们没有为它们设
置信号处理函数,则它们也可以中断某些系统调用(比如connect、epoll_wait)。

POSIX没有 规定这种行为,这是Linux独有的。

10.2 信号函数

10.2.1 signal系统调用

为一个信号设置处理函数,可以使用下面的signal系统调用:

sig参数指出要捕获的信号类型

_handler参数是_sighandler_t类型的函数指针,用于指定信号sig的处理函数

10.2.2 sigaction系统调用

设置信号处理函数的更健壮的接口是如下的系统调用:

  • sig参数指出要捕获的信号类型

  • act参数指定新的信号处理方式

  • oact参数则输出信号先前的处理方式(如果不为NULL的话)。

act和oact都是sigaction结构体类型的指针, sigaction结构体描述了信号处理的细节,其定义如下:

该结构体中的sa_hander成员指定信号处理函数

sa_mask成员设置进程的信号掩码 (确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。sa_mask是信号集sigset_t(_sigset_t的同义词)类型,该类型指定一组信号。关于信号集,我们将在后面介绍。

sa_flags成员用于设置程序收到信号时的行为,其可选值如表10-4 所示。

10.3 信号集

10.3.1 信号集函数

Linux使用数据结构sigset_t来表示一组信号,定义如下:

由该定义可见,sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符集fd_set类似。

Linux提供了如下一组函数来设置、修改、 删除和查询信号集:

10.3.2 进程信号掩码

前文提到,我们可以利用sigaction结构体的sa_mask成员来设置进程的信号掩码。

此外,如下函数也可以用于设置或查看进程的信号掩码:

10.3.3 被挂起的信号

设置进程信号掩码后,被屏蔽的信号将不能被进程接收。

如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。

如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。

如下函数可以获得进程当前被挂起的信号集

10.4 统一事件源

信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。

很显然, 信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它)太久。

一种典型的解决方案是:

  • 把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。
  • 信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。

那么主循环怎么知道管道上何时有数据可读呢?

  • 这很简单,我们只需要使用I/O复用系统调用来监听管道的读端文件描述符上的可读事件
  • 如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源。

很多优秀的I/O框架库和后台服务器程序都统一处理信号和I/O事件,比如Libevent I/O 框架库和xinetd超级服务。

小实验:统一事件源的一个简单实现

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_EVENT_NUMBER 1024
static int pipefd[2];

int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

void addfd( int epollfd, int fd )
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}
// 信号处理函数
void sig_handler( int sig )
{
	// 保留原来的errno
	// 在函数最后恢复,以保证函数的可重入性
    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[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    int ret = 0;
    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );

    //int nReuseAddr = 1;
    //setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &nReuseAddr, sizeof( nReuseAddr ) );
    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    if( ret == -1 )
    {
        printf( "errno is %d\n", errno );
        return 1;
    }
    //assert( ret != -1 );

    ret = listen( listenfd, 5 );
    assert( ret != -1 );

    epoll_event events[ MAX_EVENT_NUMBER ];
    int epollfd = epoll_create( 5 );
    assert( epollfd != -1 );
    addfd( epollfd, listenfd );

	// 使用socketpair创建管道 注册pipifd[0]上的可读事件
    ret = socketpair( PF_UNIX, SOCK_STREAM, 0, pipefd );
    assert( ret != -1 );
    setnonblocking( pipefd[1] );
    addfd( epollfd, 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;
            // 如果就绪的文件描述符是listenfd 则处理新的连接
            if( sockfd == listenfd )
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
                addfd( epollfd, connfd );
            }
            // 如果就绪的文件描述符是pipefd[0] 则处理信号
            
            else if( ( sockfd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )
            {
                int sig;
                char signals[1024];
                ret = recv( pipefd[0], signals, sizeof( signals ), 0 );
                if( ret == -1 )
                {
                    continue;
                }
                else if( ret == 0 )
                {
                    continue;
                }
                else
                {
                	// 因为每个信号占1个字节
                	// 所以按字节来逐个接收信号
                	// 我们以SIGTERM为例,来说明如何安全终止服务器主循环
                    for( int i = 0; i < ret; ++i )
                    {
                        //printf( "I caugh the signal %d\n", signals[i] );
                        switch( signals[i] )
                        {
                            case SIGCHLD:
                            case SIGHUP:
                            {
                                continue;
                            }
                            case SIGTERM:
                            case SIGINT:
                            {
                                stop_server = true;
                            }
                        }
                    }
                }
            }
            else
            {
            }
        }
    }

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

10.5 网络编程相关信号

10.5.1 SIGHUP

当挂起进程的控制终端时,SIGHUP信号将被触发。

对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件

一个典型的例子是xinetd超级服务程序。

10.5.2 SIGPIPE

默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号

我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。

引起SIGPIPE信号的写操作将设置ermo为EPIPE。

第5章提到,我们可以使用send函数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。在这种情况下,我们应该使用send函数反馈的errno值来判断管道或者 socket连接的读端是否已经关闭。

此外,我们也可以利用I/O复用系统调用来检测管道和socket连接的读端是否已经关闭。

以poll为例,当管道的读端关闭时,写端文件描述符上的POLLHUP事件将被触发;当 socket连接被对方关闭时,socket上的POLLRDHUP事件将被触发。

10.5.3 SIGURG

在Liux环境下,内核通知应用程序带外数据到达主要有两种方法:

  • 一种是第9章介绍 的I/O复用技术,select等系统调用在接收到带外数据时将返回,并向应用程序报告socket上的异常事件
  • 另外一种方法就是使用SIGURG信号

使用SIGURG信号检测带外数据是否到达

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>

#define BUF_SIZE 1024

static int connfd;

void sig_urg( int sig )
{
    int save_errno = errno;
    
    char buffer[ BUF_SIZE ];
    memset( buffer, '\0', BUF_SIZE );
    int ret = recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );
    printf( "got %d bytes of oob data '%s'\n", ret, buffer );

    errno = save_errno;
}

void addsig( int sig, void ( *sig_handler )( int ) )
{
    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[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int sock = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sock >= 0 );

    int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( sock, 5 );
    assert( ret != -1 );

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof( client );
    connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
    if ( connfd < 0 )
    {
        printf( "errno is: %d\n", errno );
    }
    else
    {
        addsig( SIGURG, sig_urg );
        fcntl( connfd, F_SETOWN, getpid() );

        char buffer[ BUF_SIZE ];
        while( 1 )
        {
            memset( buffer, '\0', BUF_SIZE );
            ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
            if( ret <= 0 )
            {
                break;
            }
            printf( "got %d bytes of normal data '%s'\n", ret, buffer );
        }

        close( connfd );
    }

    close( sock );
    return 0;
}

结语

文章仅作为个人学习笔记记录,记录从0到1的一个过程

希望对您有一点点帮助,如有错误欢迎小伙伴指正

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。