第九章 I/O复用

9.1 select系统调用

  • 在一段指定的事件内,监听用户感兴趣的文件描述符上:可读、可写、异常 事件、

9.1.1 select API

#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

//fd_set结构体定义如下
#include <typesizes.h>
#define __FD_SETSIZE 1024

#include <sys/select.h>
#define FD_SETSIZE __FD_SETSIZE
typedef long int __fd_mask;
#undef  __NFDBITS
#define __NFDBITS (8*(int)sizeof(__fd_mask))
typedef struct
{
#ifdef __USE_XOPEN
	__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->fds_bits);
#else
	__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
#define __FDS_BITS(set) ((set)->__fds_bits);
} fd_set;

  • nfds:指定监听的文件描述符的总数,通常是select监听的所有文件描述符中的最大值+1。
  • readfds、writefds、exceptfds分别指向可读、可写、异常事件对于文件描述符集合。
  • fd_set能够容纳的文件描述符由FD_SETSIZE指定。

9.1.2 文件描述符就绪条件

下列情况socket可读

  • socket内核接收缓冲区字节数大于等于低水位标记
  • socket通信对方关闭连接,此时socket读操作返回0。
  • 监听socket上有新的连接
  • socket有处理错误,可用getsockopt读取和清除错误。

下列情况socket可写

  • socket内核发送缓冲区可用字节数大于等于低水位标记
  • socket通信写操作被关闭,对写操作关闭的socket执行写操作回触发SIGPIPE信号。
  • socket使用非阻塞connect连接成功或失败之后
  • socket有处理错误,可用getsockopt读取和清除错误。

socket能处理的异常只有:socket接收到带外数据

9.2 poll系统调用

  • 与select类似,也是轮询,测试是否有就绪者
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
struct pollfd
{
	int fd;                        //文件描述符
	short events;                  //注册的事件
	short revents;                 // 实际发生的事件,由内核填充
};
  • fds是pollfd结构类型的数组,指定我没感兴趣的文案描述符上发生的可读可写异常事件。
  • poll事件类型:

9.3 epoll 系统调用

9.3.1 内核事件表

  • epoll与select和poll的区别是一组函数而不是单个函数。
  • epoll把用户关系的文件描述符事件放在内核的事件表中,无需像select和poll每次调用都要重传
  • 但是epoll需要一个额外的文件描述符,用来标志内核中的这个事件表。
  • 这个文件描述符使用epoll_create函数创建

9.3.1.1创建内核事件表

#include <sys/epoll.h>
int epoll_create(int size);
  • size参数只是给内核一个提示,告诉事件表大概需要多大,返回值是epoll文件描述符。

9.3.2 操作内核事件表

#include <sys/epoll.h>
//成功返回0,失败返回-1并设置errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

struct epoll_event{
	__uin32_t events;	//epoll事件
	epoll_data_t data;	//用户数据
};

