这是关闭 fork 上的套接字描述符的正确方法吗?

Is this the correct way to close socket descriptors on fork?

考虑这段代码:

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 error\n");
        return 1;
    }
}

据我了解,当一个进程调用 fork() 时,它的地址 space 是重复的但不是共享的,所以如果我从 child 的进程更改变量或关闭文件描述符,它不影响 parent.

就是说,在服务器接受新连接(从而创建 new_socket_fd)后,它会自行分叉,并且 child 会关闭 socket_fd (1) 因为不需要它,因为parent 仍在听 socket_fd

child 处理请求,然后关闭其 new_socket_fd (2) 并退出。

虽然 child 正在执行所有这些操作,但 parent 进程已经关闭 new_socket_fd (3) 因为连接正在由 child 处理。

问题是:这些假设是否正确?

正在将评论流转换为答案。

TL;博士

是的。问题中的描述看起来是正确的,推理是合理的。

在某些时候,您的 parent 进程应该等待 child 已经死亡的进程以防止僵尸的积累(但它不应该阻塞直到 child 死亡) .在带有 WNOHANG 参数的循环中使用 waitpid() 可能是合适的,在 parent 关闭 new_socket_fd 的循环部分。这可能会留下一个或多个僵尸,直到发出下一个传入请求。如果这是一个问题,您可以忽略 SIGCHLD(因此永远不会创建僵尸),或者您可以安排一个周期性的 wake-up,在此期间 parent 进程检查僵尸。

讨论

babon

Quick question - so when / where does the parent process close socket_fd?

parent 在退出循环或被告知停止监听套接字时关闭 socket_fd。在显示的代码中没有真正的规定,因此当 parent 进程被终止(或发生分叉失败)时它将关闭。重点是侦听套接字可用于许多连接 — 在完成侦听之前,您不想在 parent 中关闭它。

Matteo

In this case since it's an endless loop, never. The server will always be listening for up to N connections defined in listen(socket_fd, N).

请注意,listen() 调用中的 N 参数是可以排队等待侦听进程的未完成连接数。即,尚未通过 accept() 调用返回值的连接请求数。在 accept() 接受连接后可以同时激活的连接数没有限制。

Ajay Brahmakshatriya

Before the child closes the socket_fd, is the bound port mapped to both the PIDs? If there is a incoming packet, whose queue will it be put into?

传入的数据包与套接字的 'open file description'(或等价物 — 与 'file descriptor' 或 'socket descriptor' 不同)相关联。它可用于 parent 或 child,以先读取的为准。同样,传入的连接请求在 socket_fd 上排队;它们可以被 parent 或 child 接受。不过,一家人约定好了谁做什么,就不会互相碍事。

马特奥

To the parent's one I assume.

阿杰

If that is the case, the same should also happen for the packets for the new_socket_fd since both have it open. This means that the child won't be able to read the packets until the parent closes it. This can lead to race conditions.

这是基于误解。通过文件描述符,两个进程都可以使用该数据包。当进程关闭文件描述符时,它不能再访问发送到连接的信息(当然也不能在该连接上发送数据)。在那之前,除非参与者进程就哪个读取数据和哪个侦听连接达成一致,否则谁会看到什么?

马特奥

But file descriptor shouldn't interfere between parent and child; that's why closing socket_fd on the child side doesn't stop the parent from listening.

巴邦

Agreed. But I think you should close socket_fd after the while loop. In case tomorrow you change the loop to break for some condition you run the risk of keeping an open socket for no reason.

这是一个很好的做法,但循环不会退出(它是一个 while (1) 循环,失败模式会在循环外执行 return — 它可以在之前关闭套接字这样做 return)。如果程序退出,那么系统会关闭套接字,所以这并不重要,因为关闭你打开的东西是很好的管家。

阿杰

Both the file descriptors in the parent and child are different. So closing one should not affect the other. But both the copies have the same (src-ip, src-port, dest-ip, dest-port) 4-tuple and so where would a packet with such a header go?

描述符不同,但它们所指的套接字连接是相同的。该数据包可供任何读取它的进程使用 — parent 或 child.

马特奥

In my example accept_client creates the sockaddr struct for the client, so the 4-tuple goes to child's new_socket_fd.

这不太准确。首先,在 child 之前调用 accept_client()new_socket_fd 在 parent 中(仅)当该函数完成时。其次,在 fork() 之后,两个进程都可以访问 new_socket_fd,并且任何一个都可以读取客户端进程发送的数据。但是,该程序的设计使服务器返回监听更多连接请求,而 child 处理 new_socket_fd 上的传入连接 — 合理的分工。

请注意,允许 parent 处理请求并让 child 继续侦听。然而,这有悖于惯例。这意味着 'daemon' 进程侦听会在每个连接请求上更改 PID,因此很难确定当前正在侦听套接字的进程。代码中使用的常规方法是守护进程在很长一段时间内保持不变,因此记录PID以供以后进程控制(不再需要守护进程时杀死守护进程)是明智的。