[英]Using popen within a server application holds port hostage
TLDR:我有一个用 C++ 编写的 HTTP 服务器应用程序,它使用popen()
启动一些脚本。 这些脚本启动了几个守护进程: wpa_supplicant和udhcpd 。 在我的服务器停止后,那些守护进程似乎保留在我的 HTTP 服务器端口上。 为什么?
在初始化期间,我的 HTTP 服务器应用程序使用 popen() 启动脚本以启动wpa_supplicant和udhcpd以确保我的接口已准备好连接到 go。脚本执行后,我的应用程序将如您所料打开端口 80。
问题:当我的应用程序关闭并通过所有析构函数时,它使用close(int_socket_val)
正确关闭了套接字,但第二次尝试启动我的应用程序将失败,因为端口 80 不可用。
执行netstat -tulpn
显示wpa_supplicant或udhcpd挂在我的端口 80 上。有趣的是,虽然我的 HTTP 服务器仍在运行,但netstat显示相同的结果 - 因此我的 HTTP 服务器从未被列为拥有该端口。 使用killall -9 wpa_supplicant udhcpd
杀死这些应用程序将释放端口 80 并允许我再次启动我的 HTTP 服务器。 但是为什么会这样呢? 这已被证明是一个很难研究的问题。
作为参考,这是我用来启动脚本并能够读取这些调用期间返回的内容的方法:
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;
}
这不是 80 端口有某种魔力的特殊情况。 它适用于我用于开发的任何端口——在端口 50000 上启动我的 HTTP 服务器会产生相同的效果。 这里是netstat output 供参考:
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
@G。 Sleipen - 提供了对问题的准确解释。 对我来说,除了在后续系统调用中显式设置 FD_CLOEXEC 标志之外,还添加了建议的标志。 这可能对每个人来说都不是理想的,因为第二次调用不是原子的,而 SOCK_CLOEXEC 标志应该是这样,但它在您的 kernel 可能不支持 SOCK_CLOEXEC 标志的情况下提供了一个回退。 我会对它为什么不起作用的解释感兴趣,但这是我的解决方案:
int Socket::openServerSocket(uint16_t port)
{
int hSocket;
int flag;
/* Create the TCP socket */
if ((hSocket = socket(PF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP)) < 0)
{
return -1;
}
fcntl(hSocket, F_SETFD, fcntl(hSocket, F_GETFD) | FD_CLOEXEC);
/* Disable the Nagle (TCP No Delay) algorithm */
flag = 1;
if (-1 == setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag)))
{
return -1;
}
/* Set the Keep Alive property */
flag = 1;
if (-1 == setsockopt(hSocket, SOL_SOCKET, SO_KEEPALIVE, (char *)&flag, sizeof(flag)))
{
return -1;
}
/* Allow the re-use of port numbers to avoid error */
flag = 1;
if (-1 == setsockopt(hSocket, SOL_SOCKET, SO_REUSEADDR, (char *)&flag, sizeof(flag)))
{
return -1;
}
/* Set an explicit socket timeout value */
struct timeval tv;
tv.tv_sec = TIMEOUT_SEC;
tv.tv_usec = 0;
if (-1 == setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv))
{
printf("ERROR: Socket::openServerSocket->setsockopt(port timeout)\n");
return -1;
}
/* Construct the server sockaddr_in structure */
memset(&m_sockaddr, 0, sizeof(m_sockaddr)); /* Clear struct */
m_sockaddr.sin_family = AF_INET; /* Internet/IP */
m_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* Incoming addr */
m_sockaddr.sin_port = htons(port); /* server port */
/* Bind the server socket */
if (bind(hSocket, (struct sockaddr *)&m_sockaddr,
sizeof(m_sockaddr)) < 0)
{
return -1;
}
/* Listen on the server socket */
if (listen(hSocket, MAXPENDING) < 0)
{
return -1;
}
return hSocket;
}
int Socket::acceptClient(int hSocket)
{
unsigned int sockaddr_len = sizeof(m_sockaddr);
int ret = accept4(hSocket, (struct sockaddr *)&m_sockaddr, &sockaddr_len, SOCK_CLOEXEC);
fcntl(ret, F_SETFD, fcntl(ret, F_GETFD) | FD_CLOEXEC);
return ret;
}
popen()
分叉进程并在子进程中执行 shell。 孩子继承了父母的文件描述符。 当 shell 执行 udhcpd 时,这反过来会导致 fork 和 exec。 然后 udhcpd 将自行守护进程,但查看源代码看起来它不会首先关闭所有打开的文件描述符。 这意味着 udhcpd 会继续保留您的程序之前打开的套接字的文件描述符,从而使其保持活动状态。
有几种解决方法。 最简单的方法是确保您在程序中打开的任何文件描述符都设置了CLOEXEC
标志。 例如,创建你的监听套接字:
int listen_fd = socket(AF_SOMETHING, SOCK_STREAM | SOCK_CLOEXEC, 0);
这确保如果您调用popen()
,子进程将不会继承设置了该标志的文件描述符。
如果您的程序导致 udhcpd 启动,那么在它终止之前让它停止 udhcpd 也是明智的。 这也可以避免这个问题。 可能两者的结合是最好的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.