导航
导航
文章目录
  1. 多进程同步模型
    1. fork
    2. 僵尸进程
    3. wait 和 waitpid 函数
  2. 多线程同步模型
  3. pre-fork 多进程同步模型
  4. Reactor 模型

网络编程:服务端并发模型

这篇文章主要介绍几个服务端程序的并发模型。

  • 多进程同步模型。
  • 多线程同步模型。
  • pre-fork 多进程同步模型。
  • Reactor 模型。

多进程同步模型

这种模型通过创建多个进程来提高服务端的并发能力。通常的做法是服务端 accept 到客户端的请求之后,为每个客户端请求创建一个进程来处理数据。

fork

Linux 下使用 fork 函数来创建进程。由 fork 创建的新进程被称为子进程 (child process). 父进程 (parent process) 调用 fork 之后产生一个子进程。在父进程中 fork 的返回值为子进程的 PID, 在子进程中 fork 返回的是 0. 如果 fork 出错,那么返回的是 -1.

fork 调用成功之后,子进程获得父进程的数据空间、堆、栈的副本,注意子进程只是拥有副本,不共享这些存储空间部分。但是,通常 fork 并不执行一个父进程数据段、堆、栈的完全部分。作为替代,使用了写时复制 (Copy-On-Write) 技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一页。下面通过一个实例介绍一下 fork.

3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# coding=utf-8

import os

foo = 22
pid = os.fork() #: fork 调用之后得到父,子两个进程
if pid == 0:
print('child process')
foo += 1
elif pid > 0:
print('parent process')
else:
print('fork error')

print('foo = {}'.format(foo))

一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的。这个取决于内核所使用的调度算法。

fork 还有一个重要的特性就是父进程当中所有打开的文件描述符都会被“复制”到子进程当中 这里虽然是复制,但是父进程和子进程当中的文件描述符都指向同一块文件表。所以如果父进程和子进程写同一描述符指向的文件,但是没有做任何形式的同步,那么它们的输出就会相互混乱。对于网络程序来说,正确的做法是在 fork 之后,父进程和子进程各自关闭它们不需要使用的文件描述符。

3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# coding=utf-8

import os
import socket

response = b'HTTP/1.1 200 OK\r\nServer: Apache\r\nContent-Length: 81\r\nContent-Type: text/html\r\n\r\n'

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 9190))
sock.listen(1024)
while True:
client, addr = sock.accept()
pid = os.fork()
if pid > 0:
client.close()
elif pid == 0:
sock.close()
data = client.recv(4096)
client.send(response)
client.close()
else:
pass

僵尸进程

一个进程可以正常运行结束,也可以因为一些异常终止。每个进程退出时候,内核会释放该进程的所有资源,包括打开的文件描述符,占用的内存等。但是仍然为其保留一定的信息(包括进程的 PID 等)。

当一个子进程先退出时,如果父进程没有回收进程,那么子进程会变成僵尸进程。操作系统的进程号是有限的,如果僵尸进程太多,大量的进程号被占用,从而无法创建新的进程。

当父进程比子进程先退出时,父进程下的所有子进程都会被 init 进程接管,这个进程的 PID 是 1, 子进程退出后,init 进程来负责回收子进程的资源。

wait 和 waitpid 函数

那么父进程如何回收子进程呢?我们可以使用 waitwaitpid 函数。父进程调用 wait 或 waitpid 之后:

  • 如果其所有子进程都还在运行,则被阻塞。
  • 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
  • 如果它没有任何子进程,则立即出错返回。

这两个函数的主要区别是:

  • 在一个子进程终止前,wait 使其调用者阻塞,而 waitpid 有一个选项,可以使调用者不阻塞。
  • waitpid 并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

知道了回收子进程的方法,那么该如何使用呢?子进程什么时候终止?父进程调用 wait 之后要一直被阻塞吗?调用 waitpid 之后要不停的等待子进程终止吗?我们可以用信号来处理这些问题。

当一个进程正常或异常终止时,内核会向它的父进程异步发送 SIGCHLD 信号。父进程可以选择忽略该信号,或者注册一个该信号的处理函数,来做一些回收处理子进程的工作。

多线程同步模型

多进程同步模型存在一些问题:

  • 创建进程的过程中会带来一定的开销。
  • 进程间通信难度复杂,需要特殊的 IPC 技术。
  • CPU 调度切换进程开销大。

对于一个服务端程序来说,如果有大量的客户端请求,每次处理这些请求都要创建、销毁进程,这个性能会大大下降。所以出现了多线程同步模型。

线程相比于进程有如下的优点:

  • 线程的创建和上下文切换比进程的创建和上下文切换更快。
  • 线程在进程的当中,它们共享进程的资源,所以通信起来不需要 IPC 技术。

这种模型通过创建多个线程来提高服务端的并发能力。通常的做法是服务端 accept 到客户端的请求之后,为每个客户端请求创建一个线程来处理数据。

3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# coding=utf-8

import socket
import threading

response = b'HTTP/1.1 200 OK\r\nServer: Apache\r\nContent-Length: 81\r\nContent-Type: text/html\r\n\r\n'


def handle_client(client_sock):
client_sock = client.recv(4096)
client_sock.send(response)
client_sock.close()


def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 9190))
sock.listen(1024)
while True:
client, addr = sock.accept()
t = threading.Thread(target=handle_client, args=(client,))
t.start()

pre-fork 多进程同步模型

这种模型与多进程同步模型的区别在于,我们不需要在客户端请求过来之后为它创建一个进程,而是事先创建 N 个子进程,用这些子进程来处理客户端请求。通常的做法是,父进程 fork 出多个子进程,Linux 2.6 版本之前,当有请求到来时,子进程被唤醒共同对服务端 accept 进行竞争,每个子进程都有机会获得客户端请求,但是最终只有一个子进程能够 accept 成功拿到客户端请求。这种情况叫作“惊群效应”。Linux 2.6 版本之后,解决掉了 accept 的“惊群效应”问题。

3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# coding=utf-8

import os
import socket

response = b'HTTP/1.1 200 OK\r\nServer: Apache\r\nContent-Length: 81\r\nContent-Type: text/html\r\n\r\n'


def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 9190))
sock.listen(1024)

p = 0
for i in range(2):
p = 0
pid = os.fork()
if pid == 0:
break
elif pid > 0:
p = pid
else:
pass
if p == 0: #: 子进程
while True:
client, addr = sock.accept()
data = client.recv(4096)
client.send(response)
client.close()
else: #: 父进程
pass


if __name__ == '__main__':
main()

Reactor 模型

Reactor 是一种基于事件驱动的分发处理模型。一个连接里完整的网络处理一般分为 accept, read, decode, process, encode, send 这几步。我们可以把每一步封装成一个 Task, 每个 Task 都对应特定的网络事件。把这些 Task 注册到事件驱动当中,事件驱动一直轮询,当有 Task 就绪时,返回对应的网络事件,根据不同的网络事件来选择执行 (dispatch) 不同的 Handler 方法。

支持一下
请 xdd1874 喝一杯咖啡?
  • 微信扫一扫