端口解绑方法及代码修改:实现进程接收信号后自动释放端口
端口占用问题的解决方案
问题描述
我启动了一个绑定端口的TCP服务器进程,代码如下:
#include <sys/socket.h> #include <sys/un.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #define BUF_SIZE 500 #define LISTEN_BACKLOG 50 #define handle_error(msg) \ do { perror(msg); exit(EXIT_FAILURE); } while (0) int main(int argc, char *argv[]) { struct addrinfo hints; struct addrinfo *res, *rp; struct sockaddr_storage peer_addr; socklen_t peer_addr_len; int sfd, cfd; if(argc != 3) { fprintf(stderr, "Usage: %s address port\n", argv[0]); exit(EXIT_FAILURE); } memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; hints.ai_protocol = 0; hints.ai_addr = NULL; hints.ai_next = NULL; if(getaddrinfo(argv[1], argv[2], &hints, &res) != 0) handle_error("getaddrinfo"); // Try each socket until we bind for(rp = res; rp != NULL; rp = rp->ai_next){ sfd = socket(rp->ai_family, rp->ai_socktype, 0); if(sfd == -1) continue; if(bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0) break; else close(sfd); } freeaddrinfo(res); if (rp == NULL){ fprintf(stderr, "Could not bind to any socket\n"); exit(EXIT_FAILURE); } // Set the TCP socket to listen state if (listen(sfd, LISTEN_BACKLOG) == -1) handle_error("listen"); printf("Server started"); for(;;){ // Accept peer_addr_len = sizeof(struct sockaddr_storage); cfd = accept(sfd, (struct sockaddr *) &peer_addr, &peer_addr_len); if (cfd == -1) handle_error("accept"); // Fork pid_t pid = fork(); if (pid == 0){ //Child code pid_t my_pid = getpid(); char buf[BUF_SIZE]; while(1){ ssize_t nread = recv(cfd, buf, BUF_SIZE, 0); if (nread == 0) { close(cfd); exit(EXIT_SUCCESS); } else { buf[nread] = 0; fprintf(stdout, "Worker %i has received a message: %s", my_pid, buf); ssize_t nsent = send(cfd, buf, nread, 0); if (nsent != nread) fprintf(stderr, "Error sending response"); } } } close(sfd); }
但杀死进程后,无法再次使用同一端口启动程序,用sudo lsof -i -P -n | grep LISTEN检查发现端口仍处于LISTEN状态,进程还在占用。想知道:
- 如何临时解除这些端口的绑定?
- 如何修改代码,让进程收到信号时自动解绑端口?
解答
1. 临时释放端口的方法
你可以用以下两种方式快速解决当前的端口占用问题:
- 通过进程ID杀死占用进程:
从lsof的输出中找到对应端口的进程ID(PID),然后执行:
如果一次没效果,可以多执行几次,确保进程完全终止。sudo kill -9 <目标PID> - 直接通过端口杀死进程:
使用fuser命令直接定位并杀死占用指定端口的所有进程(以端口8080为例):sudo fuser -k 8080/tcp
这些都是临时应急方案,想要彻底避免这个问题,得从代码层面优化。
2. 修改代码实现优雅退出与端口释放
你的代码存在几个导致端口无法正常释放的问题:
- 父进程在
fork后错误地关闭了监听套接字sfd,导致后续无法继续接受新连接,还可能引发资源释放异常; - 没有处理终止信号,进程被强制杀死时,监听套接字可能没被正确关闭,导致端口长时间处于占用状态。
下面是优化后的代码,添加了信号处理和套接字选项配置:
#include <sys/socket.h> #include <sys/un.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <signal.h> #include <errno.h> #define BUF_SIZE 500 #define LISTEN_BACKLOG 50 #define handle_error(msg) \ do { perror(msg); exit(EXIT_FAILURE); } while (0) // 全局变量保存监听套接字,方便信号处理函数访问 static int sfd_global = -1; // 信号处理函数,用于优雅退出 void handle_signal(int sig) { printf("Received signal %d, shutting down...\n", sig); if (sfd_global != -1) { close(sfd_global); // 关闭监听套接字,释放端口 sfd_global = -1; } exit(EXIT_SUCCESS); } int main(int argc, char *argv[]) { struct addrinfo hints; struct addrinfo *res, *rp; struct sockaddr_storage peer_addr; socklen_t peer_addr_len; int cfd; // 注册信号处理函数,处理SIGINT(Ctrl+C)和SIGTERM struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = handle_signal; if (sigaction(SIGINT, &sa, NULL) == -1) handle_error("sigaction SIGINT"); if (sigaction(SIGTERM, &sa, NULL) == -1) handle_error("sigaction SIGTERM"); if(argc != 3) { fprintf(stderr, "Usage: %s address port\n", argv[0]); exit(EXIT_FAILURE); } memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; hints.ai_protocol = 0; hints.ai_addr = NULL; hints.ai_next = NULL; if(getaddrinfo(argv[1], argv[2], &hints, &res) != 0) handle_error("getaddrinfo"); // Try each socket until we bind for(rp = res; rp != NULL; rp = rp->ai_next){ sfd_global = socket(rp->ai_family, rp->ai_socktype, 0); if(sfd_global == -1) continue; // 添加SO_REUSEADDR选项,允许端口快速重用(即使处于TIME_WAIT状态) int optval = 1; if (setsockopt(sfd_global, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) handle_error("setsockopt SO_REUSEADDR"); if(bind(sfd_global, rp->ai_addr, rp->ai_addrlen) == 0) break; else close(sfd_global); } freeaddrinfo(res); if (rp == NULL){ fprintf(stderr, "Could not bind to any socket\n"); exit(EXIT_FAILURE); } // Set the TCP socket to listen state if (listen(sfd_global, LISTEN_BACKLOG) == -1) handle_error("listen"); printf("Server started, listening on %s:%s\n", argv[1], argv[2]); for(;;){ // Accept peer_addr_len = sizeof(struct sockaddr_storage); cfd = accept(sfd_global, (struct sockaddr *) &peer_addr, &peer_addr_len); if (cfd == -1) { // 如果是被信号中断,继续循环,否则报错 if (errno == EINTR) continue; handle_error("accept"); } // Fork pid_t pid = fork(); if (pid == 0){ //Child code // 子进程不需要监听套接字,关闭它 close(sfd_global); pid_t my_pid = getpid(); char buf[BUF_SIZE]; while(1){ ssize_t nread = recv(cfd, buf, BUF_SIZE, 0); if (nread == 0) { close(cfd); exit(EXIT_SUCCESS); } else if (nread == -1) { if (errno == EINTR) continue; perror("recv"); close(cfd); exit(EXIT_FAILURE); } else { buf[nread] = 0; fprintf(stdout, "Worker %i has received a message: %s", my_pid, buf); ssize_t nsent = send(cfd, buf, nread, 0); if (nsent != nread) fprintf(stderr, "Error sending response"); } } } // 父进程关闭连接套接字 close(cfd); } }
关键修改点说明:
- 全局监听套接字:定义全局变量
sfd_global保存监听套接字的文件描述符,方便信号处理函数访问; - 信号处理函数:注册
SIGINT(Ctrl+C触发)和SIGTERM(kill命令触发)的处理函数,收到信号时优雅关闭监听套接字并退出; - SO_REUSEADDR选项:添加这个套接字选项,允许端口快速重用,即使端口处于TCP的TIME_WAIT状态也能直接绑定;
- 修复父进程逻辑:父进程在
fork后关闭的是连接套接字cfd,而非监听套接字sfd,这样父进程可以持续接受新连接; - 子进程资源清理:子进程启动后立即关闭监听套接字,减少不必要的文件描述符占用;
- 中断错误处理:处理
accept和recv被信号中断的EINTR错误,避免进程意外退出。
修改后,当你用Ctrl+C或者kill <PID>终止进程时,程序会自动关闭监听套接字,释放端口,下次启动就能直接绑定同一端口了。
内容的提问来源于stack exchange,提问作者Jsevillamol




