欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Linux epoll 机制介绍和应用

最编程 2024-04-22 16:48:58
...

简介

epoll 是 Linux 系统中的一种高效的 I/O 事件通知机制,用于高效地处理大量并发网络连接。它优于传统的 selectpoll 方法,特别适用于高并发场景。

epoll 的工作方式

最小案例伪代码

import select
import socket

# 创建一个 epoll 对象
epoll = select.epoll()

# 假设 fds 是一个文件描述符列表
fds = [socket1, socket2, ...]  # socket1, socket2 等是已创建的套接字

# 将文件描述符注册到 epoll 实例上
for fd in fds:
    epoll.register(fd, select.EPOLLIN | select.EPOLLOUT)

# 等待事件发生
events = epoll.poll(timeout=10)  # 阻塞等待直到事件发生或超时

# 处理发生的事件
for fileno, event in events:
    if event & select.EPOLLIN:
        # 文件描述符可读
        data = fileno.recv(1024)
        print("Received data:", data)
    elif event & select.EPOLLOUT:
        # 文件描述符可写
        fileno.send(b"Some data")

# 完成后,注销文件描述符并关闭 epoll 实例
for fd in fds:
    epoll.unregister(fd)
epoll.close()

  • 注册文件描述符epoll.register将多个文件描述符(如网络套接字)注册到 epoll 实例。
  • 等待事件:调用 epoll_wait 函数等待这些文件描述符上的事件。
  • 事件通知:当注册的文件描述符发生事件时,epoll 机制会唤醒 epoll_wait 调用。
  • 处理事件:程序处理每个文件描述符上的事件。

epoll 的高性能关键

简易背诵版本

  • 边缘触发(ET, Edge Triggered)和水平触发(LT, Level Triggered)模式:支持两种事件通知模式。
  • 有效事件列表:只返回有事件发生的文件描述符。
  • 内核和用户空间之间的高效数据交换:减少数据拷贝,提高效率。
  • 只通知一次机制:在每次 epoll_wait 调用之间,文件描述符就绪后只会通知一次。

详细版本

  1. 边缘触发(ET, Edge Triggered)和水平触发(LT, Level Triggered)模式epoll 支持两种事件通知模式。在边缘触发模式下,epoll 仅在文件描述符状态发生变化时通知一次,这意味着如果你没有处理完所有数据,你需要记住继续检查这个文件描述符。在水平触发模式下,只要文件描述符处于可读写状态,epoll 就会不断通知。默认情况下,epoll 使用的是水平触发模式,但是边缘触发模式更适用于高性能服务器,因为它减少了不必要的事件通知。
  2. 有效事件列表:当文件描述符的状态改变(比如变得可读或可写),epoll 会将这个文件描述符添加到一个内部的“就绪”列表中。当你调用 epoll_wait 时,它仅返回这个列表中的文件描述符,而不是你监视的所有文件描述符。这意味着 epoll_wait 返回的每个文件描述符都是有事件发生的,不需要遍历整个监视集合来找出哪些文件描述符是活跃的。
  3. 内核和用户空间之间的高效数据交换epoll 使用一种有效的机制来减少内核和用户空间之间的数据拷贝。当你向 epoll 实例注册文件描述符时,epoll 只存储指向这些文件描述符的指针,而不是整个文件描述符。这减少了数据的移动,提高了效率。
  4. 只通知一次:与 selectpoll 不同,epoll 在文件描述符就绪后只会通知一次,除非再次有新的活动发生。这意味着在每次 epoll_wait 调用之间,你不会收到关于同一个文件描述符的多余通知。

epoll 的应用场景

  • 网络编程:如 HTTP 服务器、数据库服务器等。
  • 高性能服务器:处理大量并发客户端连接。
  • 实时通信系统:如实时消息传递、游戏服务器等。
  • 异步 I/O 处理:支持异步 I/O。

epoll 在 Android 中的应用

在 Android 的 EventHub.cpp 中,epoll 用于高效地处理来自各种设备的输入事件,如触摸、按键等。

epoll 的限制

  • 适用性:主要用于处理可能会阻塞的文件描述符,如网络套接字或管道。
  • 普通文件的监控:不适用于监控普通文件的内容变化。

epoll 用于 Socket 编程的示例

epoll_demo.cpp

#include <sys/epoll.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <cstring>

const int MAX_EVENTS = 10;
const int PORT = 8080;

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // 创建套接字文件描述符
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        std::cerr << "socket failed" << std::endl;
        return -1;
    }

    // 绑定套接字到端口 8080
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        std::cerr << "bind failed" << std::endl;
        return -1;
    }
    if (listen(server_fd, 3) < 0) {
        std::cerr << "listen failed" << std::endl;
        return -1;
    }

    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        std::cerr << "Failed to create epoll file descriptor" << std::endl;
        return 1;
    }

    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = server_fd;

    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event)) {
        std::cerr << "Failed to add file descriptor to epoll" << std::endl;
        return -1;
    }

    struct epoll_event events[MAX_EVENTS];

    while (true) {
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < num_events; i++) {
            if (events[i].data.fd == server_fd) {
                new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
                if (new_socket < 0) {
                    std::cerr << "accept failed" << std::endl;
                    continue;
                }

                struct epoll_event new_event;
                new_event.events = EPOLLIN;
                new_event.data.fd = new_socket;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &new_event)) {
                    std::cerr << "Failed to add new socket to epoll" << std::endl;
                    continue;
                }
            } else {
                // 处理接收到的数据
                char buffer[1024] = {0};
                ssize_t count = read(events[i].data.fd, buffer, sizeof(buffer));
                if (count == -1) {
                    std::cerr << "read error" << std::endl;
                    continue;
                } else if (count == 0) {
                    // 客户端关闭连接
                    close(events[i].data.fd);
                    continue;
                }
                std::cout << "Received data: " << buffer << std::endl;
            }
        }
    }

    close(server_fd);
    return 0;
}

注:这个程序创建了一个简单的 TCP 服务器,监听端口 8080,并使用 epoll 来处理接入的连接和读取数据。

运行步骤

  1. g++ epoll_demo.cpp -o epoll_demo
  2. ./epoll_demo

测试脚本

test.py

import socket
import time

HOST = '127.0.0.1'  # 服务器地址
PORT = 8080         # 服务器监听的端口

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    while True:
        s.sendall(b'Hello, world')
        time.sleep(5)

测试脚本运行

python3 test.py