什么是惊群
举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。
对于操作系统来说,多个进程/线程在等待同一资源时,也会产生类似的效果,其结果就是每当资源可用,所有的进程/线程都来竞争资源,会造成以下后果:
- 系统对用户进程/线程频繁的做无效的调度、上下文切换,系统性能大打折扣。
- 为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。
最常见的例子就是对于socket描述符的accept操作,当多个用户进程/线程监听在同一个端口上时,由于实际只可能accept一次,因此就会产生惊群现象。
Linux内核解决惊群问题的方法
在Linux 2.6版本之前,监听同一个socket的进程会挂在一个等待队列上,当请求到来时,会唤醒所有等待的子进程。
当时可以使用锁解决这种惊群问题。
for(;;) {
lock();// 互斥锁
int client = accept(...);
unlock();
if (client < 0) continue;
...
}
在Linux 2.6版本之后,通过引入一个标记位,解决掉了惊群问题。内核开发者增加了一个“互斥等待”选项。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:
1)当一个进程加入等待队列时,如果该进程有 WQ_FLAG_EXCLUSEVE 标志置位,它被添加到等待队列的尾部。没有这个标志,则添加到队列开始。
2)当 wake_up 在一个等待队列上被调用时,它会唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。
也就是说,对于互斥等待的行为,比如对一个Socket,多线程阻塞accept时,系统内核只会唤醒所有正在等待此事件的队列的第一个,队列中的其他进程则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket时的惊群问题。