zl程序教程

您现在的位置是:首页 >  .Net

当前栏目

IO多路复用API总结

2023-04-18 15:56:46 时间

IO 多路复用概述

I/O 多路复用技术是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。

在IO多路复用技术描述前,先讲解下同步,异步,阻塞,非阻塞的概念。

网络IO模型

linux网络IO中涉及到的模型如下:

(1)阻塞式IO

(2)非阻塞式IO

(3)IO多路复用

(4)信号驱动IO

(5)异步IO

今天不谈信号驱动IO,略过..

同步/异步

在学习IO模型的时候,我们必须明确一个概念,处理 IO 的时候,阻塞和非阻塞都是同步 IO。

只有使用了特殊的 API 才是异步 IO,例如Linux网络中的AIO。

再看下POSIX对同步和异步这两个术语的定义:

  • 同步IO操作:导致请求进程阻塞,直到I/O操作完成;
  • 异步IO操作:不导致请求进程阻塞;

通俗的理解下同步和异步

  • 同步:当执行系统调用read时,需要用户等待内核完成从内核缓冲区到用户缓冲区的数据拷贝。
  • 异步:当执行异步IO操作例如aio_read时,用户不需要等待,只需要接收内核完成操作的通知,由内核来完成数据的读取。

阻塞/非阻塞

在知晓阻塞和非阻塞都是同步 IO后,阻塞和非阻塞就很好理解了

阻塞IO:由系统调用read,导致线程一直等待数据返回。

阻塞等待模型

非阻塞IO:系统调用read后立即返回一个状态,当数据达到内核缓冲区之前都是非阻塞的,即返回一个系统调用状态。

非阻塞等待模型

闪客的动图做的非常的形象,上述gif动图来源「低并发编程」

IO多路复用

IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;

select

select 是操作系统提供的系统调用函数,select()用来等待文件描述词(普通文件、终端、伪终端、管道、FIFO、套接字及其他类型的字符型)状态的改变。是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。

通过select,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理:

select原理

头文件

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

select调用

拥塞函数,拥塞等待文件描述符事件的到来

int select(int maxfdp
 , fd_set *readset
 , fd_set *writeset
 , fd_set *exceptset
 ,struct timeval *timeout);

参数说明:

maxfdp:被监听的文件描述符的最大值,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的;

readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。

timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间,timeout == 0,select立即返回

timeval结构体

struct timeval
{      
    long tv_sec;   /*秒 */
    long tv_usec;  /*微秒 */   
};

select置位

int FD_ZERO(int fd, fd_set *fdset);   //一个 fd_set类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset);  //清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set);   //设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位

当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零

调用 select函数,拥塞等待文件描述符事件的到来 ;如果超过设定的时间,则不再等待,继续往下执行

select返回后,用FD_ISSET测试给定位是否置位:

