c++实现服务器在线重启

前些天,在网上看到一篇博文,讲述了如何用Go语言实现优雅的服务器重启,主要有以下几个目标:

  1. 不关闭现有的链接。
  2. socket能正常接受客户端的请求并缓存,待服务端进程重启后处理。
  3. 新的进程重启并替代旧的进程。

归根结底,作者的实现利用了Unix中一切皆文件的概念,将其发挥的淋漓尽致的则是Go语言。这里,首先介绍以下博文中涉及到的Go语言的特性(必要时与C++做一些对比),然后着重介绍如何用C++实现同样的功能。

Go与C++

Go语言提供一种特殊的创建listener的方式,利用Unix下一切皆文件的特性,其用文件复制一个listener,函数说明如下:

func FileListener(f *os.File) (l Listener, err error)

FileListener利用已经打开的文件f,返回一个listener的拷贝l。值得注意的是,l是由调用者负责关闭的。当l被关闭时,f没有任何影响。当然,f并不是一个普通的文件,其为函数File()的返回值。从下面函数的声明,我们可以看到File()是TCPListener类型的一个方法,它的返回值是一个listener的文件。就如同在Unix中每个设备都对应一个文件,这里是listener对应的文件。

func (l *TCPListener) File() (f *os.File, err error)

很多人看到这里会非常惊喜,Go语言居然有这么方便使用的功能。其实不然,在C++中我们可以实现相同的功能,只是没有像Go语言这样为我们封装成函数。熟悉C++ socket编程的朋友一定知道socket及accept函数1

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

int socket(int domain, int type, int protocol);

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。

protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:指定socket描述符,即socket函数的返回值。
addr:指定struct sockaddr *的指针,用于返回客户端的协议地址。
addrlen:指定协议地址的长度。

在实际的程序中,如果我们不关闭socket函数返回的函数描述符,它将一直存在。所以,在accept函数终止后,程序可以重新调用accpet函数并赋予未关闭的socket描述符,其会继续处理socket描述符接受的请求。

Go语言中,另一个功能,个人觉得在灵活程度上是C++无法比拟的,那就是goroutine。其和channel一起掌控着Go语言的并发能力。为什么叫goroutine,官方文档是这样解释的:是因为已有的词汇(线程,进程等)不能传递准确的含义。goroutine之间是并行的,轻量的,仅比分配栈空间多一点点消耗,并且随着需要在堆上分配和释放。举一个简单的例子,当程序接受到一个系统信号,需要新建一个进程去处理时,Go或许是这样处理的:

//定义channel接受signal
signals := make(chan os.Signal)
signal.Notify(signals, syscall.SIGHUP, syscall.SIGTERM)
for sig := range signals {
    if sig == syscall.SIGTERM {
        go func() {
            //处理信号
        }{}
    }
    ...
}

再来看看C++是怎么处理的:

struct sigaction action;
action.sa_handler = Handler;
sigemptyset(&action.sa_mask);

if (sigaction(SIGALRM, &action, NULL) < 0) {
    syslog(LOG_ERR, "set signal error: %s(errno: %d)\n", strerror(errno), errno);
} else {
    syslog(LOG_INFO, "set signal ok...");
}

这里Handler是一个声明为void (*handler) (int)的函数。注意,这里Handler仅能有一个int型整数。那如果我有其他需要处理的参数怎么办?

C++服务在线重启

接下来,我们来讨论一下C++如何实现服务的在线重启。其中涉及到一些知识点,这里将做简单的介绍。

首先,我们讨论一下fork函数。相信很多朋友对这个函数已经有所了解。fork函数会创建当前进程的一个子进程,子进程可以视为父进程的一个拷贝,但是他们运行在独立的内存空间中,互补影响。值得注意的是子进程并没有完全继承父进程的一切资源,其中包括2

  • The child has its own unique process ID, and this PID does not
    match the ID of any existing process group.
  • The child’s parent process ID is the same as the parent’s process
    ID.
  • The child does not inherit its parent’s memory locks.
  • Process resource utilizations and CPU time counters are reset to zero in the child.
  • The child’s set of pending signals is initially empty.
  • The child does not inherit semaphore adjustments from its parent.
  • The child does not inherit process-associated record locks from
    its parent.
  • The child does not inherit timers from its parent.
  • The child does not inherit outstanding asynchronous I/O operations from its parent, nor does it inherit any asynchronous I/O contexts from its parent.

