代码编织梦想

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适合连接数多,但活动链接少的情况。
    三者的区别
    在这里插入图片描述
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44192953/article/details/127153962

i/o多路复用服务器编程-爱代码爱编程

一、实验目的 理解I/O多路复用技术的原理。 学会编写基本的单线程并发服务器程序和客户程序。 二、实验平台 ubuntu-8.04操作系统 三、实验内容 采用I/O多路复用技术实现单线程并发服务器,完成使用一个线程处理并发客户请求的功能。 四、实验原理 除了可以采用多进程和多线程方法实现并发服务器之外,还可以采用I/O多路复用技术。通过该技

i/o复用之 select和epoll-爱代码爱编程

9在我们介绍I/O复用之前,先来看一个小例子: ... while(fgets(sendline, MAXLINE, fp) != NULL){ write(sockfd, sendline, 1) ...

i/o复用(i/o multiplexing): select, pselect, poll, ppoll, epoll-爱代码爱编程

日期:2016.05.30作者:i.sshehttps://github.com/isshe I/O复用:select, pselect, poll, epoll. 注意:本文主要介绍的是epoll相关知识

unix网络编程——i/o复用(select、poll)-爱代码爱编程

1、五种I/O模型:      (1)阻塞式I/O;(2)非阻塞式I/O;(3)I/O复用(select、poll);(4)信号驱动式I/O(SIGIO);(5)异步I/O(aio_系列函数);       一个输入操作分为两个阶段:A. 等待数据准备好;B. 从内核向进程复制数据。       一个套接字上的输入也类似,A. 等待数据从网络中到达(

i/o复用——select_会飞的猪宝宝啊的博客-爱代码爱编程

一、I/O模型 1、I/O 复用是程序能够同时监听多个文件描述符。内核一旦发现进程指定的一个或者多个I/O条件准备读,它就通知该进程。 2、I/O 复用典型用于以下网络场合: (1)当客户端同时处理多个套接字,这种情况很少出现。 (2)当客户端同时处理多个文件描述符(交互式输入和网络套接字)时,必须

Redis的I/O多路复用-爱代码爱编程

Redis的I/O多路复用 [1] 为什么 Redis 中要使用 I/O 多路复用?[2] I/O 多路复用模型[3] select[4] poll[5] epoll[6] Reactor 设计模式[7] 封装 epoll 函数[8] select & poll & epoll比较[9] 深入理解select、poll和epoll及

STM32---I/O引脚复用以及重映射(GPIO和AFIO)-爱代码爱编程

STM32F4xx官方资料: 《STM32F4中文参考手册V10》-第7章I/O 引脚复用器和映射(GPIO和AFIO) I/O 引脚复用器和映射 微控制器 I/O 引脚通过一个复用器连接到板载外设/模块,该复用器一次仅允许一个外设的复用功能 (AF) 连接到 I/O 引脚。这可以确保共用同一个 I/O 引脚的外设之间不会发生冲突。每个 I/O 引脚

《Linux高性能服务器编程》第九章 I/O复用-爱代码爱编程

Github代码地址 第九章 I/O 复用9.1 select 系统调用9.1.1 select API9.1.3 处理带外数据9.2 poll 系统调用9.3 epoll 系列系统调用9.3.1 内核事件表9.3.2 epoll_wait 函数9.3.3 LT 和 ET 模式9.3.4 EPOLLONESHOT 事件9.4 三组 I/O 复用函

Go基于I/O多路复用的TCP协议流解析实践-爱代码爱编程

在《Go经典阻塞式TCP协议流解析的实践》一文中,我们基于Go经典的阻塞I/O模型实现了一个基于TCP流的自定义协议的解析。这种one-connection-per-goroutine模型的优点就是简单、好写以及好理解,降低开发者心智负担。但一旦连接数上来,goroutine的数量就会线性增加。当面对海量连接的场景,这种模型将力不从心:系统中将存在大

第2章 高性能网络设计 - I/O多路复用-爱代码爱编程

标题:Linux C 和 C++ 后端服务器架构开发 章节:第2章 高性能网络设计 —— I/O 多路复用 作者:嘉措一郎(Gyatso Ichiro) 时间:2021-11-25 邮箱:GeekDerverloper.org@outlook.com 标签:技术博客 2.2 I/O 多路复用 重点内容 网络 I/O 模型、

Linux网络编程 - 基于 I/O 复用的聊天服务器端(epoll:LT 和 ET 模式实现)-爱代码爱编程

一  问题描述         实现聊天服务器端,使其可以在连接到服务器端的所有客户端之间交换消息。按照条件触发方式和边缘触发方式分别实现 epoll 服务器端(聊天服务器端的实现中,这两种方式不会产生太大差异)。当然,为了正常运行服务器端,需要聊天客户端,我们可以使用多线程编程模型实现聊天客户端。 二  基于 epoll 实现聊天服务器端 2.1

Linux系统编程---I/O多路复用-爱代码爱编程

文章目录 1 什么是IO多路复用2 解决什么问题说在前面I/O模型阻塞I/O非阻塞I/OIO多路复用信号驱动IO异步IO3 目前有哪些IO多路复用的方案解决方案总览常见软件的IO多路复用方案4 具体怎么用selectpollepolllevel-triggered and edge-triggered状态变化通知(edge-triggered)模式

linux 中如何使用 id 命令_奋斗的工程师的博客-爱代码爱编程

Linux 中如何使用 id 命令 我从cnaaa.com购买了服务器。 在 Linux 中,id 命令用于显示用户的真实有效的用户ID和组ID。 本文我们通过一些例子来介绍一下如何使用 id 命令。 Linux 中