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都是IO多路复用的机制。I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
(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。
poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。
(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内部定义的等待队列)。这也能节省不少的开销。
二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。
/*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);
}
}