目录
Socket
建立Socket通信
- 通过建立Socket通信的流程图可以知道会有两个地方容易发生阻塞,即建立连接的时候以及客户端和服务器端相互传输数据的时候
- EOF:是一个计算机术语,为End Of File的缩写,在操作系统中表示资料源无更多的资料可读取。资料源通常称为档案或串流。通常在文本的最后存在此字符表示资料结束
系统相关
FD:文件描述符
- Linux 服务端创建一个Socket,同时也会创建一个FD指向代表该Socket的文件
用户态、内核态
- 用户态和内核态:当进程运行在用户空间时,就处于用户态运行,否则处于内核态
- 用户空间只能执行一些相对安全的CPU指令
- 内核空间用于执行一些特权CPU指令
- 用户态与内核态的转化:比如涉及到一些Socket操作,都是在内核空间中完成的,用户空间主要通过内核空间提供的诸如read、write的系统调用函数调用内核空间完成Socket操作。比如执行read系统调用函数,此时就会将涉及到的fd内容从用户空间拷贝到内核空间、进程从用户空间切换到内核空间,这就是用户态切换为内核态。当内核空间完成socket操作后,将数据会先从网卡拷贝到内核空间(Socket缓冲区),再作为返回值从内核空间拷贝到用户空间、进程也会从内核空间切换到用户空间,这就是内核态切换为用户态
Unix 网络编程中的五种IO模型
阻塞与非阻塞,同步与异步
- 阻塞:用户进程/线程,调用内核进程,不能马上得到结果,而导致用户进程进入等待队列
- 非阻塞:用户进程,调用内核进程,能马上得到一个结果,用户进程继续运行
- 同步:用户进程,调用内核进程,用户进程等待调用结果
- 异步:用户进程,调用内核进程,用户进程不等待调用结果
- 阻塞、非阻塞 都是同步的,因为这两种调用方式都需要等待调用结果
BIO
NIO
IO多路复用
- select、poll、epoll 的执行都是在内核中执行的,用户进程只是发起对这三个函数的调用
select
- nfds:告诉内核空间三个文件描述符集合中最大的文件描述符最大值,明确告知内核空间需要检查的fd有多少
- timeval:超时时间
- 如果为0,则表示内核空间不等待,直接判断是否有正常的数值的fd并返回
- 如果超时时间设置为-1,则表示需要内核空间一直等待,直到有正常数值的fd才返回
- 用户空间调用select函数,首先会将所有需要监听的fd集合从用户空间拷贝一份到内核空间,然后内核空间轮询所有fd集合中所有fd,返回就绪的fd数量。但是用户空间并不知晓fd集合中哪个fd是就绪的,所以此时用户空间还需要检查fd集合中所有fd,检查到以后对就绪的fd进行后续处理,然后发起下一次的select调用
- 如果内核进程轮询所有fd均没有已经就绪的,此时内核进程会有两种选择:
- 1.继续下一次轮询遍历,直到有准备就绪的fd,这种办法会占用大量的CPU
- 2.让该用户进程阻塞(目前的做法),直到网卡接收到数据包并将该数包据通过DMA的方式写入到指定的内存中,然后会通过中断信号告诉CPU,CPU响应中断请求调用中断的处理程序进行后续处理:
- 首先根据数据包的IP和端口号找到对应的socket,然后将该数据保存到该socket的接收队列
- 然后检查该socket对应的等待队列中是否有阻塞进程等待,如果有的话则会唤醒该进程,然后内核空间会检查这个fd集合,返回准备就绪的fd数量到用户空间结束select的阻塞
fd_set fd集合
- 内核进程通过 select 函数仅返回了准备就绪的fd数量,需要用户进程自己遍历fd集合,所以就需要改变fd集合中对应监听的fd的状态告知用户空间放到集合中哪些fd就绪了
- 如上图所示,fd_set为一个16长度的long类型数组,是通过 bitmap 来表示的,最多有1024位,即最多一次对1024个fd进行监听,调用select函数前后表达不同的含义,调用select前fd集合的位图可以看出来本次监听的fd是1和3,调用select函数后可以得知只有为3的fd准备就绪了
select总结
- 1024的限制可以被更该,但是需要重新编译Linux系统
- 每次调用select函数前后,还是需要用户态与内核态的切换
- fd集合在select函数调用前后表示的含义不同,所以每次调用select函数都需要将fd集合重置成我们需要监听的fd
- 用户进程还是会阻塞,只是阻塞都集中在一个对象上–>select 函数
poll
- poll新定义了一种数据结构pollfd,通过pollfd中的events和revents判断fd是否准备就绪,而且需要监听的fd集合在用户空间时是由pollfd组成的数组,但是拷贝到内核空间后是由pollfd组成的链表
poll总结
- 与select相比,通过这种新定义的数据结构作为fd集合这样就不受限于1024位,也不再是同一个bit位表示fd是否就绪,也不用每次函数调用前都需要重置fd集合,然后与select就没有其他差别(用户进程还是会阻塞,只是阻塞都集中在一个对象上–>poll 函数)
epoll
epoll_create、epoll_ctl
- epoll_ctl函数主要是操作epoll_create生成的epoll文件描述符所指向的在内核中开辟的空间
- 操作种类(op)有三种:
- 1.向这个开辟的内核空间中新增fd
- 2.从这个开辟的内核空间中删除fd
- 3.更新这个开辟的内核空间中的fd
- epoll_ctl函数对这个开辟的内核空间中的fd具体的操作类型:1.监听读、2.监听写
-
epoll_wait
- epoll_wait函数和select/poll函数类似,都是用于给用户进程调用内核进程完成对fd的监听,并得到监听的结果
- maxevents表示:用户空间调用该函数时,如果内核空间返回100个就绪的fd,但是用户空间最多只能处理其中的n个,这个n就是用户空间此次需要监听并得到结果的最大fd数
epoll流程
调用epoll_create
- rbr:将用户空间指明需要监听的fd通过红黑树的形式存储
- rdllist:存储来自rbr中已经就绪的fd
- wq:如果用户进程指明需要监听的所有fd均没有准备就绪,此时该用户进程会阻塞,存放到这个等待队列,等待唤醒
调用epoll_ctl
- 此时用户进程调用epoll_ctl函数,将需要监听的fd作为参数,告诉内核进程将该fd添加到rbr指向的红黑树,并且会把这个fd封装成数据结构epitem
- epitem中除了存储需要监听的单个fd以外还有其他属性,rbn指明了该fd在红黑树中的位置、ffd指明了fd就绪时需要存放的rdllist、ep指明了该fd属于具体的哪个eventpoll对象(epoll文件)、pwqlist是一个等待队列指明了该fd就绪时需要调用的ep_poll_callback回调函数,唤醒wq中对应的用户进程
调用epoll_wait
- 调用epoll_wait函数,检查eventpoll对象的就绪队列中是否有就绪的fd,如果没有就绪的fd,则此时用户进程进入阻塞,存放到wq中等待唤醒。否则返回就绪的一个或多个fd,数量不大于maxevents
- 被唤醒的流程:网卡接收到数据包并将该数包据通过DMA的方式写入到指定的内存中,然后会通过中断信号告诉CPU,CPU响应中断请求调用中断的处理程序进行后续处理:首先根据数据包的IP和端口号找到对应的socket,然后将该数据保存到该socket的接收队列,然后通过对应的epitem中的pwqlist中的回调函数将epitem添加到就绪队列rdllist,最后唤醒对应的用户进程继续执行epoll_wait函数
epoll总结
- 用户进程想要监听的fd,不再是直接从用户空间拷贝到内核空间,而是通过调用epoll_ctl函数,直接让内核空间将该fd添加并维护到eventpoll对象的rbr指向的红黑树中
水平触发与边缘触发
- 水平触发牺牲了一定性能但更注重安全
- 边缘触发牺牲了安全性更注重性能