accept惊群研究

一:惊群的定义
惊群是多进程多线程编程中的一个常见问题,就是当多个进程和线程在同时阻塞等待同一个事件时,如果这个事件发生,会唤醒所有的进程,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。
二:accept惊群
在使用多进程处理客户端-服务器连接时,往往会出现惊群现象,即在主进程listen之后调用fork创建多个子进程之后,这些子进程由于等待客户端的connect链接而处于睡眠状态,而每次只有一个链接进入,内核会所有的子进程来处理,往往只有一个进程能够获得链接,而且他的子进程又要重新回到睡眠状态。
以前旧版本的Linux没有解决accept惊群,目前的Linux已经解决了accept惊群。下面展示实验部分内容。
server.c文件如下所示:
#include <sys/types.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <sys/wait.h>  
#include <stdio.h>  
#include <string.h>  
#define PROCESS_NUM 3  
int main()  
{  
    int fd = socket(PF_INET, SOCK_STREAM, 0);  
    int connfd;  
    int pid;  
    char sendbuff[1024];  
    struct sockaddr_in serveraddr;  
    serveraddr.sin_family = AF_INET;  
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    serveraddr.sin_port = htons(1234);  
    bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));  
    listen(fd, 1024);  
    int i;  
    for(i = 0; i < PROCESS_NUM; i++)  
    {  
        int pid = fork();  
        if(pid == 0)  
        {  
            while(1)  
            {  
                connfd = accept(fd, (struct sockaddr*)NULL, NULL);  
                snprintf(sendbuff, sizeof(sendbuff), "accept PID is %d\n", getpid());  
                
                send(connfd, sendbuff, strlen(sendbuff) + 1, 0);  
                printf("process %d accept success!\n", getpid());  
                close(connfd);  
            }  
        }  
    }  
    int status;  
    wait(&status);  
    return 0;  
}
为了简单起见,主进程只创建两三个子进程,共三个子进程来处理客户端链接。
client.c文件如下所示:
#include <stdio.h>  
#include <unistd.h>  
#include <string.h>  
#include <stdlib.h>  
#include <arpa/inet.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
int main(int argc, char *argv[])  
{  
   unsigned short port = 1234;             // 服务器的端口号  
   char *server_ip = "192.168.188.139";       // 服务器ip地址  
     
   if( argc > 1 )       //函数传参,可以更改服务器的ip地址                                   
   {         
       server_ip = argv[1];  
   }     
   if( argc > 2 )      //函数传参,可以更改服务器的端口号                                     
   {  
       port = atoi(argv[2]);  
   }  
 
   int sockfd;  
   sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字  
   if(sockfd < 0)  
   {  
       perror("socket");  
       exit(-1);  
   }  
     
   // 设置服务器地址结构体  
   struct sockaddr_in server_addr;  
   bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址  
   server_addr.sin_family = AF_INET;   // IPv4  
   server_addr.sin_port = htons(port); // 端口  
   inet_pton(AF_INET, server_ip, &server_addr.sin_addr.s_addr);    // ip  
      // 主动连接服务器  
   int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));       
   if(err_log != 0)  
   {  
       perror("connect");  
       close(sockfd);  
       exit(-1);  
   }  
     
   char send_buf[512] = {0};  
   printf("send data to %s:%d\n",server_ip,port);  
   while(1)  
   {  
       printf("send:");  
       fgets(send_buf,sizeof(send_buf),stdin); // 输入内容  
       send_buf[strlen(send_buf)-1]='\0';  
       send(sockfd, send_buf, strlen(send_buf), 0);   // 向服务器发送信息 
 
   }  
 close(sockfd);  
     
   return 0;  
}
其中为了实验方便,将服务器socket设定为:192.168.188.139:1234,大家可以根据自己的ip重新设定。
接下来编译运行server,然后使用ps –ef|grep ./server观察

jack      4341  4340  0 05:36 pts/6    00:00:00 ./server

jack      4342  4340  0 05:36 pts/6    00:00:00 ./server

jack      4343  4340  0 05:36 pts/6    00:00:00 ./server
接下来观察 /proc/[pid]/status目录下的内容结果如下所示:
PID voluntary_ctxt_switchesnonvoluntary_ctxt_switches
4341 1 0
4342 1 0
4343 1 0
其中voluntary_ctxt_switches 代表主动放弃CPU的次数,nonvoluntary_ctxt_switches: 代表被动放弃CPU的次数,接着执行./client,在./server终端中出现process 4341 accept success!然后重新观察/proc/[pid]/status目录,结果如下:
PID voluntary_ctxt_switchesnonvoluntary_ctxt_switches
4341 2 0
4342 1 0
4343 1 0
结果表明只有4341号进程获得了一次CPU,并且处理了connect之后又主动放弃了CPU,其余子进程仍然处于睡眠状态,并未发生上下文切换。
目前内核解决了accept的惊群问题,并未解决epoll惊群,https://www.pureage.info/2015/12/22/thundering-herd.html,该博客网址有epoll惊群源码,和为何不解决epoll惊群的原因。大家可以按照上述方法去验证一下epoll惊群。
三 内核如何解决accept惊群
因为涉及到内核,水平有限,不能进行实验验证。只能从原理上大概分析一下。
通常情况下,我们首先能想到的就是进行加锁操作,这样一来,获得锁的进程阻塞与accept调用,而未获得锁的进程阻塞于锁的获取。这样可以有一定的性能提高,但是当unlock(),释放锁之后,同样会面临锁的争抢,也会出现惊群现象。
内核开发者增加了一个“互斥等待”选项。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:
1)当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.
2)当 wake_up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。
        也就是说,对于互斥等待的行为,比如如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此时间的队列 的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。
其实惊群的根本原因在于资源竞争,假如内核可以将资源竞争改为资源分配,这样内核就有了主动权,惊群问题就可以得到缓解。一般的设计可以在父进程中进行accept调用,然后将已连接套接字传递给某个子进程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值