前面介绍了select处理,这一章继续介绍另外一种I/O多路服用的机制:epoll。来比较下两种机制的不同点。
select: 调用过程如下:

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间

总结:

select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

对于select的几个缺点。epoll的改进机制如下:

对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

  对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。

  对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

总结:

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销

epoll的接口函数很简单,只有3个函数

1. int epoll_create(int size);

创建一个 epoll 的句柄, 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);

epoll 的事件注册函数,它不同与 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

第一个 参数是 epoll_create() 的返回值,

第二个 参数表示动作,用三个宏来表示:

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 队列里

3. 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 表示已超时。 epoll有两种工作方式:

LT level triggered 水平触发模式,

同时支持阻塞和非阻塞的socket。在这种模式中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行I/O操作,如果你不做任何操作,还是会继续通知你。(没处理这个流还是一直通知你)

ET edge triggered 边缘触发模式

只支持非阻塞的socket。效率比LT高。这种工作模式下,当从epoll_wait调用获取到事件后,如果没有把这次事件对应的套接字处理完,那么在这个套接字中没有心的时间再次到来时,ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT只要有数据就总可以获取。

参考下面这个图:

实现代码如下:

//网络编程服务端

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <errno.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <arpa/inet.h>//htons()函数头文件

#include <netinet/in.h>//inet_addr()头文件

#include <fcntl.h>

#include <sys/epoll.h>

#include "pub.h"

#define MAXSOCKET 20

int main(int arg, char *args[])

{

if (arg < 2)

{

printf("please print one param!\n");

return -1;

}

//create server socket

int listen_st = server_socket(atoi(args[1]));

if (listen_st < 0)

{

return -1;

}

/*

* 声明epoll_event结构体变量ev,变量ev用于注册事件,

* 数组events用于回传需要处理的事件

*/

struct epoll_event ev, events[100];

//生成用于处理accept的epoll专用文件描述符

int epfd = epoll_create(MAXSOCKET);

//把socket设置成非阻塞方式    setnonblock(listen_st);

//设置需要放到epoll池里的文件描述符

ev.data.fd = listen_st;

//设置这个文件描述符需要epoll监控的事件

/*

* EPOLLIN代表文件描述符读事件

*accept,recv都是读事件

*/

ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;

/*

* 注册epoll事件

* 函数epoll_ctl中&ev参数表示需要epoll监视的listen_st这个socket中的一些事件

*/

epoll_ctl(epfd, EPOLL_CTL_ADD, listen_st, &ev);

while (1)

{

/*

* 等待epoll池中的socket发生事件,这里一般设置为阻塞的

* events这个参数的类型是epoll_event类型的数组

* 如果epoll池中的一个或者多个socket发生事件,

* epoll_wait就会返回,参数events中存放了发生事件的socket和这个socket所发生的事件

* 这里强调一点,epoll池存放的是一个个socket,不是一个个socket事件

* 一个socket可能有多个事件,epoll_wait返回的是有消息的socket的数目

* 如果epoll_wait返回事件数组后,下面的程序代码却没有处理当前socket发生的事件

* 那么epoll_wait将不会再次阻塞,而是直接返回,参数events里面的就是刚才那个socket没有被处理的事件

*/

int nfds = epoll_wait(epfd, events, MAXSOCKET, -1);

if (nfds == -1)

{

printf("epoll_wait failed ! error message :%s \n", strerror(errno));

break;

}

int i = 0;

for (; i < nfds; i++)

{

if (events[i].data.fd < 0)

continue;

if (events[i].data.fd == listen_st)

{

//接收客户端socket

int client_st = server_accept(listen_st);

/*

* 监测到一个用户的socket连接到服务器listen_st绑定的端口

*

*/

if (client_st < 0)

{

continue;

}

//设置客户端socket非阻塞                setnonblock(client_st);

//将客户端socket加入到epoll池中

struct epoll_event client_ev;

client_ev.data.fd = client_st;

client_ev.events = EPOLLIN | EPOLLERR | EPOLLHUP;

epoll_ctl(epfd, EPOLL_CTL_ADD, client_st, &client_ev);

/*

* 注释:当epoll池中listen_st这个服务器socket有消息的时候

* 只可能是来自客户端的连接消息

* recv,send使用的都是客户端的socket,不会向listen_st发送消息的

*/

continue;

}

//客户端有事件到达

if (events[i].events & EPOLLIN)

{

//表示服务器这边的client_st接收到消息

if (socket_recv(events[i].data.fd) < 0)

{

close_socket(events[i].data.fd);

//接收数据出错或者客户端已经关闭

events[i].data.fd = -1;

/*这里continue是因为客户端socket已经被关闭了,

* 但是这个socket可能还有其他的事件,会继续执行其他的事件,

* 但是这个socket已经被设置成-1

* 所以后面的close_socket()函数都会报错

*/

continue;

}

/*

* 此处不能continue,因为每个socket都可能有多个事件同时发送到服务器端

* 这也是下面语句用if而不是if-else的原因,

*/

}

//客户端有事件到达

if (events[i].events & EPOLLERR)

{

printf("EPOLLERR\n");

//返回出错事件,关闭socket,清理epoll池,当关闭socket并且events[i].data.fd=-1,epoll会自动将该socket从池中清除                close_socket(events[i].data.fd);

events[i].data.fd = -1;

continue;

}

//客户端有事件到达

if (events[i].events & EPOLLHUP)

{

printf("EPOLLHUP\n");

//返回挂起事件,关闭socket,清理epoll池                close_socket(events[i].data.fd);

events[i].data.fd = -1;

continue;

}

}

}

//close epoll    close(epfd);

//close server socket    close_socket(listen_st);

return 0;

}

