Linux.中国 - 开源社区

 找回密码
 骑士注册

QQ登录

微博登录


搭个 Web 服务器(三)

2016-10-9 14:42    评论: 3 收藏: 4    

进程分叉后不关闭重复的套接字会发生什么?

我们来看看,如果我们不在父进程与子进程中关闭重复的套接字描述符会发生什么。下面是刚才的并发服务器代码的修改版本,这段代码(webserver3d.py 中,服务器不会关闭重复的描述符):

#######################################################
# 并发服务器 - webserver3d.py                          #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#######################################################
import os
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5


def handle_request(client_connection):
    request = client_connection.recv(1024)
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)


def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))

    clients = []
    while True:
        client_connection, client_address = listen_socket.accept()
        ### 将引用存储起来,否则在下一轮循环时,他们会被垃圾回收机制销毁
        clients.append(client_connection)
        pid = os.fork()
        if pid == 0:  ### 子进程
            listen_socket.close()  ### 关闭子进程中多余的套接字
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)  ### 子进程在这里结束
        else:  ### 父进程
            # client_connection.close()
            print(len(clients))

if __name__ == '__main__':
    serve_forever()

用以下命令来启动服务器:

$ python webserver3d.py

curl 命令连接服务器:

$ curl http://localhost:8888/hello
Hello, World!

好,curl 命令输出了来自并发服务器的响应内容,但程序并没有退出,而是仍然挂起。到底发生了什么?这个服务器并不会挂起 60 秒:子进程只处理客户端连接,关闭连接然后退出,但客户端的 curl 命令并没有终止。

所以,为什么 curl 不终止呢?原因就在于文件描述符的副本。当子进程关闭客户端连接时,系统内核会减少客户端套接字的引用计数,将其变为 1。服务器子进程退出了,但客户端套接字并没有被内核关闭,因为该套接字的描述符引用计数并没有变为 0,所以,这就导致了连接终止包(在 TCP/IP 协议中称作 FIN)不会被发送到客户端,所以客户端会一直保持连接。这里也会出现另一个问题:如果你的服务器长时间运行,并且不关闭文件描述符的副本,那么可用的文件描述符会被消耗殆尽:

使用 Control-C 关闭服务器 webserver3d.py,然后在 shell 中使用内置命令 ulimit 来查看系统默认为你的服务器进程分配的可用资源数:

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 3842
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 3842
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

你可以从上面的结果看到,在我的 Ubuntu 机器中,系统为我的服务器进程分配的最大可用文件描述符(文件打开)数为 1024。

现在我们来看一看,如果你的服务器不关闭重复的描述符,它会如何消耗可用的文件描述符。在一个已有的或新建的终端窗口中,将你的服务器进程的最大可用文件描述符设为 256:

$ ulimit -n 256

在你刚刚运行 ulimit -n 256 的终端窗口中运行服务器 webserver3d.py

$ python webserver3d.py

然后使用下面的客户端 client3.py 来测试你的服务器。

#######################################################
# 测试客户端 - client3.py                              #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#######################################################
import argparse
import errno
import os
import socket


SERVER_ADDRESS = 'localhost', 8888
REQUEST = b"""\
GET /hello HTTP/1.1
Host: localhost:8888

"""


def main(max_clients, max_conns):
    socks = []
    for client_num in range(max_clients):
        pid = os.fork()
        if pid == 0:
            for connection_num in range(max_conns):
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.connect(SERVER_ADDRESS)
                sock.sendall(REQUEST)
                socks.append(sock)
                print(connection_num)
                os._exit(0)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Test client for LSBAWS.',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        '--max-conns',
        type=int,
        default=1024,
        help='Maximum number of connections per client.'
    )
    parser.add_argument(
        '--max-clients',
        type=int,
        default=1,
        help='Maximum number of clients.'
    )
    args = parser.parse_args()
    main(args.max_clients, args.max_conns)

在一个新建的终端窗口中,运行 client3.py 然后让它与服务器同步创建 300 个连接:

$ python client3.py --max-clients=300

过一会,你的服务器进程就该爆了。这是我的环境中出现的异常截图:

这个例子很明显——你的服务器应该关闭描述符副本。

僵尸进程

但是,即使你关闭了描述符副本,你依然没有摆脱险境,因为你的服务器还有一个问题,这个问题在于“僵尸zombies”!

没错,这个服务器代码确实在制造僵尸进程。我们来看看怎么回事。重新运行你的服务器:

$ python webserver3d.py

在另一个终端窗口中运行以下 curl 命令:

$ curl http://localhost:8888/hello

