Linux高并发服务器开发---从网络IO到IO多路复用

诗人和酒 · · 781 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

Netcat软件的基本使用

Netcat(简写nc)是一个强大的网络命令工具,能够在linux中执行与TCP、UDP相关的操作,例如端口扫描,端口重定向、端口监听甚至远程连接。

在这里,我们使用 nc 来模拟一台接收message的服务器,和一台发送message的客户端。

1、安装 nc 软件

sudo yum install -y nc

2、使用 nc 创建一台监听9999端口的服务器

nc -l -p 9999     # -l表示listening,监听

启动成功后 nc 进行阻塞

3、新建一个bash,使用 nc 创建一个发送message的客户端

nc localhost 9999

在控制台上输入要发送的信息,查看服务端是否接收到

4、查看上面的nc进程中的文件描述符

ps -ef | grep nc  # 查看nc的进程号,这里假设是2603

ls /proc/2603/fd  # 查看2603进程下的文件描述符

[图片上传中...(image-f69c24-1615989342820-15)]

可以看到这个进程下有一个socket,这就是nc的客户端和服务端之间创建的一个socket

经过这一系列的操作,相信我们对Netcat软件有了基本的了解,下面来介绍BIO

strace追踪系统调用

strace软件说明: 它是一个可以追踪系统调用和信号的软件,通过它我们来了解BIO

环境说明: 这里演示的都是基于老版本的linux,因为新版本的linux都不用BIO了,演示不出来

1、使用strace来追踪系统调用

sudo yum install -y strace              # 安装strace软件

mkdir ~/strace                          # 新建一个目录,存放追踪的信息

cd ~/strace                             # 进入到这个目录

strace -ff -o out nc -l -p 8080   # 使用strace追踪后边的命令进行的系统调用
                                  # -ff 表示追踪后面命令创建的进程及子进程的所有系统调用,
                                  # 并根据进程id号分开输出到文件
                                  # -o  表示追踪到的信息输出到指定名称的文件,这里是out  

2、查看服务端创建的系统调用

在上一步进入的目录下,出现了一个 out.pid 文件,里的内容都是 nc -l -p 9999 这个命令执行后的系统调用过程,使用vim命令来查看

vim out.92459    # nc进程id为92459

这里accept()方法进行了阻塞,它要等待其他socket对它进行连接

3、客户端连接,查看系统调用

退出vim,使用tail来进行查看

tail -f out.92459

-f 参数:当文件有追加的内容,可以实时地打印在控制台,这样就能很方便来查看客户端连接后进行的系统调用

nc localhost 8080

查看系统调用

image.png

这里客户端连接后,accept() 方法获取到客户端连接并返回文件描述符4,这个4就是服务端新创建的socket,用于和这个客户端进行通信

之后使用多路复用器poll来监听服务端上文件描述符4和0,0是标准输入文件描述符,哪个有事件发生就读取哪个文件描述符,如果都没有事件发生就进行堵塞

4、客户端发送message,查看系统调用

image.png

客户端向服务端发送数据,服务端就能从socket中监听到有事件发生,就能进行相应的处理,处理完继续堵塞,等待下一个事件发生

5、服务端发送数据到客户端,查看系统调用

image.png

服务端发数据,肯定从键盘输入,也就是标准输入0,从0中读取到数据发送给socket 4

BIO(阻塞式IO)

在我们第三节中,我们使用 strace 工具查看了 nc 软件使用过程中的系统调用,其实上一节中体现的就是BIO,我们把上面的一系列系统调用总结一下,根据直观的理解BIO

1、单线程模式

1.1、过程演示

1、服务端启动

image.png

启动服务端,等待socket连接,accept()方法阻塞

2、客户端连接,未发送数据

image.png

连接客户端,accept() 方法执行,未收到client1发送的数据,read()方法阻塞

3、另一个客户端连接

image.png

由于read()方法阻塞,无法执行到accept()方法,所以这样cpu一次只能处理一个socket

1.2、存在的问题

上面的模型存在很大的问题,如果客户端与服务端建立了连接,客户端迟迟不发数据,进程就会一直堵塞在read()方法上,这样其他客户端也不能进行连接,也就是一次只能处理一个客户端,对客户很不友好

1.3、如何解决

其实要解决这个问题很简单,利用多线程就可以,只要连接了一个socket,操作系统分配一个线程来处理,这样read()方法堵塞在每个线程上,不堵塞主线程,就能操作多个socket了,有哪个线程中的socket有数据,就读哪个socket

2、多线程模式

1.1 过程演示

image.png
  • 程序服务端只负责监听是否有客户端连接,使用 accept() 阻塞
  • 客户端1连接服务端,就开辟一个线程(thread1)来执行 read() 方法,程序服务端继续监听
  • 客户端2连接服务端,也开辟一个线程,执行read()方法
  • 任何一个线程上的socket有数据发送过来,read()就能立马读到,cpu就能进行处理

