背景
原文链接: http://blog.youkuaiyun.com/ordeder/article/details/21721141
1.进程A在n端口上监听,即调用listen(listenfd,backlog);
2.之后A调用fork产生子进程B,此时B拷贝了A的listenfd,该描述符使用的是相同的“文件表项”(具体参考 http://blog.youkuaiyun.com/ordeder/article/details/21716639)
3.那么A进程和B进程将共享一个socket,具体图解如下:
惊群现象
在该模型下(多个子进程同时共享监听套接字)即可实现服务器并发处理客户端的连接。这里要注意的是,计算机三次握手创建连接是不需要服务进程参数的,而服务进程仅仅要做的事调用accept将已建立的连接构建对应的连接套接字connfd(可参考 http://blog.youkuaiyun.com/ordeder/article/details/21551567)。多个服务进程同时阻塞在accept等待监听套接字已建立连接的信息,那么当内核在该监听套接字上建立一个连接,那么将同时唤起这些处于accept阻塞的服务进程,从而导致“惊群现象”的产生,唤起多余的进程间影响服务器的性能(仅有一个服务进程accept成功,其他进程被唤起后没抢到“连接”而再次进入休眠)。
实例分析
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h> /* basic system data types */
#include<sys/socket.h> /* basic socket definitions */
#include<netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include<arpa/inet.h> /* inet(3) functions */
#include<sys/epoll.h> /* epoll function */
#include<fcntl.h>
#include<stdlib.h>
#include<errno.h>
#include<stdio.h>
#include<string.h>
#include<sys/select.h>
#define WORKERSIZE 3
void waitall()
{
pid_t cpid;
while(1)
{
cpid = wait(NULL);
if(cpid==-1){
perror("end of wait");
break;
}
printf("worker pid#%d exit...\n",cpid);
}
}
void worker_hander(int listenfd)
{
fd_set rset;
int cnt = 100,connfd,rc;
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec=0;
printf("worker pid#%d is waiting for connection...\n",getpid());
while(1)
{
FD_ZERO(&rset);
FD_SET(listenfd,&rset);
//rc = select(listenfd+1,&rset,NULL,NULL,&tv);//设置为非阻塞状态
rc = select(listenfd+1,&rset,NULL,NULL,NULL);//设置为阻塞
if(rc == -1) perror("select");
else if(rc>0 && FD_ISSET(listenfd,&rset))
{
//sleep(1);//第四种,让三个进程都有足够的时间资源唤起(防止可能出现某个进程已近开始进行accept结束了,另一个进程还未被唤起(调度的问题))
printf("worker pid#%d 's listenfd is readable\n",getpid(),rc);
connfd = accept(listenfd,NULL,0);
if(connfd == -1)
{
perror("accept error");
continue;
}
printf("worker pid#%d create a new connection...\n",getpid());
sleep(1);
close(connfd);
}
}
}
int main(int argc,char*argv[])
{
int listenfd,connfd;
struct sockaddr_in cliaddr,servaddr;
int queuelen=5,i,flag;
pid_t cpid[WORKERSIZE];
listenfd = socket(AF_INET,SOCK_STREAM,0);
//此处设置listenfd的为阻塞
/*flag = fcntl(listenfd,F_GETFL,0);
fcntl(listenfd,F_SETFL,flag|O_NONBLOCK);*/
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,"172.20.52.140",&servaddr.sin_addr);
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(2989);
bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
listen(listenfd,queuelen);
for(i=0;i<WORKERSIZE;i++)
{
cpid[i]=fork();
if(cpid[i] == -1){
perror("fork error");
waitall();
exit(0);
}
if(cpid[i]==0){
worker_hander(listenfd);
exit(0);
}
}
waitall();
return 0;
}
不同设置的结果输出:
一个客户端发起链接,而服务端有三个服务进程在监听同一端口,从而内核会唤起所有处于监听该端口的服务进程,从而导致惊群现象的发生。服务端设置不通的阻塞情况,可得如下不同的结果:
一个客户端发起链接,而服务端有三个服务进程在监听同一端口。服务端设置不通的阻塞情况,可得如下不同的结果。
1.select设置为阻塞,listenfd设置为阻塞或非阻塞,结果如下:
worker pid#25583 is waiting for connection...
worker pid#25584 is waiting for connection...
worker pid#25585 is waiting for connection...
worker pid#25585 's listenfd is readable
worker pid#25585 create a new connection...
分析:三个服务进程被唤起,25585第一个调度所以执行accept,所以此时连接被取走,其他两个进程之后开始被调度,而此时连接数目为0,又进入随眠...
2.select设置为非阻塞,listenfd设置为阻塞,结果如下:
worker pid#25743 is waiting for connection...
worker pid#25744 is waiting for connection...
worker pid#25745 is waiting for connection...
worker pid#25745 's listenfd is readable
worker pid#25744 's listenfd is readable
worker pid#25743 's listenfd is readable
worker pid#25745 create a new connection...
分析:三个服务进程都select都成功返回可读的套接字,从而各个进程都唤起处于阻塞的accept,但是只有一个进程建立链接,其余两个进程没有获取到链接而又进入睡眠...
3.select设置为非阻塞,listenfd设置为为非阻塞,结果如下:
worker pid#25240 is waiting for connection...
worker pid#25241 is waiting for connection...
worker pid#25242 is waiting for connection...
worker pid#25242 's listenfd is readable
worker pid#25240 's listenfd is readable
worker pid#25241 's listenfd is readable
worker pid#25242 create a new connection...
accept error: Resource temporarily unavailable
accept error: Resource temporarily unavailable
分析:三个服务进程都进入accept,但是只有一个进程获取到链接,其他两个进程没有获取到链接而出错
4.在select设置为阻塞,select和accept之间添加sleep(1),accept的套接字为非阻塞,结果如下:
worker pid#30689 is waiting for connection...
worker pid#30690 is waiting for connection...
worker pid#30691 is waiting for connection...
worker pid#30691 's listenfd is readable
worker pid#30691 create a new connection...
worker pid#30690 's listenfd is readable
worker pid#30689 's listenfd is readable
accept error: Resource temporarily unavailable
accept error: Resource temporarily unavailable
分析:三个进程被唤起后,由于有sleep(1),三个进程都被调度了,所以select都能查找到连接,故而从select返回,但是在accept处,只有一个进程得到了连接