【Linux内幕】epoll原理及代码示例

news/2024/7/9 16:18:03 标签: epoll

1.简述

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

2.select

了解epoll本质要从操作系统进程调度的角度来看数据接收。阻塞是进程调度的关键一环,指的是进程在等待某事件(如接收到网络数据)发生之前的等待状态,recv、select和epoll都是阻塞方法。

//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);   
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)

这是一段最基础的网络编程代码,先新建socket对象,依次调用bind、listen、accept,最后调用recv接收数据。recv是个阻塞方法,当程序运行到recv时,它会一直等待,直到接收到数据才往下执行。
操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态,比如上述程序运行到recv时,程序会从运行状态变为等待状态,接收到数据后又变回运行状态。操作系统会分时执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。

但是简单的方法往往有缺点,主要是:
其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历(遍历进程A关心的所有socket,需要注意的是添加从等待队列头部添加,删除通过回调直接实现,所以每个socket的等待队列不用遍历),而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
其二,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次(这一次遍历是在应用层)。

epoll_24">3.epoll的设计思路

epoll是在select出现N多年后才被发明的,是select和poll的增强版本。epoll通过以下一些措施来改进效率。

措施一:功能分离

select低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。如下图所示,每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程(解耦)。显而易见的,效率就能得到提升。
在这里插入图片描述

相比select,epoll拆分了功能,功能分离,使得epoll有了优化的可能。
措施二:就绪列表
select低效的另一个原因在于程序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。如下图所示,计算机共有三个socket,收到数据的sock2和sock3被rdlist(就绪列表)所引用。当进程被唤醒后,只要获取rdlist的内容,就能够知道哪些socket收到数据。
为方便理解后续的内容,我们先复习下epoll的用法。如下的代码中,先用epoll_create创建一个epoll对象epfd,再通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据。

int s = socket(AF_INET, SOCK_STREAM, 0);   
bind(s, ...)
listen(s, ...)
 
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中
 
while(1){
    int n = epoll_wait(...)
    for(接收到数据的socket){
        //处理
    }
}

在这里插入图片描述

4. 代码示例

服务端代码

