LinuxNP - I/O多路复用之epoll

本文深入解析epoll,一种高效的I/O多路复用技术,对比select/poll,介绍其工作原理,包括水平触发和边缘触发模式,以及如何在C语言中实现TCP回音客户端和服务端的epoll应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

  • 什么是epoll?
  • 为什么要使用epoll?
  • 如何使用epoll?
  • 查看epoll官方帮助文档
  • epoll版TCP回音客户端
  • epoll版TCP回音服务端


<一> 什么是epoll?

epoll = enhance poll 增强版轮询,是poll/select 的升级版;

IO多路复用:同一个进程下处理多个IO数据流(单进程);

<二> 为什么要使用epoll?

使用epoll在对fd进行监视时,如果有满足条件的fd,则该fd会被添加到一个新的数组区域,该区域存放所有满足条件的fd;

获取/判断满足条件的fd时,遍历当前数组即可;

但上述原理和select原理基本一致,那epoll增强了什么?

epoll在轮询(poll)时,提供了两种触发方式:

  1. Level Triggered 水平触发(EPOLLLT)
  2. Edge Triggered 边缘触发(EPOLLET)

水平触发(LT):

当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。

如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。

边缘触发(ET):

当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。

如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。

<三> 如何使用epoll?

1> 首先需要创建一个epoll文件描述符,指定epoll可以监视fd的最大值

#define MAX_EVENTS 10

int epfd = epoll_create(MAX_EVENTS);
if(epfd == -1){
    perror("epoll_create");    //stdio.h
    exit(EXIT_FAILURE);        //stdlib.h
}

2> 指定需要监视fd的活动(读/写/...)

struct epoll_event ev, events[MAX_EVENTS];

int fd = 0;
ev.events = EPOLLIN|EPOLLET;
ev.data.fd = fd;

3> 将指定活动添加进入epoll实例

if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,ev) == -1){
    perror("epoll_ctl:0");
    exit(EXIT_FAILURE);
}

4> 使用循环,开启epoll监视

for(;;){
    int num = epoll_wait(epfd,rev,MAX_EVENTS,-1);
    if(num == -1){
	perror("epoll_wait");
	break;
    }
    //...
    int i=0;
    for(;i<num;i++){
        if(events[i].data.fd == fd){
            size = read(0,buffer,sizeof(buffer));
        }
        //...
}
}

<四> 查看epoll官方帮助文档

man epoll

<五> epoll版TCP回音客户端

修改后客户端处理函数:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#define MAX_EVENTS 2


/* 服务器对客户端请求的处理 */
void server_process(int sc){
    int size = 0;
    char buffer[1024];
    for(;;){
    	size = read(sc,buffer,sizeof(buffer));
   		if(size == 0){
			return;
    	}
		write(sc,buffer,size);
		write(1,buffer,size);
		memset(buffer,0,sizeof(buffer));
	}
}

/* 客户端处理数据 */
void client_process(int ss){
    int size = 0;
    char send_buffer[1024];    //发送缓冲区1K
    char recv_buffer[1024];    //接收缓冲区1K

	int epfd = epoll_create(2);
	int fd = 0;
	struct epoll_event ev1,ev2;
	struct epoll_event events[MAX_EVENTS];
	ev1.data.fd = fd;
	ev1.events = EPOLLIN|EPOLLET;
	ev2.data.fd = ss;
	ev2.events = EPOLLIN|EPOLLET;
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev1)==-1){
		perror("epoll_ctl:0");
		exit(EXIT_FAILURE);
	}
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,ss,&ev2)==-1){
		perror("epoll_ctl:ss");
		exit(EXIT_FAILURE);
	}

	memset(send_buffer,0,sizeof(send_buffer));
	memset(recv_buffer,0,sizeof(recv_buffer));
        for(;;){
		int num = epoll_wait(epfd,events,MAX_EVENTS,-1);
		printf("%i\n",num);
		if(num == -1){
			perror("epoll_wait");
			exit(EXIT_FAILURE);
		}
		int i=0;
		for(;i<num;i++){
			int tfd = events[i].data.fd;
			if(tfd == fd){
				printf("!!!\n");
				size = read(fd,send_buffer,sizeof(send_buffer));
				write(ss,send_buffer,size);
				memset(send_buffer,0,sizeof(send_buffer));
			}else 
			if(tfd == ss){
				printf("???\n");
				size = read(ss,recv_buffer,sizeof(recv_buffer));
				write(1,recv_buffer,size);
				memset(recv_buffer,0,size);
			}
		}
    }
}

程序分析:

epoll_wait超时时间设置为-1,会立即阻塞所有的文件描述符(0被阻塞,终端等待输入数据),

读取后0中被写入数据,边缘被触发,再次read后会直接从有数据的0中读取,写入ss后,ss可读边缘触发,执行回显输出;

由于边缘触发ET只会提醒一次,所以需要确保缓冲区的内容被全部读取,否则会出现 "粘包" 现象,若解决 "粘包" 问题可取消边缘触发。

<六> epoll版TCP回音服务端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define PORT 8888
#define IP "192.168.12.101"
#define BACKLOG 2
#define MAX_EVENTS 10

int main(){
    int ss,sc;    /*ss:socket-server sc:socket-client*/
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    int err;    /*处理各个步骤的返回值,进行错误判断*/
    pid_t pid;    /*处理客户端连接后产生的子进程*/
	struct epoll_event ev,events[MAX_EVENTS];
	int epfd; 
	epfd = epoll_create(MAX_EVENTS);
    
    ss = socket(AF_INET,SOCK_STREAM,0);
    if(ss < 0){
        perror("socket");
        exit(EXIT_FAILURE);
    }

    bzero(&(server_addr.sin_zero),8);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr(IP);
    
    err = bind(ss,(struct sockaddr*)&server_addr,sizeof(server_addr));
    if(err == -1){
        perror("bind");
        exit(EXIT_FAILURE);
    }

	ev.data.fd = ss;    /*listen‘s fd*/
	ev.events = EPOLLIN;
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,ss,&ev)==-1){
		perror("epoll_ctl:ss");
		exit(EXIT_FAILURE);
	}
    err = listen(ss,BACKLOG);
    if(err == -1){
        perror("listen");
        exit(EXIT_FAILURE);
    }

    /*主循环*/
    for(;;){
		printf("hello\n");
		int num = epoll_wait(epfd,events,MAX_EVENTS,-1);
		if(num == -1){
			perror("epoll_wait");
			exit(EXIT_FAILURE);
		}
		printf("world!\n");
		int i=0;
		for(;i<num;i++){
			if(events[i].data.fd == ss){
        		u_int addr_len = sizeof(struct sockaddr);
				sc = accept(ss,(struct sockaddr*)&client_addr,&addr_len);
				if(sc == -1){
					perror("accept");
					continue;
				}
				ev.events = EPOLLIN|EPOLLET;
				ev.data.fd = sc;
				if(epoll_ctl(epfd,EPOLL_CTL_ADD,sc,&ev)==-1){
					perror("epoll_ctl:sc");
					exit(EXIT_FAILURE);
				}
			}else{
				server_process(sc);
			}
		}
    }
}

程序分析:

使用epoll监听ss,epoll_wait监听机制取代了fork线程机制,减少线程切换开销。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值