`
simohayha
  • 浏览: 1400012 次
  • 性别: Icon_minigender_1
  • 来自: 火星
社区版块
存档分类
最新评论

linux已经不存在惊群现象

阅读更多
惊群也就是指多个进程阻塞在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到,其他都会报错。所以说对现在的服务器设计并没有多大的帮助。或者说这是新式的惊群。
分享到:
评论
7 楼 dear531 2014-05-19  
还得补充一句,惊群了之后,数据打印显示,只有一个子线程继续接受了客户的连接并处理了他的数据。
6 楼 dear531 2014-05-19  
我用select试验了,用的ubuntu12.10,内核3.5.0-26,先是产生了listen描述符,然后开始fork一千个子进程,每个子进程都select后进行accept,然后用nc工具连接服务器,得到如下效果:
i :0
... ...

i :978
i :999
i :998
i :981
i :997
i :976
i :994
i :990
i :993
i :991
return i :862
return i :903
return i :883
return i :946
return i :991
return i :981
return i :993
4567890    /* child proccess send data */

如上可以看到,但凡带有return开头的i值打印,均为select返回后打印,即select被惊,也就是select函数被内核通知为可读,在睡眠的情况下被唤醒。
而单单只有i开头的是,在每个子进程在fork之后是否完成了子进程创建的打印。
而1000个子进程这样的返回数量,不知道是不是叫惊群,我只能给大家展示下表象,不能为大家总结任何结论,因为水平有限!
5 楼 dear531 2014-05-19  
我也很想知道,楼主说的,可以看到,是怎么看才能看到accept只有一个被唤醒!
4 楼 caravsapm70 2010-06-13  
不是这么做的。讨论惊群,不能在应用层看,要看内核在accept时,是会wakeup一个还是全部。如果wakeup全部,但是返回一个给应用层的话,一楼的例子也是通过,但是效率会受到很大的影响,就是说,不适合要求效率的场合。
在2.6内核下,内核只会wakeup一个进程。就是说,2.6的内核下面,不会出现惊群的现象。
3 楼 sunzixun 2010-01-17  
hp unix 这样做了吗?...

你的accept上锁不就得了。。。

楼主linux 研究的很深入。。。
2 楼 simohayha 2010-01-03  
donghrat 写道
请问用的是哪个版本的内核源码?


我看的是32,不过据说老的内核也早就这样做了。
1 楼 donghrat 2010-01-03  
请问用的是哪个版本的内核源码?

相关推荐

    深入浅出Linux惊群:现象、原因和解决方案.docx

    ### 深入浅出Linux惊群:现象、原因与解决方案 #### 一、引言 在探讨Linux下的“惊群”现象之前,我们首先需要理解这一术语的基本含义及其背后的技术背景。“惊群”(Wake Storm)是指在操作系统中,多个进程或线程...

    linux epoll

    总结来说,`epoll`在Linux系统中是处理并发I/O的关键工具,但必须正确配置以避免惊群现象。通过设置`EPOLLONESHOT`标志以及合理的进程间通信策略,我们可以构建出高效、稳定的多进程Web服务器。

    《Linux系统与应用》教学课件—03Linux用户与组群管理.pdf

    《Linux系统与应用》教学课件—03Linux用户与组群管理.pdf《Linux系统与应用》教学课件—03Linux用户与组群管理.pdf《Linux系统与应用》教学课件—03Linux用户与组群管理.pdf《Linux系统与应用》教学课件—03Linux...

    linux c++ 守护线程,判断程序是否运行,不存在就启动

    在Linux系统中,C++编程时常常需要创建守护线程(daemon thread)来执行特定的任务,比如监控系统状态、定时任务或确保某个服务始终运行。守护线程是一种长期运行的后台进程,它不依赖于终端会话,即使用户注销或者...

    Linux课程群建设研究.pdf

    Linux作为开源操作系统,已经成为业界的重要平台,因此,培养计算机专业学生掌握Linux技能以适应市场需求变得至关重要。 文章首先分析了目前高校Linux课程教学面临的机遇和挑战。自2005年起,国内高校逐步开设Linux...

    Linux现象及其对计算机软件保护的启示.pdf

    Linux现象及其对计算机软件保护的启示.pdf

    LINUX设备驱动程序

    在Linux系统中,设备驱动程序是操作系统与硬件设备之间的桥梁,它们使得操作系统能够高效地管理和控制硬件资源。这里我们主要探讨的是与嵌入式开发相关的Linux设备驱动程序,特别是针对ARM架构,如6410处理器的开发...

    linux 系统下安装IE必备的三个包wine cabextract ies4linux

    在使用IES4Linux之前,确保已经安装了Wine和CabExtract,并且注意,由于这个工具是针对特定版本的,所以可能不适用于最新的Linux发行版或者更新的IE版本。 在安装过程中,你可能需要处理一些依赖问题,比如库的版本...

    Linux vmtools的Linux.iso下载

    首先启动虚拟机软件VM(虚拟Linux系统 rhel4 已经安装完毕) 1.设置VMware的cd-rom→ Use ISO image → 本文件(linux.iso) 2.启动虚拟机 3.用超级用户root登录 4.登录成功后,Ctrl+Alt ,取出鼠标,点选菜单栏,vm → ...

    PL2303 linux驱动

    描述中的“亲测可用”意味着这个驱动程序经过了实际测试,已经在某个或多个Linux环境中成功运行,并且能够实现USB到串口的正常通讯。这为用户提供了可靠性保证,表明下载和安装该驱动后,应该可以顺利地在Linux系统...

    linux telnet客户端安装包

    在Linux系统中,SSH客户端通常已经预装,使用`ssh`命令即可进行安全的远程连接。 **总结** 安装"telnet-0.17-47.el6.x86_64.rpm"这个RPM包能让你的64位Linux服务器具备使用Telnet客户端的能力,从而进行远程登录。...

    Linux是使用CAJViewer

    不过,这种方法可能存在性能损耗和稳定性问题,因为Wine并不是一个完美的模拟器。 另一种方法是利用开源项目,例如使用Evince或者Okular这样的文档阅读器。虽然它们原生并不支持CAJ格式,但可以通过安装额外的插件...

    windows下查看识别linux硬盘工具

    5. 不建议进行写操作:虽然Ext2IFS支持读写操作,但频繁在Linux分区上进行写操作可能会导致数据丢失或文件系统损坏,因为Linux的文件系统特性和Windows存在差异。 总的来说,通过使用如Ext2IFS这样的工具,Windows...

    Linux操作系统课群改革与教学模式探讨.pdf

    3. 缺乏体系化课程设计:Linux课程通常被视为单独的课程,没有形成一个涵盖基础、应用和开发的完整课程群,导致学生对Linux的理解不全面。 4. 教学方式传统:传统的讲授方式可能限制了学生的主动性和创新思维,不...

    解决 linux 无法创建、删除用户问题.html

    linux入门,创建、删除用户的方法,关于创建新用户,系统提示已经存在,删除用户,系统提示用户不存在的问题,涉及到的命令有:userdel -r、useradd、vipw、vipw -s

    基于ARM平台Linux+Xenomai系统搭建及主站、LinuxCNC移植(LCD版).pdf

    备注:这里移植的LinuxCNC实时性能测试(latency-test)有问题,翻阅英文网页说的是ARM平台不支持LinuxCNC(虽然可以运行,但应该不可以实际运用到工业控制中),得用LinuxCNC的分支——MachineKit,最近在着手处理...

    libsigar-amd64-linux.so和libsigar-x86-linux.so

    libsigar支持多种操作系统,包括但不限于Linux、Windows、Solaris和AIX。 在Linux环境下,libsigar提供了两个针对不同处理器架构的动态链接库:libsigar-amd64-linux.so是专为64位AMD(Advanced Micro Devices)...

    linuxcnc软件手册

    总的来说,LinuxCNC软件手册是一份非常详尽的用户指南,对于希望深入学习和使用LinuxCNC的用户来说,是一份不可或缺的参考资料。通过对手册的学习,用户可以掌握LinuxCNC的基本操作和高级配置,从而有效地进行数控...

    消除linux下的屏幕偏移现象和调整屏幕刷新率

    一些linux用户(常见的是nvidia显卡用户)在配置完X服务器后,已经可以进入xwin桌面,只是屏幕是歪的,怎么办?当然,用户可以利用显示器本身自带的调节按钮将它校正过来,但这样一来,你回到win下就发现win的屏幕歪向...

    《Linux设备驱动开发详解-基于最新的Linux4.0内核》源码

    《Linux设备驱动开发详解-基于最新的Linux4.0内核》是一本深入探讨Linux设备驱动程序开发的专业书籍,其源码提供了丰富的实践示例,帮助读者理解如何在Linux操作系统下编写和调试驱动程序。该书涵盖了从基础概念到...

Global site tag (gtag.js) - Google Analytics