/******************************************************************************
* Copyright (C) 2020, 协议森林.
*
* File Name:    tcpepoll.c
* Author:       协议森林
* Date:         2020-12-22
* Description:
-------------------------------------------------------------------------------
功能汇总:
        1)关于epoll的用法
        2)服务端代码

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

#define MAXEVENTS 100


// 初始化服务端的监听端口。
static int CreateServer(int port)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if (sock < 0)
    {
        printf("socket() failed.\n"); 
        return -1;
    }

    int opt = 1;
    unsigned int len = sizeof(opt);
    //一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);
    setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &opt, len);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(port);

    if(bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 )
    {
        printf("bind() failed.\n"); 
        close(sock); 
        return -1;
    }

    if(listen(sock, 5) != 0)//最大可连接为5
    {
        printf("listen() failed.\n"); 
        close(sock);
        return -1;
    }

    return sock;
}

// 把socket设置为非阻塞的方式。
static int setnonblocking(int sockfd)
{
    if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1)
    {
        return -1;
    }

    return 0;  
}  

int main(int argc,char *argv[])
{
    //校验输入参数完整性
    if(argc != 2)
    {
        printf("usage:./tcpepoll port,for example:./tcpepoll 8080.\n"); 
        return -1;
    }

    // 建立监听的socket
    int ListenSock = CreateServer(atoi(argv[1]));
    printf("ListenSock=%d\n", ListenSock);
    if (ListenSock < 0)
    {
        printf("CreateServer() failed.\n");
        return -1;
    }

    int   EpollFd = 0;
    char  Buffer[1024];
    memset(Buffer, 0, sizeof(Buffer));

    //epoll创建一个描述符,max_size标识监听数目最大数
    EpollFd = epoll_create(1);

    // 添加监听描述符事件
    struct epoll_event ev;
    ev.data.fd = ListenSock;
    ev.events = EPOLLIN;
    epoll_ctl(EpollFd, EPOLL_CTL_ADD, ListenSock, &ev);

    while (1)
    {
        //存放有事件发生的结构数组
        struct epoll_event events[MAXEVENTS];

        //等待监视的socket有事件发生
        int infds = epoll_wait(EpollFd, events, MAXEVENTS, -1);
        // printf("epoll_wait infds=%d\n",infds);

        // 返回失败。
        if (infds < 0)
        {
            printf("epoll_wait() failed.\n"); 
            break;
        }

        // 超时。
        if (infds == 0)
        {
            printf("epoll_wait() timeout.\n"); continue;
        }

        // 遍历有事件发生的结构数组。
        for (int ii = 0; ii < infds; ii++)
        {
             printf("infds[%d]. ListenSock[%d].events[%d].data.fd[%d]\n",infds,ListenSock,ii,events[ii].data.fd);
            if((events[ii].data.fd == ListenSock) &&(events[ii].events & EPOLLIN))
            {
                // 如果发生事件的是ListenSock,表示有新的客户端连上来。
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int clientsock = accept(ListenSock, (struct sockaddr*)&client, &len);
                printf("~~~~~~clientsock[%d]\n",clientsock);
                if (clientsock < 0)
                {
                    printf("accept() failed.\n"); 
                    continue;
                }

                // 把新的客户端添加到epoll中。
                memset(&ev, 0, sizeof(struct epoll_event));
                ev.data.fd = clientsock;
                ev.events = EPOLLIN;
                epoll_ctl(EpollFd, EPOLL_CTL_ADD, clientsock, &ev);

                printf ("client(socket=%d) connected ok.\n",clientsock);

                continue;
            }
            else if (events[ii].events & EPOLLIN)
            {
                // 客户端有数据过来或客户端的socket连接被断开。
                char Buffer[1024];
                memset(Buffer, 0, sizeof(Buffer));

                // 读取客户端的数据。
                ssize_t isize = read(events[ii].data.fd, Buffer, sizeof(Buffer));

                // 发生了错误或socket被对方关闭。
                if (isize <=0)
                {
                    printf("client(eventfd=%d) disconnected.\n",events[ii].data.fd);

                    // 把已断开的客户端从epoll中删除。
                    memset(&ev,0,sizeof(struct epoll_event));
                    ev.events = EPOLLIN;
                    ev.data.fd = events[ii].data.fd;
                    epoll_ctl(EpollFd, EPOLL_CTL_DEL, events[ii].data.fd, &ev);
                    close(events[ii].data.fd);
                    continue;
                }

                printf("recv(eventfd=%d,size=%d):%s\n",events[ii].data.fd, isize, Buffer);

                //把收到的报文发回给客户端。
                write(events[ii].data.fd,Buffer,strlen(Buffer));
            }//end else if (events[ii].events & EPOLLIN)
            else
            {
                printf("this is nothing.\n");
            }
        }
    }//end while(1)

    close(EpollFd);

    return 0;
}

客户端代码:

/******************************************************************************
* Copyright (C) 2020, 协议森林.
*
* File Name:    clientepoll.c
* Author:       协议森林
* Date:         2021-1-22
* Description:
-------------------------------------------------------------------------------
功能汇总:
        1)关于epoll的用法
        2)客户端代码

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

int main(int argc, char *argv[])
{
    int sockfd;
    struct sockaddr_in servaddr;
    char buf[1024];

    //校验输入参数是否完整
    if (argc != 3)
    {
        printf("usage:./tcpclient ip port, for example:./tcpclient 127.0.0.1 8080\n");
        return -1;
    }

    //创建TCP套接字
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0))<0)
    {
        printf("socket() failed.\n");
        return -1;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(atoi(argv[2]));
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
    {
        printf("connect(%s:%s) failed.\n", argv[1], argv[2]);
        close(sockfd);
        return -1;
    }

    printf("Now network connect ok.\n");

    for (int ii=0; ii < 10000; ii++)
    {
        // 从命令行输入内容。
        memset(buf, 0, sizeof(buf));
        printf("Please input:");
        scanf("%s", buf);

        if(write(sockfd, buf, strlen(buf)) <=0)
        {
            printf("write() failed.\n");
            close(sockfd);
            return -1;
        }

        memset(buf, 0, sizeof(buf));
        if(read(sockfd, buf, sizeof(buf)) <=0)
        {
            printf("read() failed.\n");
            close(sockfd);
            return -1;
        }

        printf("recv:%s\n",buf);
    }
}

在这里插入图片描述

加入讨论

在这里插入图片描述

参考文献

1.如果这篇文章说不清epoll的本质,那就过来掐死我吧!


http://www.niftyadmin.cn/n/1113916.html

相关文章

初次使用博客

2018.10.31初次使用博客&#xff0c;希望以后能坚持写博客&#xff0c;将所学知识&#xff0c;笔记记录于此。转载于:https://www.cnblogs.com/dlfeng/p/9883746.html

思科交换机配置单播MAC地址过滤

1、其他厂商&#xff1a; 在华为&#xff0c;华三等设备上&#xff0c;我们都有“黑洞MAC地址表项” 的配置&#xff0c;其特点是手动配置、不会老化&#xff0c;且重启后也不会丢失。例如如下示例&#xff1a; 黑洞表项是特殊的静态MAC地址表项&#xff0c;丢弃含有特定源MAC地…

9.算术运算符

等待转载于:https://www.cnblogs.com/heart-Cewet/p/7522562.html

ASA Failover

1、ASA Failover状态&#xff1a; 配置Failover需要两个相同的ASA设备&#xff0c;两个ASA设备通过专门的故障转移链路相互连接。 ASA Failover有两种状态&#xff1a;Active/Active failover & Active/Standby failover. A/A&#xff1a;该状态的Failover两个ASA设备都…

【QT开发】基本入门工程

1.简述 Qt 是一个1991年由Qt Company开发的跨平台C图形用户界面应用程序开发框架。它既可以开发GUI程序&#xff0c;也可用于开发非GUI程序&#xff0c;比如控制台工具和服务器。2008年&#xff0c;Qt Company科技被诺基亚公司收购&#xff0c;Qt也因此成为诺基亚旗下的编程语…

互联网中哪些最被消费者嗤之以鼻

日子中的繁忙可想而知&#xff0c;网络营销的方法也现已成为一种趋势&#xff0c;网络营销现已充满着咱们的衣食住行。咱们网购原本也是网络营销的一种&#xff0c;关于那些taobao的卖家来说&#xff0c;他们就是网络运营的一份子。还有蕞广泛的网上订购机票或许车票&#xff0…

Bootstrap时间控件常用配置项

1.给下面4个文本框初始化 $(function(){  $("#orderStartTime,#orderEndTime,#preSaleStartTime,#preSaleEndTime").datetimepicker({    dateFormat:yy-mm-dd,    showSecond: true, //显示秒    timeFormat: HH:mm:ss,//格式化时间    stepHou…

【设计模式】linux中利用信号量演示生产者与消费者模型

1.简介 linux sem 信号量是一种特殊的变量&#xff0c;访问具有原子性&#xff0c; 用于解决进程或线程间共享资源引发的同步问题。用户态进程对 sem 信号量可以有以下两种操作&#xff1a; 等待信号量&#xff1a;当信号量值为 0 时&#xff0c;程序等待&#xff1b;当信号量…