accept和epoll惊群问题剖析

更新时间:2023-07-19 09:35:20 阅读: 评论:0

accept和epoll惊群问题剖析
⽂章⽬录
惊群问题
1. 不使⽤epoll/lect的情况下多进程是如何共享端⼝监听的?
不使⽤多路复⽤的情况,进程要接收TCP连接必然要调⽤accept并且被阻塞,直到有⼀条连接到达。
单进程:⼀次只能处理⼀个连接,业务处理完毕之后clo掉客户端连接,然后再调⽤accept。
多进程⼀:⼀个主进程accept连接,来了⼀个连接再fork⼀个⼦进程,把来的连接给⼦进程处理,主进程继续监听。这种模式只有⼀个进程监听,不涉及多进程争抢的问题,不会有惊群效应。经期可以吃阿胶糕吗
多进程⼆:主进程fork出⼀批⼦进程,⼦进程继承了⽗进程的监听端⼝,⼤家共享,⼀起监听,在Linux2.6版本之前,此时就会有accept惊群问题。
linux内核中关于accept部分的核⼼代码,Linux提供了accept4的系统调⽤,accept4最终将调⽤上述的inet_csk_accept,⽽
inet_csk_accept最终调⽤inet_csk_wait_for_connect,如果此时没有连接可⽤,内核会将当前进程挂起,其中的点在于挂起进程使⽤的是prepare_to_wait_exclusive这个函数,不存在多进程唤醒
// 进程调⽤accept时会进⼊inet_csk_accept,这是accept的核⼼所在
struct sock *inet_csk_accept(struct sock *sk,int flags,int*err)
{
struct inet_connection_sock *icsk =inet_csk(sk);
struct sock *newsk;
int error;
lock_sock(sk);
/* We need to make sure that this socket is listening,
* and that it has something pending.
*/
error =-EINVAL;
// 确认socket处于监听状态
if(sk->sk_state != TCP_LISTEN)
goto out_err;
/* Find already established connection */
/*接下来要找到⼀个建⽴好的连接*/
if(reqsk_queue_empty(&icsk->icsk_accept_queue)){// 如果sock的连接队列是空
long timeo =sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error =-EAGAIN;
if(!timeo)// 如果设置了⾮阻塞模式则直接返回,err是喜闻乐见的-EAGAIN
goto out_err;
// 如果处于阻塞模式,则进⼊inet_csk_wait_for_connect,进程将处于阻塞状态,直接到新到的连接唤醒
error =inet_csk_wait_for_connect(sk, timeo);
if(error)
goto out_err;
}
// 到这⾥,连接队列会有⾄少⼀条可⽤连接⽤到返回
newsk =reqsk_queue_get_child(&icsk->icsk_accept_queue, sk);
WARN_ON(newsk->sk_state == TCP_SYN_RECV);
out:
relea_sock(sk);
return newsk;
out_err:
newsk =NULL;
newsk =NULL;
*err = error;
goto out;
}
EXPORT_SYMBOL(inet_csk_accept);
// inet_csk_wait_for_connect会将进程挂起,直到被新到的连接唤醒
static int inet_csk_wait_for_connect(struct sock *sk,long timeo)
{
struct inet_connection_sock *icsk =inet_csk(sk);
DEFINE_WAIT(wait);// 定义⼀个等待节点,⽤于挂在socket监听队列下
int err;
for(;;){
斤斤计较的意思
// 使⽤prepare_to_wait_exclusive确认互斥等待,在⼀个事件到达后内核只会唤醒等待队列中的⼀个进程
prepare_to_wait_exclusive(sk_sleep(sk),&wait,
TASK_INTERRUPTIBLE);
瓮鸡relea_sock(sk);
if(reqsk_queue_empty(&icsk->icsk_accept_queue))
喜剧大师卓别林// 再⼀次判断队列是否空,空则进⼊调度,此时当前进程将被挂起
timeo =schedule_timeout(timeo);
lock_sock(sk);
err =0;
if(!reqsk_queue_empty(&icsk->icsk_accept_queue))
break;
err =-EINVAL;
蛇和锯子的故事
if(sk->sk_state != TCP_LISTEN)
break;
err =sock_intr_errno(timeo);
if(signal_pending(current))
break;小学科学
err =-EAGAIN;
if(!timeo)
break;
}iphone查找
finish_wait(sk_sleep(sk),&wait);
return err;
}
linux的内核对进程唤醒提供了两种模式,⼀种是prepare_to_wait,⼀种是prepare_to_wait_exclusive,exclusive即互斥,如果调⽤的是prepare_to_wait_exclusive,则在对⼀个等待队列进程唤醒的时候,只会唤醒⼀个进程,⽽prepare_to_wait没有设置互斥位,会将挂在这个等待队列上的所有进程全部唤醒
高质量发展的内涵综上可知,在普通的多进程共享监听端⼝的情况下,内核对⼀个新的连接事件的到达,只会唤醒其中⼀个进程
可直接看以上流程图,⽗进程创建的监听socket fd1由fork出来的两个⼦进程共享,这时候⼦进程的两个fd在内核中是属于同⼀个⽂件,被记录在open files table这个表中,接下来第4、5步,两个⼦进程同时调⽤accept进⾏阻塞监听,两个进程都会被挂起来,内核会在这个socket的等待队列wait queue链表中将两个PID记录下来以便唤醒;在第8步中⼀个连接事件到达,内核将对应socket下的等待队列取出来,对于tcp连接事件⽽⾔,内核对⼀个连接事件只会唤醒⼀个进程,取出wait queue链表的第⼀个节点,将对应的进程唤醒,此时PID1进程的accept成功取到连接并返回⽤户态,PID2没有被唤醒
其实在linux 2.6之前的版本中,accept也会全量唤醒wait queue中的所有进程,同样造成了惊群效应,在2.6中增加了互斥标志,修复了这个问题
accept惊群问题:
多个线程分别accept同⼀个socket,当事件来临时,唤醒所有的线程,但是只有⼀个线程会处理,其余线程得不到这个事件,只能⽩⽩被唤醒。Linux2.6版本之后引⼊了⼀个标记为WQ_FLAG_EXCLUSIVE解决了这种惊群效应。这个在内核就已经处理了。
2. epoll下共享监听端⼝的⾏为
与直接accept不同,epoll需要先调⽤epoll_create在内核中创建⼀个epoll⽂件。
接下来看如何把要监听的socket fd挂在epoll上,这个过程调⽤的是epoll_ctl,将fd向内核传递,内核实际上会做两个事情将fd挂在红⿊树中
调⽤⽂件设备驱动的poll回调指针(这是重点)
当⽤户调⽤epoll_ctl的添加事件的时候,在第6步中,epoll会把当前进程挂在fd的等待队列下,但是默认情况下这种挂载不会设置互斥标志,意思着当设备有事情产⽣进⾏等待队列唤醒的时候,如果当前队列有多个进程在等待,则会全部唤醒。
可想⽽知,在下⾯的epoll_wait调⽤中,如果多个进程将同⼀个fd添加到epoll中进⾏监听,当事件到达的时候,这些进程将被⼀起唤醒
但是唤醒并不⼀定会向⽤户态返回,因为唤醒之后epoll还要遍历⼀次就绪列表,确认有⾄少⼀个事件发⽣才会向⽤户态返回。
到此,我们可以想象出epoll是如何造成accept惊群的:
1. 当多个进程共享同⼀个监听端⼝并且都使⽤epoll进⾏多路复⽤的监听时,epoll将这些进程都挂在同⼀个等待队列下
2. 当事件产⽣时,socket的设备驱动都会尝试将等待队列的进程唤醒,但是由于挂载队列的时候使⽤的是epoll的挂载⽅式,没有设置互
斥标志(取代了accept⾃⼰挂载队列的⽅式,如第⼀节所述),所以这个队列下的所有进程将全部被唤醒
3. 唤醒之后此时这些进程还处于内核态,他们都会⽴刻检查事件就绪列表,确认是否有事件发⽣,对accept⽽⾔,accept->poll⽅法将
会检查在当前的socket的tcp全连接列表中是否有可⽤连接,如果是则返回可⽤事件标志
4. 当所有进程都被唤醒,但是还没有进⾏去真正做accept动作的时候,所有进⾏的事件检查都认为accept事件可⽤,所以这些进⾏都向
⽤户态返回
5. ⽤户态检查到有accept事件可⽤,这时他们将会真正调⽤accept函数进⾏连接的获取
6. 此时只会有⼀个进⾏能真正获取连接,其他进⾏都会返回EAGAIN错误,使⽤strace -p PID命令可以跟踪到这种错误
7. 并不是所有进程都会返回⽤户态,关键点在于这些被唤醒的进⾏在检查事件的过程中,如果已经有进程成功accept到连接了,这时别
的进程将不会检查到这个事情,从⽽他们会继续休眠,不会返回⽤户态。
8. 虽然不⼀定会返回⽤户态,但也造成了内核上下⽂切换的发⽣,其实也是惊群效应的表现
3. 内核对惊群效应的解决
根本原因在于epoll的默认⾏为是对于多进程监听同⼀⽂件不会设置互斥,进⽽将所有进程唤醒,后续的内核版本主要提供了两种解决⽅案
1. 既然默认不会设置互斥,那就加⼀个互斥功能好了: linux4.5内核之后给epoll添加了⼀个EPOLLEXCLUSIVE的标志位,如果设置了
这个标志位,那epoll将进程挂到等待队列时将会设置⼀下互斥标志位,这时实现跟内核原⽣accept⼀样的特性,只会唤醒队列中的⼀个进程
2. 第⼆种⽅法:linux
3.9内核之后给socket提供SO_REUSEPORT标志,这种⽅式解决得更彻底,他允许不同进程的socket绑定到同
⼀个端⼝,取代以往需要⼦进程共享socket监听的⽅式,这时候,每个进程的监听socket将指向open_file_tables下的不同节点,也就是说不同进程是在⾃⼰的设备等待队列下被挂起的,不存在共享fd的问题,也就不存在被同时唤醒的可能时,⽽内核则在驱动中将设置了SO_REUSEPORT并且绑定同⼀端⼝的这些socket分到同⼀个group中,当有tcp连接事件到达的时候,内核将会对源IP+源端⼝取
hash然后指定这个group中其中⼀个进程来接受连接,相当于在内核级别中实现了⼀个负载均衡。
关于SO_REUSEPORT,参见
基于以上两种⽅法,其实epoll⽣态在⽬前来说不存在所谓的惊群效应了,除⾮:你溢⽤epoll,⽐如多进程之间共享了同⼀个epfd(⽗进程创建epoll由多个⼦进程来调⽤),那就不能怪epoll了,因为这时候多个进程都被挂到这个epoll下,这种情况下,已经不是仅仅是惊群效应的问题了,⽐如说,A进程在epoll挂了socket1的连接事件,B进程调⽤了epoll_wait,由于属于同⼀个epfd,当socket1产⽣事件的时候,进程B也会被唤醒,⽽更严重的事情在于,在B的空间下并不存在socket1这个fd,从⽽把问题搞得很复杂。总结:千万不要在多线程/多进程之间共享epfd

本文发布于:2023-07-19 09:35:20,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/89/1087580.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:进程   唤醒   内核   连接   队列   事件   等待
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图