Linux 练习十三 (Linux网络编程epoll + 源码练习)

文章介绍了epoll作为Linux内核提供的I/O多路复用机制,相较于select和poll的增强之处,包括其创建、控制接口的使用,以及边缘触发和水平触发两种模式。通过一个即时聊天的示例展示了epoll的实际应用,并对比了epoll与select的优缺点。epoll在处理大量连接时表现出更高的效率和灵活性。

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


使用环境:Ubuntu18.04
使用工具:VMWare workstations ,xshell

  作者在学习Linux的过程中对常用的命令进行记录,通过思维导图的方式梳理知识点,并且通过xshell连接vmware中ubuntu虚拟机进行操作,并将练习的截图注解,每句话对应相应的命令,读者可以无障碍跟练。
  本次练习的重点在于Linux的网络编程epoll。
这部分的内容是第十二篇的后续,考虑到内容太多,读起来很累,所以将epoll的部分分离放在这篇。

4 EPOLL多路复用

4.1 epoll介绍

epoll 是在 2.6 内核中提出的,是之前的 select 和 poll 的增强版本。相对于 select 和 poll 来说,epoll更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。同时 epoll 的性能更好,因此在工作中首选 epoll。后面会介绍select和epoll的区别。

4.2 epoll接口的使用

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  1. int epoll_create(int size); 创建一个句柄,size告诉内核这个监听的数据一共有多大。不同于select的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在 linux 下如果查看/proc/进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被耗尽。
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 不同于select函数在监听时告诉内核要监听什么类型事件,而epoll是先注册监听的事件类型。
    第一个参数是epoll的句柄
    第二个参数表示动作用三个宏来表示,EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
    第三个参数是需要监听的fd
    第四个参数是告诉内核需要监听什么事,struct epoll_event结构体:
struct epoll_event {
	__uint32_t events; /* Epoll events */
	epoll_data_t data; /* User data variable */
};

其中的events可以是如下几个宏:

作用
EPOLLIN表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
EPOLLOUT表示对应的文件描述符可以写;
EPOLLPRI表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR表示对应的文件描述符发生错误;
EPOLLHUP表示对应的文件描述符被挂断;
EPOLLET将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

epoll的两种触发模式:

  • LT(level triggered)水平触发,是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。当epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。传统的select/poll都是这种模型的代表。
  • ET (edge-triggered)边缘触发,是高速工作方式,只支持non-block socket。在这种模式下,当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。
  • ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
  1. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 等待事件的发生,类似于select调用。参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size,参数 timeout 是超时时间(毫秒,0 会立即返回,-1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。

4.3 示例:使用epoll实现即时聊天

通用头文件head.h

#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <time.h>
#include <grp.h>
#include <pwd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/time.h>
#include <strings.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <signal.h>
#include <pthread.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/epoll.h>
#define ARGS_CHECK(argc,num) {if(argc!=num) {printf("error args\n");return -1;}}
#define ERROR_CHECK(ret,retval,func_name) {if(ret==retval) {printf("errno=%d,",errno);fflush(stdout);perror(func_name);return -1;}}
#define THREAD_ERR_CHECK(ret,func_name) {if(ret!=0) {printf("%s failed,%d %s\n",func_name,ret,strerror(ret));return -1;}}

服务器端server.c

#include"head.h"

int main(int argc,char** argv)
{
	ARGS_CHECK(argc,3);
	int sfd;	
	sfd = socket(AF_INET,SOCK_STREAM,0);	//socket描述符
	ERROR_CHECK(sfd,-1,"socket");
	printf("sfd = %d\n",sfd);
	int reuse = 1,ret;
	//bind绑定之前,先设定一下端口重用
	ret = setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(int));
	ERROR_CHECK(ret,-1,"setsockopt");
	struct sockaddr_in ser_addr;	//定义服务端描述结构体
	bzero(&ser_addr,sizeof(ser_addr));//清空结构体
	ser_addr.sin_family=AF_INET;//代表要进行ipv4通信
    ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序
    ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序
    ret = bind(sfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));	//绑定sfd
    ERROR_CHECK(ret,-1,"bind");
    ret = listen(sfd,10);	//对sfd进行监听
    ERROR_CHECK(ret,-1,"listen");
	int new_fd;	//新的传输文件的描述符
	struct sockaddr_in client_addr;
    bzero(&client_addr,sizeof(client_addr));
    socklen_t addr_len=sizeof(client_addr);
	new_fd = accept(sfd,(struct sockaddr*)&client_addr,&addr_len);//接收队列中的建立连接请求,没有就阻塞
	ERROR_CHECK(new_fd,-1,"accept");
    printf("client ip=%s,port=%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
    
    //编写即时聊天
    char buf[128] = {0};
    int epfd = epoll_create(1);//创建一个句柄,即epoll描述符
    struct epoll_event event,evs[2];
    
    //先注册标准输入输出
    event.data.fd=STDIN_FILENO;
    event.events=EPOLLIN;//监控是否可读
    
    //用epfd描述符注册EPOLL_CTL_ADD一个监听STDIN_FILENO的事件event
    ret = epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&event);
    ERROR_CHECK(ret,-1,"epoll_ctl");
    
    //再用该结构体注册new_fd,和标准输入用同一个epoll描述符
    event.data.fd=new_fd;
    event.events = EPOLLIN;
    ret = epoll_ctl(epfd,EPOLL_CTL_ADD,new_fd,&event);
    ERROR_CHECK(ret,-1,"epoll_ctl");
    int ready_fd_num,i;
    while(1){
    	//监听描述对应的事件,-1表示时间不确定,evs用于获取内核中epfd对应的2个事件
		ready_fd_num = epoll_wait(epfd,evs,2,-1);
		for(i = 0;i<ready_fd_num;i++){
			if(evs[i].data.fd==STDIN_FILENO){	//如果是标准输入,就将内容读到缓冲区,并且发送给客户端
				bzero(buf,sizeof(buf));
				ret=read(STDIN_FILENO,buf,sizeof(buf));
				if(!ret){
                    printf("服务端想断开连接\n");
                    return 0;
                }
                send(new_fd,buf,strlen(buf),0);
			}
			if(evs[i].data.fd==new_fd){	//如果new_fd可读,则读入到缓冲区,并且打印出来
                bzero(buf,sizeof(buf));
                //服务器接收数据
                ret=recv(new_fd,buf,sizeof(buf),0);
                ERROR_CHECK(ret,-1,"recv");
                if(!ret) { //代表对方断开了
                    printf("客户端断开了连接\n");
                    return 0;
                }
                printf("客户端:%s\n",buf);
            }
		}
	}
	return 0;
}

