高级IO--poll,epoll

poll

接口流程介绍:
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
 fds: 要监控的描述符事件结构体
  struct poll{ int nfds; //要监控的描述符   short events; //想要监控的事件   short revents; //实际就绪的事件}
   events: POLLIN----可读  POLLOUT----可写
   revents:当poll接口调用返回的时候,这个描述符实际就绪的事件就会被写入revents中,程序员通过这个成员进行判断
 nfds:第一个参数描述符事件结构体数量
 timeout:超时等待事件----毫秒
 返回值:返回值大于0表示就绪的描述符个数,返回值=0表示监控超时,返回值<0表示监控出错

流程:

1.定义描述符事件结构体数组,将需要监控描述符以及对应的事件信息填充到数组中

eg:struct pollfd fds[10]; fds[0].fd = 0; fds[0].events=POLLN | POLLOUT;----对标准输入监控可读事件以及可写事件

2.发起监控调用poll,将数组中数据拷贝到内核中进行轮询遍历监控,有描述符就绪或者超时等待后返回,返回时将这个描述符就绪的事件填充到对应结构体的revents中(如果描述符就绪,则revents中的数据为0)
3.监控调用返回后,程序员在程序中遍历数组中每个节点的revents确定当前节点的描述符就绪了什么事件,进而进行对应操作

#include<poll.h>
#include<unistd.h>
#include <stdio.h>
int main(){
  struct pollfd poll_fd;
  poll_fd.fd = 0;
  poll_fd.events = POLLIN;
  for(; ;){
    int ret = poll(&poll_fd, 1, 1000);
    if(ret < 0){
      perror("poll");
      continue;
    }
    if(ret == 0){
      printf("poll timeout\n");
      continue;
    }
    if(poll_fd.revents == POLLIN){
      char buf[1024] = {0};
      read(0, buf, sizeof(buf)-1);
      printf("stdin:%s", buf);
    }
  }
}

优缺点分析:

1.poll通过描述符事件结构体的方式简化了select的三种描述符集合的操作流程
2.poll所能监控的描述符,没有了最大数量限制(要监控多少描述定义多大数组即可)
3.poll每次监控不需要重新定义事件结构体

1.监控原理是在内核中进行轮询判断,会随着描述符的增多而性能下降
2.无法跨平台移植
3.每次监控调用返回后也需要程序员在程序中进行遍历判断才能知道哪个描述符就绪了哪个事件
4.依然每次需要将数组拷贝到内核中监控
5.监控的超时等待事件最精细到毫秒

epoll:linux下最好的多路转接模型
接口流程介绍:

1.在内核中创建eventpoll结构体,返回一个描述符作为代码中的操作句柄

int epoll_creat(int size);
size:要监控描述符的最大数量,在linux下2.6.8之后被忽略,只要大于0即可

2.对需要监控的描述符组织事件结构体,将描述符以及对应事件结构添加到内核的eventpoll结构体中

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epfd:epoll操作句柄
op:EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DELL;
fd:想要监控的描述符
event:监控描述符对应的事件结构体信息
stuct epoll_event{
  uint32_t events; //表示监控的事件,以及监控调用返回后事件就绪的事件----EPOLLIN/EPOLLOUT
  union epoll_data_t{ int fd; //描述符 void *ptr;}
每一个需要监控的描述符都会有一个对应的事件结构,当描述符就绪了监控事件后,就会将这个结构体返回给程序员

3.开始监控,当有描述符就绪或者等待超时后调用返回

int epoll_wait(int epfd, struct epoll_event* evs, int maxevents, int timeout);
epfd:epoll的操作句柄,通过这个句柄找到内核中指定的eventpoll结构;
evs:epoll_event描述符的事件结构体首地址,用于获取就绪的描述符对应的事件结构体>>maxevents:evs数组的节点数量,主要防止就绪太多,向evs中放置的时候越界访问
timeout:超时等待时间----毫秒

封装一个epoll类

class Epoll{
public:
	Epoll(){
		_epfd = epoll_create(1);
		if(_epfd < 0){
			perror("epoll create error");
			exit(0);
		}
	}
	bool Add(TcpSocket& sock){
		int fd = sock.Getfd();
		//定义一个描述符对应的事件结构体 epoll_event;
		strcut epoll_event ev;
		ev.data.fd = fd;
		ev.events = EPOLLIN;
		//epoll_ctl(epoll句柄,操作类型,监控的描述符,描述符对应的事件结构)
		int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
		if(ret < 0){
			perror("epoll_ctl_add error");
			return false;
		}  
		return true;
	}
	
	bool Del(TcpSocket& sock){
		int fd = sock.Getfd();
		int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, NULL);
		if(ret < 0){
			perror("epoll_ctl_del error");
			return false;
		}
		return true;
	}
	
	bool Wait(std::vector<TcpSocket>* list, int timeout = 3000){
		//epoll_wait(句柄,接受就绪结构的数组,数组节点数量,超时事件)
		//返回值大于0表示就绪的个数
		struct epoll_event evs[10];
		int ret = epoll_wait(_epfd, evs, 10, timeout);
		if(ret < 0){
			perror("epoll_wait error");
			return false;
		}
		else if(ret == 0){
			printf("epoll timeout\n");
			return false;
		}
		for(int i = 0; i < ret; i++){
			if(evs[i].events & EPOLLIN){ //判断是否就绪的可读事件
				TcpSocket sock;
				sock.SetFd(evs[i].data.fd);
				list->push_back(sock);
			}
			if(evs[i].events & EPOLLOUT){ //判断是否就绪可读事件
				//可写事件操作
			}
		}
		return true;
	}
private:
	int _epfd;
};

使用epoll实现并发服务器

#include<iostream>
#include"select.hpp"

int main(int argc, char* argv[]){
  if(argc != 3){
    std::cout << "Usage:./main ip port\n";
    return -1;
  }
  std::string ip = argv[1];
  uint16_t port = std::stoi(argv[2]);
  TcpSocket lst_sock;
  CHECK_RET(lst_sock.Socket());
  CHECK_RET(lst_sock.Bind(ip, port));
  CHECK_RET(lst_sock.Listen());

  Epoll e;
  e.Add(lst_sock);
  while(1){
    std::vector<TcpSocket> list;
    bool ret = e.Wait(&list);
    if(ret == false){
      continue;
    }
    for(auto sock : list){
      if(sock.GetFd() == lst_sock.GetFd()){
        //就绪的描述符和监听套接字描述符,表示需要获取新连接
        TcpSocket new_sock;
        ret = lst_sock.Accept(&new_sock);
        if(ret == false){
          continue;
        }
        e.Add(new_sock);
      }else{
        //就绪的描述符不是监听套接字,就是通信套接字,则进行recv
        std::string buf;
        ret = sock.Recv(&buf);
        if(ret == false){
          sock.Close();
          continue;
        }
        std::cout << "client say: " << buf << std::endl;
        std::cout << "sever say: ";
        std::cin >> buf;
        ret = sock.Send(buf);
        if(ret == false){
          sock.Close();
		      e.Del(sock);
		      continue;
        }
      }
    }
  }
  lst_sock.Close();
  return 0;
}

epoll优缺点分析:

1.监控的描述符没有数量上限
2.所有描述符事件信息只需要向内核拷贝一次
3.监控采用异步阻塞,性能不会随着描述符增多而下降
4.直接向程序员返回就绪描述符事件信息,可以让程序员在程序中直接对就绪的描述符操作,而没有空遍历

1.无法跨平台
2.监控的超时时间最多精细到毫秒

IO事件就绪:

可读事件:接收缓冲区中的数据大小大于低水位标记;
可写事件:发送缓冲区中的剩余空间大小大于低水位标记;
低水位标记:就是一个基准值,通常默认一个字节;

epoll的事件触发模式:如何触发IO就绪事件

水平触发:默认触发模式

对于可读事件:接收缓冲区的数据大小大于低水位标记,就会触发
对于可写事件:发送缓冲区剩余空间大小大于低水平标记,就会触发
边缘触发:需要在epoll_event结构体中的events成员中设置EPOLLET
对于可读事件:(不关注接收缓冲区是否有数据)每次有新数据到来时候,才会触发一次事件(每次新数据到来最好能够一次性将所有数据读出,否则epoll的边缘不会触发第二次,只有等下一次有新数据到来的时候才会触发)
对于可写事件:剩余空间只有从无到有的时候才会触发

为什么要有边缘触发?水平触发和边缘触发哪个好?

边缘触发,是有新数据到来就会触发事件

假设现在要接收一条数据,但是发现缓冲区中的数据不完整,如果读取数据出来,就需要额外维护,等到下一条触发事件的时候,补全上一条数据,若是因为数据不完整,不把数据读出来,则水平触发会一直触发事件(但是读取又读取不到完整的数据),这时候使用边缘触发比较好,因为边缘触发是在有新数据到来时才会触发事件
常用于一种一直触发事件,但是又不是每一次都要操作的情况

epoll的惊群问题:

一个执行流中,若添加了特别多的描述符进行监控,则轮询处理会比较慢,因此会在多个执行流中创建epoll,每个epoll监控一部分描述符,分摊压力----但是这种操作因为无法确定哪个操作符活跃,有可能存在均衡问题。
每个执行流epoll都监控所有描述符,谁抢到事件谁处理
一个描述符有事件到来,若是惊起多个epoll怎么办?
加锁

多路转接模型进行服务器并发处理与多进程/多线程并发并行处理有什么区别?

多路转接模型进行服务器并发处理:指的是在单执行流中进行轮询处理就绪的描述符,若描述符较多,则很难做到负载均衡(最后一个描述符要很长时间,前边描述符处理完了才能处理到)
多进程/多线程多执行流进行并发/并行:指的是操作系统通过轮询调度执行流实现每个执行流的处理

系统内核层面的负载均衡,不需要用户态做过多操作

基于两种方式特点:通常两者可以搭配一起使用
多路转接模型监控大量描述符,哪个描述符就绪有事件了,再去创建执行流进行处理,防止直接为描述符创建执行流,但是描述符没有事件到来,空耗资源
多路转接模型仅适用于:有大量描述符需要监控,但是同一时间只有少量活跃的场景

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值