IO/多路复用(select/poll/epoll)

目录

一、高级IO

二、五种基本的IO模型

三、同步通信 VS 异步通信

四、阻塞 VS 非阻塞

五、IO多路转接

0.最基本的Socket模型

UDP的Sock编程过程:

TCP的编程流程

1.为什么需要IO多路转接

1.1多进程模型

1.2多线程模型

1.3 IO多路复用

2.IO多路转接之select

3.IO多路转接之poll

4.IO多路转接之epoll


一、高级IO

IO的过程是在内核中的过程,是input & output

IO过程分成两步:等待和拷贝数据

二、五种基本的IO模型

1.阻塞IO:在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式

2.非阻塞IO:如果内核还未准备好数据,操作系统仍会直接返回,并返回EWOULDBLOCK的错误码

注意:非阻塞IO需要程序员手动的搭配循环的方式判断资源的就绪情况,这对CPU是一种消耗

3.信号驱动IO:内核将数据准备好的时候,通过信号SIGIO来通知应用进程进行IO操作

4.IO多路转接:IO多路转接能够同时等待多个文件描述符的就绪状态

5.异步IO:由内核将数据拷贝完成时,通知应用进程。

总结:任何IO过程中,都包含两个步骤。一个是等待,一个是拷贝。在实际的应用场景中,等待消耗的时间往往都远远高于拷贝时间,让IO更高效,最核心的办法就是让等待的时间尽量少。

三、同步通信 VS 异步通信

同步和异步关心的是消息通信机制

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.

四、阻塞 VS 非阻塞

阻塞和非阻塞关注的是程序在等待调用结果时的状态

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

五、IO多路转接

0.最基本的Socket模型

众所周知,要想客户端和服务器能在网络中通信,那必须得使用Socket编程,它是可以跨主机通信的通信方式

UDP的Sock编程过程:

服务端:创建套接字,绑定地址信息,收发数据

客户端:创建套接字,不绑定地址信息(也可以绑定),收发数据,关闭套接字

创建套接字的含义:

将进程和网卡进行绑定,进程可以从网卡当中接收数据,也可以通过网卡发送数据

绑定地址信息的含义:绑定ip,绑定端口,是为了在网络中可以标识出来一台主机和一个进程

TCP的编程流程

服务端:创建套接字,绑定地址信息,监听,获取新连接,收发数据,关闭套接字

客户端:创建套接字,不绑定地址信息(可以绑定),发起连接,收发数据,关闭连接

监听的含义:监听tcp客户端新的连接,桶客户端建立tcp连接。注意:这个时候,TCP连接建立在内核当中就完成了

获取新连接的含义:获取新连接的套接字描述符,每一个TCP连接会产生一个新的套接字描述符

发起连接的含义:向服务端发起TCP连接

1.为什么需要IO多路转接

前面提到的UDP/TCP Socket流程只能一对一通信,因为使用的时同步阻塞的方式,在服务端还没有处理完一个客户端的网络IO时,或者读写的操作发生阻塞时,其它客户端是无法于服务端连接的。

服务端只一个客户端服务,这想想都浪费。所以我们需要对其进行改进。改进的方式有以下几种

1.1多进程模型

多线程也就是为每个客户端分配一个进程来处理请求。

服务器的主进程负责监听客户的连接,一旦与客户端连接完成,accept() 函数就会返回一个「已连接 Socket」,这时就通过 fork() 函数创建一个子进程,实际上就把父进程所有相关的东西都复制一份,包括文件描述符、内存地址空间、程序计数器、执行的代码等。

因为子进程会复制父进程的文件描述符,于是就可以直接使用已连接的Socket和客户端进行通信了,可以发现,子进程不需要关心[监听Socket],只需要关心[已连接Socket];父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心[已连接Socket],只需要关心[监听Socket]。

因为进程占用的系统资源相对较大,而且进程间切换的消耗是很大的,对性能上就会有相应的影响。

