【网络编程】I/O复用

文章详细介绍了Linux系统中的select、poll和epoll三种I/O复用函数,包括它们的使用方法、参数解释、超时处理和事件模式。重点讨论了select的文件描述符就绪条件,处理带外数据的方式,以及epoll的LT和ET模式,EPOLLONESHOT事件。文章最后对比了三者在性能和使用上的区别,强调了epoll在效率和可扩展性上的优势。
摘要由CSDN通过智能技术生成


一、select系统调用

  select系统调用的用途是:在一段指定时间以内,监听用户感兴趣的文件描述符上的可读、可写和异常事件。

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

  nfds设置为被监听文件描述符的总数,即所有文件描述符中最大值加一。
  readfds,writefds,exceptfds分别表示可读,可写,异常等事件的文件描述符集合,其类型fd_set仅包含一个整数数组,每个元素的每一位标记一个文件描述符,select中包含了以下宏来对fd_set进行操作。

#include<sys/select.h>
FD_ZERO(fd_set* fdset);                //清除fdset的所有位
FD_SET(int fd, fd_set* fdset);         //设置fdset的位fd
FD_CLR(int fd, fd_set* fdset);         //清除fdset的位fd
int FD_ISSET(int fd, fd_set* fdset);   //测试fdset的位fd是否被设置

  timeout用来设置select函数的超时时间。其中用到了timeval结构体

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

  select成功时返回就绪文件的文件描述符,如果超时返回0,失败时返回-1并设置error。如果在select等待期间,程序收到信号,select立即返回-1,并设置errno为EINTR。

1.1、文件描述符就绪条件(socket可读条件)

《Linux高性能服务器编程》P148
注:网络编程中select能处理的异常情况只有一种:socket上接收到带外数据。

1.2、处理带外数据

  下面的代码描述了select是如何处理socket处于两种不同的就绪状态,即接收到普通数据处于可读状态和接收到带外数据处于异常状态。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
    if(argc <= 2){
        printf("usage:%s ip_address port_number/n", basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = 0;
    struct sockaddr_in address;        //新建一个ipv4的socket结构体
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;      //设置ipv4地址族
    inet_pton(AF_INET, ip, &address.sin_addr); //将传进来的点分十进制转化为网络字节序表示的ip地址
    address.sin_port = htons(port);  //将传进来的主机字节序转化为网络字节序

    int listenfd = socket(PF_INET, SOCK_STREAM, 0);   //创建一个使用ipv4协议的TCP连接socket
    assert(listenfd >= 0);  //其值为假,则终止运行并弹出错误信息
    ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));//将address所指向的地址分配给文件描述符listenfd
    assert(ret != -1);
    ret = listen(listenfd, 5);//开始监听socket,监听队列长度为5
    assert(ret != -1);

    struct sockaddr_in client_address;//创建一个客户端的socket连接用来存储客户端的地址信息
    socklen_t client_addrlength = sizeof(client_address);
    int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);//接受listenfd和client_address的socket连接,并将相关信息保存下来
    //accept是从监听队列中取出连接,并且将连接的客户地址存在client_address。
    if(connfd < 0){
        printf("errno is: %d\n", errno);
        close(listenfd);
    }
    char buf[1024];
    fd_set read_fds;
    fd_set exception_fds;
    FD_ZERO(&read_fds);
    FD_ZERO(&exception_fds);

    while(1){
        memset(buf, '\0', sizeof(buf));

        FD_SET(connfd, &read_fds);
        FD_SET(connfd, &exception_fds);
        ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);
        if(ret < 0){
            printf("selection failure\n");
            break;
        }

        if(FD_ISSET(connfd, &read_fds)){  //read_fds中是否设置了connfd,是的话说明监听到了connfd上有可读事件
            ret = recv(connfd, buf, sizeof(buf) - 1, 0);
            if(ret <= 0) break;
            printf("get %d bytes of normal data: %s\n", ret, buf);
        }
        else if(FD_ISSET(connfd, &exception_fds)){//exception_fds中是否设置了connfd,是的话说明监听到了connfd上有异常事件
            ret = recv(connfd, buf, sizeof(buf) - 1, MSG_OOB);
            if(ret <= 0) break;
            printf("get %d bytes of oob data: %s\n", ret, buf);
        }
        close(connfd);
        close(listenfd);
        return 0;
    }
}

