目录
一、Linux 对网络通信的实现
Linux 网络 IO 模型
同步和异步
关注的是调用方是否主动获取结果
同步:同步的意思就是调用方需要主动等待结果的返回。
异步:异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等。
阻塞和非阻塞
主要关注的是等待结果返回调用方的状态
阻塞:是指结果返回之前,当前线程被挂起,不做任何事。
非阻塞:是指结果在返回之前,线程可以做一些其他事,不会被挂起。
两者的组合
- 同步阻塞:同步阻塞基本也是编程中最常见的模型,打个比方你去商店买衣服,你去了之后发现衣服卖完了,那你就在店里面一直等,期间不做任何事(包括看手机),等着商家进货,直到有货为止,这个效率很低。
- 同步非阻塞:同步非阻塞在编程中可以抽象为一个轮询模式,你去了商店之后,发现衣服卖完了,这个时候不需要傻傻的等着,你可以去其他地方比如奶茶店,买杯水,但是你还是需要时不时的去商店问老板新衣服到了吗。
- 异步阻塞:异步阻塞这个编程里面用的较少,有点类似你写了个线程池,submit 然后马上 future.get(),这样线程其实还是挂起的。有点像你去商店买衣服,这个时候发现衣服没有了,这个时候你就给老板留给电话,说衣服到了就给我打电话,然后你就守着这个电话,一直等着他响什么事也不做。这样感觉的确有点傻,所以这个模式用得比较少。
- 异步非阻塞:异步非阻塞。好比你去商店买衣服,衣服没了,你只需要给老板说这是我的电话,衣服到了就打。然后你就随心所欲的去玩,也不用操心衣服什么时候到,衣服一到,电话一响就可以去买衣服了。
Linux 下的五种 I/O 模型
总的来说,阻塞 IO 就是 JDK 里的 BIO 编程,IO 复用就是 JDK 里的 NIO 编程,Linux 下异步 IO 的实现建立在 epoll 之上,是个伪异步实现,而且相比 IO 复用,没有体现出性能优势,使用不广。非阻塞 IO 使用轮询模式,会不断检测是否有数据到达,大量的占用 CPU 的时间,是绝不被推荐的模型。信号驱动 IO 需要在网络通信时额外安装信号处理函数,使用也不广泛。
阻塞 IO 模型
I/O 复用模型
比较上面两张图,IO 复用需要使用两个系统调用(select 和 recvfrom),而 blocking IO 只调用了一个系统调用(recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。
所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
从 Linux 代码结构看网络通信
Linux 内核的源码包含的东西很多,在 Linux 的源代码中,网络设备驱动对应的逻辑位于
driver/net/ethernet, 其中 intel 系列网卡的驱动在 driver/net/ethernet/intel 目录下。协议栈模块代码位于 kernel 和 net 目录。
其中 net 目录中包含 Linux 内核的网络协议栈的代码。子目录 ipv4 和 ipv6 为 TCP/IP 协议栈的 IPv4 和 IPv6 的实现,主要包含了 TCP、UDP、IP 协议的代码,还有 ARP 协议、ICMP 协议、IGMP 协议代码实现,以及如 proc、ioctl 等控制相关的代码。
站在网络通信的角度,源代码组织的表现形式如下:
网络协议栈是由若干个层组成的,网络数据的流程主要是指在协议栈的各个层之间的传递。一个 TCP 服务器的流程按照建立 socket()函数,绑定地址端口 bind()函数,侦听端口 listen() 函数,接收连接 accept()函数,发送数据 send()函数,接收数据 recv()函数,关闭 socket()函数的顺序来进行。
与此对应内核的处理过程也是按照此顺序进行的,网络数据在内核中的处理过程主要是在网卡和协议栈之间进行:从网卡接收数据,交给协议栈处理;协议栈将需要发送的数据通过网络发出去。
由下图中可以看出,数据的流向主要有两种。应用层输出数据时,数据按照自上而下的顺序,依次通过应用 API 层、协议层和接口层;当有数据到达的时候,自下而上依次通过接口层、协议层和应用 API 层的方式,在内核层传递。
应用层 Socket 的初始化、绑定(bind)和销毁是通过调用内核层的 socket()函数进行资源的申请和销毁的。
发送数据的时候,将数据由应用 API 层传递给协议层,协议层在 UDP 层添加 UDP 的首部、 TCP 层添加 TCP 的首部、IP 层添加 IP 的首部,接口层的网卡则添加以太网相关的信息后,通过网卡的发送程序发送到网络上。
接收数据的过程是一个相反的过程,当有数据到来的时候,网卡的中断处理程序将数据从以太网网卡的 FIFO 对列中接收到内核,传递给协议层,协议层在 IP 层剥离 IP 的首部、UDP 层剥离 UDP 的首部、TCP 层剥离 TCP 的首部后传递给应用 API 层,应用 API 层查询 socket 的标识后,将数据送给用户层匹配的 socket。
在 Linux 内核实现中,链路层协议靠网卡驱动来实现,内核协议栈来实现网络层和传输层。
内核对更上层的应用层提供 socket 接口来供用户进程访问。
二、Linux 下的 IO 复用编程
select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,并等待读写完成。
文件描述符 FD
在 Linux 操作系统中,可以将一切都看作是文件,包括普通文件,目录文件,字符设备文件(如键盘,鼠标…),块设备文件(如硬盘,光驱…&#x