IO 有两种操作,同步 IO 和 异步 IO。同步 IO 指的是,必须等待 IO 操作完成后,控制权才返回给用户进程。异步 IO 是,无须等待 IO 操作完成,就将控制权返回给用户进程。
当一个网络 IO (假设是 read )发生时,它会涉及两个系统对象,一个是调用这个 IO的进程,另一个是系统。当一个 read 操作发生时,它会经历两个阶段:1. 等待数据准备;2. 将数据从内核拷贝到进程中。
4种网络 IO 模型
为了解决网络 IO 中的问题,学者们提出了4种网络 IO 模型:
在Linux下,默认情况下所有的 socket 都是阻塞的。一个典型的读操作流程如图:
阻塞和非阻塞的概念描述的是用户线程调用内核 IO 操作的方式:阻塞是指 IO 操作需要彻底完成后才返回到用户空间;而非阻塞是指 IO 操作被调用后立即返回给用户一个状态值,不需要等到 IO 操作彻底完成。阻塞 IO 模型的特点就是 IO 执行的两个阶段(等待数据和拷贝数据)都被阻塞了
除非特别指定, 几乎所有的 IO 接口(包括 socket 接口)都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 send()
的同时,线程处于阻塞状态,则在此期间,线程将无法执行任何运算或响应任何网络请求。一个简单的改进方案是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
对于多线程服务器来说,如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应的效率,而线程与进程本身也更容易进入假死状态。这时可以考虑使用“线程池”或“连接池”。“线程池”旨在降低创建和销毁线程的频率,使其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务 “连接池”是指维持连接的缓存池,尽量重用已有的连接,降低创建和关闭连接的频率。但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。这时可以用非阻 塞模型来尝试解决这个问题。
在Linux 下,可以通过设置 socket 使 IO 变为非阻塞状态。当一个非阻塞的 socket执行read 操作时,流程如图所示:
在非阻塞式 IO 中,用户进程其实需要不断地主动询问 kernel 数据是否准备好。非阻塞的接口相比于阻塞型接口的显著差异在于被调用之后立即返回。
上述模型因为循环调用 recv()
将大幅度占用 CPU 使用率,不被推荐使用。
多路 IO 复用,有时也称为事件驱动 IO。它的基本原理就是有个函数(如 select )会不断地轮询所负责的所有 socket ,当某个 socket 有数据到达了,就通知用户进程, IO 复用模型的流程如图所示:
这个模型和阻塞 IO 的模型其实并没有太大的不同,事实上还更差一些,因为这里需要使用两个系统调用(select和recvfrom),而阻塞 IO 只调用了一个系统调用(recvfrom)。用 select 的优势在于它可以同时处理多个连接。所以,如果处理的连接数不是很高的话,使用 select/epoll的Web server不一定比使用多线程的阻塞 IO Web server 性能更好,可能延迟还更大; select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
相比其他模型,使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU 资源,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
异步 IO 模型
异步 IO 模型的流程如图所示:
各个 IO 模型的比较如图所示:
在非阻塞 IO中,虽然进程大部分时间都不会被阻塞,但是它仍然要求进程去主动检查,并且当数据准备完成以后,也需要进程主动地再次调用 recvfrom 来将数据拷贝到用户内存中。而异步 IO 则完全不同,它就像是用户进程将整个 IO 操作交给了他人(内核)完成,然后内核做完后发信号通知。在此期间,用户进程不需要去检查 IO 操作的状态,也不需要主动地拷贝数据。
和select 函数一样 poll 函数也可以用于执行多路复用 IO,poll函数原型定义如下:
int poll(struct pollfd * fds,unsigned int nfds ,int timeout);
每一个 poll 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。
timeout 参数指定等待的毫秒数,无论 IO 是否准备好, poll 都会返回。timeout 指定为负数值时表示无限超时,使 poll() 一直挂起直到一个指定事件发生。timeout为0指示 poll 调用立即返回并列出准备好的 IO 文件描述符,但并不等待其他的事件。这种情况下, poll() 的返回值, 一旦被选举出来,立即返回
epoll 是之前 select和poll 的增强版本。相对于 select和poll 来说,epoll 更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间之间的数据拷贝只需一次。
无论是 select、poll还是epoll都需要内核把fd消息通知给用户空间,如何避免不必要的内存拷贝就显得尤为重要。在这点上, epoll 是通过内核与用户空间 mmap 处于同一块内存实现的。
网络分析工具
ping是 TCP/IP 协议的一部分。利用 ping 命令可以检查网络是否连通,可以很好地帮助分析和判定网络故障。应用格式: ping 空格IP 地址,该命令还可以加许多参数使用。ping 发送一个 ICMP (Internet Control Messages Protocol ,因特网信报控制协议),请求消息给目的地并报告是否收到所希望的 ICMP echo ( ICMP 声应答),它是用来检查网络是否通畅或者网络连接速度的命令。它所利用的原理是这样的:利用网络上机器 IP 地址的唯一性,给目标 IP 地址发送一个数据包,再要求对方返问一个同样大小的数据包来确定两台网络机器是否连接相通以及时延是多少。 使用 ping 检查连通性有以下6个步骤:
tcpdump 可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对协议、主机、网络或端口的过滤,并提供 and、or、 not 等逻辑语句来帮助去掉无用的信息,对于网络维护和防止入侵都是非常有用的工具,并根据使用者的定义对网络上的数据包进行截获和分析。 tcpdump 采用命令行方式,它的命令格式为: tcpdump [ -adeflnNOpqStvx] [ -c 数量 ] [ -F 文件名] [-i 网络接口] [-r 文件名] [-s snaplen] [-T 类型] [-w 文件名] [表达式] 表达式是一个正则表达式, tcpdump 利用它作为过滤报文的条件,如果一个报文满足表达式的条件,则这个报文将会被捕获。 tcpdump使用示例: 想要截获所有 210.27.48.1 的主机收到的和发出的所有的数据包,使用如下命令: tcpdump host 210.27.48.1 想要截获主机 210.27.48.1 和主机 210.27.48.2或210.27.48.3 的通信: tcpdump host 210.27.48.1 and \ (210.27.48.2 or 210.27.48.3 \) 获取主机 210.27.48.1 除了和主机 210.27.48.2 之外所有主机通信的 ip 包: tcpdump ip host 210.27.48.1 and ! 210.27.48.2 如果想要获取主机 10.27.48.1 接收或发出的 telnet 包: tcpdump tcp port 23 host 210.27.48.1 如果想要获取在端口 6666 上通过的包 tcpdump port 6666 如果想要获取在网卡 ethl 上通过的包 tcpdump -i ethl
netstat 命令用于显示与 IP、TCP、UDP和ICMP 协议相关的统计数据,一般用于检验本机各端口的网络连接情况。netstat 是在内核中访问网络及相关信息的程序,它能提供 TCP 连接、对 TCP和UDP 的监听及获取进程内存管理的相关报告 nets tat 的命令格式如下所示: netstat [-acCeFghilMnNoprstuvVwx] [-A<网络类型>] [--ip]
lsof (list open file) 是一个列出当前系统打开文件的工具。Linux 境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据 ,还可以访问网络连接和硬件。所以如传输控制协议( TCP )和用户数据报协议( UDP )套接字等,系统在后台都为该应用程序分配了一个文件描述符,无论这个文件的本质如何,该文件描述符为应用程序与基础操作系统之间的交互提供了通用接口。