typedef union epoll_data{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;
  • epfd是内核事件表的文件描述符,也就是epoll_create创建的文件描述符。
  • fd是要操作的文件描述符
  • op是指定的操作类型:
    • EPOLL_CTL_ADD:往事件表中注册fd事件
    • EPOLL_CTL_MOD:修改fd上的注册事件
    • EPOLL_CTL_DEL:删除fd上的注册事件
  • event参数指定事件,epoll支持的事件类型与poll基本相同,差别在于poll支持的事件类型前加上E,epoll有EPOLLET和EPOLLONESHOT两个事件。
  • data成员用于存储用户数据,一般使用fd,或者用ptr指向的对象中有fd成员。

9.3.2 epoll_wait函数

  • 该函数在一段时间内等待一组文件描述符上的事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • 成功时返回就绪文件描述符的个数,失败返回-1并设置errno。如果设置timeout,时间到还是没有事件发生,就返回0。
  • maxevents指定最多监听多少事件,必须大于0
    timeout参数设置毫秒超时时间。
  • epollwait如果检测到时间,就将所有在epfd上注册的,就绪的事件复制到第二个参数events指向的数组中。
    例如:
//对于poll返回的就绪必须遍历
assert(poll(fds, MAX_EVENT_NNUMBER, -1) != -1);
//遍历已注册的文件描述符,查找其中的就绪
for(int i = 0; i != MAX_EVENT_NUMBER; ++i){
	if(fds[i].revents & POLLIN){
		int sockfd = fds[i].fd;
		//处理sockfd
		...
	}
}
//所以epoll返回就绪的文件描述符
int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
for(int i = 0; i != ret; ++i){
	int sockfd = events[i].data.fd;
	//处理socket
	...
}

9.3.3 LT和ET模式

  • LT:level trigger,电平触发;ET:edge trigger,边沿触发。
    默认LT,ET高效
  • 区别:采用LT时,epoll_wait检测到事件发生并通知程序,程序可以不立刻处理,这样下次调用epoll_wait,它会再次通知程序。采用ET时,epoll_wait检测到事件发生并通知程序,程序必须立刻处理,如果不处理,后续调用epoll_wait不会再次通知了。
  • Et降低了同一个epoll重复触发的次数。
  • LT和ET区别例子:
#include "../create_sockfd.h"
#include <sys/epoll.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE  10

//将文件描述符fd上的可读事件EPOLLIN注册到内核事件表中,参数enable_et表示是否开启ET模式
void addfd(int epollfd, int fd, bool enable_et){
    epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = fd;
    if(enable_et){
        event.events |= EPOLLET;
    }
    int ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    if(ret == -1){
        printf("epoll ctl error: errno:%d\n", errno);
        exit(0);
    }
    setnonblocking(fd);
}

//LT模式工作流程
void LTProcess(epoll_event* events, int number, int epollfd, createSockfd *sfd){
    char buf[BUFFER_SIZE];
    for(int i = 0; i != number; ++i){
        int sockfd = events[i].data.fd;
        //接受到新的连接请求
        if(sockfd == sfd->sockfd){
            printf("LT new connect\n");
            //接受请求
            sfd->acceptfd();
            //注册事件
            addfd(epollfd, sfd->connfd, false);
        } else if(events[i].events & EPOLLIN){
            //只要socket对于的读缓存还有没读出来的数据,这段代码就会被触发。
            printf("LT trigger.\n");
            memset(buf, '\0', BUFFER_SIZE);
            int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
            if(ret <= 0){
                close(sockfd);
                continue;
            }           
            printf("LT:Get %d bytes of content: %s\n", ret, buf);
        } else {
            printf("Only support EPOLLIN.\n");
        }
    }
}

//ET工作模式
void ETProcess(epoll_event* events, int number, int epollfd, createSockfd *sfd){
    char buf[BUFFER_SIZE];
    for(int i = 0; i != number; ++i){
        int sockfd = events[i].data.fd;
        //接受到新的连接请求
        if(sockfd == sfd->sockfd){
            //接受请求
            sfd->acceptfd();
            //注册事件
            addfd(epollfd, sfd->connfd, false);
        } else if(events[i].events & EPOLLIN){
            //这段代码不会重复触发,所以要一次读完。
            printf("ET trigger.\n");
            while(1){
                memset(buf, '\0', BUFFER_SIZE);
                int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
                if(ret < 0){
                    //对于非阻塞IO下边条件成立代表数据全部读完
                    if((errno == EAGAIN) || (errno == EWOULDBLOCK)){
                        printf("read later.\n");
                        break; //跳出while循环 
                    }
                    //没读完出错
                    close(sockfd);
                    break;
                }else if(ret == 0){ //对方已经关闭连接
                    close(sockfd);
                    break;
                }else {        
                    printf("ET:Get %d bytes of content: %s\n", ret, buf);
                }
            }
        } else {
            printf("Only support EPOLLIN.\n");
        }
    }
}

int main(int argc, char *argv[]){
    if(argc <= 3){
        printf("Usage: %s ip_address port_number et[lt].\n", basename(argv[0]));
        return 1;
    }

    createSockfd sockfd(argv[1], atoi(argv[2]));
    assert(sockfd.bindSockfd() != -1);
    assert(sockfd.listenfd(5) != -1);
    epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    assert(epollfd != -1);
    
    if(strcmp(argv[3], "et") == 0){
        addfd(epollfd, sockfd.sockfd, true);
    }else if(strcmp(argv[3], "lt") == 0){
        addfd(epollfd, sockfd.sockfd, false);
    } else {
        printf("Only support lt and et, you provided %s\n", argv[3]);
        return 1;
    }
    while(1){
        int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if(ret < 0){
            printf("epoll_wait error.\n");
            return 1;
        }
        if(strcmp(argv[3], "et") == 0){
            ETProcess(events, ret, epollfd, &sockfd);
        }else if(strcmp(argv[3], "lt") == 0){
            LTProcess(events, ret, epollfd, &sockfd);
        }
    }
    return 0;
}

以LT模式运行程序./testetel 127.0.0.1 12355 lt
使用以下程序发送数据到端口

#include "../create_sockfd.h"
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#define BUFF_SIZE 1024

int main(int argc, char *argv[]){
    if (argc <= 2){
        std::cout << "usage: " << basename(argv[0]) << " ip_address port_number" << std::endl;
        return 1;
    }
    createSockfd sockfd(argv[1], atoi(argv[2]));
   if (sockfd.connectSockfd() < 0){
        std::cout << "connect faild." << std::endl;
    } else {
        char data[BUFF_SIZE];
        memset(data, '\0', BUFF_SIZE);
        scanf("%s", data);
        printf("data = %s\n", data);
        printf("ret = %zu\n", send(sockfd.sockfd, data, strlen(data), 0));

    }
    return 0;
}

服务器端的输出,因为是LT,所以分三次接收

LT new connect
LT trigger.
LT:Get 9 bytes of content: 123456789
LT trigger.
LT:Get 9 bytes of content: 101112134
LT trigger.
LT:Get 1 bytes of content: 5

对于et模式,一次接收

ET trigger.
ET:Get 9 bytes of content: 123456789
ET:Get 9 bytes of content: 132135465
ET:Get 5 bytes of content: 13135

9.4 三组IO复用函数的比较

  • poll和select都是轮询,事件复杂度是O(n)
  • epoll是采用回调的方式,内核检测到就绪的文件描述符时,就会触发回调函数。算法时间复杂度是O(1)。
  • 对于活动连接多的时候,epoll_wait可能会因为回调函数触发频繁导致效率降低。
  • 所以epoll适合连接数多,但活动链接少的情况。
    三者的区别
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值