1.2、存在的问题

上面这个多线程模型,看似已经十分的完美,其实也有很大的问题。每来一个客户端,就要开辟一个线程,如果来1万个客户端,那就要开辟1万个线程。在操作系统中,用户态不能直接开辟线程,需要调用cpu的80软中断,让内核来创建的一个线程,这其中还涉及到用户状态的切换(上下文的切换),十分耗资源。

1.3、如何解决

第一个办法:使用线程池,这个在客户端连接少的情况下可以使用,但是用户量大的情况下,你不知道线程池要多大,太大了内存可能不够,也不可行

第二个办法:因为read()方法堵塞了,所有要开辟多个线程,如果什么方法能使read()方法不堵塞,这样就不用开辟多个线程了,这就用到了另一个IO模型,NIO(非阻塞式IO)

NIO(非阻塞式IO)

1、过程演示

1、服务端刚创建,没有客户端连接


image.png

在NIO中,accept()方法也是非阻塞的,它在一个while死循环中

2、当有一个客户端进行连接时

image.png

3、当有第二个客户端进行连接时

image.png

2、总结

在NIO模式中,一切都是非阻塞的:

  • accept()方法是非阻塞的,如果没有客户端连接,就返回error
  • read()方法是非阻塞的,如果read()方法读取不到数据就返回error,如果读取到数据时只阻塞read()方法读数据的时间

在NIO模式中,只有一个线程:

  • 当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间遍历一次,看这个socket的read()方法能否读到数据
  • 这样一个线程就能处理多个客户端的连接和读取了

3、存在的问题

NIO成功的解决了BIO需要开启多线程的问题,NIO中一个线程就能解决多个socket,看似已经 perfect,但是还存在问题。

这个模型在客户端少的时候十分好用,但是客户端如果很多,比如有1万个客户端进行连接,那么每次循环就要遍历1万个socket,如果一万个socket中只有10个socket有数据,也会变量一万个socket,就会做很多无用功。而且这个遍历过程是在用户态进行的,用户态判断socket是否有数据还是调用内核的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大

因为这些问题的存在,IO多路复用应运而生

IO Multiplexing(IO多路复用)

IO多路复用有三种实现方式,select、poll、epoll,现在让我们来看看这三种实现的真面目吧

1、select

image.png
image.png

1.1 优点

select 其实就是把NIO中用户态要遍历的 fd 数组拷贝到了内核态,让内核态来遍历,因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,这样遍历判断的时候就不用一直用户态和内核态频繁切换了

从代码中可以看出,select系统调用后,返回了一个置位后的&rset,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率

1.2 存在的问题

1、bitmap最大1024位,一个进程最多只能处理1024个客户端2、&rset不可重用,每次socket有数据就相应的位会被置位3、文件描述符数组拷贝到了内核态,仍然有开销4、select并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历

2、poll

2.1 代码例子

在poll中,文件描述符有一份独立的数据结构pollfd,传入poll中的是pollfd的数组,其他的实现逻辑和select一样

image.png

2.2 优点

1、poll使用pollfd数组来代替select中的bitmap,数组没有1024的限制,可以一次管理更多的client2、当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回0,实现了pollfd数组的重用

2.3 缺点

poll 解决了select缺点中的前两条,其本质原理还是select的方法,还存在select中原来的问题

1、pollfds数组拷贝到了内核态,仍然有开销2、poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历

3、epoll

3.1 代码例子

image.png

3.2 事件通知机制

1、当有网卡上有数据到达了,首先会放到DMA(内存中的一个buffer,网卡可以直接访问这个数据区域)中2、网卡向cpu发起中断,让cpu先处理网卡的事3、中断号在内存中会绑定一个回调,哪个socket中有数据,回调函数就把哪个socket放入就绪链表中

3.3 详细过程

首先epoll_create创建epoll实例,它会创建所需要的红黑树,以及就绪链表,以及代表epoll实例的文件句柄,其实就是在内核开辟一块内存空间,所有与服务器连接的socket都会放到这块空间中,这些socket以红黑树的形式存在,同时还会有一块空间存放就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。

3.4 水平触发和边沿触发

Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!

3.5 优点

epoll是现在最先进的IO多路复用器,Redis、Nginx,linux中的Java NIO都使用的是epoll

1、一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小2、使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket

Linux、C/C++技术交流群 整理了一些个人觉得比较好Linux服务器架构师学习书籍、大厂面试题、和热门技术教学视频资料(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享有需要的可以自行添加哦!

image.png

以上不足的地方欢迎指出讨论,觉得不错的朋友,希望能得到您的点赞支持


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:诗人和酒

查看原文:Linux高并发服务器开发---从网络IO到IO多路复用

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

781 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传