在服务器应用程序中使用popen会使端口成为人质



TLDR:我有一个用C++编写的HTTP服务器应用程序,它使用popen()启动一些脚本。脚本启动几个守护程序:wpa_pplientudhcpd。在我的服务器停止后,这些守护程序似乎会占用我的HTTP服务器端口。为什么?

在初始化过程中,我的HTTP服务器应用程序使用popen((启动一个脚本来启动wpa_pplientudhcpd,以确保我的接口准备就绪。脚本执行后,我的应用程序将打开端口80,正如您所期望的那样。

问题:当我的应用程序关闭并通过所有析构函数时,它正确地使用close(int_socket_val)关闭了套接字,但由于端口80不可用,尝试再次启动我的应用软件将失败。

执行netstat -tulpn显示wpa_supplientudhcpd挂在我的端口80上。有趣的是,当我的HTTP服务器仍在运行时,netstat显示了相同的结果,因此我的HTTP server从未被列为拥有该端口。用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输出供参考:

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_supplient挂在端口上——这部分似乎是随机的:

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。Sleepen-提供了对问题的准确解释。对我来说,诀窍在于除了在随后的系统调用中显式设置FD_CLOEXEC标志外,还添加了建议的标志。这可能不适合所有人,因为第二次调用不像SOCK_CLOEXEC标志那样是原子调用,但在内核可能不支持SOCK_CLOXEC标志的情况下,它提供了一个回退。我很想解释一下为什么它不起作用,但这是我的解决方案:

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终止之前让它停止也是谨慎的。这也可以避免这个问题。不过,两者结合可能是最好的。

相关内容

  • 没有找到相关文章

最新更新