if(FD_ISSET(fd, &rset)   
{ 
    ... 
    //do something  
}

fd_set结构体

fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。

select使用

整个 select 的流程图如下:

Demo1: 基于select的点对点通信

基于select的点对点通信

简易聊天室select版本

运行效果如下:

简易聊天室select版本

完整代码阅读全文转跳或者发送文末关键字..

poll调用

Poll就是监控文件是否可读的一种机制,作用与select一样。

#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

参数说明

struct pollfd

fds:是一个struct pollfd结构类型的数组,列出了我们需要poll()检查的文件描述符

typedef struct pollfd {
        int fd;           /* 需要被检测或选择的文件描述符*/
        short events;     /* 对文件描述符fd上感兴趣的事件 */
        short revents;    /* 文件描述符fd上当前实际发生的事件*/
} pollfd_t;
events:想要监听的事件

revents:实际上发生的事件
POLLIN
POLLOUT
POLLPRI
POLLRDHUB
POLLHUP
POLLERR

nfds

指定了fds中元素的个数,nfds_t为无符号整形

timeout

决定阻塞行为,一般如下:

  • -1:一直阻塞到fds数组中有一个达到就绪态或者捕获到一个信号
  • 0:不会阻塞,立即返回
  • >0:阻塞时间

返回值

  • >0:数组fds中准备好读、写或出错状态的那些socket描述符的总数量;
  • ==0:数组fds中没有任何socket描述符准备好读、写,或出错;此时poll超时
  • -1:poll函数调用失败

poll使用

#include <stdio.h>
#include <poll.h>
#include <string.h>
int main()
{
 int timeout = 0;      
 char buf[1024];
 struct pollfd fd_poll[1];    //设置只有一个事件
 while(1){
  fd_poll[0].fd = 0;      
  fd_poll[0].events = POLLIN;
  fd_poll[0].revents = 0;    
  memset(buf, '', sizeof(buf));
  switch( poll(fd_poll, 1, -1) ){
   case 0:
    perror("timeout!");
    break;
   case -1:
    perror("poll");
    break;
   default:
    {
     if( fd_poll[0].revents & POLLIN )
     {

      gets(buf);
      printf("buf : %s
",buf);
     }
    }
    break;
  }
 }
 return 0;
}

makefile

tcp_poll:tcp_poll.c
    gcc -o $@ $^
.PHONY:clean
clean:
    rm -f tcp_poll

epoll调用

epoll没有对描述符数目的限制,它所支持的文件描述符上限是整个系统最大可以打开的文件数目,例如,在1GB内存的机器上,这个限制大概为10万左右。

epoll只有 epoll_createepoll_ctlepoll_wait 这三个系统调用。

第一步,创建一个 epoll 句柄

第二步,向内核添加、修改或删除要监控的文件描述符。

第三步,发起了 select() 调用

epoll原理

其定义如下:

#include <sys/epoll.h>

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);

epoll_create

#include <sys/epoll.h>

int epoll_create(int size);

调用epoll_create方法创建一个epoll的句柄,使用完epoll后使用close函数进行关闭

epoll_ctl

#include <sys/epoll.h>

int epoll_ctl(int epfd //第一个参数epfd:epoll_create函数的返回值。
 , int op   //第二个参数events:表示动作类型。有三个宏来表示
 , int fd   //第三个参数fd:需要监听的fd。
 , struct epoll_event *event);//第四个参数event:告诉内核需要监听什么事件。

op:

  • EPOLL_CTL_ADD:注册新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从 epfd 中删除一个 fd。

fd:需要注册监视对象文件描述符

struct epoll_event

// 感兴趣的事件和被触发的事件
struct epoll_event {
    __uint32_t events; // Epoll events
    epoll_data_t data; // User data variable
};
// 保存触发事件的某个文件描述符相关的数据
typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;
Epoll Events:

EPOLLIN:表示对应的文件描述符可读(包括对端Socket);

EPOLLOUT:表示对应的文件描述符可写;

EPOLLPRI:表示对应的文件描述符有紧急数据可读(带外数据);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET:将EPOLL设为边缘触发(Edge Triggered),这是相对于水平触发(Level Triggered)而言的.

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket,需要再次添加.

例如:

struct epoll_event ep_ev;
int accept_sock = accept(listen_sock,(struct sockaddr*)&remote,&len);
ep_ev.events = EPOLLIN | EPOLLET;
ep_ev.data.fd = accept_sock;
epoll_ctl(epoll_fd,EPOLL_CTL_ADD,accept_sock,&ep_ev)

epoll_wait

收集在epoll监控的事件中已经发生的事件

#include <sys/epoll.h>

int epoll_wait(int epfd  //第一个参数epfd:epoll_create函数的返回值。
 , struct epoll_event *events 
 , int maxevents
 , int timeout);   //超时时间(毫秒)

第一个参数epfd:epoll_create函数的返回值。

第二个参数events:是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据赋值到这个event数组中,不会去帮助我们在用户态分配内存)

第三个参数maxevents:maxevents告诉内核这个events数组有多大,这个maxevents的值不能大于创建epoll_create时的size。

第四个参数:是超时时间(毫秒),如果函数调用成功,则返回对应IO上已准备好的文件描述符数目,如果返回0则表示已经超时。

基于epoll的简易http服务器

基于epoll的简单回显服务器

浏览器输入:http://服务器ip:8000/例如,http://49.234.35.128:8000/

基于epoll的简易http服务器