如何使用poll()优雅关闭基于poll循环的数据库服务器
如何使用poll()优雅关闭基于poll循环的数据库服务器
看起来你已经搭好了一个基于poll()的多客户端数据库服务器框架,但目前的实现收到终止信号(比如Ctrl+C触发的SIGINT)时会直接退出,很可能导致数据库未保存、客户端连接未正常关闭这类问题。下面我一步步教你改造代码,实现优雅关闭——也就是安全收尾所有资源后再退出。
一、先设置全局退出标志
首先我们需要一个全局变量来标记服务器是否需要退出,绝对不能在信号处理函数里直接调用exit(),因为信号处理函数里不能调用像printf()、close()这类非异步信号安全的函数。
在代码顶部(比如srvpoll.h或者main所在的文件)添加全局变量:
#include <stdbool.h> #include <errno.h> // 全局退出标志,volatile确保编译器不会优化它 volatile sig_atomic_t g_quit = 0;
然后实现信号处理函数:
void handle_signal(int sig) { if (sig == SIGINT) { g_quit = 1; // 只设置退出标志,不做其他复杂操作 } }
二、改造poll循环,支持优雅退出
现在修改你的poll_loop函数,让它能检测到g_quit标志,并且在退出前完成所有收尾工作:
void poll_loop(int dbfd, unsigned short port, struct dbheader_t **dbhdr, struct employee_t **employees) { init_clients(CLIENTS); int nfds=1; struct pollfd fds[257]; memset(&fds,'\0',sizeof(fds)); struct sockaddr_in mysocket; memset(&mysocket,0,sizeof(struct sockaddr_in)); mysocket.sin_family=AF_INET; mysocket.sin_port=htons(port); mysocket.sin_addr.s_addr=inet_addr("127.0.0.1"); int sockfd,new_fd; if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); exit(EXIT_FAILURE); } int yes=1; setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof yes); if(bind(sockfd,(struct sockaddr *)&mysocket,sizeof(struct sockaddr_in))==-1) { perror("bind"); exit(EXIT_FAILURE); } if(listen(sockfd,BACKLOG)==-1) { perror("listen"); exit(EXIT_FAILURE); } fds[0].fd=sockfd; fds[0].events=POLLIN; while(1) { // 检查退出标志,触发则跳出循环执行收尾 if (g_quit) { printf("\n收到退出信号,开始优雅关闭...\n"); break; } // 重新构建客户端fd列表(原逻辑保留) int ii=1; for(int i=0;i<256;++i) { if(CLIENTS[i].fd!=-1) { fds[ii].fd=CLIENTS[i].fd; fds[ii].events=POLLIN; ++ii; } } // 改成1秒超时,确保能及时检测退出标志 int n_events= poll(fds,nfds,1000); if(n_events==-1) { // 被信号中断时,继续循环而非退出 if (errno == EINTR) continue; perror("poll"); exit(EXIT_FAILURE); } if(n_events==0) { // 超时无事件,继续循环 continue; } // 仅在未触发退出时,才接受新连接 if (!g_quit && (fds[0].revents& POLLIN)) { if((new_fd=accept(sockfd,NULL,NULL))==-1) { perror("accept"); continue; } int slot=freeSlot(CLIENTS); if(slot==-1) { printf("server full\n"); close(new_fd); } else { CLIENTS[slot].fd=new_fd; CLIENTS[slot].state=STATE_CONNECTED; nfds++; printf("Slot %d has fd %d\n",slot,CLIENTS[slot].fd); } n_events--; } // 处理客户端请求(原逻辑保留) for(int i=1;i<=nfds&&n_events>0;++i) { if(fds[i].revents&POLLIN) { n_events--; int fd=fds[i].fd; int client_slot=find_fd(CLIENTS,fd); if(client_slot==-1) { printf("client does not exist\n"); continue; } ssize_t bytes_read=read(fd,CLIENTS[client_slot].buffer,sizeof(CLIENTS[client_slot].buffer)); if(bytes_read<=0) { printf("Client %d has disconnected or error\n",client_slot); close(fd); CLIENTS[client_slot].fd=-1; memset(CLIENTS[client_slot].buffer,'\0',sizeof(CLIENTS[client_slot].buffer)); nfds--; } else { if(CLIENTS[client_slot].state==STATE_CONNECTED||CLIENTS[client_slot].state!=STATE_MSG) { CLIENTS[client_slot].state=STATE_HELLO; } handle_client_fsm(dbfd,dbhdr,employees,&CLIENTS[client_slot]); } } } } // 优雅关闭的核心收尾工作 printf("开始清理资源...\n"); // 1. 关闭监听socket,停止接受新连接 close(sockfd); // 2. 关闭所有活跃客户端连接 for (int i = 0; i < 256; ++i) { if (CLIENTS[i].fd != -1) { printf("关闭客户端连接 %d\n", CLIENTS[i].fd); close(CLIENTS[i].fd); CLIENTS[i].fd = -1; } } // 3. 强制保存数据库到文件(调用你已有的持久化函数) printf("保存数据库状态...\n"); if (write_employees(dbfd, *dbhdr, *employees) != STATUS_SUCCESS) { fprintf(stderr, "警告:数据库保存失败,部分数据可能丢失\n"); } }
三、完善main函数的信号注册和最终收尾
修改main函数,确保信号处理正常工作,并且在poll_loop返回后完成最后清理:
// 声明全局退出标志(如果放在头文件中可省略) extern volatile sig_atomic_t g_quit; int main(int argc, char *argv[]) { // 注册SIGINT信号处理 if(signal(SIGINT, handle_signal)==SIG_ERR) { perror("signal"); return 1; } // 原命令行解析逻辑保留 char *filepath =NULL; bool newfile=false; int dbfd=-1; struct dbheader_t *dbhd=NULL; struct employee_t *employees = NULL; unsigned short port=8080; // 可从命令行参数解析 int c; while((c= getopt(argc,argv,"nf:a:lr:u:p:"))!=-1) { switch(c) { case 'n': newfile=true; break; case 'f': filepath = optarg; break; case 'p': port=atoi(optarg); break; // 其他参数处理保留... case '?': print_usage(argv); return 1; default: return -1; } } // 原数据库初始化逻辑保留(打开/创建、读取数据等) if(filepath==NULL) { printf("Filepath is a required argument\n"); print_usage(argv); return 0; } // ...省略数据库打开、读取等代码... // 启动poll循环,循环退出后执行收尾 poll_loop(dbfd, port, &dbhd, &employees); // 最终资源清理 printf("完成最终清理...\n"); if (dbfd != -1) close(dbfd); free(dbhd); free(employees); printf("服务器已优雅退出\n"); return 0; }
关键注意事项
- 信号处理函数的限制:只能在信号处理函数里设置标志,不能调用
printf()、close()这类非异步安全函数,避免触发未定义行为。 - poll的EINTR错误:当
poll()被信号中断时会返回-1且errno=EINTR,这时候要继续循环,不要直接退出。 - 停止接受新连接:触发退出标志后,不再处理新的
accept()请求,避免收尾过程中新增客户端。 - 数据库强制保存:一定要在退出前调用持久化函数,确保所有内存中的修改都写入文件。
内容来源于stack exchange
相关产品推荐
相关产品推荐