现在,运行 ps 环境,来查看正在运行的 Python 进程。下面是我的环境中 ps 的运行结果:

$ ps auxw | grep -i python | grep -v grep
vagrant   9099  0.0  1.2  31804  6256 pts/0    S+   16:33   0:00 python webserver3d.py
vagrant   9102  0.0  0.0      0     0 pts/0    Z+   16:33   0:00 [python] <defunct>

你看到第二行中,pid 为 9102,状态为 Z+,名字里面有个 <defunct> 的进程了吗?那就是我们的僵尸进程。这个僵尸进程的问题在于:你无法将它杀掉!

就算你尝试使用 kill -9 来杀死僵尸进程,它们仍旧会存活。自己试试看,看看结果。

这个僵尸到底是什么,为什么我们的服务器会造出它们呢?一个僵尸进程zombie是一个已经结束的进程,但它的父进程并没有等待(waited)它结束,并且也没有收到它的终结状态。如果一个进程在父进程退出之前退出,系统内核会把它变为一个僵尸进程,存储它的部分信息,以便父进程读取。内核保存的进程信息通常包括进程 ID、进程终止状态,以及进程的资源占用情况。OK,所以僵尸进程确实有存在的意义,但如果服务器不管这些僵尸进程,你的系统将会被壅塞。我们来看看这个会如何发生。首先,关闭你运行的服务器;然后,在一个新的终端窗口中,使用 ulimit 命令将最大用户进程数设为 400(同时,要确保你的最大可用描述符数大于这个数字,我们在这里设为 500):

$ ulimit -u 400
$ ulimit -n 500

在你刚刚运行 ulimit -u 400 命令的终端中,运行服务器 webserver3d.py

$ python webserver3d.py

在一个新的终端窗口中,运行 client3.py,并且让它与服务器同时创建 500 个连接:

$ python client3.py --max-clients=500

然后,过一会,你的服务器进程应该会再次爆了,它会在创建新进程时抛出一个 OSError: 资源暂时不可用 的异常。但它并没有达到系统允许的最大进程数。这是我的环境中输出的异常信息截图:

你可以看到,如果服务器不管僵尸进程,它们会引发问题。接下来我会简单探讨一下僵尸进程问题的解决方案。

我们来回顾一下你刚刚掌握的知识点:

  • 如果你不关闭文件描述符副本,客户端就不会在请求处理完成后终止,因为客户端连接没有被关闭;
  • 如果你不关闭文件描述符副本,长久运行的服务器最终会把可用的文件描述符(最大文件打开数)消耗殆尽;
  • 当你创建一个新进程,而父进程不等待(wait)子进程,也不在子进程结束后收集它的终止状态,它会变为一个僵尸进程;
  • 僵尸通常都会吃东西,在我们的例子中,僵尸进程会吃掉资源。如果你的服务器不管僵尸进程,它最终会消耗掉所有的可用进程(最大用户进程数);
  • 你不能杀死(kill)僵尸进程,你需要等待(wait)它。
查看其它分页:

发表评论


最新评论

我也要发表评论

linux [Chrome 54.0|Mac 10.11] 2016-11-11 22:43
1
来自广东的 Chrome 54.0|Mac 10.11 用户 发表于 2016-11-11 11:32 的评论:
楼主,想问一下mac是不是无法查看僵尸进程?我在mac下运行下面这段代码:
#include <unistd.h>

int main(void)
{
        int i;
        for (i = 0; i < 10; ++i) {
                if (!fork()) {
                        return 0;
                }
        }

        sleep(100);

        return 0;
}
编译成main,然后ps -ef | grep 'main'之后根本无法查看到<defunct>的进程啊。
看 STATE 字段里面 Z 的进程,这是僵尸、
回复
来自广东的 Chrome 54.0|Mac 10.11 用户 2016-11-11 11:32
楼主,想问一下mac是不是无法查看僵尸进程?我在mac下运行下面这段代码:
#include <unistd.h>

int main(void)
{
        int i;
        for (i = 0; i < 10; ++i) {
                if (!fork()) {
                        return 0;
                }
        }

        sleep(100);

        return 0;
}
编译成main,然后ps -ef | grep 'main'之后根本无法查看到<defunct>的进程啊。
回复
来自浙江杭州的 Chrome 54.0|Mac 10.11 用户 2016-10-26 11:46
感谢分享!
2 回复

热点评论

来自浙江杭州的 Chrome 54.0|Mac 10.11 用户 2016-10-26 11:46
感谢分享!
2
返回顶部

分享到微信朋友圈

打开微信,点击底部的“发现”,
使用“扫一扫”将网页分享至朋友圈。