1.linux高并发之IO多路复用select,poll和epoll的区别
IO多路复用:
就是假设现在要设计一个高性能服务器,需要多个客户端与之连接,采用多线程的话多线程切换带来的开销太大。所以就会考虑采用单线程的方式,解决方案就是IO多路复用,IO多路复用一般有select(),poll(),epoll()方式,他们都是对连接进服务端的客户端socket进行监控,例如现在有100个客户端socket,那么就监控这100个,如果这100个socket有信息进入,则IO多路复用就会返回,否则,就阻塞。即IO多路复用可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写。
正因为阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫多路复用。
- select()
//首先创建文件描述符数组fds[5],这部分代码省略,总之就是一个线程监听了5个套接字
while(1){
FD_ZERO(&ret);
for(int i=0;i<5;i++){
FD_SET(fds[i],&rset);//rset是一个bitmap,0,1代表哪个文件描述符是被监听的,1024个坑位
}
puts("round again");
select(max+1,&rset,NULL,NULL,NULL);
//rset完全从用户态拷贝到了内核态,完全交由内核态来判断是否有是数据到来,
//如果没有数据到来,select函数就一直处于阻塞状态
//如果有数据到来,内核需要做两件事情,
//第一件是有数据的套接字的FD置1,这里FD指的是rset中的位
//第二件是select函数返回
for(int i=0;i<5;i++){
if(FD_ISSET(fd[i],&rset)){
memset(buffer,0,MAXBUF);
read(fds[i],buffer,MAXBUF);
puts(buffer);
}
}
}
缺点:
- bitmap大小有上限
- rset不可重用,重复更新,消耗时间复杂度
- rset从用户态拷贝到内核态时间仍有时间开销
- rset虽然被置位,但是不知道具体的置位点,最后需要O(n)的时间复杂度来找到具体位置
- poll
struct pollfd{
int fd;//文件描述符
short events;//需要在意的事件
short revents;//对events的一个回馈
}
//初始化pollfds
while(1){
puts("round again");
poll(pollfds,5,5000);
//同样把pollfds拷贝到内核态,内核空间监听套接字的数据
//如果有数据到来需要做两件事:
//1.pollfds.revents置位
//2.poll 返回
for(int i=0;i<5;i++){
if(pollfds[i].revents&POLLIN){
pollfds[i].revents=0;
memset(buffer,0,MAXBUF);
read(pollfds[i].fd,buffer,MAXBUF);
puts(buffer);
}
}
}
相比于select,poll解决1,2两点,3,4点还是保留,工作原理一样。
- epoll
while(1){
puts("round again");
nfds=epoll_wait(epfd,events,5,10000);
//内核空间和用户空间共享epfd
//有数据到来时:
//1.“置位”,重排
//2.返回有多少套接字有数据到来
for(i=0;i<nfds;i++){
memset(buffer,0,MAXBUF);
read(events[i].data.fd,buffer,MABUF);
puts(buffer);
}
}
select的问题全部解决
2.手撕一个最简单的server端服务器
- 基本流程
- 创建套接字
- 配置服务器地址相关参数
- 将两者绑定
- 监听套接字上的端口
- 在上面创建的套接字上等待连接,并打开一个新的套接字用于与请求之间的交互
- 在发送缓冲区中写入响应
- 关闭连接
#include<iostream>
#include<sys/socket.h>
#include<netinet/in.h>
#include <strings.h>
#include <assert.h>
#include <zconf.h>
#include <cstring>
#define PORT 8099
#define LISTENQ 5
int main(){
int listenfd,connfd;
struct sockaddr_in servaddr;
listenfd=socket(PF_INET,SOCK_STREAM,0);
assert(listenfd!=-1);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(PORT);
int bind_ok=bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
assert(bind_ok!=-1);
int listen_ok=listen(listenfd,LISTENQ);
assert(listenfd!=-1);
std::string buffer="hello";
int write_ok;
while(1) {
connfd = accept(listenfd, NULL, NULL);
assert(connfd != -1);
write_ok = write(connfd, buffer.c_str(), strlen(buffer.c_str()));
if (write_ok < 0) {
std::cout << "failed" << std::endl;
close(connfd);
return 0;
}
close(connfd);
}
}
3.线程池
1.线程池的概念
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行任务结束后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
2.使用线程池的原因
多线程运行时间,系统不断的启动和关闭新线程,成本非常高,会过度消耗系统资源,以及过度切换线程的危险,从而可能导致系统资源的崩溃。这时线程池就是最好的选择了。
3.线程池的c++实现
线程池的主要组成成分有三个部分:
- 任务队列(Task Queue)
- 线程池(Thread Pool)
- 完成队列(Completed Tasks)
4.基于事件驱动的reactor模式
Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O,中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。
Reactor是一种事件驱动机制,和普通函数调用的不同之处在于:应用程序不是主动的调用某个API完成处理,而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。用“好莱坞原则”来形容Reactor再合适不过了:不要打电话给我们,我们会打电话通知你。
5.边沿触发与水平触发的区别
边缘触发(edge-triggered)
简称:ET,它只支持非阻塞socket。你可以设定一个值,当达到这个值时才会触发。它只通知一次。如果不对事件进行处理,它将会将其丢弃。
水平触发(level-triggered)
简称:LT,它只支持阻塞和非阻塞两种模式,它是一有事件发生触发,如果你不将其进行处理,它将不会将事件丢弃,它将会一直提示。
6.阻塞IO与非阻塞IO的区别
阻塞IO,指的是需要内核IO操作彻底完成后,才返回用户空间执行的操作。阻塞指的是用户空间程序的执行状态。
非阻塞IO,指的是用户空间的程序不需要等待内核IO彻底完成,可以立即返回用户空间执行用户操作,即处于非阻塞的状态,与此同时内核会立即返回给用户的一个状态值。
简单来说:阻塞是指用户空间(调用线程)一直在等待,而不能干别的事情。非阻塞是指用户空间(调用线程)拿到内核返回的状态值就返回自己的空间,IO操作可以干就干,不可以干,就去干别的事情。