参考
线程池
I/O多路复用:select、poll、epoll
Linux IO模式及 select、poll、epoll详解
IO多路复用
epoll底层实现过程
彻底学会使用epoll(六)——关于ET的若干问题总结
epoll EPOLLL、EPOLLET模式与阻塞、非阻塞
事件处理和并发模式
18 | 单服务器高性能模式:PPC与TPC
单服务器高性能模式:Reactor与Proactor
服务端处理事件的两种模式–Reactor和Proactor
谈半同步/半异步网络并发模型(以及半同步/半反应堆模式)
Reactor线程模型
Linux下多线程服务器Reactor模式总结
高效并发模式,半同步半异步和领导者追随者
Reactor模式的理解
并发模型
单服务器高性能关键之一就是服务器采取的并发模型,并发模型的关键设计点有两点:
- 服务器如何管理连接;
- 服务器如何处理请求。
服务器如何管理连接对应IO模型,有同步阻塞IO、同步非阻塞IO、IO多路复用、异步IO;服务器如何处理请求对应进程模型,有单进程、多进程/线程。根据不同的IO模型和进程模型,常见的并发模型有PPC(Process Per Connection)、TPC(Thread Per Connection)、Reactor、Proactor。
PPC/TPC模型和进程/线程池
PPC/TPC模型管理连接采用同步阻塞IO模型,处理请求采用多进程/线程模型。Linux Socket编程(二)中实现的服务端对多个客户端同时响应属于PPC/TPC模型,即
- 父进程接受连接;
- 父进程fork子进程/创建子线程;
- 子进程/子线程处理连接请求;
- 子进程/子线程关闭连接请求。
PPC/PPC模式最主要的问题是每个连接都需要创建进程/线程,连接结束后进程/线程就销毁了。对于单个任务处理时间短,且需要处理的任务数量大的场景,大量线程的创建和销毁本身就有很大的开销。Linux Socket编程(三)中实现的线程池可以减少线程本身带来的开销。
以下内容源自:IO多路复用
I/O多路复用
以网络IO为例,在IO操作过程会涉及到两个对象:
- 调用这个IO的process (or thread);
- 系统内核(kernel)。
在一个IO操作过程中,以read为例,会涉及到两个过程: - 等待数据准备好(Waiting for the data to be ready);
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
I/O多路复用是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。阻塞I/O有一个比较明显的缺点是在I/O阻塞模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,需要多个进程或者多个线程,但是这种方式效率不高。非阻塞的I/O需要轮询查看流是否已经准备好了,比较典型的方式是忙轮询。
忙轮询
忙轮询方式是通过不停的把所有的流从头到尾轮询一遍,查询是否有流已经准备就绪,然后又从头开始。如果所有流都没有准备就绪,那么只会白白浪费CPU时间。轮询过程可以参照如下:
while true {
for i in stream[]; {
if i has data
read until unavailable
}
}
无差别的轮询方式
为了避免白白浪费CPU时间,可以采用另外一种轮询方式,无差别的轮询方式。即通过引进一个代理,这个代理为select/poll,这个代理可以同时观察多个流的I/O事件。当所有的流都没有准备就绪时,会把当前线程阻塞掉;当有一个或多个流的I/O事件就绪时,就从阻塞状态中醒来,然后轮询一遍所有的流,处理已经准备好的I/O事件。轮询的过程可以参照如下:
while true {
select(streams[])
for i in streams[] {
if i has data
read until unavailable
}
}
如果I/O事件未准备就绪,那么我们的程序就会阻塞在select处。我们通过select那里只是知道了有I/O事件准备好了,但不知道具体是哪几个流(可能有一个,也可能有多个),所以需要无差别的轮询所有的流,找出已经准备就绪的流。可以看到,使用select时,我们需要O(n)的时间复杂度来处理流,处理的流越多,消耗的时间也就越多。
最小轮询方式
无差别的轮询方式有一个缺点就是,随着监控的流越来越多,需要轮询的时间也会随之增加,效率也会随之降低。所以还有另外一种轮询方式,最小轮询方式,即通过epoll方式来观察多个流,epoll只会把发生了I/O事件的流通知我们,我们对这些流的操作都是有意义的,时间复杂度降低到O(k),其中k为产生I/O事件的流个数。轮询的过程如下:
while true {
active_stream[] = epoll_wait(epollfd)
for i in active_stream[] {
read or write till unavailable
}
}
select/poll/epoll都是采用I/O多路复用机制的,其中select/poll是采用无差别轮询方式,而epoll是采用最小的轮询方式。
IO多路复用的优势
I/O多路复用的优势并不是对于单个连接能处理的更快,而是在于可以在单个线程/进程中处理更多的连接。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。(个人理解:每accept
一个服务端的请求,无论IO事件是否准备就绪,就开辟一个线程处理,与I/O多路复用为准备就绪的IO事件开辟线程处理相比,需要更多的线程同时运行。因此,当短时间内处理大量的连接时候,I/O多路复用可以减少系统所需最大线程数目。)
Reactor模式的实现
Reactor为同步非阻塞模式,使用IO多路复用统一监听事件,收到事件后分配给某个进程/线程。在LInux系统下,I/O模型采用单epoll实现多路复用,线程池负责任务的处理。
epoll
epoll底层实现
首先epoll_create创建一个epoll文件描述符,底层同时创建一个红黑树,和一个就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须是该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。ET模式不会检查,只会调用一次。
epoll操作
// MyEpoll.h
#ifndef MY_EPOLL_H
#define MY_EPOLL_H
#include <poll.h>
#include <sys/epoll.h>
#include <vector>
class MyEpoll{
public:
// 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大;返回一个描述符epollfd。
int createEpoll(int max_events){
evs.resize(max_events);
return epoll_create(max_events);
}
// 等待epollfd上准备就绪的io事件,最多返回max_events个事件,这个maxevents的值不能大于创建epoll_create()时的size。
int waitEpoll(int epollfd, int max_events, int timeout){
return epoll_wait(epollfd, &evs[0], max_events, timeout);
}
// 操作事件(添加、删除和修改对scokfd的监听事件)
void operateEvent(int epollfd, int sockfd, int op, int event){
struct epoll_event ev;
ev.events = event;
ev.data.fd = sockfd;
if(epoll_ctl(epollfd, op, sockfd, &ev) < 0){
perror("epoll_ctl error");
}
}
struct epoll_event& getEvs(int index){
return evs[index];
}
private:
// 存放每次epoll_wait后从内核得到的准备就绪的io事件(accept、recv、send)的集合
vector<epoll_event> evs;
}
#endif
MyEpoll.h
封装了epoll的三个操作:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
需要注意的是,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll使用
MyEpoll myEpoll; // 封装的epoll
int main(int argc, const char* argv[]){
int server_sockfd = start_up();
// epoll初始化
int epollfd = myEpoll.createEpoll(MAX_EVENTS); //epoll描述符
if(epollfd < 0){
perror("epoll_create error");
return -1;
}
myEpoll.operateEvent(epollfd, server_sockfd, EPOLL_CTL_ADD, EPOLLIN);
int timeout = -1; // 超时时间,ms(-1表示infinite)
//epoll
while(1){
// 等待epollfd上的IO事件,最多返回MAX_EVENTS个事件。
// 该函数返回需要处理的事件数目,如返回0表示已超时。
int ret = myEpoll.waitEpoll(epollfd, MAX_EVENTS, timeout);
if(ret < 0){
perror("epoll_wait error");
break;
}else if(ret == 0){
printf("timeout ...\n");
continue;
}
for(size_t i = 0; i < ret; i++){
int fd = myEpoll.getEvs(i).data.fd;
int event = myEpoll.getEvs(i).events;
// 根据描述符的类型和事件类型进行处理
if (fd == server_sockfd && (event & EPOLLIN) ){
AcceptConnect(epollfd, fd);
}else if(event & EPOLLIN){
RecvData(epollfd, fd);
}else if(event & EPOLLOUT){
SendData(epollfd, fd);
}
}
}
return 0;
}
单epoll+线程池实现
#include <iostream>
#include <stdio.h>
#include <cstring> // void *memset(void *s, int ch, size_t n);
#include <sys/types.h> // 数据类型定义
#include <sys/socket.h> // 提供socket函数及数据结构sockaddr
#include <arpa/inet.h> // 提供IP地址转换函数,htonl()、htons()...
#include <netinet/in.h> // 定义数据结构sockaddr_in
#include <ctype.h> // 小写转大写
#include <unistd.h> // close()、read()、write()、recv()、send()...
#include <thread>
#include <sys/epoll.h>
#include <fcntl.h>
#include <unordered_map>
#include "ThreadPool.h"
#include "fixed_thread_pool.h"
#include "MyEpoll.h"
using namespace std;
// 函数perror()用于抛出最近的一次系统错误信息
#define BUFFER_SIZE 1<<20
#define READ_SIZE 16
void AcceptConnect(int epollfd, int server_sockfd);
void RecvData(int epollfd, int client_sockfd);
void SendData(int epollfd, int client_sockfd);
// 实现客户端发送小写字符串给服务端,服务端将小写字符串转为大写返回给客户端
void requestHandling(int epollfd, int client_sockfd);
// 服务端启动:创建套接字socket(),绑定IP地址和端口bind(),监听套接字的端口号listen()。返回服务端的套接字。
int start_up();
struct ClientData{
char buf[BUFFER_SIZE];
int len;
int data_size = 0;
struct sockaddr_in client_addr;
};
const int flag = 0; // 0表示读写处于阻塞模式, MSG_DONTWAIT -> 非阻塞
const int port = 8080;
const int MAX_EVENTS = 5000; // epoll最多处理的连接数
unordered_map<int,ClientData*> clientData; // 保存每个连接的相关数据
ThreadPool pool(thread::hardware_concurrency());
MyEpoll myEpoll; // 封装的epoll
int main(int argc, const char* argv[]){
int server_sockfd = start_up();
// epoll初始化
int epollfd = myEpoll.createEpoll(MAX_EVENTS); //epoll描述符
if(epollfd < 0){
perror("epoll_create error");
return -1;
}
myEpoll.operateEvent(epollfd, server_sockfd, EPOLL_CTL_ADD, EPOLLIN);
int timeout = -1; // 超时时间,ms(-1表示infinite)
//epoll
while(1){
// 等待epollfd上的IO事件,最多返回MAX_EVENTS个事件。
// 该函数返回需要处理的事件数目,如返回0表示已超时。
int ret = myEpoll.waitEpoll(epollfd, MAX_EVENTS, timeout);
if(ret < 0){
perror("epoll_wait error");
break;
}else if(ret == 0){
printf("timeout ...\n");
continue;
}
for(size_t i = 0; i < ret; i++){
int fd = myEpoll.getEvs(i).data.fd;
int event = myEpoll.getEvs(i).events;
// 根据描述符的类型和事件类型进行处理
if (fd == server_sockfd && (event & EPOLLIN) ){
AcceptConnect(epollfd, fd);
}else if(event & EPOLLIN){
RecvData(epollfd, fd);
}else if(event & EPOLLOUT){
SendData(epollfd, fd);
}
}
}
return 0;
}
void AcceptConnect(int epollfd, int server_sockfd){
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_sockfd < 0){
perror("accept error");
}else{
clientData[client_sockfd] = new ClientData();
clientData[client_sockfd]->client_addr = client_addr;
// 将此连接设置为非阻塞模式
// bool flags = fcntl(client_sockfd, F_GETFL, 0);
// fcntl(client_sockfd, F_SETFL, flags | O_NONBLOCK);
// 将此连接加入epoll监听队列
myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_ADD, EPOLLIN);
char ipbuf[128];
printf("Connect client iP: %s, port: %d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf,
sizeof(ipbuf)), ntohs(client_addr.sin_port));
}
}
void RecvData(int epollfd, int client_sockfd){
char* buf = clientData[client_sockfd]->buf;
struct sockaddr_in& client_addr = clientData[client_sockfd]->client_addr;
// read data
int len = recv(client_sockfd, buf, READ_SIZE, flag);
if (len == -1) {
myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_DEL, EPOLLIN); //删除监听
close(client_sockfd);
perror("read error");
}else if(len == 0){ // len为0表示当前处理请求的客户端断开连接
myEpoll.operateEvent(epollfd,client_sockfd, EPOLL_CTL_DEL, EPOLLIN); //删除监听
close(client_sockfd);
char ipbuf[128];
printf("Disconnect client iP: %s, port: %d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf,
sizeof(ipbuf)), ntohs(client_addr.sin_port));
}else{
cout << len << endl;
clientData[client_sockfd]->len = len;
char ipbuf[128];
printf("Recvive from client iP: %s, port: %d, str = %s\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf,
sizeof(ipbuf)), ntohs(client_addr.sin_port),buf);
pool.enqueue(requestHandling, epollfd, client_sockfd); // 单reactor+多线程(线程池)
// requestHandling(epollfd, client_sockfd); // 单reactor+单线程模式
}
}
void SendData(int epollfd, int client_sockfd){
char* buf = clientData[client_sockfd]->buf;
struct sockaddr_in client_addr = clientData[client_sockfd]->client_addr;
int data_size = clientData[client_sockfd]->data_size;
if(send(client_sockfd, buf, data_size, flag) == -1){
perror("write error");
close(client_sockfd);
myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_DEL, EPOLLOUT);
}else{
char ipbuf[128];
printf("Send to client iP: %s, port: %d, str = %s\n",inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf,
sizeof(ipbuf)), ntohs(client_addr.sin_port), buf);
myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_MOD, EPOLLIN);
}
memset(buf,'\0',BUFFER_SIZE); // 清空buf
}
void requestHandling(int epollfd, int client_sockfd){
char* buf = clientData[client_sockfd]->buf;
int len = clientData[client_sockfd]->len;
// 小写转大写
for(int i = 0; i < len; i++) {
buf[i] = toupper(buf[i]);
}
myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_MOD, EPOLLOUT); // 描述符对应的事件由读改为写
}
int start_up(){
int server_sockfd = socket(PF_INET,SOCK_STREAM,0);
if(server_sockfd == -1){
close(server_sockfd);
perror("socket error!");
}
// /* Enable address reuse */
// int on = 1;
// int ret = setsockopt( server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(server_sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr)) == -1){
close(server_sockfd);
perror("bind error");
}
if(listen(server_sockfd, 10) == -1){
close(server_sockfd);
perror("listen error");
}
printf("Listen on port %d\n", port);
return server_sockfd;
}
epoll工作模式LT和ET
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:
- LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
- ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
将单epoll+线程池实现代码中对client_sockfd的操作设置为LT模式(默认),并注释掉写事件完成后,将epollfd对client_sockfd监听的写事件改为读事件那一行,即
myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_MOD, EPOLLIN);
客户端连接以后发送请求,epoll会不停的触发client_sockfd写事件。
…
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
…
当设置成ET模式后,epoll不会再次触发写事件。
Listen on port 8080
Connect client iP: 127.0.0.1, port: 37956
Recvive from client iP: 127.0.0.1, port: 37956, str = asd
Send to client iP: 127.0.0.1, port: 37956, str = ASD