二、poll系统调用

  poll系统调用是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。

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

  fds是一个pollfd类型的数组。它的定义如下:

struct pollfd{
	int fd;         //文件描述符
	short events;   //注册的事件
	short revents;  //实际发生的事件,由内核填充
}

  nfds参数指定了被监听事件集合fds的大小。
  timeout参数指定了poll的超时值。当timeout为-1时,poll调用永远堵塞,直到某个事件发生,当timeout为0时,timeout立即返回。

三、epoll系列系统调用

3.1、内核事件表

  epolllinux特有的I/O复用函数,它与select,poll的差异在于:

  • epoll使用一组函数来完成任务而不是单个函数。
  • epoll把用户关心的文件描述符上的事件放在内核的一个事件表中,无须像selectpoll一样每次调用都要传入文件描述符或者事件集。所以epoll需要一个文件描述符来唯一表示该内核事件表。

  下面的函数用来创建内核事件表的文件描述符。

#include <sys/epoll.h>
int epoll_create(int size);  //创建内核事件表的文件描述符
//size指定了内核事件表有多大

  下面的函数用来操作epoll的内核事件表。

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

  epfd指的是某个内核事件表的文件描述符
  fd指的是要操作的文件描述符
  op指定操作类型,有以下三种:EPOLL_CTL_ADD,往事件表中注册fd上的事件。EPOLL_CTL_MOD,修改fd上的注册事件。EPOLL_CTL_DEL,删除fd上的注册事件。
  event指定事件,其定义为:

struct epoll_event{
	__uint32_t events;  //epoll事件
	epoll_data_t data;  //用户数据
}
typedef union epoll_data{
	void* ptr;
	int fd;
	uint32_t u32;
	uint64_t u63;
}epoll_data_t;

3.2、epoll_wait函数

  这是epoll系统调用的主要接口,作用是在一段时间内等待一组文件描述符上的事件。

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

  timeout的含义与polltimeout含义相同。
  maxevents指定最多监听多少个事件,必须大于0 。
  epoll_wait如果检测到事件,就将所有的就绪事件从内核事件表中取出并复制到第二个参数events指向的数组中。它不像selectpoll的数组参数既用于传入用户注册的事件,又用于输出内核检测到的事件,这就极大的提高了应用程序索引就绪文件描述符的效率。

3.3、LT和ET模式

  epoll对文件描述符的操作有两种模式:LT(Level Trigger,电平触发)和ET(Edge Trigger,边沿触发)两种模式。LT模式是默认的工作模式,相当于一个效率较高的pollET模式是epoll的高效工作模式。
  对于采用LT模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。
  对于采用ET模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后, 应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
  由此可见,ET模式相较于LT模式,减少了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

  • 注:每个使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符时阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态。

3.4、EPOLLONESHOT事件

  EPOLLONESHOT事件可以实现一个socket连接在任一时刻都只被一个线程处理。对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
  所以,一旦注册了EPOLLONESHOT事件的socket被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下次可读时,其EPOLLIN事件能被触发,从而让其他工作线程有机会继续处理这个socket。

四、select、poll、epoll三组I/O复用函数比较

系统调用selectpollepoll
事件集合用户通过三个参数分别传入感兴趣的可读、可写以及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重复这三个参数统一处理所有事件类型,因此只需一个事件集参数,用户通过pollfd,events传入感兴趣的事件,内核通过修改pollfd,revents反馈其中的就绪事件内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll,wait时,无须反复传入用户感兴趣的事件,epoll_wait系统调用的参数events仅用来反馈就绪的事件
应用程序索引就绪事件的事件复杂度O(n)O(n)O(1)
最大支持文件描述符数量一般有最大值限制6553565535
工作模式LTLT支持ET高效率模式
内核实现和工作效率采用轮询的方式来检测就绪事件,算法的时间复杂度为O(n)采用轮询的方式来检测就绪事件,算法的时间复杂度为O(n)采用回调的方式来检测就绪事件,算法的时间复杂度为O(1)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Rockict_z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值