【阅读】《Linux高性能服务器编程》——第十章:信号


Linux产生进程的条件:

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号;
  • 系统异常
  • 系统状态变化
  • 运行kill命令或者调用kill函数

10.1 Linux信号概述

10.1.1 发送信号

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

// Linux下,一个进程给其他进程发送信号
// sig:信号;pid:目标进程
int kill(pid_t pid, int sig);
// 该函数成功时返回0,失败时返回-1并设置errno
pid参数含 义
pid > 0信号发送给PID为pid的进程
pid = 0信号发送给本进程组内其他进程
pid = -1信号发送给除init进程外所有进程,需要发送者拥有对目标进程发送信号的权限
pid < -1信号发送给组ID为-pid的进程组中所有的成员

  Linux定义的信号值都大于0,若sig=0,则kill函数不发送任何信号。但sig=0可以用来检测目标进程或进程组是否存在。但此检测方式不可靠,一方面由于进程PID的回绕导致被检测的PID不是我们期望的进程PID;另一方面这种检测方法不是原子操作。

errno含 义
EINVAL无效的信号
EPERM该进程没有权限发送信号给任何一个目标进程
ESRCH目标进程或进程组不存在

10.1.2 信号处理方式

#include <signal.h>

// 目标进程在收到信号时,需要定义一个接收函数来处理,其原型为:
typedef void (*__sighandler_t)(int);

  信号处理函数只能带一个整型参数,该参数用来指示信号类型。信号处理函数应该是可冲入的,否则容易引发竞态条件。

#include <bits/signum.h>

// 其他两种处理方式
// SIG_IGN忽略目标信号;SIG_DFL使用信号的默认处理方式
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)

10.1.3 Linux信号

  Linux的可用信号都定义在bits/signum.h头文件中,包括标准信号和POSIX实时信号。其默认信号有:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件(Core)、暂停进程(Stop)、继续进程(Cont)。

10.1.4 中断系统调用

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


10.2 信号函数

10.2.1 signal系统调用

#include <signal.h>

// sig:指出要捕获的信号类型,_handler用于指定信号sig的处理函数
_sighandler_t signal(int sig, _sighandler_t _handler);
// 成功时返回类型为_sighandler_t的函数指针,错误返回SIG_ERR并设置errno

10.2.2 sigaction系统调用

#include <signal.h>

// 更健壮的信号处理函数接口
// sig:要捕获的信号类型;act:制定新的信号处理方式;oact:输出信号前的处理方式
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);

// sigaction结构体定义:
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   __sigatcion_handler.sa_sigaction
#else
    _sighandler_t sa_handler;
#endif

    _sigset_t sa_mask;
    int sa_flags;
    void(*sa_restorer)(void);
};

10.3 信号集

10.3.1 信号集函数

#include <bits/sigset.h>
#include <signal.h>

// Linux用数据结构sigset_t来表示一组信号
#define _SIGSET_NWORDS (1024/(8*sizeof(unsigned long int)))
typedef struct{
    unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

int sigemptyset(sigset_t* _set);                // 清空信号集
int sigfillset(sigset_t* _set);                 // 在信号集中设置所有信号
int sigaddset(sigset_t* _set, int _signo);      // 将信号_signo添加至信号集中
int sigdelset(sigset_t* _set, int _signo);      // 将信号_signo从信号集中剔除
int sigismember(_const sigset_t*, int _signo);  // 测试_signo是否在信号集中

10.3.2 进程信号掩码

#include <signal.h>

// _set参数指定新的信号掩码;_oset输出原来的信号掩码,_how设定进程信号掩码的方式
int sigprocmask(int _how, _const sigset_t* _set, sigset_t* _oset);
_how参数含 义
SIG_BLOCK新的进程信号掩码是其当前值和_set指定信号集的并集
SIG_UNBLOCK新的进程信号掩码是其当前值和~_set的交集
SIG_SETMASK直接将信号掩码设置为_set

10.3.3 被挂起的信号

  设置进程信号掩码后,被屏蔽的信号将不能被进程接受,如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起的信号的屏蔽,则它能立即被进程接收到。

#include <signal.h>

// 获得被挂起的信号集
// set参数用于保存被挂起的信号集
int sigpending(sigset_t* set);

10.4 统一事件源

  信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽太久。其典型解决方案是:把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它指示简单的通知主循环程序接收信号,并将信号值传递给主循环。主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道将信号值传递给主函数,主函数通过I/O服用系统调用来监听管道读端文件描述符上的可读事件即可统一事件源。

实例:统一事件源

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet.h>
#include <arpa.inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <signal.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;
}

// 将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中,参数enable_et指定是否对fd启用ET模式
void addfd(int epollfd, int fd, bool enable_et){
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if(enable_et){
        event.events |= 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);

    // 创建TCP socket,绑定在端口port上
    int listenfd = socket(PF_INET, SOCK_STERAM, 0);
    assert(listenfd>0);
    ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    if(ret = -1){
        printf("errno is %d\n", errno);
        return 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创建管道,注册pipefd[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{
                    // 因为每个信号值占一个字节,所以按照字节逐步接受信号
                    for(int i=0;i<ret;i++){
                        switch(signals[i]){
                            case SIGCHLD:
                            case SIGHUP:{
                                continue;
                            }
                            case SIGTERM:
                            case SIGEINT:{
                                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信号来强制服务器重读配置文件。

10.5.2 SIGPIPE

  默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号,需要对此信号进行捕获或忽略。程序接收到SIGPIPE信号默认的行为是结束进程,引起SIGPIPE信号的写操作将设置errno为EPIPE。
  可利用I/O复用系统调用来检测管道和socket连接的读端是否关闭。以poll为例,当管道的读端关闭时,写端文件描述符上的POLLHUP事件将被处罚,当socket连接被对方关闭时,socket上的POLLRDHUP事件将被触发。

10.5.3 SIGURG

  在Linux环境下,内核通知应用程序带外数据主要有两种方法:1-I/O复用技术,select等系统调用在接收到带外数据时返回,并向程序报告socket上的异常事件。2-使用SIGURG信号。

实例:用SIGURG检测带外数据是否到来

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

#define BUF_SIZE 1024

static int connfd;

// SIGURG信号处理函数
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){
    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 sock = socket(PF_INET, SOCK_STERAM, 0);
    assert(sock>0);
    ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    if(ret = -1){
        printf("errno is %d\n", errno);
        return 1;
    }
    ret = listen(sock, 5);
    assert(ret!=-1);

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock,(struct sockaddr*)&client, &client_addrlength);
    if(connfd<0){
       printf("errno is: %d\n", errno); 
    }
    else{
        addsig(SIGURG, sig_urg);
        // 使用SIGURG信号之前,必须设置socket的宿主进程或进程组
        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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甄姬、巴豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值