【深入理解 Linux 的 Epoll:高效的 I/O 事件处理机制】

引言

epoll 全称 eventpoll,是 linux 内核实现IO多路转接/复用(IO multiplexing)的一个实现。IO多路转接的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。epoll是select和poll的升级版,相较于这两个前辈,epoll改进了工作方式,因此它更加高效。

  1. 对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的。
  2. select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
  3. select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
  4. 程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测
    使用epoll没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制

当多路复用的文件数量庞大、IO流量频繁的时候,一般不太适合使用select()和poll(),这种情况下select()和poll()表现较差,推荐使用epoll()。

Epoll的核心组件

1、Epoll 实例:介绍如何通过 epoll_create1 创建一个 epoll 实例。
2、控制操作:使用 epoll_ctl 添加、修改或删除监视的文件描述符。
3、等待事件:如何使用 epoll_wait 等待和响应事件。

Epoll的函数详解

epoll_create
epoll_create()函数的作用是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。

int epoll_create(int size);
函数参数 size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了。
函数返回值:
	失败:返回-1
	成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的epoll实例了

epoll_ctl
epoll_ctl()函数的作用是管理红黑树实例上的节点,可以进行添加、删除、修改操作。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数参数:
	epfd:epoll_create() 函数的返回值,通过这个参数找到epoll实例
	op:这是一个枚举值,控制通过该函数执行什么操作
		EPOLL_CTL_ADD:往epoll模型中添加新的节点
		EPOLL_CTL_MOD:修改epoll模型中已经存在的节点
		EPOLL_CTL_DEL:删除epoll模型中的指定的节点
	fd:文件描述符,即要添加/修改/删除的文件描述符
	event:epoll事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
		events:委托epoll检测的事件
			EPOLLIN:读事件, 接收数据, 检测读缓冲区,如果有数据该文件描述符就绪
			EPOLLOUT:写事件, 发送数据, 检测写缓冲区,如果可写该文件描述符就绪
			EPOLLERR:异常事件
	data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出。
函数返回值:
	失败:返回-1
	成功:返回0

epoll_wait

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

函数参数:
	epfd:epoll_create() 函数的返回值, 通过这个参数找到epoll实例
	events:传出参数, 这是一个结构体数组的地址, 里边存储了已就绪的文件描述符的信息
	maxevents:修饰第二个参数, 结构体数组的容量(元素个数)
	timeout:如果检测的epoll实例中没有已就绪的文件描述符,该函数阻塞的时长, 单位ms 毫秒
		0:函数不阻塞,不管epoll实例中有没有就绪的文件描述符,函数被调用后都直接返回
		大于0:如果epoll实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
		-1:函数一直阻塞,直到epoll实例中有已就绪的文件描述符之后才解除阻塞
函数返回值:
	成功:
		等于0:函数是阻塞被强制解除了, 没有检测到满足条件的文件描述符
		大于0:检测到的已就绪的文件描述符的总个数
	失败:返回-1

Epoll的使用

服务器代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include<sys/epoll.h>

#define nums 128

int main()
{
   
    // 1.创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    if (lfd == -1) {
        perror("socket");
        exit(0);
    }

    //2.将监听套接字与本地ip、端口绑定在一起
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(30001);   //转成大端端口
    /*
       INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
       这个宏可以代表任意一个IP地址
       这个宏一般用于本地的绑定操作
    */
    addr.sin_addr.s_addr = INADDR_ANY;   // 这个宏的值为0 == 0.0.0.0
    //绑定
    int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
    if (ret == -1) {
        perror("bind");
        exit(0);
    }

    //3.设置监听
    int lit = listen(lfd, 128);  //最多128个
    if (lit == -1) {
        perror("listen");
        exit(0);
    }

    //创建epoll实例,参数大于0就可以
    int epfd = epoll_create(1);
    if (epfd==-1) {
        perror("epoll_create");
        exit(0);
    }

    //为epoll实例添加监听套接字节点
    struct epoll_event ev;
    ev.events = EPOLLIN;  // 检测lfd读读缓冲区是否有数据
    ev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, & ev);   //ev在函数里会进行一次值的拷贝
    

    struct epoll_event evs[1024];
    int size = sizeof(evs) / sizeof(evs[0]);
    //循环检测
    while (1)
    {
        // 调用一次, 检测一次
        int num = epoll_wait(epfd, evs, size, -1);     //会将检测到的写入evs,从0到num-1的位置上
        for (int i = 0; i < num; ++i)
        {
            int fd = evs[i].data.fd;
            
            if (fd==lfd) {
                //如果是监听文件描述符(监听套接字)
                int tfd = accept(fd,NULL,NULL);
                //新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了

                //重复利用ev
                ev.events = EPOLLIN;
                ev.data.fd = tfd;
                int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev);
                if (ret==-1) {
                    perror("epoll_ctl-accept");
                    exit(0);
                }
            }
            else {
                char buf[1024];
                memset(buf, 0, sizeof(buf));
                int len = recv(fd,buf,sizeof(buf),0);
                if (len==0) {
                    //客户端关闭连接
                    printf("客户端已经断开了连接\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                    close(fd);
                }
                else if (len > 0) {
                    printf("客户端say:%s\n",buf);
                    //发送给客户端
                    send(fd,buf,len,0);
                }
                else {
                    perror("recv");
                    exit(0);
                }

            }
        }

    }
    return 0;
}

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

#define IP "服务器的ip地址"

int main()
{
    // 1.创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);

    if (fd == -1) {
        perror("socket");
        exit(0);
    }

    //2.连接服务器
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(30001);   //转成大端端口
    inet_pton(AF_INET,IP,&addr.sin_addr.s_addr);
    //连接
    int ret = connect(fd,(struct sockaddr*) &addr,sizeof(addr));
    if (ret == -1) {
        perror("connect");
        exit(0);
    }
    
    
    //3.和服务器通信
    int number = 0; //通信次数
    while (1)
    {
        char buffer[1024];
        sprintf(buffer,"hello,server...%d\n",number++);
        //发数据到服务器
        send(fd,buffer,strlen(buffer)+1,0);


        //接收服务器传来的数据
        memset(buffer,0,sizeof(buffer));
        int len = recv(fd,buffer,sizeof(buffer),0);
        if (len > 0 ) {
            printf("server say: %s\n",buffer);
        }
        else if (len==0) {
            printf("server was close...\n");
            break;
        }
        else {
            perror("recv");
            break;
        }
        sleep(1);  //每隔1秒发送一条数据

    }

    //4.断开连接, 关闭套接字
    close(fd);  
   

    return 0;
}

本文部分来源:https://subingwen.cn/linux/epoll/#2-%E6%93%8D%E4%BD%9C%E5%87%BD%E6%95%B0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值