客户端client.c

#include"head.h"

int main(int argc,char** argv)
{
    ARGS_CHECK(argc,3);
    int sfd;
    sfd=socket(AF_INET,SOCK_STREAM,0);//初始化一个网络描述符,对应了一个缓冲区
    ERROR_CHECK(sfd,-1,"socket");
    printf("sfd=%d\n",sfd);
    struct sockaddr_in ser_addr;
    bzero(&ser_addr,sizeof(ser_addr));//清空
    ser_addr.sin_family=AF_INET;//代表要进行ipv4通信
    ser_addr.sin_addr.s_addr=inet_addr(argv[1]);//把ip的点分十进制转为网络字节序
    ser_addr.sin_port=htons(atoi(argv[2]));//把端口转为网络字节序
    //客户端就要去连接服务器
    int ret=connect(sfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
    ERROR_CHECK(ret,-1,"connect");
    //编写即时聊天

    char buf[128]={0};
    fd_set rdset;
    while(1)
    {
        //清空集合并写入要监控的描述符
        FD_ZERO(&rdset);
        FD_SET(STDIN_FILENO,&rdset);
        FD_SET(sfd,&rdset);
        //监控哪一个描述符就绪
        ret=select(sfd+1,&rdset,NULL,NULL,NULL);
        if(FD_ISSET(STDIN_FILENO,&rdset))//如果标准输入可读
        {
            bzero(buf,sizeof(buf));
            ret=read(STDIN_FILENO,buf,sizeof(buf));
            if(!ret)
            {
                printf("I want go\n");
                break;
            }
            send(sfd,buf,strlen(buf)-1,0);//发送对应的字符串到对端,不带\n
        }
        if(FD_ISSET(sfd,&rdset))//如果sfd可读
        {
            bzero(buf,sizeof(buf));
            //服务器接收数据
            ret=recv(sfd,buf,sizeof(buf),0);
            ERROR_CHECK(ret,-1,"recv");
            if(!ret)//代表对方断开了
            {
                printf("byebye\n");
                break;
            }
            printf("%s\n",buf);
        }
    }
    close(sfd);
}

在这里插入图片描述

4.4 epoll和select的优缺点

epoll和select都是Linux下的I/O多路复用机制,但是它们的实现方式不同。select使用的是轮询的方式,而epoll使用的是事件通知的方式。因此,epoll在处理大量连接时,效率更高,而且不会随着连接数的增加而降低效率。另外,epoll支持水平触发和边缘触发两种模式,而select只支持水平触发模式。

select和epoll都是用于I/O多路复用的机制,但是它们有一些区别:

select的优点:

  1. select是标准的系统调用,几乎所有的操作系统都支持它;

  2. select支持的文件描述符数量没有上限,可以处理大量的连接;

  3. select可以同时处理多种类型的I/O事件,包括读、写和异常事件。

select的缺点:

  1. select每次调用都需要将所有的文件描述符从用户空间复制到内核空间,效率较低;

  2. select返回的文件描述符集合是一个线性的数组,每次遍历都需要遍历整个数组,效率较低;

  3. select对文件描述符的监控是“水平触发”,即只要文件描述符上有数据可读或可写,就会一直通知应用程序,这会导致应用程序频繁地被唤醒。

epoll的优点:

  1. epoll是Linux特有的系统调用,效率较高;

  2. epoll使用“事件触发”的方式,只有当文件描述符上有数据可读或可写时才会通知应用程序,避免了频繁唤醒应用程序的问题;

  3. epoll支持的文件描述符数量没有上限,可以处理大量的连接。

epoll的缺点:

  1. epoll只能在Linux系统上使用,不具有通用性;

  2. epoll的API比select复杂,使用起来较为困难;

  3. epoll不能同时处理异常事件,需要额外的处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值