今天做项目,借鉴nginx的代码,做了一个优雅的重启,在重启过程中不断服务,保持系统可用性。具体的过程如下
1. 注册信号量
程序在运行过程中,当kill -s SIGUSR1 pid的shell语句执行时,程序接收到该信号,执行sig_reload函数, 将全局变量reload = 1
static void sig_reload(int signo)
{
reload = 1;
}
struct sigaction sa_reload;
sa_reload.sa_handler = sig_reload;
sigaction(SIGUSR1, &sa_reload, NULL);
2. 主程序逻辑
我的主进程在while的循环反复执行, 当reload = 1 的时候,先将reload置回0,以免重复调用reload,接着执行CoreExecNewBin的函数, 这个函数接下去会讲。它的主要功能是创建子进程,父进程及时退出,子进程执行新的bin文件。
父进程在退出时,使stop= 1 从而使得父进程stop,从而执行停止的逻辑,释放资源。
while (!stop)
{
sleep(10);
int ret;
if (reload == 1)
{
reload = 0;
log_debug("reloading");
ret = CoreExecNewBin();
if (ret != 0)
{
log_error("fail in reloading");
continue;
}
log_debug("reload success");
stop = 1;
}
}
3.CoreExecNewBin函数
这里关键的CoreExecNewBin函数来了
它先fork进程,根据pid判断父子进程,父进程直接退出, 子进程获取全局变量g_szenv,然后复制给envp,envp作为执行新的bin文件的环境变量
static int CoreExecNewBin()
{
char *fds = NULL;
int pid = fork();
switch (pid) {
case -1:
log_error("fork in CoreExecNewBin got error");
return -1;
case 0: /* child */
break;
default: /* parent */
return 0;
}
char *envp[] = { NULL, NULL };
envp[0] = g_szenv;
log_error("fork env %s", g_szenv);
execve(g_argv[0], g_argv , envp);
}
如上可能,实现了功能,可是有两个疑问,
1.如果父进程绑定了一个端口,那么这个端口已经被占用,子进程绑定同样的端口时fail,怎么办?
2.如上的环境变量起到什么作用。
那么就扯到环境变量的问题上了,本文的环境变量记录了被占用的fd,从fd可以获取端口号,从而做下述逻辑
我在绑定端口的时候,有4步要走:
1. 从环境变量读取当前的环境变量,下面是读取的代码,
首先用getenv获取得到char类型的环境变量,如果为null,则表示没有端口被占用,那么返回0.
然后解析环境变量的值,从它获得绑定的fd
UnresolveDesc通过得到的fd,获得与其绑定的addr,如果这个addr与现在想要绑定的addr一样,则dup一个fd1,返回fd1.
char *p, *q;
int sock;
char *inherited;
inherited = getenv(NC_ENV_FDS);
//log_debug("sock addr %s", UnresolveDesc(sock));
char address[NI_MAXHOST + NI_MAXSERV];
if (inherited == NULL) {
return 0;
}
strncpy(address, listen_address, sizeof(address));
log_debug("address %s", address);
for (p = inherited, q = inherited; *p; p++) {
if (*p == ';') {
sock = cpa_atoi(q, p - q);
//log_debug("listen_address %s", UnresolveDesc(sock));
if (strcmp(address, UnresolveDesc(sock)) == 0) {
log_debug("get inherited socket %d for '%s' from '%s'", sock,
address, inherited);
sock = dup(sock);
log_debug("dup inherited socket as %d", sock);
return sock;
}
q = p + 1;
}
}
return 0;
char *UnresolveDesc(int sd)
{
struct sockaddr *addr;
socklen_t addrlen;
int status;
addr = (struct sockaddr *)malloc(sizeof(struct sockaddr));
status = getsockname(sd, addr, &addrlen);
if (status < 0) {
return "unknown";
}
return UnresolveAddr(addr, addrlen);
}
2. 判断通过环境变量获得的fd值,如果fd<= 0 , 新绑定一个fd2,否则fd值作为现在fd使用
if (fd <= 0)
{
if ((fd = SockBind(&m_Addr, blog, 0, 0, 1/*reuse*/, 1/*nodelay*/, 1/*defer_accept*/)) == -1)
{
return -1;
}
}
3. 通过环境变量,关闭已经打开的fd,从而让父进程的fd关闭
int CleanSocketEnv()
{
int sock = 0;
char *inherited;
char *p, *q;
inherited = getenv(NC_ENV_FDS);
if (inherited == NULL) {
return 0 ;
}
for (p = inherited, q = inherited; *p; p++) {
if (*p == ';') {
sock = cpa_atoi(q, p - q);
close(sock);
q = p + 1;
}
}
return 0;
}
4. 将现在占用的fd写入全局变量,作为下次的reload的环境变量
int len = 0;
memcpy(g_szenv, NC_ENV_FDS, strlen(NC_ENV_FDS));
len = strlen(NC_ENV_FDS);
memcpy(g_szenv + len, "=", strlen("="));
len += strlen("=");
sprintf(g_szenv + len, "%d;", fd);
log_debug("global env is %s", g_szenv);
对了比较重要的一点是: 在accept 新的fd的时候要设置,不然在子进程会继承已经连接的fd,从而导致僵尸连接
int status = fcntl(newfd, F_SETFD, FD_CLOEXEC);