IO可以分为两个步骤,等待+实际的读写。
比如调用read时,指定套接字文件始终没有数据可以读取(读事件没有就绪),那么read将一直阻塞,直到数据的到来
多路转接是一种高效的IO方式,可以同时监听多个套接字文件,但是不是一个一个的阻塞等待,而是等待多个文件,当有文件就绪时,立即进行IO操作。这样就可以使IO的等待时间重叠,提高效率,减少CPU的空闲时间。常见的多路转接技术有select,poll,epoll,它们都是系统调用接口,具体实现被系统封装与隐藏了。
select是一个多路复用(转接)输入/输出模型,它可以让程序监听多个套接字,在其中的一个或多个套接字准备好读写或者发送异常时通知程序
select函数原型如下:
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout)
select出错返回-1,并设置errno。返回0表示没有事件发生。有事件发生时,返回值表示发生事件的文件描述符数量
设置了超时时间,select就会返回在这段时间内,发生事件的文件描述符数量吗? 答案是不一定,发生了一个或多个事件,select可能也会返回。所以select可能提前返回,并且发生事件的文件描述符数量是不确定的,可能为1,可能大于1。我们要用FD_ISSET宏对每个文件描述符进行判断并处理
至于说fd_set结构体,这是一个long int类型(长度与操作系统和编译器有关,32位及以前的系统,长度为4字节,64位系统,长度为8字节。可以用sizeof检查你的平台上long int的大小)的数组。用来存放文件描述符,每一比特位表示一个文件描述符,1/0表示该文件描述符的某一事件是否发生的状态。有以下4个宏可以处理fd_set结构体
可以直接位操作fd_set,但推荐使用宏来操作fd_set,因为fd_set的内部实现因平台而异,直接位操作不仅破坏其封装性和可移植性,还可能引发错误
关于最后一个参数timeout,它是struct timeval类型的指针,以下是struct timeval结构体成员的具体信息
// tv_sec表示秒,tv_usec表示微秒
struct timeval {time_t tv_sec; /* seconds */suseconds_t tv_usec; /* microseconds */
};
使用tcp协议的四个步骤:socket,bind,listen,accept。为提高IO效率,使用select函数,
所以select可以根据监听套接字文件的状态变化,检测是否有读事件发生:如果文件从LISTEN->READABLE,就表示发生了读事件。除了LISTEN和READABLE状态,还有一些常见状态
select根据文件的状态,来判断是否有读事件,写事件或者异常事件发生,以返回发生事件的数量
#include "Socket.hpp"
#include void usage(char *process_name)
{cout << "usage: " << process_name << " port"<< endl;
}// 历史套接字数组和它的长度,长度为1024,表示可以同时运行的套接字数量
// 其实select只能同时监听1024个套接字
int fd_array[sizeof(fd_set) * 8] = {0};
int arr_num = (sizeof(fd_array) / sizeof(fd_array[0]));
// 数组初始值
#define DFL -1
#define BUF_SIZE 1024// select监听到了事件的发生,调用HandlerEvent处理事件
void HandlerEvent(int listen_sock, fd_set& readfds)
{for (int i = 0; i < arr_num; ++i){// 跳过默认值,寻找需要监听的套接字if (fd_array[i] == DFL)continue;// 如果发生了读事件if (FD_ISSET(fd_array[i], &readfds)){if (fd_array[i] == listen_sock){// 有新连接了,判断是否能获取该连接int j = 0;for (j = 0; j < arr_num; ++j){if (fd_array[j] == DFL)break;}if (j == arr_num){cerr << "当前队列已满" << endl;}else{// 处理事件uint16_t peer_port;string peer_ip;int server_sock = tcpSock::Accept(listen_sock, &peer_ip, &peer_port);if (server_sock < 0){cerr << errno << ": " << strerror(errno) << endl;// accept失败,暂时不管这个连接了continue;}// 将其添加到历史数组中fd_array[j] = server_sock;cout << peer_ip << "[" << peer_port << "] 连接..." << endl;}} // end of if (fd_array[i] == listen_sock)else{// 创建读缓冲区char read_buffer[BUF_SIZE] = {0};// 普通IO事件就绪,此时读取不会阻塞int ret = recv(fd_array[i], (void*)&read_buffer, sizeof(read_buffer) - 1, 0);// 读取出错if (ret < 0){cerr << errno << ": " << strerror(errno) << endl;// 注意,程序不要直接退出,应该关闭该服务套接字close(fd_array[i]);fd_array[i] = DFL;}// 对端关闭else if (0 == ret){cout << "peer close..." << endl;close(fd_array[i]);fd_array[i] = DFL;}// 读取成功else{ read_buffer[ret] = '\0';// 这里需要对读取的信息进行处理,暂时用打印替代cout << read_buffer;}}}}
}int main(int argc, char *argv[])
{// 判断调用者是否传入了端口号if (argc != 2){usage(argv[0]);exit(-1);}// 创建套接字int listen_sock = tcpSock::Socket();// 将用户传入的端口绑定到套接字上tcpSock::Bind(listen_sock, atoi(argv[1]));// 使监听套接字处于监听状态tcpSock::Listen(listen_sock);// 初始化历史套接字数组for (int i = 0; i < arr_num; ++i){fd_array[i] = DFL;}// 默认将listen套接字设置进fd数组fd_array[0] = listen_sock;// 不断地检测事件的发生while (true){fd_set readfds = {0};int max_fd = DFL;// 添加读事件监听集for (int i = 0; i < arr_num; ++i){// 默认值不需要监听,直接跳过,找要监听的套接字if (fd_array[i] == DFL)continue;// 设置套接字到监听事件集中FD_SET(fd_array[i], &readfds);// 需要维护select的第一个参数if (fd_array[i] > max_fd)max_fd = fd_array[i];}// 设置超时时间为5秒struct timeval timeout = {5, 0};// 只关心读事件int n = select(max_fd + 1, &readfds, nullptr, nullptr, &timeout);switch (n){case 0:cout << "没有事件发生,但超时了..." << endl;break;case -1:cerr << errno << ":" << strerror(errno) << endl;break;default:HandlerEvent(listen_sock, readfds);break;}}return 0;
}
poll也是一个用于实现多路转接的函数,其原型如下
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:struct pollfd类型的指针,可以想象成数组,存储了多个struct pollfd
nfds:表示fds的长度,需要监听的套接字数量,可以存在无效套接字
timeout:单位为毫秒,超时时间的设置
函数执行成功,返回监听的描述符集合中,发生事件的描述符个数。如果超时且没有任何事件发生,就返回0。如果调用失败,poll返回-1,并设置errno
poll比select使用简单,select有两个主要问题
poll就是为解决select的这两个问题而生的。这里有一个结构体:struct pollfd
// pollfd结构
struct pollfd {int fd; /* file descriptor */short events; /* requested events */short revents; /* returned events */
};
该结构体指明了需要监听的文件描述符,需要监听的事件(用户告诉内核),以及监听到的事件(内核告诉用户)。events和revents字段可以用以下常量来指定或测试不同类型的事件
关于这些字段或者更多的字段,需要用到时可以上网查。
这一小节所谈论的poll是指poll系统调用
poll系统调用是用户向内核发起的一种请求,使用poll系统调用可以监听多个文件描述符的状态,等待其中一个或多个就绪或超时。poll系统调用会调用poll函数,poll函数是文件描述符所属的设备或对象提供的一种检查状态的接口,它通常是一个设备驱动程序中实现的函数。poll系统调用在执行过程中,会遍历fds数组中的每个元素,并且调用其对应文件描述符的poll函数来检查其状态,将结果保存在revents域中。如果没有任何文件描述符就绪,poll系统调用会将当前进程挂起到等待队列中,并进入休眠状态。当有设备发生IO事件时,内核会唤醒等待队列中的进程,并重新检查fds数组中的每个元素。
不同类型的文件描述符,如套接字,终端,管道,可能含有不同的poll函数实现。但poll函数主要有两个功能
所以poll系统调用是用户和内核的一种交互方式,而poll函数则是poll系统调用执行过程中所需的一个接口函数
#include "Socket.hpp"
#include void usage(char *process_name)
{cout << "usage: " << process_name << " port"<< endl;
}#define FDS_SIZE 1024
struct pollfd fds[FDS_SIZE] = {0};
nfds_t fds_count = 1;
#define DFL -1
#define BUF_SIZE 1024// select监听到了事件的发生,调用HandlerEvent处理事件
void HandlerEvent(int listen_sock)
{for (int i = 0; i < fds_count; ++i){// 跳过默认值,寻找需要监听的套接字if (fds[i].fd == DFL)continue;// 如果发生了读事件if (fds[i].revents & POLLIN){// 有新连接了,判断是否能获取该连接if (fds[i].fd == listen_sock){int j = 0;for (j = 0; j < fds_count; ++j){if (fds[i].fd == DFL)break;}if (j == FDS_SIZE){cerr << "当前队列已满" << endl;}else{// 处理事件uint16_t peer_port;string peer_ip;int server_sock = tcpSock::Accept(listen_sock, &peer_ip, &peer_port);if (server_sock < 0){cerr << errno << ": " << strerror(errno) << endl;// accept失败,暂时不管这个连接了continue;}// 将其添加到监听队列中fds[j].fd = server_sock;// 监听读事件fds[j].events |= POLLIN;fds[j].revents = 0;++fds_count;cout << peer_ip << "[" << peer_port << "] 连接..." << endl;}} // end of if (fds[i].fd == listen_sock)else// 普通IO事件就绪,此时读取不会阻塞{// 创建读缓冲区char read_buffer[BUF_SIZE] = {0};// 读取数据int ret = recv(fds[i].fd, (void*)&read_buffer, sizeof(read_buffer) - 1, 0);// 读取出错if (ret < 0){cerr << errno << ": " << strerror(errno) << endl;// 注意,程序不要直接退出,应该关闭该服务套接字close(fds[i].fd);fds[i].fd = DFL;}// 对端关闭else if (0 == ret){cout << "peer close..." << endl;close(fds[i].fd);fds[i].fd = DFL;}// 读取成功else{ read_buffer[ret] = '\0';// 这里需要对读取的信息进行处理,暂时用打印替代cout << "收到了: " << read_buffer;}}}}
}int main(int argc, char *argv[])
{// 判断调用者是否传入了端口号if (argc != 2){usage(argv[0]);exit(-1);}// 创建套接字int listen_sock = tcpSock::Socket();// 将用户传入的端口绑定到套接字上tcpSock::Bind(listen_sock, atoi(argv[1]));// 使监听套接字处于监听状态tcpSock::Listen(listen_sock);// 默认将listen套接字设置进fd数组fds[0].fd = listen_sock;fds[0].events |= POLLIN;fds[0].revents = 0;fds_count = 1;// 初始化fds数组for (int i = 1; i < FDS_SIZE; ++i){fds[i].fd = DFL;fds[i].events = 0;fds[i].revents = 0;}// 设置超时时间为2秒int timeout = 2000;// 不断地检测事件的发生while (true){// 只关心读事件int n = poll(fds, FDS_SIZE, timeout);switch (n){case 0:cout << "没有事件发生,但超时了..." << endl;break;case -1:cerr << errno << ":" << strerror(errno) << endl;break;default:HandlerEvent(listen_sock);break;}}return 0;
}
对比select,poll有以下的优点
但是poll还是没有解决以下问题
epoll有三个主要接口:epoll_creat,epoll_ctl,epoll_wait
#include
int epoll_create(int size);
epoll_create用于创建一个epoll对象,返回其描述符,失败返回-1并设置errno。关于其唯一参数size:自从Linux2.6版本之后,可以忽略该参数,但是要将其设置为大于0的值
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
对epoll描述符的控制接口,可以实现对epoll控制描述符的添加,删除,修改
以下是struct epoll_event的具体成员
typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};
events可以是下面几个宏的集合:
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在监控事件中发生的事件
select与poll对于事件的监听采用轮询检测的方式,而epoll不再使用这种低效且浪费资源的方式,转而使用回调函数。利用内核的事件通知机制,当监听的文件描述符发生状态变化时,通过回调函数将其加入到就绪链表中,然后用户可以通过就绪链表获取已经就绪的文件描述符
epoll在内核中主要涉及以下数据结构:
epoll主要用三个结构来管理和存储fd:eventpoll,epitem,eppoll_entry。eventpoll是一个全局对象,对应一个epoll实例。其红黑树和双向链表分别用来存储所有注册到epoll的fd和所有就绪的fd。每个注册到epoll中的fd都有一个对应的epitem对象,它含有一个红黑树节点和一个双向链表节点,分别用来插入到eventpoll中的两个数据结构中,epitem包含了一个eppoll_entry的链表(其pwqlist结构就是用来链接eppoll_entry的)。eppoll_entry包含了等待队列头和等待队列节点(wait_queue_t),用来将fd挂载到设备驱动程序提供的等待队列上,并注册回调函数。
关于红黑树与就绪链表:
eppoll_entry主要用于ep_insert和ep_remove两个函数中,eppoll_entry在函数中的作用是:在等待队列中添加和删除文件描述符
当设备发送IO事件时,设备驱动会遍历其等待队列头对应的链表,并调用每个节点上注册的回调函数
综上,__poll_wait和ep_poll_callback都有将epitem加入到就绪队列中的功能,不同的是:__poll_wait是在设备驱动中被调用,而ep_poll_callback是在epoll_wait中被调用。__poll_wait是在设备事件发生时被动添加epitem,而ep_poll_callback是在用户请求时主动检查并添加epitem
struct epitem {struct rb_node rbn; /* 红黑树节点 */struct list_head rdllink; /* 双向链表节点 */struct epitem *next; /* 指向下一个epitem */struct epoll_filefd ffd; /* 文件描述符和文件指针 */struct eventpoll *ep; /* 所属的eventpoll指针 */struct epoll_event event; /* 事件类型和数据 */
};struct epoll_item {struct rb_node rbn;struct list_head rdllink;int nwait;struct list_head pwqlist;struct epitem *epi;
};
epitem和epoll_item是两个不同的结构
epoll相对于select和poll的优势:
#include "Socket.hpp"
#include #define MAXEVENTS 1024
#define BUF_SIZE 1024#define CRT_FAL -1
#define CTL_FAL -2
#define WAIT_FAL -3
#define ACP_FAL -4class epoll_server
{
public:epoll_server(uint16_t port, int listen_sockfd = -1, int epoll_fd = -1): _listen_sockfd(listen_sockfd), _epoll_fd(epoll_fd), _port(port){}~epoll_server(){if (-1 != _listen_sockfd)close(_listen_sockfd);if (-1 != _epoll_fd)close(_epoll_fd);}// 监听套接字的初始化void init_server();// 使用epoll进行IOvoid run_server();private:// IO事件的处理void handler_event(struct epoll_event* revs, int n);private:// 监听套接字fd与epoll实例fdint _listen_sockfd;int _epoll_fd;// epoll_server绑定的端口号uint16_t _port;
};void epoll_server::init_server()
{// 创建sock,绑定端口并使之处于监听状态_listen_sockfd = tcpSock::Socket();tcpSock::Bind(_listen_sockfd, _port);tcpSock::Listen(_listen_sockfd);cout << "init_server done" << endl;
}void epoll_server::run_server()
{_epoll_fd = epoll_create(128);if (-1 == _epoll_fd){cerr << errno << ": " << strerror(errno) << endl;exit(CRT_FAL);}struct epoll_event ev = {0};ev.events = EPOLLIN;ev.data.fd = _listen_sockfd;// 注册文件到epoll实例int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, _listen_sockfd, &ev);if (-1 == ret){cerr << errno << ": " << strerror(errno) << endl;exit(CTL_FAL);}struct epoll_event revs[MAXEVENTS] = {0};int timeout = 2000; // 设置超时时间2秒while (true){int n = epoll_wait(_epoll_fd, revs, MAXEVENTS, timeout);switch (n){case -1:cerr << errno << ": " << strerror(errno) << endl;exit(WAIT_FAL);break;case 0:cout << "超时事件内没有事件发生..." << endl;break;default:handler_event(revs, n);break;}}
}void epoll_server::handler_event(struct epoll_event* revs, int n)
{for (int i = 0; i < n; ++i){// 发生了读事件if (revs[i].events & EPOLLIN){// 监听到一个新连接if (revs[i].data.fd == _listen_sockfd){string peer_ip;uint16_t peer_port;int server_sock = tcpSock::Accept(_listen_sockfd, &peer_ip, &peer_port);if (-1 == server_sock){cerr << errno << ": " << strerror(errno) << endl;exit(ACP_FAL);}// 向epoll实例中注册这个服务套接字struct epoll_event ev= {0};ev.events = EPOLLIN;ev.data.fd = server_sock;int ret = epoll_ctl(_epoll_fd, EPOLL_CTL_ADD, server_sock, &ev);if (-1 == ret){cerr << errno << ": " << strerror(errno) << endl;exit(CTL_FAL);}cout << "与客户端[" << peer_ip << "]:" << peer_port << "连接成功" << endl;}// 监听到普通IO事件else{char read_buff[BUF_SIZE] = {0};recv(revs[i].data.fd, read_buff, BUF_SIZE, 0);cout << "普通IO:" << read_buff;}}// 发生了写事件,暂时不处理else{}}
}
// main.cc
#include "epoll_server.hpp"void usage(char *process_name)
{cout << "usage: " << process_name << " port"<< endl;
}int main(int argc, char* argv[])
{// 判断调用者是否传入了端口号if (argc != 2){usage(argv[0]);exit(-1);}epoll_server eserver(atoi(argv[1]));eserver.init_server();eserver.run_server();return 0;
}