参考连接:
epoll
内存中加载的第一个程序是操作心态内核kernel, 管理硬件
内存被划分为用户空间和内核空间, 我们普通的程序是不能访问内核空间的任何东西的, 是处于安全的考虑.
BIO
我们先来看一个例子, 一个tcp server:
#-*- coding:utf8 -*-
import socket
import threading
bind_ip = "0.0.0.0" #绑定ip:这里代表任何ip地址
bind_port = 8888
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((bind_ip, bind_port))
# 最大连接数为5
server.listen(5)
print "[*] Listening on %s:%d" % (bind_ip, bind_port)
# 这是客户处理进程
def handle_client(client_socket):
#打印出客户端发送得到的内容
request = client_socket.recv(1024)
print "[*] Received: %s" % request
#发送一个数据包
client_socket.send("ACK!")
client_socket.close()
while True:
client,addr = server.accept()
print "[*] Accepted connection from: %s:%d" % (addr[0], addr[1])
#挂起客户端线程,处理传人的数据
client_handler = threading.Thread(target=handle_client, args=(client,))
client_handler.start()
这里解释一下, 为什么用多线程, client_socket.recv(1024)会阻塞程序执行, 如果不用多线程, 其他的client在阻塞期间就连不进来. 这是最早的使用多线程解决并发的方案.
我们用strace命令运行, strace命令的作用是抓取程序发生的系统调用.
# -ff 跟踪由fork调用所产生的子进程
# 如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号.
strace -ff -o ./ooxx python tcpServer.py
如果是mac下, 使用替代命令dtruss:
# 需要sudo
sudo -f dtruss
PID/THRD SYSCALL(args) = return
[*] Listening on 0.0.0.0:8888
3782/0xfe98: open("/dev/dtracehelper\0", 0x2, 0xFFFFFFFFEB162F10) = 3 0
3782/0xfe98: ioctl(0x3, 0x80086804, 0x7FFEEB162E20) = 0 0
3782/0xfe98: close(0x3) = 0 0
第一列: 进程号, 第二列: 系统调用, 第三列: 系统调用返回结果
如果出现dtrace: system integrity protection is on, some features will not be available错误
可以从安全模式关闭:
- 引导你的Mac进入恢复模式(Recovery Mode):重新启动它,按住cmd+R直到一个进度条出现。
- 进入实用工具菜单。选择终端
- 输入此命令禁用系统完整性保护(System Integrity Protection):
csrutil disable - 或者仅禁用dtrace限制:
csrutil clear # 恢复默认配置
csrutil enable --without dtrace
分析系统调用日志
运行之后, 我的当前文件目录下新生成了一个文件ooxx.103, 103是进程id.
文件中有这么一行:
write(1, "[*] Listening on 0.0.0.0:8888\n", 30) = 30
对应的是
print("[*] Listening on %s:%d" % (bind_ip, bind_port))
也就是标准输出.
文件最后大概长这个样子, 我稍微简化了一下, 主要突出socket相关的调用:
....
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 5) = 0
write(1, "[*] Listening on 0.0.0.0:8888\n", 30) = 30
accept4(3,
它在accept4(3,这里就阻塞在这儿了, 因为我们还没有客户端连接进来.
我们先插入一个知识点, 关于目录/proc/103, 这个目录下是关于进程103相关的信息, 在liunx下一切皆文件, 所以进程相关的信息也以文件的方式体现.
我们关注/proc/103下两个文件夹, task和fd.
# task目录的文件代表的是该进程相关的进程和线程
# 目前就只有它自己
ls /proc/103/task
103
# fd目录下的文件代表是文件描述符
# 0 是标准输入 1 是标准输出 3 是标准错误 4 是我们建立的socket
ls -l /proc/103/fd
lrwx------ 1 root root 64 Jul 20 06:07 0 -> /dev/pts/1
lrwx------ 1 root root 64 Jul 20 06:07 1 -> /dev/pts/1
lrwx------ 1 root root 64 Jul 20 06:07 2 -> /dev/pts/1
lrwx------ 1 root root 64 Jul 20 06:13 4 -> 'socket:[37410]'
我们还可以拿netstat -natp显示tcp的连接:
netstat -natp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 194/python3
(Netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等)
现在我们来发起一个tcp的连接, 再来观察系统调用以及task和fd的变化.
我们用nc来发起tcp连接:
nc localhost 8888
多了一个ooxx.133文件, 因为我们会新开一个线程处理请求, 133是线程号.
ls /proc/103/task
103 133
ls -l /proc/103/fd
lrwx------ 1 root root 64 Jul 20 06:07 0 -> /dev/pts/1
lrwx------ 1 root root 64 Jul 20 06:07 1 -> /dev/pts/1
lrwx------ 1 root root 64 Jul 20 06:07 2 -> /dev/pts/1
lrwx------ 1 root root 64 Jul 20 06:13 4 -> 'socket:[37410]'
lrwx------ 1 root root 64 Jul 20 06:13 4 -> 'socket:[37411]' # 多了一个socket
netstat -natp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:8888 0.0.0.0:* LISTEN 103/python3 # 只有服务器才LISTEN
tcp 0 0 127.0.0.1:60960 127.0.0.1:8888 ESTABLISHED 150/nc # 这是客户端nc发起时的socket
tcp 0 0 127.0.0.1:8888 127.0.0.1:60960 ESTABLISHED 103/python3 # 这是服务器端响应时创建的socket
ooxx.103:
listen(3, 5) = 0
write(1, "[*] Listening on 0.0.0.0:8888\n", 30) = 30
# 以下新增的部分
accept4(3, {sa_family=AF_INET, sin_port=htons(60960), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_CLOEXEC) = 4
write(1, "[*] Accepted connection from: 12"..., 46) = 46
mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7fcd163dc000
mprotect(0x7fcd163dd000, 8388608, PROT_READ|PROT_WRITE) = 0
# 创建线程
clone(child_stack=0x7fcd16bdbfb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fcd16bdc9d0, tls=0x7fcd16bdc700, child_tidptr=0x7fcd16bdc9d0) = 199
futex(0x558c882b7b90, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 0, NULL, 0xffffffff) = 0
futex(0x7fcd18a459e8, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 0, {tv_sec=1595227652, tv_nsec=810365000}, 0xffffffff) = -1 EAGAIN (Resource temporarily unavailable)
futex(0x7fcd18a45a40, FUTEX_WAIT_PRIVATE, 2, NULL) = -1 EAGAIN (Resource temporarily unavailable)
futex(0x7fcd18a45a40, FUTEX_WAKE_PRIVATE, 1) = 0
# 阻塞等下下一个连接
accept4(3
ooxx.133:
set_robust_list(0x7fcd16bdc9e0, 24) = 0
mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7fcd0e3dc000
munmap(0x7fcd0e3dc000, 29507584) = 0
munmap(0x7fcd14000000, 37601280) = 0
mprotect(0x7fcd10000000, 135168, PROT_READ|PROT_WRITE) = 0
futex(0x558c882b7b90, FUTEX_WAKE_PRIVATE, 1) = 1
futex(0x7fcd18a459e8, FUTEX_WAKE_PRIVATE, 1) = 0
futex(0x7fcd18a45a40, FUTEX_WAKE_PRIVATE, 1) = 0
# 等待接送数据, 因为这是在线程中, 所以才不影响主进程accpet其他的请求
recvfrom(4,
使用nc发生一下数据:
nc localhost 8888
aaa # 发送的数据
ACK! # 服务器返回的数据
ooxx.133:
# 接受到数据
recvfrom(4, "aaa\n", 1024, 0, NULL, NULL) = 4
# 程序继续执行
write(1, "[*] Received: b'aaa\\n'\n", 23) = 23
# 发生`ACK!`到客户端
sendto(4, "ACK!", 4, 0, NULL, 0) = 4
总结:
上面演示了一次tcp连接的请求过程对应的系统调用.
socket(...) = 3 # 返回3, 是文件描述符, 就代表了这个socket
bind(3, ... port) = 0
listen(3 ...) # 把fd = 3的socket设置为监听状态
accept4(3 ) = 4 # 收到一个连接, 返回一个文件描述符号代表这个请求的socket
read recvfrom # 读取数据
(可以通过man命令查找这些系统调用的帮助文档, man listen man 2 bind 等, 这里的2代表是查看系统调用的命令, 因为shell命令也有个叫bind)
fd3是监听套接字, fd4是连接套接字. 所以监听套接字只负责监听.
其中accept4, recvfrom都会阻塞程序执行, 上面的例子是通过多线程的方式来解决阻塞的问题.
如果不用多线程, 在recvfrom等待的期间, 其他客户端是没有办法accept进来的.
这种通信模型我们称为BIO(Blocking IO), 因为有阻塞.
NIO (non blocking io)
BIO的问题显而易见, 因为阻塞, 我们不得不多开线程, 线程会增加资源消耗(内存和cpu的调度)
系统调用也提供了非阻塞的方式, 详情请man socket. 如下是对应的python代码.
from socket import *
import time
s=socket(AF_INET,SOCK_STREAM)
s.bind(('127.0.0.1',8888))
s.listen(5)
s.setblocking(False) #设置socket的接口为非阻塞
conn_l=[] # 存储和server的连接 的 连接
del_l=[] # 存储和和server的断开 的 连接
while True:
try:
# 这个过程是不阻塞的
conn,addr=s.accept() # 当没人连接的时候会报错,走exception(<- py中是except)
conn_l.append(conn)
except BlockingIOError:
print(conn_l)
for conn in conn_l:
try:
data=conn.recv(1024)
if not data:
del_l.append(conn)
# 这个过程是不阻塞的
data=conn.recv(1024) # 不阻塞
if not data: # 如果拿不到data
del_l.append(conn) # 在废弃列表中添加conn
continue
conn.send(data.upper())
except BlockingIOError:
pass
except ConnectionResetError:
del_l.append(conn)
for conn in del_l:
conn_l.remove(conn)
conn.close()
del_l=[]
对于非阻塞的socket, 我们只有通过轮询的方式, 不断的需要是否有新连接(accept), 是否有连接发送数据(recv).
这种方式有什么问题呢?
当我们有很多个连接时, 每一次循环, 我们要对每个连接进行recvfrom的系统调用, 实际上接受数据的只有少数的连接, 这也是一种浪费.
select
上面的问题这么解决呢? 为了应对这个问题, 内核提供了一个新的系统调用select.
select(多个文件描述符号) = 对于描述符号的状态
传人的是多个文件描述符, 这里可以理解为多个socket(一个文件描述符(整数)对应一个socket) 返回的多个文件描述符对应的状态, 比如可以读取了(我猜的) 然后程序再遍历文件描述符的状态, 手动的发起recvfrom.
这样的话, 系统调用的次数就减少了很多了.
epoll
select虽然解决的多次系统调用的问题, 但是还是会循环的查询多个文件描述符的状态, 只是这部分工作被放在了kernel中进行.
epoll对这个问题进行了改进, 首先要明确的是, epoll也是一组系统调用.
它的思路其实是事件的机制, 触发事件是由中断来完成, 比如说网卡接收到数据就会产生一个硬中断, 告诉cpu我这里由个事件需要你处理一下.
epoll_create = fd8 # 创建epoll 返回一个文件描述符, 代表epoll在kernel中的一个内存区域
epoll_ctl(fd8, add, fd3, accept) # 添加socket fd3到epoll fd8 accpt是监听的事件
epoll_wait(fd8, events) # 阻塞等待epoll fd8上的io事件, events是事件集合
events可以是以下几个宏的集合
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
重写服务器:
python下select库可以帮我们在默认操作系统下选择最合适的select, poll, epoll这三种多路复合模型
from socket import *
import select
s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('127.0.0.1',8888))
s.listen(5)
s.setblocking(False) #设置socket的接口为非阻塞
read_l=[s,] # 数据可读通道的列表
while True:
# 监听的read_l中的socket对象内部如果有变化,那么这个对象就会在r_l
# 第二个参数里有什么对象,w_l中就有什么对象
# 第三个参数 如果这里的对象内部出错,那会把这些对象加到x_l中
# 1 是超时时间
r_l,w_l,x_l=select.select(read_l,[],[],1)
print(r_l)
for ready_obj in r_l:
# 监听socket触发, 表示有新连接
if ready_obj == s:
conn,addr=ready_obj.accept() #此时的ready_obj等于s
# 相当也执行了epoll_ctl(fd8, add, fd3, accept)
read_l.append(conn)
else:
try:
data=ready_obj.recv(1024) #此时的ready_obj等于conn
if not data:
ready_obj.close()
read_l.remove(ready_obj)
raise Exception('连接断开')
ready_obj.send(data.upper())
except ConnectionResetError:
ready_obj.close()
read_l.remove(ready_obj)