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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容