Skip to content

Latest commit

 

History

History
161 lines (84 loc) · 11.9 KB

project.md

File metadata and controls

161 lines (84 loc) · 11.9 KB

项目

什么是 reactor 模式,与proactor的区别?

在传统thread-per-connection用法中,每个线程负责一个对外的连接,线程在真正处理请求之前需要从套接字中读取请求,在读取完成之前线程会一直处于阻塞状态,这会导致线程资源被占用,服务器效率将大大降低,尤其是处理高并发请求时。

reactor模式包括非阻塞IO+IO多路复用也就是事件驱动型IO,它的程序结构一般是一个主线程+IO线程,一般可以搭配one-loop-per-thread,也就是创建多个IO线程,以提高服务器在高并发下的吞吐量和响应效率。

IO线程的主要结构是一个event loop,用于循环地处理读写事件和定时事件;

主线程负责使用IO多路复用的的系统调用,一般是poll和epoll,在IO多路复用的套接字上可以注册多个连接的套接字的读写事件或者定时事件以及相应的回调函数,也就是说一个线程可以负责一个或多个连接,一般最高到几千到几万,当注册的事件到达就唤醒相应的IO线程做网络IO的工作。

前者使用同步IO的IO多路复用,后者使用异步IO。即主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.

reactor 模式的优点?

编程不难、效率不错;

不仅可以用于读写 socket、连接的建立, 甚至 DNS 解析都可以用非阻塞方式进行, 以提高并发度和吞吐量, 对于 IO 密集型应用是个不错的选择.

基于事件驱动的编程模型的缺点?

它要求事件回调函数必须是非阻塞的, 对于涉及网络 IO 的请求响应式协议, 它容易割裂业务逻辑, 使其散布于多个回调函数之中, 相对不容易理解与维护.

one loop(或者说 reactor) per thread 优点?

1、线程数目基本固定, 可以在程序启动的时候设置, 不会频繁创建与销毁;

2、可以很方便地在线程间调配负载;

3、IO 事件发生的线程是固定的, 同一个 TCP 连接不必考虑事件并发.

阻塞与非阻塞,同步与异步?

广义上:

调用者必须循环自去查看事件有没有发生,这种情况是同步;

调用者不用自己去查看事件有没有发生,而是等待着注册在事件上的回调函数通知自己,这种情况是异步

调用者在事件没有发生的时候,一直在等待事件发生,不能去处理别的任务这是阻塞;

调用者在事件没有发生的时候,可以去处理别的任务这是非阻塞

针对于IO来说:

同步和异步是针对应用程序和内核的交互而言的;

同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,

异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。

阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式;

阻塞方式下读取或者写入函数将一直等待,

非阻塞方式下,读取或者写入函数会立即返回一个状态值。

什么是 IO 多路复用/事件驱动型 IO,优缺点,适用场景?

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)

  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

在IO多路复用中,kernel会“监视”所有select/poll/epoll负责的socket,当任何一个socket中的数据准备好了,select/poll就会返回,epoll就会执行fd对应的回调函数。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()/poll()/epoll_wait()函数就可以返回。

IO多路复用的好处就在于单个进程/线程就可以同时处理多个网络连接的IO。

当用户进程调用了select,那么整个进程会被block。在IO multiplexing Model中,对于每一个socket,一般都设置成为non-blocking。但是整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,这对于连接数量比较大的服务器来说根本不能满足。可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。或者也可以选择多进程的解决方案( Apache就是这样实现的)。

由于select/poll同样是全程阻塞,并且需要使用两个系统调用(select/poll + recvfrom或其他),而阻塞IO只需要一个系统调用(recvform),所以select/poll不一定比多线程阻塞IO性能更好,比如当连接的数量不是很大的话,前者延迟可能更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

不同于select的fd_set,poll的pollfd并没有最大数量限制。

select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。

epoll_create()的参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。

epoll优点在于:

1、监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2、IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。

epoll特别适用于连接数量多,但活动连接较少的情况。如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。

文件描述符数量限制?

//ubuntu18.04 虚拟机
cat /proc/sys/fs/file-max //结果95107   系统的文件描述符上限(是所有用户上限的总和)
ulimit -n //结果1024        soft limit  单个用户的文件描述符上限
ulimit -Hn //结果1048756    hard limit

单进程的hard limit超过了系统上限,这个原因不知道,不过实际上产生进程限制效果的是soft limit,只要注意这个不要修改过头应该就好了。

select函数本身限制,主要是头文件中FD_SETSIZE的大小,一般来说是1024,这就限定了select函数中的文件描述符上限,当然可以做修改,但是需要重新编译内核,而且效果由于select的实现机制,会比较差。由于进程的上限会先起作用,所以最后收到限制的原因就是:①进程文件描述符上限;②select中FD_SETSIZE大小

epoll描述符限制的原因就是进程限制和系统限制,在修改这两个限制之后,epoll的连接数就取决于内存大小了

水平触发与边缘触发的区别?有什么问题?

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。同时支持block和no-block socket

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait时,不会再次响应应用程序并通知此事件。只支持no-block socket

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

当epoll工作在ET模式下时,对于读操作,如果read一次没有读尽buffer中的数据,那么下次将得不到读就绪的通知,造成buffer中已有的数据无机会读出,除非有新的数据再次到达。对于写操作,主要是因为ET模式下fd通常为非阻塞造成的一个问题——如何保证将用户要求写的数据写完。要解决上述两个ET模式下的读写问题,我们必须实现:

a. 对于读,只要buffer中还有数据就一直读;

b.对于写,只要buffer还有空间且用户请求写的数据还未写完,就一直写

在边缘触发的时候要把套接字中的数据读干净,那么当有多个套接字时,在读的套接字一直不停的有数据到达,如何保证其他套接字不被饿死?

使用Linux epoll模型水平触发模式,当socket可写时,会不停的触发 socket 可写的事件,如何处理?

第一种最普遍的方式:

需要向 socket 写数据的时候才把 socket 加入 epoll ,等待可写事件。

接受到可写事件后,调用 write 或者 send 发送数据。。。

当所有数据都写完后,把 socket 移出 epoll。

这种方式的缺点是,即使发送很少的数据,也要把 socket 加入 epoll,写完后在移出 epoll,有一定操作代价。

一种改进的方式: 开始不把 socket 加入 epoll,需要向 socket 写数据的时候,直接调用 write 或者 send 发送数据。

如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驱动下写数据,全部数据发送完毕后,再移出 epoll。

这种方式的优点是:数据不多的时候可以避免 epoll 的事件处理,提高效率。

Linux中的EAGAIN含义

Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。

从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。

例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。

又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。

如何维护客户端ID增长?

项目中遇到什么问题?如何解决的?心得体会?

项目后续还可怎样优化或改进