进程间的切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

1.2多线程模型

既然进程间切换的消耗很大,所以我们就想到了多线程模型

线程只是进程中的一个执行流,一个进程里的多个线程可以共享进程的部分资源,比如文件描述符列表,进程空间,代码,全局数据,堆,共享库等,这些共享资源在进程切换的时候是不需要进行切换的,而只需要切换线程的私有数据、寄存器等不共享的数据,因此相对于进程来说线程的开销要小的多。

当服务器与客户端 TCP 完成连接后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。

主线程只负责监听,当存在[已连接的Socket]的文件描述符的时候就创建一个线程与客户端建立连接。

如果每来一个连接就创建一个线程,线程运行完后,还得操作系统销毁线程,频繁的创建和销毁线程,系统开销也是很大的。

所以就有了线程池的存在。

我们可以使用线程池的方式避免线程的频繁创建和销毁,所谓的线程池,就是提前创若干个线程,这样有新连接建立的时候,将这个已连接的Socket放入一个队列里,然后线程池里的线程负责从队列中取出已连接的Socket进程处理。

注意:这个队列是全局的,每个线程都会操作,为了避免多线程竞争,线程在操作这个队列前要加锁。

其实尽管多线程已经减少了对资源的需求量了,但是当连接数量较大的时候,所消耗的资源仍然是一个不小的数字。

1.3 IO多路复用

对于上面的这种情况,我们只能想办法让一个进程维护多个Socket这样就可以有效的减少对资源的需求了。

一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时如果控制的较少(1毫秒以内),那么一秒内就可以处理上千个请求,多个请求复用一个进程,这就是多路复用。

select/poll/epoll内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核获取多个事件。

2.IO多路转接之select

优点:

  • 遵循posix标准,可以跨平台使用,可以在win平台使用,也可以在linux平台使用
  • select的超时时间可以精确到微秒

缺点:

  • select监控文件描述符的时候,采用轮询遍历的方式,随着监控的文件描述符越多,监控(轮询)效率越低
  • select监控文件描述符的个数是由上限的,上限取决于内核当中的宏_FD_SESIZE,这个宏的值为1024
  • select在返回文件描述符的时候,会将就绪的文件描述符从事件集合当中移除掉,导致二次监控的时候程序员需要再次手动添加
  • 在返回就绪文件描述的时候,是返回了一个事件集合,并不是将就绪的文件描述符数值直接返回给调用者,需要调用者使用FD_ISSET函数进行判断哪些文件描述符就绪了

3.IO多路转接之poll

优点:

  • 提出了事件结构的方式,在给poll函数传递参数的时候,不需要分别添加到”事件集合“当中。
  • 事件结构的大小可以根据程序员自己进行定义,并没有上限的要求。
  • 不用再监控到就绪后,重新添加文件描述符

缺点:

  • 不支持跨平台
  • 内核也是对事件结构数组监控的时候采用轮询遍历的方式

4.IO多路转接之epoll

4.1 epoll解决了select和poll的缺陷,被公认为Linux2.6下性能最好的多路I/O就绪通知方法

第一点:epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。

第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

epoll 的方式即使监听的 Socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了,上限就为系统定义的进程打开的最大文件描述符个数。

4.2 epoll的工作模式

LT:水平触发:只要达到触发事件的条件,则一定乐此不疲的触发(当读就绪/写就绪的时候,则一定乐此不疲的通知)

ET:边缘触发:当达到触发事件的条件后,只会触发一次(当读就绪/写就绪的时候,只会 通知一次),边缘触发模式一般和非阻塞 I/O 搭配使用。

注意:边缘触发消耗的资源少,但只会触发一次,所以需要程序员一次将数据全部读回来

          水平触发,当读就绪/写就绪的时候会一直触发,不需要一次性将数据全部读回来,但是会一直占用CPU资源。

猜你喜欢

转载自blog.csdn.net/qq_57822158/article/details/126067524