请考虑以下代码:
socket_fd = start_server(port);
while (1){
new_socket_fd = accept_client(socket_fd);
int pid = fork();
if (pid == 0){
//I am the child server process
close(socket_fd); <------(1)
do_stuff_with_client(new_socket_fd, buffer);
close(new_socket_fd); <------(2)
exit(0);
} else if (pid > 0){
//I am the parent server process
close(new_socket_fd); <------(3)
} else {
fprintf(stderr, "Fork errorn");
return 1;
}
}
据我了解,当一个进程调用fork()时,它的地址空间是重复的,但不共享,所以如果我从子进程更改变量或关闭文件描述符,它不会影响父进程。
也就是说,在服务器接受新连接(从而创建new_socket_fd
)后,它会自行分叉,子服务器关闭socket_fd
(1),因为它不是必需的,因为父级仍在侦听其socket_fd
。
子项处理请求,然后关闭其new_socket_fd
(2) 并退出。
当子进程执行所有这些操作时,父进程已经关闭new_socket_fd
(3),因为连接由子进程处理。
问题是:这些假设是否正确?
将评论流转换为答案。
TL;博士
是的。问题中的描述看起来是正确的,推理是合理的。
在某些时候,您的父进程应该等待已死亡的子进程以防止僵尸的积累(但在子进程死亡之前不应阻塞)。 在循环中使用带有WNOHANG
参数的循环中的waitpid()
可能是合适的,在循环中父级关闭new_socket_fd
的部分。 这可能会留下一个或多个僵尸,直到发出下一个传入请求。 如果这是一个问题,您可以忽略SIGCHLD
(因此永远不会创建僵尸),或者您可以安排定期唤醒,在此期间父进程检查僵尸。
讨论
巴邦问道
快速问题 - 那么父进程何时/何地关闭socket_fd?
父级在退出循环或被告知停止侦听套接字时关闭socket_fd
。在显示的代码中没有真正的规定,因此当父进程被杀死(或发生分叉失败)时,它将关闭。重点是侦听套接字可用于许多连接 - 在完成侦听之前,您不想在父连接中关闭它。
马泰奥指出
在这种情况下,因为它是一个无限循环,所以永远不会。服务器将始终侦听
listen(socket_fd, N)
中定义的最多 N 个连接。
请注意,listen()
调用中的 N 参数是可以排队等待侦听进程的未完成连接数。即尚未通过accept()
调用返回值的连接请求数。它对accept()
接受连接后可以并发处于活动状态的连接数没有限制。
阿杰婆罗门夏特里问道。
在子端口关闭
socket_fd
之前,绑定端口是否映射到两个 PID?如果有传入数据包,它将放入谁的队列?
传入数据包与套接字的"打开文件描述"(或等效项 - 不同于"文件描述符"或"套接字描述符")相关联。父母或孩子都可以使用它,以先阅读者为准。同样,传入的连接请求在socket_fd
上排队;它们可以被父母或孩子接受。然而,这个家庭已经同意谁做什么,这样他们就不会妨碍彼此。
马泰奥评论道。
我假设是父母的。
阿杰回应
如果是这种情况,
new_socket_fd
的数据包也应该发生同样的情况,因为两者都已打开。这意味着在父级关闭数据包之前,子项将无法读取数据包。这可能会导致竞争条件。
这是基于误解。 数据包可通过文件描述符提供给两个进程。 当进程关闭文件描述符时,它无法再访问发送到连接的信息(当然,也无法在该连接上发送数据)。 在那之前,除非参与者处理同意哪个读取数据以及哪个侦听连接,否则这是一个彩票,除非参与者处理
同意马泰奥回应
但是文件描述符不应该干扰父端和子端;这就是为什么在子端关闭
socket_fd
不会阻止父端侦听。
巴邦评论道
同意。但我认为你应该在 while 循环后关闭
socket_fd
。如果明天您将循环更改为某些情况而中断,您将冒着无缘无故保持打开插座的风险。
这是一个很好的做法,但循环不会退出(这是一个while (1)
循环,并且故障模式会return
出循环——它可以在执行该return
之前关闭套接字)。 如果程序退出,则系统会关闭套接字,因此这并不重要,因为关闭您打开的内容是很好的内务管理。
阿杰笔记
父项和子项中的文件描述符都不同。因此,关闭一个不应影响另一个。但是两个副本都有相同的(src-ip,src-port,dest-ip,dest-port)4元组,那么具有这种标头的数据包会去哪里?
描述符不同,但它们引用的套接字连接是相同的。 数据包可用于读取它的任何进程 - 父进程或子进程。
马泰奥回应
在我的示例中,
accept_client
为客户端创建sockaddr
结构,因此 4 元组转到子元组new_socket_fd
。
这不太准确。 首先,accept_client()
在有孩子之前就被召唤;当该函数完成时,new_socket_fd
位于父项中(仅)。 其次,在fork()
之后,两个进程都可以访问new_socket_fd
,并且任何一个进程都可以读取客户端进程发送的数据。 但是,该程序的设计使服务器返回侦听更多连接请求,而子节点在new_socket_fd
上处理传入的连接 - 这是一种合理的分工。
请注意,允许父级处理请求,子级继续侦听。 但是,这违背了惯例。 这意味着侦听"守护进程会更改每个连接请求上的 PID,从而难以确定当前正在侦听套接字的进程。 代码中使用的传统方法是守护进程在很长一段时间内保持不变,因此记录 PID 以供以后的进程控制(在不再需要守护进程时杀死守护进程)是明智的。