多路I/O复用模型

socket

1.socket,也就是我们所说的套接字,通俗来讲,就是当客户端给我们发送数据的时候需要一个入口,而服务端接受数据的时候需要一个出口,而这两个“口子”就是socket,类似两个人打电话,电话其实就类似于socket

2.linux分为用户空间(用户态)和内核空间(内核态),用户空间只能执行比较安全一点的命令,像socket都是基于内核空间来使用,read,write都是socket封装的系统调用函数,当我们使用这些函数的时候,系统会切换到内核态,由内核来完成这些操作
3.当我们在用户空间调用read函数时,数据会先从网卡拷贝到内核空间,也就是socket缓存区,然后在拷贝到用户空间。write刚好相反,会先从用户空间拷贝到内核空间,再由内核空间拷贝到网卡

fd

在linux中,一切皆文件,一切资源都可以通过文件来访问和管理。而fd,就是我们说的文件描述符,非负整数,类似于资源文件的索引(符号),指向某个资源,内核利用fd来访问和管理

同步阻塞模型:

当我们客户端发起一个socket连接时,这个时候循环去执行这个socket,等待客户端的连接,如果客户端存在多个的话,就会就行阻塞,当处理完一个客户端请求,socket才能继续往下执行,处理其他客户端请求
优点:单线程,资源占用率小
缺点:请求多时,会发生阻塞,进行等待,服务端会一直得不到响应

同步非阻塞模型:

1.当客户端没有和服务端发生请求的时候,服务端会不断的进行socket连接请求,返回一个不正常的fd,直到服务端与客户端发生了请求,这个时候才会返回一个正常的fd
2.和同步阻塞区别在于,像accept,read这种系统函数调用的时候,同步非阻塞是不会阻塞住的,当数据没有到来之前,同步非阻塞也不会进行等待,而是返回一个不正常的值,直到请求到客户端发送的数据,返回一个正常的fd
优点:减少了进程间的阻塞
缺点:加大了系统的开销,需要不断进行进程间的切换,在用户态和内核态不断的进行操作

io多路复用三种模式

select

nfds 3个监听的文件描述符最大值 + 1
readfds要监听的可读文件描述符集合
writefds 要监听的可写文件描述符集合
writefds 要监听的异常文件描述符集合
timeval  本次调用的超时时间 0:超时 ,小于:出错 ,大于: 已就绪的文件描述符数量
//获取就绪事件
int selectint nfds fd_set *readfds fd_set *writefds fd_set *exceptfds struct timeval * timeout )
fd_set 在入参和出参表达的意思是不一样的,在入参的时候表示监听哪些fd,在出参的时候监听哪些fd准备就绪,底层是通过位图来实现的,也就是0和1
流程:
假如服务端监听了4个socket,然后拿到4个socket对应的fd,
用户态去调用socket,首先会将4个fd拷贝到内核态,
内核态会对这4个fd进行遍历,
如果发现有对应的socket发送来数据,代表这个socket对应的fd已就绪,就会对当前这个fd打上一个标签,并且返回一个整数值,有几个fd就绪,就返回几
用户态拿到这些返回值并不能区分是哪个fd就绪,会对这些fd进行遍历O(N)次,找到已经就绪的fd
如果遍历一次fd集合,发现没有fd就绪
select会将一个当前的一个用户进程阻塞起来,当客户端像服务端发送数据的时候,会将这个数据保存到网卡,网卡通过DMA的方式将这个数据包保存到指定的内存中,然后会通过中断信号告诉cpu有指定的数据包到达,cpu会响应中断信号进行处理。
首先会根据数据包的ip和端口号找到对应的socket,然后把数据保存到socket的一个接收队列,然后再检查这个等待队列里面是否有进程进行阻塞等待,如果有的话,就会唤醒该进程,重新检查一遍fd集合是否有fd准备就绪,如果有的话就返回,重复这个流程
优点:不需要每个fd都需要进行一次用户态和内核态的切换
缺点:单进程监听的fd默认1024
        每次返回fd不能准确的知道是哪个,需要进行遍历操作
        fd_set每次调用完需要重置

poll

数据结构进行了一定的优化,使用两个变量来存储监听fd和就绪fd,不用每次执行完重置fd_set ,底层使用了链表,额没有1024的限制。其余执行流程和select基本类似
优点:不需要每个fd都需要进行一次用户态和内核态的切换
缺点:每次返回fd不能准确的知道是哪个,需要进行遍历操作

epoll

对应三个函数

创建epoll
//size 要监听的文件描述符数量
//epoll 返回的文件描述符
int epoll_create(int size)

epoll_create: 这个函数会返回一个int,代表我们创建的一个对应的文件描述符(fd),后面就可以通过返回的这个fd来操作epoll

事件注册
//epfd 返回的文件描述符 epoll_create 返回
//op 操作类型 1新增 2删除 3更新
//fd 本次要操作的文件描述符
//epoll_event 要监听的事件 读事件 写事件等
//retutn 调用成功返回0 不成功返回-1
int epoll_ctl(epfd,op,fd,epoll_event)

获取就绪事件
//epfd 返回的文件描述符 epoll_create 返回
//events 用于回传就绪事件
//maxevents 每次能处理的最大事件数
//timeout 等待I/O的超时时间 -1阻塞  0非阻塞
//return 大于0:已就绪的文件描述符数  等于0:超时 小于0:出错
int epoll_wait(epfd,events, maxevents,timeout )
首先通过epoll_create创建出来一个epoll
epoll在底层的数据结构对应的是eventpoll,这个数据结构主要有三个参数
wq   等待队列  (当我们没事件就绪,这个时候会有进程进行阻塞,会将我们这个进程关联到等待队列里面,以便我们后续有进程到来,可以去唤醒这个进程)
rdllist  就绪队列(用来存储我们有哪些事件已经就绪,可以很方便的知道)
rbr   红黑树(可以通过红黑树的这种方式,把我们需要操作的文件存储起来,进行高效的增删改查)


当我们调用epoll_ctl的时候,会将fd拷贝到内核态,并且将它封装成epitem这种数据空间
里面除了有关联eventpoll的值外,还会有个很重要的参数,等待队列(ep_poll_callback)

当我们调用epoll_wait的时候,会先去看就绪队列是否有以及就绪的数据,如果没有就会通知cpu阻塞当前进程,放到等待队列,让出cpu给其他的进程
流程:
假设客户端有数据发送过来,会通过网络传输到网卡上,网卡会通过DMA写到我们指定的内存里面,会通过给cpu发送个中断信号,cpu会响应这个中断操作,会根据ip加端口找到对应的socket,然后socktet会将这个数据放到一个接收队列里面去,然后会调用ep_poll_callback这个回到函数,这个回调函数会将我们当前的这个数据添加到就绪队列里面去,会唤醒等待队列,唤醒成功后接着回去判断就绪列表是够有就绪事件,如果有就直接返回用户空间

 

posted @ 2022-07-12 13:38  -韩  阅读(217)  评论(0编辑  收藏  举报