高并发模型

一、基础知识

1.pc、ios、android、移动web,通常通过负载均衡服务器(nginx)进行任务分发,经过多web服务器,多业务服务器 ,到数据库或分布式文件系统等;

2.高并发服务器模型:多进程并发模型,多线程并发模型,多路io复用模型(select并发模型、poll并发模型、epoll并发模型);

3.单核电脑是通过分时复用技术,利用cpu快速切换时间片来达到人感觉上的多进程同时运行;

4.Tcp/ip模型,网络接口层(mac地址)、网络层(ip地址)、传输层(tcp/udp端口号)、应用层(应用层协议);

5.操作系统核心:内存管理、进程管理、网络管理、驱动管理。

二、常见高并发模型

1.aio是linux2.6以后内核实现的异步IO;

2.epoll作为select的linux的替代品,解决了select的fd_set的限制。性能优于select;

3.libevent是一个跨平台异步解决方案,他根据不同的平台提供了不同的异步方案,采用Reactor模型实现;

4.Boost::asio是一个跨平台的网络及底层IO的C++编程库,实现了对TCP、UDP、ICMP、串口的支持。对于读写方式,ASIO支持同步和异步两种方式。采用了epoll来实现,插入了大量的信号处理;

5.muduo采用Reactor模型实现的网络库,只支持Linux 2.6.x下的并发非阻塞TCP网络编程,不跨平台,不支持udp和ipv6。吞吐量方面muduo比libevent快,在事件处理效率方面,muduo与libevent总体比较接近,muduo吞吐量比boost.asio高;

6.ACE也是很经典的网络库。

三、Select、poll和epoll的区别:

        select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

1、select实现

1)使用copy_from_user从用户空间拷贝fd_set到内核空间

2)注册回调函数pollwait

3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll等)

4)以tcp_poll为例,其核心实现就是pollwait,也就是上面注册的回调函数。

5)pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout使select的进程(也就是current)进入睡眠。当设备驱动发现自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

8)把fd_set从内核空间拷贝到用户空间。

总结:

select的几大缺点:

1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销fd越多会越大;

2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销fd越多会越大;

3)select支持的文件描述符数量有限,默认是1024。

2、poll实现

        poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

3、Epoll

(1)epoll使用了内存映射(mmap)技术,将内核和用户空间指向同一块内存。

(2)epoll原理概述

    select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

A.调用epoll_create时,做了以下事情:

    内核帮我们在epoll文件系统里建了个file结点;

    在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket;

    建立一个list链表,用于存储准备就绪的事件。

B.调用epoll_ctl时,做了以下事情:

    把socket放到epoll文件系统里file对象对应的红黑树上;

    给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。

C.调用epoll_wait时,做了以下事情:

    观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。

总结:

1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

4.epoll的两种模式LT和ET

    二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。

5.源码示例(源码来自网络)
/*poll example*/
/*如何索引poll返回的就绪文件描述符*/
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
/*必须遍历所有已注册文件描述符并找到其中的就绪者(当然,可以利用ret来稍作优化)*/
for(int i = 0; i < MAX_EVENT_NUMBER; ++i)
{
    if(fds[i].revents & POLLIN)
    {
    int sockfd = fds[i].fd;
        //deal with sockfd.
    }
}


/*epoll example*/
int epfd = epoll_create(MAXSIZE);

struct epoll_event ev,events[5000];
//设置与要处理的事件相关的文件描述符
ev.data.fd=listenfd;
//设置要处理的事件类型
ev.events=EPOLLIN|EPOLLET;
//注册epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

int nfds = epoll_wait(epfd,events,6000,-1);
//处理所发生的所有事件     
for(int i = 0; i< nfds; ++i)
{
    //new accept.
    if(events[i].data.fd == listenfd)
    {
        printf("listen=%d\n",events[i].data.fd);
        connfd = accept(listenfd,(sockaddr *)(&clientaddr), &clilen);
        if(connfd<0)
        {
            perror("connfd<0");
            exit(1);
        }            
        setnonblocking(connfd);                 
        char *str = inet_ntoa(clientaddr.sin_addr);
        std::cout<<"connec_ from >>"<fd = sockfd;
        new_task->next=NULL;
                                                
        //fprintf(stderr,"sockfd %d",sockfd);
        //添加新的读任务
        pthread_mutex_lock(&mutex);
        if(readhead == NULL)
        {
            readhead = new_task;
            readtail = new_task;
        }   
        else
        {   
            readtail->next=new_task;
            readtail=new_task;
        }   
        //唤醒所有等待cond1条件的线程
        pthread_cond_broadcast(&cond1);
        pthread_mutex_unlock(&mutex);  
        continue;
    }
    else if(events.events & EPOLLOUT)
    {   
        //fprintf(stderr,"EPOLLOUT");
        num++;
        rdata=(struct user_data *)events[i].data.ptr;
        sockfd =rdata->fd;
        if(old == sockfd)
        {
            fprintf(stderr,"repreted sockfd=%d\n",sockfd);
            //exit(1);
        }
        old=sockfd;       
        //fprintf(stderr,"write  %d\n",num);
        int size=write(sockfd, rdata->line, rdata->n_size);
        //fprintf(stderr,"write=%d delete rdata\n",size);
        fprintf(stderr,"addr=%x fdwrite=%d size=%d\n",rdata,rdata->fd,size);
        
        if(rdata!=NULL)//主要问题导致delete重复相同对象 events返回对象相同
        {
            delete rdata;
            rdata=NULL;
        }
        
        //设置用于读操作的文件描述符
        //fprintf(stderr,"after delete rdata\n");
        ev.data.fd=sockfd;
                             
        //设置用于注测的读操作事件
        ev.events=EPOLLIN|EPOLLET;

        //修改sockfd上要处理的事件为EPOLIN
        res = epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
        
        while(res==-1)
        {
            //fprintf(stderr,"out error");
            exit(1);
        }
        //fprintf(stderr,"out EPOLLOUT\n");
        continue;
    }
    else if(events.events&(EPOLLHUP|EPOLLERR))
    {
        //fprintf(stderr,"EPPOLLERR\n");
        int fd=events.data.fd;
        if(fd>6000)
        {
            fd=((struct user_data*)(events.data.ptr))->fd;
        }
        //设置用于注测的读操作事件
        ev.data.fd=fd;
        ev.events=EPOLLIN|EPOLLET|EPOLLOUT;

        //修改sockfd上要处理的事件为EPOLIN
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev);
    }
}

你可能感兴趣的:(C++)