Linux系统上的信号

《Linux高性能服务器编程》阅读笔记:

  信号机制就像单片机上的中断机制一样,中断机制需要一个中断源,同理,信号机制也需要信号源。信号的来源有:
  (1)用户:用户通过终端键入特殊字符(如ctrl+c,针对前台进程)
  (2)进程:运行kill命令或者kill()系统调用
  (3)系统:系统异常(如浮点异常、访问非法内存)、系统状态发生变化(如alarm定时器引起SIGALARM信号)

  信号机制的过程无非在于信号的发送、接收和处理,其中在接收环节我们还需要了解进程的信号掩码(用于屏蔽/使能指定进程接收某信号)。

1. 程序中信号的发送和处理

1.1 发送信号

1.1 发送信号
  一个进程给其它进程发送信号的API是kill()系统调用,原型如下:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

  (1)pid指定目标进程

pid>0,信号发送给PID为pid的进程
pid=0,信号发送给本进程组内的其他进程
pid=-1,信号发送给除init进程外的所有进程,但发送者需要具有对目标进程发送信号的权限
pid<-1,信号发送给组ID为-pid的进程组中的所有成员

  (2)sig指定信号
  Linux定义的信号值都大于0,可通过shell命令”kill -l”列出所有信号源:
这里写图片描述
  若sig取值为0则kill()函数不发生任何信号,可用来测试目标进程/进程组是否存在。不过这是一种不可靠的操作(原因之一此操作并非原子操作)。

  (3)返回值
  函数执行成功返回0,失败返回-1并设置errno。常见的errno取值有:

EINVAL: 无效的信号
EIPERM: 该进程没有权限发送信号给任何一个目标进程
ESRCH: 目标进程或进程组不存在
1.2 信号的处理

1.2.1 signal()系统调用
  signal()系统调用可以为一个(捕获到的)信号设置处理函数,其原型如下:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

  (1)signmm指出要捕获的信号。
  (2)handler是一个sighandler_t类型的函数指针,指针指向的函数即signum信号的处理函数。
  (3)函数执行成功返回一个sighandler_t类型的函数指针,该指针是前一次调用signal()时传入的函数指针,或者是signum信号默认的处理函数的指针(SIG_DEF)。下面是Linux系统针对信号处理函数3个默认的处理函数指针:

#define SIG_DFL ((__sighandler_t)0)     /* 默认处理 */
#define SIG_IGN ((__sighandler_t)1)     /* 忽略 */
#define SIG_ERR ((__sighandler_t)-1)    /* 出错,用于返回的 */

  SIG_DFL表示使用信号的默认处理方式,如:结束进程、忽略信号、结束进程并生成core文件、暂停进程、继续进程。函数执行失败返回SIG_ERR并设置errno。

1.2.2 sigaction()系统调用
  sigaction()是设置信号处理函数更健壮的系统调用,原型如下:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

  (1)signum指出要捕获的信号。
  (2)act指定新的信号处理方式
  (3)oldact是输出型参数,用于返回之前的信号处理方式(不设置为NULL的话)。act和oldact都是sigaction结构体类型的指针。sigaction结构体原型如下:

struct sigaction {
   void     (*sa_handler)(int);                         /* 信号处理函数 */
   void     (*sa_sigaction)(int, siginfo_t *, void *);  /* 另外一个信号处理,默认使用上一个函数,
                                                           若要使用本函数需要在sa_flags设置(见下) */
   sigset_t sa_mask;                                    /* 设置进程的信号掩码 */
   int      sa_flags;                                   /* 设置程序收到信号时的行为 */
   void    (*sa_restorer)(void);                        /* 不使用 */
};

  需要解释的参数:
  (4)sa_mask是指在进程原有的信号掩码的基础上增加掩码,用于设置哪些信号不能发送给本进程,其类型sigset_t表示信号集类型:

#ifndef __ASSEMBLY__
typedef struct {
        unsigned long sig[_NSIG_WORDS];
} sigset_t;

  可见,sigset_t实际上是一个长整型数组,数组的每一元素的每一位表示一个信号(跟select()系统调用中的fd_set类似)。下面函数用于设置、修改、删除和查询sigset_t信号集:

#include <signal.h>
int sigemptyset(sigset_t *set);                     //清空信号集
int sigfillset(sigset_t *set);                      //在信号集中设置所有信号,即若参数是等于上面的sa_mask则屏蔽所有信号
int sigaddset(sigset_t *set, int signum);           //将signum信号添加到信号集set中
int sigdelset(sigset_t *set, int signum);           //将signum信号从set信号集中删除
int sigismember(const sigset_t *set, int signum);   //测试signum信号是否在信号集中

  (5)sa_flags用于设置程序收到信号时的行为,取值有:

SA_NOCLDSTOP: signum为SIGCHLD,子进程暂停时不产生SIGCHLD信号
SA_NOCLDWAIT: signum为SIGCHLD,子进程结束时不产生僵尸进程
SA_SIGINFO: 使用sa_sigaction作为信号处理函数(默认是sa_handler)
SA_ONSTACK/SA_STACK: 重新调用被该信号终止的系统调用
SA_NODEFER/SA_NOMASK: 当程序接收到信号并进入对应信号处理函数后不屏蔽该信号。进程在执行信号处理函数时默认情况下是不再接收
                      同种信号,否则会引起一些竟态条件
SA_RESETHAND/SA_ONESHOT: 信号处理函数执行完毕后恢复信号的默认处理方式
SA_INTERRUPT: 中断系统调用

1.2.3 进程的信号掩码
  上述,sigaction()系统调用中sa_mask成员用于设置进程的信号掩码,另外sigpromask()也可以设置/查看进程的信号掩码:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

  (1)how指定设置进程信号掩码的方式,可选值:

SIG_BLOCK: 新的进程信号掩码是当前值和set执行的信号集的并集
SIG_UNBLOSK: 新的进程信号掩码是当前值和~set信号集的交集,即set指定的信号集不被屏蔽
SIG_SETMASK: 直接将进程信号掩码设置为set

  (2)set指定新的信号掩码
  (3)oldset是输出型参数,返回原来的信号掩码(不为NULL的话)。注意若set为NULL则进程信号掩码不变,此时仍然可以通过oldset获得进程当前的信号掩码。
  (4)函数执行成功返回0,失败返回-1并设置errno。

  本进程设置进程信号掩码(来屏蔽信号)后,若其它进程发来被屏蔽的信号,本进程将不能被接收该信号,而操作系统会将该信号设置为本进程的一个被挂起的信号。当程序员取消对本进程挂起的信号的屏蔽后,则该信号将立即被进程接收到。获取当前进程被挂起的信号集的系统调用是sigpending(),原型如下:

#include <signal.h>
int sigpending(sigset_t *set);

  (1)set参数是输出型参数,用于返回被挂起的进程。需要注意,进程即使多次接收到同一个被挂起的信号,sigpending()系统调用也只能反映一次,且当程序员再次使用sigprocmask()使能该挂起的信号时,该信号的处理函数也只能被触发一次。
  (2)函数执行成功返回0,失败返回-1并设置errno。

  进程每个运行时刻的信号掩码是程序员必须清晰的,在多进程/多线程编程环境中,需以进程/线程为单位来处理信号和信号掩码(不能设想子进程、子线程与父进程、主线程具有相同的信号特征)

2. 统一事件源

  信号机制是一个异步事件,即信号处理函数和程序的主循环是两条不同的执行路线。信号处理函数需要尽可能快的执行完毕,以确保不会错过同一个信号源的下一次到来(在执行信号处理函数时,该进程还是可以接收其它信号)。一种典型的解决方案是: 将信号的主要处理逻辑放在程序的主循环中实现,当信号处理函数被触发时,它只负责简单地通知主循环程序接收到信号,并将信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。

  实现方法为:信号处理函数使用管道(无名管道即可,因为处理函数和主循环函数处于同一进程空间)和主循环交互,即信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。主循环如何知道管道上有数据可读,这就通过之前学的IO复用系统调用来监听管道的读端文件描述符上的可读事件。这样,信号事件和其他的IO事件已被被IO复用处理,即统一事件源。

  统一事件源是很多IO框架(如libevent)和后台服务器程序(xinetd)都使用的技术。下面代码是统一事件源的简单实现:

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

#define ERRP(con, ret, ...) do                              \
{                                                           \
    if (con)                                                \
    {                                                       \
        perror(__VA_ARGS__);                                \
        ret;                                                \
    }                                                       \
}while(0)

#define MAX_EVENT 1024
static const char* ip = "192.168.239.136";
static int port = 9660;
static int pipefd[2];   //[0]是读端、[1]是写端

//设置目的文件描述符为非阻塞
int set_fd_non_block(int fd)
{
    int old_opt = fcntl(fd, F_GETFL);
    int new_opt = old_opt | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_opt);    
    return old_opt;
}

//将描述符的就绪读事件加入到epoll内核事件表中
void add_to_event_in(int epfd, int fd)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;    
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);

    //设置fd为非阻塞IO
    set_fd_non_block(fd);
}