Linux c编程:I/O多路复用之epoll的更多相关文章

  1. unix网络编程——I/O多路复用之epoll

    1. 基本概念 当程序进行IO时,如果数据尚未准备好,那么IO将处于阻塞状态.当某个进程有多个打开的文件,比如socket,那么其后的所有准备好读写的文件将受到阻塞的影响而不能操作.不借助线程,单一进 ...

  2. Linux 网络编程的5种IO模型:多路复用(select/poll/epoll)

    Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 背景 我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO中,对于其中的 阻塞/非阻塞IO 进行了 ...

  3. python 网络编程 IO多路复用之epoll

    python网络编程——IO多路复用之epoll 1.内核EPOLL模型讲解     此部分参考http://blog.csdn.net/mango_song/article/details/4264 ...

  4. 非阻塞套接字编程, IO多路复用(epoll)

    非阻塞套接字编程: server端 import socket server = socket.socket() server.setblocking(False) server.bind(('', ...

  5. linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO(转载)

      IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作.那么我们对与外部设备的操作都可以看做对文件进行操作.我们对一个文件的读写,都通过调用内核提供的系统调用:内核给我们返回一个file ...

  6. linux下编程epoll实现将GPS定位信息上报到服务器

    操作系统:CentOS 开发板:fl2440 开发模块:A7(GPS/GPRS),RT3070(无线网卡) ********************************************** ...

  7. Python(七)Socket编程、IO多路复用、SocketServer

    本章内容: Socket IO多路复用(select) SocketServer 模块(ThreadingTCPServer源码剖析) Socket socket通常也称作"套接字" ...

  8. Linux 系统编程

    简介和主要概念 Linux 系统编程最突出的特点是要求系统程序员对它们工作的的系统的硬件和操作系统有深入和全面的了解,当然它们还有库和系统调用上的区别. 系统编程分为:驱动编程.用户空间编程和网络编程 ...

  9. Linux网络编程(六)

    网络编程中,使用多路IO复用的典型场合: 1.当客户处理多个描述字时(交互式输入以及网络接口),必须使用IO复用. 2.一个客户同时处理多个套接口. 3.一个tcp服务程序既要处理监听套接口,又要处理 ...

  10. Linux网络编程(四)

    在linux网络编程[1-3]中,我们编写的网络程序仅仅是为了了解网络编程的基本步骤,实际应用当中的网络程序并不会用那样的.首先,如果服务器需要处理高并发访问,通常不会使用linux网络编程(三)中那 ...

随机推荐

  1. 倍福TwinCAT(贝福Beckhoff)基础教程5.1 TwinCAT-1 获取和设置系统时间

    使用功能块NT_GetTime,NETID填写两个单引号表示本机,START就是一个触发信号,一般的功能块都需要一个上升沿触发执行,最后的输出类型都是让系统自己决定,然后统一把这些变量放到全局变量中( ...

  2. openlayers对接百度地图新方法

    上次给大家提供的openlayers对接百度地图有些问题,是因为没有进行分辨率设置,也没有进行相应的平面坐标转换,获取getURL的方法还是没有变化的 getURL: function (bounds ...

  3. iOS 通用button 上图下字

    UIButton *first = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, kHeight(80), kHeight(80))]; [firs ...

  4. 【Java】Java_20 Scanner获取键盘输入

    使用Scanner类可以很方便的获取用户的键盘输入,Scanner是一个基于正则表达式子的文本扫描器,他可以从文件.输入流.字符串中解析出基本类型值和字符串值. 例子示意: package com.o ...

  5. zabbix监控端口

    使用zabbix自带key监控进程与端口 每个公司都有自己的程序,自己的进程名与端口监听,对于nagios来说,这些都经常需要自己去写插件,但是zabbix不需要,它自己就有监控进程与端口的key. ...

  6. JAVA静态导入(inport static)详解

    在Java 5中,import语句得到了增强,以便提供甚至更加强大的减少击键次数功能,虽然一些人争议说这是以可读性为代价的.这种新的特性成为静态导入. 当你想使用static成员时,可以使用静态导入( ...

  7. Unable to connect to a repository at URL 解决方法

    提示"Unable to connect to a repository at URL 'svn://localhost/project1/'" or “Can't connect ...

  8. 怎样封装RESTful Web Service

    所谓Web Service是一个平台独立的,低耦合的.自包括的.可编程的Web应用程序.有了Web Service异构系统之间就能够通过XML或JSON来交换数据,这样就能够用于开发分布式的互操作的应 ...

  9. JSON-Schema 最科学的表单验证模式

    相关传送门: # JSON schema与表单验证 https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651226711&i ...

  10. rbg大神的主页

    http://www.rossgirshick.info/ Ross Girshick (rbg)Research ScientistFacebook AI Research (FAIR) r...@ ...