fork的作用是复制出一个与原进程一样的子进程,它们完成的是同样的操作。但是,在更新了程序,我们想要重新启动时,fork是无法完成的,这就需要另一类函数,exec函数族。

exec 函数族提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。新的进程从原进程继承了以下内容3

  • nice value
  • semadj values
  • process ID
  • parent process ID
  • process group ID
  • session membership
  • real user ID
  • real group ID
  • supplementary group IDs
  • time left until an alarm clock signal
  • current working directory
  • root directory
  • file mode creation mask
  • file size limit
  • process signal mask
  • pending signal
  • tms_utime, tms_stime, tms_cutime, and tms_cstime
  • resource limits
  • controlling terminal
  • interval timers

这里我们着重注意signal mask,新进程会继承原进程的signal mask,或者说继承原进程中系统默认以及用户自定义的信号处理机制。但是,由于sigation调用信号处理函数对信号进行处理时,阻塞了进程对之后信号的接收,这种情况会一直持续到新启动的进程中,所以新进程无法处理新到来的信号。为了解决这种情况,我们只需一条语句,将signal mask重置4

action.sa_flags = 0;  

这里,将signal mask重置还有另外一个重要的原因,那就是当系统调用信号中断后,待信号处理完成,系统调用会有以下两种处理方式:

Interruption of system calls and library functions by signal handlers .If a signal handler is invoked while a system call or library function call is blocked, then either:
1. the call is automatically restarted after the signal handler returns; or
2. the call fails with the error EINTR.

程序运行中具体的行为取决于signal mask是否被设置了SA_RESTART5。针对本文的内容,当SA_RESTART为被设置时,accept函数就会被中断,否则当程序收到SIGHUP等信号处理完成后,accept仍会重启。这时,新旧两个Server会同时运行。下面是一个例子,感兴趣的读者可以注释掉 action.sa_flags = 0; 和 action.sa_flags |= SA_RESTART;观察程序的结果。

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <syslog.h>
#include <string.h>
#include <signal.h>
#include <fstream>
#include <ctime>
#include <iostream>


#define MAXLINE 4096

void handler(int sig) {
    std::cout<<"hello"<<std::endl;
}

int main(int argc, char const* argv[])
{
    int    listenfd, connfd;
    struct sockaddr_in     servaddr;
    char    buff[4096];
    int     n;

    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
        printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

//    timeval timeout = {1, 0};
//    if (setsockopt(listenfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeval)) < 0)
//    {
//        exit(1);
//    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(6666);

    if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
        printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    if( listen(listenfd, 10) == -1){
        printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
        exit(0);
    }

    struct sigaction action;
    action.sa_handler = handler;
    sigemptyset(&action.sa_mask);
//    action.sa_flags = 0;  
    action.sa_flags |= SA_RESTART;

    sigaction(SIGALRM, &action, NULL);

    alarm(3);

    printf("======waiting for client's request======\n");

    if((connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){
         printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
     }
     n = recv(connfd, buff, MAXLINE, 0);
     buff[n] = '\0';

    std::cout<<buff<<std::endl;

    close(connfd);

    close(listenfd);

    //close the log
    closelog();

    while (1) {
        std::cout<<"wait..."<<std::endl;
        sleep(1);
    }
    return 0;
}

最后,在强调一个内容close-on-exec。每个文件描述符都有一个close-on-exec标志。默认情况下,这个标志最后一位被设置为 0。这个标志符的具体作用在于当开辟其他进程调用exec()族函数时,在调用exec函数之前为exec族函数释放对应的文件描述符。所以,当你的程序无法正确运行时,检查一下你的程序是否设置了close-on-exec关闭了已经打开的sockfd。

到这里,本文所涉及到的要点基本都介绍完了,完整的代码在这里。还请多多指导。

总结

本文用C++实现了一种在线服务重启功能,主要原理是在signal mask中取消SA_RESTART的设置,当收到外部信号时中断accept的运行,并调用exec函数重新启动程序,在新进程中用accept函数重新监控socket描述符。原进程在处理完客户端所有请求后就会终止,新进程将继续运行。

参考文献

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值