//信号处理函数
void sig_handler(int sig)
{
    int errno_bak = errno;
    int msg = sig;
    send(pipefd[1], (char* )&msg, 1, 0);    //将信号值写入管道
    errno = errno_bak;
}

int add_signal(int sig)
{
    struct sigaction sa;
    bzero(&sa, sizeof(sa));
    sa.sa_handler = sig_handler;
    sa.sa_flags |= SA_RESTART;  //重新调用被该信号终止的系统调用,保证服务端程序一直运行
    sigfillset(&sa.sa_mask);    //屏蔽其它所有所有信号

    return sigaction(sig, &sa, NULL);
}

int main(void)
{
    int i, j;

    //创建监听socket
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    ERRP(socket_fd <= 0, return -1, "socket");

    //命名socket
    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 ret = bind(socket_fd, (struct sockaddr* )&address, sizeof(address));
    ERRP(ret < 0, goto ERR1, "connect");

    //为socket创建监听队列
    ret = listen(socket_fd, 5);
    ERRP(ret < 0, goto ERR1, "listen");

    //创建epoll内核事件表
    int epfd = epoll_create(5);
    ERRP(epfd < 0, goto ERR1, "epoll_create");

    //将监听socket的可读事件注册到epoll内核事件表中
    struct epoll_event events[MAX_EVENT];
    add_to_event_in(epfd, socket_fd);

    //使用socketpair创建一对可相互通信的套接字,类似于管道的读端、写端
    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
    ERRP(ret < 0, goto ERR2, "socketpair");

    //设置管道的写端为非阻塞
    set_fd_non_block(pipefd[1]);

    //将管道的可读事件加入epoll内核事件列表
    add_to_event_in(epfd, pipefd[0]);

    //进程被挂起时系统发送给该进程的信号
    add_signal(SIGHUP); ERRP(ret < 0, goto ERR2, "SIGHUP: sigaction");    
    //子进程退出时发给父进程的信号
    add_signal(SIGCHLD); ERRP(ret < 0, goto ERR2, "SIGCHLD: sigaction");    
    //kill或killall命令发送的信号
    add_signal(SIGTERM); ERRP(ret < 0, goto ERR2, "SIGTERM: sigaction");    
    //用户在程序运行终端中键入ctrl+c发送的信号
    add_signal(SIGINT); ERRP(ret < 0, goto ERR2, "SIGTERM: sigaction");

    char is_stop = 0;
    int connfd;
    while (!is_stop)
    {
        int cnt = epoll_wait(epfd, events, MAX_EVENT, -1);
        ERRP(cnt < 0 && errno != EINTR, goto ERR3, "epoll_wait");

        for (i = 0; i < cnt; i++)
        {
            int fd = events[i].data.fd;                        
            if (fd == socket_fd)    //等于监听fd说明有客户端连接本服务器
            {
                struct sockaddr_in cli_addr;
                socklen_t addr_len = sizeof(cli_addr);
                connfd = accept(socket_fd, (struct sockaddr* )&cli_addr, &addr_len);
                add_to_event_in(epfd, connfd);  //将和客户端通信的fd加入epoll内核事件表
            }
            else if (fd == pipefd[0] && events[i].events & EPOLLIN) //管道的读端可读说明信号处理函数写入信号值
            {
                int sig;
                char buf[1024] = {0};

                ret = recv(pipefd[0], buf, sizeof(buf), 0);
                if (ret == -1)
                    continue;
                else if (ret == 0)
                    continue;
                else
                {
                    for (j = 0; j < ret; j++)   //一个信号值占据1字节
                    {
                        switch (buf[i])
                        {
                            case SIGCHLD:
                            case SIGHUP:
                                continue;
                            case SIGTERM:
                                printf("Get signal of \"kill -SIGTERM pid\" or \"killall\"\n");
                                is_stop = 1;  
                                break;                            
                            case SIGINT:
                                printf("Get signal of \"Ctrl + c\"\n");
                                is_stop = 1;                                
                        }
                    }
                }
            }
            else if (fd == connfd && events[i].events & EPOLLIN)
            {
                char buf[1024] = {0};                
                ret = recv(connfd, buf, sizeof(buf), 0);
                if (ret <= 0)
                {
                    close(connfd);
                    continue;
                }
                else
                {
                    printf("recv data: %s\n", buf);    
                }
            }
        } 
    }

ERR3:
    close(pipefd[0]);  
    close(pipefd[1]);

ERR2:
    close(epfd);

ERR1:
    close(socket_fd);   
    return 0;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值