惊群也就是指多个进程阻塞在accept,当有连接完成,会唤醒所有进程。
经过测试,发现现在的内核已经修复了这个问题,当有多个进程阻塞在accept,只会唤醒一个进程。
下面这个是一篇论文,就是讲这个问题的。
http://www.usenix.org/event/usenix2000/freenix/full_papers/molloy/molloy.pdf
这里会先测试,然后分析内核代码。
下面是服务端的测试代码(很丑陋的代码,只是测试用):
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <strings.h>
#define SERV_PORT 9999
int main(int argc,char **argv)
{
int listenfd,connfd;
pid_t childpid,childpid2;
socklen_t clilen;
struct sockaddr_in cliaddr,servaddr;
listenfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl (INADDR_ANY);
servaddr.sin_port = htons (SERV_PORT);
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
listen(listenfd,1000);
clilen = sizeof(cliaddr);
if( (childpid = fork()) == 0)
{
while(1)
{
connfd = accept(listenfd,(struct sockaddr *) &cliaddr,&clilen);
printf("fork 1 is [%d],error is %m\n",connfd);
}
}
if( (childpid2 = fork()) == 0)
{
while(1){
connfd = accept(listenfd,(struct sockaddr *) &cliaddr,&clilen);
printf("fork 2 is [%d],error is %m\n",connfd);
}
}
sleep(100);
return 1;
}
可以看到我们fork两个进程同时阻塞在accept。当客户端connect完成之后,只有第一个子进程被唤醒。
不过这里要注意当用select这类监控listen的句柄的时候,有连接到来,这时所有的进程都会被唤醒的。这里的原因是因为对于select来说,数据不是互斥的,也就是说有可能就需要多个进程同时读取资源。而对于accept,只有可能是一个进程取得数据,因此这里如果用select监控listen的句柄然后阻塞的话,当有连接到来就会唤醒所有的进程,这里测试程序就不贴了。
ok,接下来我们来看源码中是如何做得。
首先我们知道当accept的时候,如果没有连接则会一直阻塞(没有设置非阻塞),而阻塞代码是在inet_csk_wait_for_connect中,这个代码我们前面已经分析过了,一次你我们来看代码片断:
/*
* True wake-one mechanism for incoming connections: only
* one process gets woken up, not the 'whole herd'.
* Since we do not 'race & poll' for established sockets
* anymore, the common case will execute the loop only once.
*
* Subtle issue: "add_wait_queue_exclusive()" will be added
* after any current non-exclusive waiters, and we know that
* it will always _stay_ after any new non-exclusive waiters
* because all non-exclusive waiters are added at the
* beginning of the wait-queue. As such, it's ok to "drop"
* our exclusiveness temporarily when we get woken up without
* having to remove and re-insert us on the wait queue.
*/
for (;;) {
prepare_to_wait_exclusive(sk->sk_sleep, &wait,
TASK_INTERRUPTIBLE);
这里注释非常详细,就是说它是exclusive的,然后我们来看prepare_to_wait_exclusive,它很简单就是将当前的进程加到socket的等待队列sk_sleep中:
void
prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
///最关键的在这里我们看到设置等待队列的flag为EXCLUSIVE,设置这个就是表示一次只会有一个进程被唤醒,我们等会就会看到这个标记的作用。
wait->flags |= WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
//加入到等待队列。
if (list_empty(&wait->task_list))
__add_wait_queue_tail(q, wait);
set_current_state(state);
spin_unlock_irqrestore(&q->lock, flags);
}
这边添加的分析完了,我们来看唤醒的实现。
下面分析的代码,我前面的blog基本已经分析完了,因此下面只是一些代码片断。
首先我们知道当有tcp连接完成,就会从半连接队列拷贝sock到连接队列,这个时候我们就可以唤醒阻塞的accept了。ok,我们来看关键的代码,首先是tcp_v4_do_rcv:
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
if (!nsk)
goto discard;
if (nsk != sk) {
if (tcp_child_process(sk, nsk, skb)) {
rsk = nsk;
goto reset;
}
return 0;
}
}
这段代码就是从半连接队列拷贝到连接队列的过程。这里我们只需要看tcp_child_process。这个函数用来处理新建的子socket。
int tcp_child_process(struct sock *parent, struct sock *child,
struct sk_buff *skb)
{
int ret = 0;
int state = child->sk_state;
if (!sock_owned_by_user(child)) {
///处理子socket
ret = tcp_rcv_state_process(child, skb, tcp_hdr(skb), skb->len);
/* Wakeup parent, send SIGIO */
///关键在这里,我们可以看到这里唤醒父socket。
if (state == TCP_SYN_RECV && child->sk_state != state)
parent->sk_data_ready(parent, 0);
}
.................................
return ret;
}
我们这里看到有两个条件一个是state==TCP_SYN_RECV,另一个是child->sk_state!=state,当都满足我们就会调用sk_data_ready.然后唤醒父socket。
我们一个个来看。
这里传递进来的子套接字child,的状态我们知道是在创建新的socket的时候通过inet_csk_clone设置为TCP_SYN_RECV的,也就是当我们收到syn,并发出syn ack之后,我们再次接收到对端的数据,此时我们就新建一个socket然后设置状态为TCP_SYN_RECV.
因此这里状态必须为TCP_SYN_RECV。而当我们进入tcp_rcv_state_process处理之后,如果状态变化,哪只可能变为establish,也就是三次握手完成,因此这时的状态必须不为TCP_SYN_RECV.
因此当三次握手完毕后,我们会调用sk_data_ready通知父socket,而前一篇blog我们知道tcp中这个函数是sock_def_readable。而这个函数会调用wake_up_interruptible_sync_poll来唤醒队列。接下来我们就来看这个函数。
#define wake_up_interruptible_sync_poll(x, m) \
__wake_up_sync_key((x), TASK_INTERRUPTIBLE, 1, (void *) (m))
void __wake_up_sync_key(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
.....................................
///这个函数才是最终的处理函数。
__wake_up_common(q, mode, nr_exclusive, wake_flags, key);
spin_unlock_irqrestore(&q->lock, flags);
}
然后就是__wake_up_common函数。这里注意传递进来的第三个参数是1.
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
///开始遍历。
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
///唤醒等待队列,这里可以看到如果条件都满足的话,只会唤醒一个元素的。
if (curr->func(curr, mode, wake_flags, key) &&(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
然后我们就来看这几个判断条件。
1 curr->func(curr, mode, wake_flags, key)
这个是注册函数的执行。
我们主要来看后两个条件。
2 flags & WQ_FLAG_EXCLUSIVE
flags必须为EXCLUSIVE,我们还记得accept等待连接的时候注册等待队列就是设置的为EXCLUSIVE标记。
3 !--nr_exclusive
这个值是唤醒几个exclusive的元素。而我们当可读的时候传递进来的值就是1。也就是说这个值也会为真。
因此当唤醒accept的时候,只会唤醒一个进程。在新的内核,惊群现象也已经是不存在的了。
PS:不过现在大多数服务器的设计都是fork后select监控listen的句柄。这个时候自然会被全部唤醒,然后accept,只有一个能accept到,其他都会报错。所以说对现在的服务器设计并没有多大的帮助。或者说这是新式的惊群。
分享到:
相关推荐
深入浅出Linux惊群:现象、原因和解决方案.docx
linux c++ 守护线程,判断程序是否运行,不存在就启动
《Linux系统与应用》教学课件—03Linux用户与组群管理.pdf《Linux系统与应用》教学课件—03Linux用户与组群管理.pdf《Linux系统与应用》教学课件—03Linux用户与组群管理.pdf《Linux系统与应用》教学课件—03Linux...
linux unzip命令不存在及ZIP离线安装
Linux现象及其对计算机软件保护的启示.pdf
linux\linux教材全集 linux\linux教材全集 linux\linux教材全集 linux\linux教材全集
linux 下,采用epoll模型,演示web服务器“惊群”现象。 创建8个子进程,即工作进程,同时持有监听套接字,当有新连接 时,8个子进程被扰动。
小编在一次项目测试中,发现一些bug,window与linux项目部署-linux文件路径不存在问题,本文给出了解决方案,需要的朋友可以参考下
对于linux,patch_dll.bat里面只有MentorKG.exe -patch最重要,mgls.dll不存在无所谓,环境变量在user目录的.bashrc中设置LM_LICENSE_FILE,如果已经存在LM_LICENSE_FILE,就再写一行,分号分隔什么的是windows里面...
linux入门,创建、删除用户的方法,关于创建新用户,系统提示已经存在,删除用户,系统提示用户不存在的问题,涉及到的命令有:userdel -r、useradd、vipw、vipw -s
基于Linux的一个专门检测已经编译好了的程序是否存在死循环等错误的程序,有源代码以及开发文档。
经过近20年的发展,Linux操作系统已经成为当今最成功的开源软件之一,使用广泛,影响深远。随着Linux操作系统功能的不断丰富和完善,Linux内核的源代码也从最初的几万行增加到如今的数百万行,庞大无比,对于Linux...
Linux基础部分,Linux是一种自由和开放源码的操作系统,存在着许多不同的Linux版本,但它们都使用了Linux内核。Linux可安装在各种计算机硬件设备中,比如手机、平板电脑、路由器、台式计算机Linux是一种自由和开放...
首先启动虚拟机软件VM(虚拟Linux系统 rhel4 已经安装完毕) 1.设置VMware的cd-rom→ Use ISO image → 本文件(linux.iso) 2.启动虚拟机 3.用超级用户root登录 4.登录成功后,Ctrl+Alt ,取出鼠标,点选菜单栏,vm → ...
2.1.1. Linux中已经实现Nor Flash驱动 4 2.1.1.1. 在开发板相关部分添加对应nor flash初始化相关代码 4 2.1.1.2. Linux通用nor flash驱动m25p80.c简介 5 2.1.2. Linux中已实现了U盘挂载,以方便拷贝要升级的文件 8 ...
韦东山LINUX群答疑大全
Linux服务器中高负载现象故障排查方法.pdfLinux服务器中高负载现象故障排查方法.pdf
Linux服务器中高负载现象故障排查指南.pdfLinux服务器中高负载现象故障排查指南.pdf
第一章 Linux 入门教程 Linux,在今天的广大电脑爱好者心中已经不再是那个遥不可及的新东西了,如果说几 年前的 Linux 是星星之火的话, 如今 Linux 不仅在服务器领域的应用取得较大进展, 而且在 桌面应用领域也有...
备注:这里移植的LinuxCNC实时性能测试(latency-test)有问题,翻阅英文网页说的是ARM平台不支持LinuxCNC(虽然可以运行,但应该不可以实际运用到工业控制中),得用LinuxCNC的分支——MachineKit,最近在着手处理...