一. poll
和select一摸一样的定位,适用场景也是一样的
1. 和select的区别
- poll解决了select能检测的文件描述符是有上限的
- 将用户告诉内核让OS帮我们关心哪些文件描述符上面的哪些事件。将内核告诉用户哪些文件描述符上的哪些事件是已就绪的。
poll将这两个过程分离,不用在每次调用poll的时候,重新添加fd以及fd关心的事件
2. poll函数
#include <poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);
- 是一个结构体指针(相当于一个结构体数组),代表你想告诉OS哪些文件描述符上的哪些事件要让OS帮你关心
- 代表你这个结构体指针,一共有几个元素
- 代表超时时间。
1. 设为1000,则每隔1S timeout一次
2. 设为0 即为非阻塞轮询
3. 设为5000,则每隔5S timeout一次,在这5S 内阻塞
4. -1 代表永久阻塞
返回值
1. >0 代表有几个文件描述符就绪
2. =0 代表timeout
3. <0 代表失败
struct pollfd 结构
struct pollfd{
int fd; //哪个文件描述符
short events; //用户告知内核你要给我关心哪些事件
short revents; //内核告知用户哪些文件描述符上的哪些事件已经就绪
}
events与revents的取值
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级待数据可读(Linux不支持) | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
重点关注POLLIN和POLLOUT
3. 实现
- 因为传入的是结构体指针,所以我们想给它弄多大,就弄多大,只要系统空间足够,所以他能检测的文件描述符是没有上限的
- 在代码实现时,不用在每次调用poll时重新添加需要关心的文件描述符
sock.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
namespace ns_sock{
class Sock{
public:
static int Socket(){
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0){
std::cerr << "socket error" << std::endl;
exit(1);
}
int opt = 1;
setsockopt(sock,SOCK_SOCKET,SOCK_REUSEADDR,&opt,sizeof(opt));
}
static bool Bind(int sock,unsigned short port){
struct sockaddr_in local;
memet(&local,0,sizeof(0));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){
std::cerr << "bind error" << std::endl;
exit(2);
}
return true;
}
static bool Listen(int sock,int backlog){
if(listen(sock,backlog) < 0){
std::cerr << "listen error" << std::endl;
exit(3);
}
return true;
}
}
}
poll_server.hpp
#include "sock.hpp"
#include <poll.h>
namespace std{
class PollServer{
private:
int listen_sock;
int port;
public:
PollServer(int _port):port(_port)
{}
void InitServer(){
listen_sock = ns_sock::Sock::Socket();
ns_sock::Sock::Bind(listen_sock,port);
ns_sock::Sock::Listen(listen_sock,5);
}
void Run(){
struct pollfd rfds[64];
for(int i = 0; i < 64; ++i){
rfds[i].fd = -1;
rfds[i].events = 0;
rfds[i].revents = 0;
}
for(;;){
switch(poll(rfds,64,-1)){
case 0:
std::cout << "timeout" << std::endl;
break;
case 1:
std::cout << "poll error" << std::endl;
break;
default:
for(int i = 0; i < 64; ++i){
if(rfds[i].fd == -1){
continue;
}
if(rfds[i].revents & POLLIN){
if(rfds[i].fd = listen_sock){
std::cout<<"get a new link..." <<std::endl
}
else{
//recv
}
}
}
}
break;
}
}
}
~PollServer()
{}
};
}
server.cc
#include "poll_server.hpp"
#include <iostream>
#include <string>
#include <cstdlib>
static void Usage(std::string proc){
std::cerr << "Usage" << "\n\t" << proc << "port" << std::endl;
}
int main(int argc,char* argv[]){
if(argc != 2){
Usage(argv[0]);
exit(4);
}
unsigned short port = atoi(argv[1]);
ns_poll::PollServer* ps = new ns_poll::PollServer(port);
ps->InitServer();
ps->Run();
return 0;
}
4. poll缺点
- poll的检测机制与select一样,所以OS在检测fd就绪时, 需要遍历。所以当有大量的连接的时候,内核同步poll底层遍历,成本会越来越高。
- 虽然输入输出分离了,但输入输出还是需要拷贝
二. epoll
定位与select与poll一致
1. epoll初识
按照 man 手册的说法:是为处理大批量句柄而作了改进的poll
它是在2.5.44内核(epoll(4) is a new API introduced in Linux kernel 2.544)
它几乎具备了之前所说的一切优点,被公认为Linux2.6性能最好的多路I/O就绪通知方法
2. epoll的核心工作
等 (帮你进行文件描述符就绪等待的工作)
3. epoll的相关系统调用
不像 select、poll 只提供了一个函数调用,它提供了三个相关的系统调用
epoll_create
创建epoll模型(调用其系统调用,会在OS层面上创建epoll相关数据结构)
int epoll_create(int size);
//返回值为一个文件描述符
- 自从Linux2.6.8之后,size参数是被忽略的
- 用完之后,必须调用close()关闭
epoll_ctl
向epoll模型中添加一些东西
向特定的 epoll模型 进行添加、删除、修改用户想关心哪些文件描述符上面的哪些事件
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型
参数:
1. epoll_create()的返回值(epoll的句柄)
2. 表示动作,用三个宏表示
3. 需要监听的fd
4. 告诉内核需要监听什么事
第二个参数的取值:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
底层结构:
typedef union epoll_data{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event{
uint32_t events; //关注一下fd上的事件
epoll_data_t data; //附加参数、附加内容(现在不用)
};
events可以是几个宏的集合:
EPOLLIN:表示对于的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据来
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
EPOLLONESHOT:只监听一次事件,监听完这次事件后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll_wait
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
参数:
1. 在哪个epoll模型进行等待。在一个过程中可存在多个epoll模型
2. 从OS拿到用户曾经想关心的哪些文件描述符上的哪些事件就绪。events是分配好的epoll_event结构体数组(内核只负责把数据拷贝到这个events数组中,不会去帮助我们在用户态分配内存)
3. maxevents告知内核这个events有多大,这个maxevent的值不能大于创建epoll_create()时的size
4. 参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)
返回值:
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目。如果返回0表示已超时,返回小于0表示函数失败
所以在epoll中,整个过程,由三个函数分别承担:
用户告知内核:你要帮我关注一下哪些文件描述符上的哪些事件(epoll_ctl())
内核告知用户:哪些文件描述符上的哪些事件已经就绪了(epoll_wait())
select和poll虽然传参有差别,使用有差别,但他们都要做一件事,调用时是用户告知内核,返回时是内核告知用户(一个函数承担两种工作职责)
select需借助第三方数组,poll也借助了,但不仅仅是保存文件描述符(用户定义,用户维护,所有增删查改都得自己维护)
epoll不需要借助第三方数组,都由OS帮epoll做了