高效IO——多路转接之poll
Posted 两片空白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高效IO——多路转接之poll相关的知识,希望对你有一定的参考价值。
目录
前言
poll是针对select的缺点,做了改进。
主要针对了select的两个缺点:
- select等待的文件描述符有上限,为sizeof(fd_set)*8。
- selectfd_set参数是输入输出参数,每次select前都需要重新设定fd_set变量。
poll改进为:
- 可以等待的文件描述符没有上限。
- 文件描述符事件输入和输出用的不是一个变量,每次poll前不需要重新设定。
但是poll和select功能是一样的,主要的工作还是等多个文件描述符的时间就绪。
一.poll函数接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
接口作用:监听多个文件的事件是否就绪。
参数 | 作用 |
fds | poll函数监听的结构列表。一个元素中,报汉三个部分:文件描述符,监听事件集合,返回事件集合。指针指向struct pollfd数组。下面有详细介绍。 |
nfds | 表示fds数组的长度 |
timeout | 表示poll函数的超时时间范围毫秒。设置为-1:阻塞等待;0:非阻塞等待;具体某一值:等待时间。 |
返回值:
- 小于0:poll调用出错
- 等于1:超过等待事件
- 大于0:表示等待的文件中有多少个文件事件就绪。
1.1 struct pollfd 结构介绍
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
成员 | 含义 |
fd | 需要监听文件的文件描述符 |
events | 用户通知内核,需要监听的事件,这个是由用户设置 |
revents | 内核通知用户,监听事件是否就绪,这个是有内核设置 |
events和revents取值:
我们主要用到的是POLLIN和POLLOUT两个。
在struct pollfd中,events和revents的类型是short类型。上面的每一个值都是一个宏,在二进制中都只有一个位为1,并且为1的那一位位置不同。
当需要监视一个文件的多个事件时,我们可以将不同宏按位或起来,这个方法为多标志位法。
当需要监视一个文件的多个事件时,可以使用多标志法,将不同的宏或起来。
比如:需要监视一个文件的读和写事件,对应宏为POLLIN和POLLOUT。
假设:POLLIN的二进制表示为0000 0000 0000 0001
POLLOUT的二进制表示为0000 0000 0000 0010
按位或起来后:即events = POLLIN | POLLOUT = 0000 0000 0000 0011
内核怎么是知道要监视文件的哪个事件:
操作系统只要将事件按位与上对应宏,如果为真,就说明需要监视对应的事件。
即:if(events & POLLIN)为真说明需要监视读事件。if(events & POLLOUT)说明需要监视写事件。
用户怎么确认什么事件就绪:
判断返回的pollfd结构的revents,与对应事件按位与即可。
如:if(revents & POLLIN)说明读事件就绪。
1.2 poll简单使用
编写一个从标准输入读数据 的poll
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
int main(){
struct pollfd fds[1];
//监视标准输入的读
fds[0].fd = 0;
fds[0].events = POLLIN;//由用户填写
fds[0].revents = 0;//由内核填写
int timeout = -1;
char c = 0;
while(1){
int n = poll(fds,1,timeout);
if(n > 0){
//就绪
if(fds[0].revents & POLLIN){
printf("read ready...\\n");
int s = read(0, &c, 1);
if(s > 0){
printf("%c",c);
}
else{
perror("read error\\n");
}
}
}
else if(n == 0){
//超时
printf("poll timeout...\\n");
}
else{
//出错
perror("poll error\\n");
}
}
return 0;
}
注意:如果一个事件就绪,但是不进行处理,该事件会一直是就绪的。
比如:监听标准输入的读事件,当输入字符,此时标准输入的读事件是就绪的,但是,如果用户不去读,标准输入的读事件就一直是就绪的。
二.socket就绪条件
读就绪
- 在socket内核中,接收缓存区的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读取该文件。
- 监听的socket上有了新的连接请求,socket连接请求也是以读的方式获取的。
- socketTCP通信,对端关闭连接,此时对该socket读返回0.
- socket上有未处理的错误。
写就绪
- socket内核中,发送缓冲区中的空闲位置大小,大于或者等于低水位标记。
- socket使用非阻塞connect连接成功或失败之后。
- socket的写操作被关闭,对于写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
- socket上有为读取的错误。
异常就绪:
- socket收到带外数据。
三.poll优缺点
3.1 poll优点
对比select:
- 可以等待的文件描述符没有上限。
- oillfd结构包含了要监视的event和就绪的revents,输入输出变量不是一个,在调用poll前不需要再重新设定。
3.2 poll缺点
poll监听的文件描述符数量增多时:
- 在内核,要知道那些文件的事件是就绪的,需要对所有要监控的文件进行轮询检测,这样当监听的文件过多时,效率不会很高。
- 每次调用poll都需要把大量的pollfd结构从用户拷贝到内核中。每次调用poll都需要重新将要监视文件事件重新从用户拷贝到内核。
四.poll的使用
- 要用一个pollfd数组来保存要监视的文件,但是不需要在poll前重新设定数组。
- poll可以等待的文件是无限的,pollfd数组有大小限制,但是这是用户限制的,并不影响poll可以等待无限个文件。
- 判断一个文件是哪个事件,用pollfd的revents与对应的事件按位与判断。
- 如果就绪文件为是socket的返回值,说明有连接来了,要将accpet返回值加入到等待数组中,反之说明IO就绪,进行读或者写。
- 读就绪,当读返回值为0,说明对端关闭,不需要等待,close对应文件,并且在数组中删除。
连接:
#pragma once
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#define BACKLOG 5
using namespace std;
class Sock{
public:
static int Socket(){
int sock = 0;
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
cerr<<"socket error"<<endl;
exit(1);
}
return sock;
}
static void Bind(int sock, int port){
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htons(INADDR_ANY);
if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){
cerr << "bind error"<<endl;
exit(2);
}
}
static void Listen(int sock){
if(listen(sock,BACKLOG) < 0){
cerr << "listen error"<<endl;
exit(3);
}
}
static int Accept(int sock){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
return accept(sock, (struct sockaddr *)&peer, &len);
}
static void SetSockOpt(int sock){
int opt =1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
};
服务器代码:
#pragma once
#include "Sock.hpp"
#define SIZE 20
class PollServer{
private:
int _lsock;
int _port;
struct pollfd fds[SIZE];//保存监视的文件
public:
PollServer(int lsock = -1, int port = 8081)
:_lsock(lsock)
,_port(port)
{}
void InitServer(){
_lsock = Sock::Socket();
Sock::SetSockOpt(_lsock);
Sock::Bind(_lsock, _port);
Sock::Listen(_lsock);
//初始化监视的文件
for(int i=0; i < SIZE; i++){
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
//加入连接套接字
fds[0].fd = _lsock;
fds[0].events = POLLIN;
fds[0].revents = 0;
}
//找到文件描述符为-1的加入到数组中
void AddSock2Fds(int sock){
int i =0;
for(; i < SIZE; i++){
if(fds[i].fd == -1){
break;
}
}
if(i >= SIZE){
cout<<"full"<<endl;
close(sock);
}
else{
fds[i].fd = sock;
fds[i].events = POLLIN;//设置为读事件
fds[i].revents = 0;
}
}
//删除一个不用监视的文件
void DelSock(int i){
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
void Handler(){
for(int i = 0; i < SIZE; i++){
//用revents判断是否就绪
if(fds[i].revents & POLLIN){
if(fds[i].fd == _lsock){
//有连接来了,加入到fds中
int sock = Sock::Accept(_lsock);
if(sock >= 0){
cout <<"get a link..."<<endl;
AddSock2Fds(sock);
}
else{
cerr << "accept error"<<endl;
}
}
else{
//IO就绪
char buf[10240] ={0};
int n = recv(fds[i].fd, buf, sizeof(buf),0);
if(n > 0){
cout << "client# "<<buf<<endl;
}
else if(n == 0){
//对端关闭
cout<<"client close..."<<endl;
close(fds[i].fd);
DelSock(i);
}
else{
cerr << "recv error"<<endl;
close(fds[i].fd);
DelSock(i);
}
}
}
}
}
void Start(){
int timeout = -1;
while(1){
int num = poll(fds, SIZE, timeout);
if(num > 0){
Handler();
}
else if(num == 0){
//超时
cerr << "timeout..."<<endl;
}
else{
cerr << "poll error..."<<endl;
}
}
}
~PollServer(){
for(int i =0; i < SIZE; i++){
if(fds[i].fd != -1){
close(fds[i].fd);
}
}
}
};
#include "PollServer.hpp"
void Notice(){
cout << "Notice:\\n\\t"<<"please port"<<endl;
}
int main(int argc, char* argv[]){
if(argc != 2){
Notice();
exit(6);
}
PollServer *ps = new PollServer(atoi(argv[1]));
ps->InitServer();
ps->Start();
delete ps;
return 0;
}
以上是关于高效IO——多路转接之poll的主要内容,如果未能解决你的问题,请参考以下文章
五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证
五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证