网络编程常识
1、IO多路复用:select、poll、epoll的区别(非常重要,几乎必问,回答得越底层越好,要会使用)
select:select(int maxfdp1 ,fd_set *restrict readfds , fd_set *restrict writefds ,fd_set *restrict exceptfds , struct timeval *restrict tvptr);
第一个参数是记录最大文件描述符+1,最大是指在中间三个参数中最大
中间三个参数是文件描述符集,发生可读、可写、报错三种事件的文件描述符会存在描述符集中等待处理。
最后一个是等待时间
单个可监听最大事件数为1024个(64位为2048),扫描是线性轮询,每次都要扫描所有的fd,会浪费很多时间,并且会维护一个存储fd的空间,在内核与用户空间传递数据时开销很大
当有I/O事件发生,select会轮询所有流,找出发生事件的描述符。所以时间复杂度为O(n)
poll:与select不同,poll并未按照事件构造一个描述符集,而是构建一个描述符结构体,向每个结构体中表面文件描述符以及我们对该文件描述符感兴趣的事件。当该描述符发生我们关注的事件后就会返回并说明事件类型。
基于链表存储的poll没有最大监听限制,但是每次还是将fd数组放进内核进行轮询处理,与select没有本质区别,O(n)。而且无论fd是否活跃都将其传入同样浪费空间,poll是水平触发的,如果一个fd被报告后没有做处理,下次还会报告
epoll:epoll可以理解为event poll,它与前两者有本质的区别。epoll会具体的把某个流发生了怎样的事件告知我们,不是轮询所有文件描述符。所以此时我们对流做的都是有效的。O(1)。
epoll可以选择LT(默认)和ET(高速/边缘触发)模式,当处于默认模式下,如果fd中有数据没有被一次读完,那么就一直会报告;ET模式下只报告一次,所以ET模式下需要一次读完。ET模式是为了防止有大量不需要读写的fd存在,降低效率。
epoll有监听上限,但是很大,跟内存有关;使用活跃fd调用回调函数的方式解决了轮询的弊端,效率提升;使用了mmap加速传递。
总结:
能监听的总数上,select 1024,poll没限制,epoll有,但是很大
fd激增带来的问题:select和poll性能线性下降,epoll在socket活跃少的时候不会下降,但所有链接都同时活跃也会性能下降。
数据传递:select和poll通过内核拷贝,epoll内核空间与用户空间共享内存。
底层实现:
select&poll:
1)使用copy_from_user将fd_set拷贝到内核
2)注册回调函数_pollwait
3)遍历所有fd,调用对应的poll方法(socket就是sock_poll,sock_poll也会根据tcp与udp的区别选择tcp_poll或udp_poll),以tcp_poll为例,其主要实现就是注册的回调函数_pollwait;
_pollwait的主要工作就是将当前进程(current)挂到等待序列中(不同设备有不同序列,tcp就是sk->sk_sleep),需注意的是挂入等待序列并不一定休眠;当完成一次socket缓存到内核缓存的拷贝之后就会唤醒序列中已睡眠的进程。当方法返回时就会返回一个描述读写是否就绪的mask掩码,用来给fd_set赋值
4)如果遍历完所有的fd还没有返回一个可读写的mask掩码,就会调用schedule_timeout将当前进程休眠。在休眠过程中如果完成拷贝就会被唤醒。如果一直没有被唤醒,等待时间超过timeout后cpu会强制将其唤醒,这时会重新遍历所有fd,查看有没有准备就绪的。
5)结束等待,将fd_set拷贝回用户空间
poll与select类似,只不过fd_set传入改为poll结构体
epoll:
epoll是上述两种方法的改进,克服了拷贝空间多、遍历两种缺点
针对第一个缺点,epoll_tcl在注册新事件的时候就将新的fd拷入内核,所以fd只用拷贝一次。
针对第二个缺点,epoll不使用轮询的方法,而是只将进程挂起一次,为每个fd都指定一个回调函数,当有fd读取就绪时,调用回调函数将fd放入一个链表中。而epoll_wait的实际工作就是使用schedule_timeout睡一会,然后检查这个链表:类似select中第4步;
2、手撕一个最简单的server端服务器(socket、bind、listen、accept这四个API一定要非常熟练)
待完成
3、线程池
线程安全的非阻塞队列
4、基于事件驱动的reactor模式和Preactor模式
对于Preactor和Reactor,一句话就可以概括,reactor是异步阻塞IO,Preactor是异步非阻塞IO;
reactor:1)注册文件描述符与读/写就绪事件以及事件对应的处理器
2)等待事件到来
3)事件到来后完成第一步
4)读取数据进行处理
Preactor:1)注册要等待的文件描述符与等待的事件以及事件对应的处理器,与前者不同的是,Preactor不关心读/写就绪事件,只关心读取完成事件。
2)等待事件到来
3)等待的同时,将读就绪的事件拷贝到用户空间
4)事件分离器分离事件,对于读取完成事件可以直接处理已经拷贝完成的数据,不需要再读取
可以看到,前者读取靠用户,后者完全依靠内核
5、边沿触发与水平触发的区别
边沿触发只触发一次,水平触发如果没有处理(干净)就一直触发。
6、非阻塞IO与阻塞IO区别
同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
同步与异步,是用户端选择的与内核交互的方式,而阻塞与非阻塞只是函数实现的方式。同步是主线程在内核完成读取操作时专心处理“读取”这一项工作。异步就是“不专心”,做其他工作;阻塞是完成读取操作时主线程堵塞,非阻塞则是不将其阻塞。
以读取socket数据为例
同步阻塞:使用read读取相应端口,内核会先将socket拷贝到内核空间再拷贝到用户空间,在这个过程中主线程全程阻塞,直到完成读取。
同步非阻塞:使用read读取,此时因为函数为非阻塞,那么主线程就可以自由选择工作,但是因为是同步处理,此时主线程需要不断询问内核是否完成读取。
异步非阻塞:使用read读取,此时主线程可以做其他工作,直到内核完成读取,内核会发送一个读取完成的信号,直到接到这个信号,主线程才会回来处理数据。
异步阻塞:当主线程处于阻塞状态,也就无所谓同步异步了,只能同步等待。但是为什么说epoll是异步阻塞IO呢?因为epoll_wait函数是阻塞函数,会等待数据从socket拷贝到内核,在这个拷贝的过程中主线程没有“专心读取”,而只是等待,所以既是阻塞也是异步。
实际上只有IO有四种,上述阻塞与非阻塞,还有多路IO(IO复用)与异步IO
IO复用:poll select epoll都符合这个思想:当有数据传入时,先让内核拷贝,完成后通知主线程,主线程将其拷贝到用户空间进行处理
异步IO:类似IO复用,只不过即使内核拷贝完成了也不通知主线程,知道彻底完成拷贝任务,数据已经完整拷贝到用户空间了,亟待处理时,才会让主线程处理数据。