C++ HTTP服务器调用popen启动脚本后端口被守护进程占用问题
摘要
我用C++编写了一个HTTP服务器应用,通过popen()调用脚本启动wpa_supplicant和udhcpd两个守护进程。服务器停止后,这些守护进程会占用服务器端口,导致无法重启,这是为什么?
问题详情
初始化阶段,我的HTTP服务器通过popen()执行脚本启动wpa_supplicant和udhcpd以确保网络接口就绪,之后服务器正常监听80端口。
问题现象:当服务器关闭并执行所有析构函数,且已通过close(int_socket_val)正确关闭套接字后,再次启动服务器会失败,提示80端口不可用。
执行netstat -tulpn查看,发现端口80被wpa_supplicant或udhcpd占用;甚至服务器运行时,netstat也显示端口由这两个进程持有,而非HTTP服务器本身。执行killall -9 wpa_supplicant udhcpd可释放端口,使服务器能重启,但原因未知。
相关代码参考
启动脚本的C++实现
std::string ConnectionManager::exec(const std::string& command, bool strip) { char buffer[EXEC_BUFFER_LEN]; std::string result = ""; // Open pipe to file FILE* pipe = popen(command.c_str(), "r"); if (!pipe) { std::cout << "ERROR: ConnectionManager::exec() - failed to open command: " << command << std::endl; return result; } // read till end of process: while (!feof(pipe)) { // use buffer to read and add to result if (fgets(buffer, EXEC_BUFFER_LEN, pipe) != NULL) { result += buffer; } } pclose(pipe); if ( strip ) { removeLineEndings(result); } return result; }
netstat输出示例
端口被udhcpd占用:
root@device:/usr/bin# netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name ......... tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 4267/udhcpd ......... root@device:/usr/bin#
端口被wpa_supplicant占用:
root@device:/usr/bin# netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name ......... tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 4393/wpa_supplicant ......... root@device:/usr/bin#
调用守护进程的脚本片段
wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf udhcpc -i wlan0
核心原因是进程文件描述符继承:
- 当你用
popen()启动脚本时,子进程(包括脚本本身以及它启动的wpa_supplicant/udhcpd)会继承父进程(你的HTTP服务器)所有打开的文件描述符,其中就包括监听80端口的套接字。 - 即便你的服务器调用
close()关闭了套接字,只要还有其他进程(比如被继承了该套接字的守护进程)持有这个描述符,内核就不会真正释放端口的监听状态。 - 加上你用
-B或默认方式把wpa_supplicant/udhcpd启动成了守护进程,它们会脱离父进程会话,但依然持有继承来的套接字描述符,导致端口被一直占用。
解决方法
1. 启动守护进程前关闭无关文件描述符
修改脚本,在启动守护进程时显式关闭所有非必要的文件描述符,避免继承:
# 关闭标准输入输出外的所有描述符后启动进程 exec wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf 0>&- 1>&- 2>&- exec udhcpc -i wlan0 0>&- 1>&- 2>&-
或者用setsid彻底隔离会话和描述符:
setsid wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf < /dev/null > /dev/null 2>&1 setsid udhcpc -i wlan0 < /dev/null > /dev/null 2>&1
2. 给监听套接字加复用选项
在创建HTTP服务器的监听套接字时,设置SO_REUSEADDR和SO_REUSEPORT选项,这样即使端口处于异常占用状态,也能重新绑定:
int opt = 1; if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)) < 0) { perror("setsockopt failed"); exit(EXIT_FAILURE); }
注意:这只是缓解方案,无法从根源解决描述符继承问题。
3. 用fork()+exec()替代popen()
popen()会默认继承所有描述符,改用fork()后,在子进程中先关闭所有不需要的文件描述符(包括监听套接字),再执行脚本:
pid_t pid = fork(); if (pid == 0) { // 子进程:关闭所有无关文件描述符 for (int fd = 3; fd < getdtablesize(); fd++) { close(fd); } // 执行脚本 execl("/path/to/your/script.sh", "script.sh", NULL); exit(EXIT_FAILURE); } else if (pid > 0) { // 父进程:等待子进程结束(可选) waitpid(pid, NULL, 0); } else { perror("fork failed"); }
这种方式能彻底避免子进程继承监听套接字。
4. 服务器退出时主动清理守护进程
在HTTP服务器的析构函数或退出逻辑中,主动杀死wpa_supplicant和udhcpd,确保它们释放所有资源:
system("killall wpa_supplicant udhcpd");
更严谨的方式是记录启动时的进程PID,退出时精准杀死对应实例,避免误杀其他同名进程。
内容的提问来源于stack exchange,提问作者Mark Taylor

