6

网络编程学习——Linux epoll多路复用模型 - zj城城城城

 2 years ago
source link: https://www.cnblogs.com/zjccc/p/16107538.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

后端开发的应该都知道Nginx服务器,Nginx是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。后端部署中一般使用的就是Nginx反向代理技术。

Nginx 相较于 Apache 具有占有内存少,稳定性高等优势,并发能力强的优点。它所使用的网络通信模型就是epoll。

(*注:epoll模型编程实例需要先了解红黑树、tcp/ip、socket、文件描述符fd、阻塞、回调等概念)

epoll介绍

一、epoll模型概念整理

传统的并发服务器Apache,使用的是多进程/线程模型,每一个客户端请求都要开启一个进程去处理,占用的资源大。

epoll是一个I/O多路复用模型,可以用一个进程去处理处理多个客户端。

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。关于select和poll是更早的多路复用IO模型,这里不做介绍。

相对于select和poll来说,epoll更加灵活,没有描述符限制。

epoll使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll是基于事件驱动模型。展开应该叫event poll,事件轮询(猜测),所以程序围绕着event运行。

二、epoll模型详细运行过程

在Linux中,epoll模型相关的有3个系统API,通过man 2查看手册。

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);

*此外还有一个close(),create返回的是一个文件描述符int epoll_fd,结束时和普通fd一样要关闭,保证逻辑的完整性。

1.第一步,创建eventepoll结构体

当某一进程调用epoll_create函数时(参数size是事件最大数量,实际上这只是给内核的一个参考值,Linux2.6.8以后这个参数被忽略,但是api文档仍然建议填写),

Linux内核会创建一个eventpoll结构体,并返回一个int epoll_fd,这就是epoll通过一个文件描述符操作多个文件描述符的方法。

这个结构体中有两个成员与epoll的使用方式密切相关。

eventpoll结构体如下所示:

struct eventpoll{
  struct rb_root rbr;
  struct list_head rdlist;
};

其中rbr是一个红黑树,它的每个结点用来存储用户关心的事件(用户关心的事件,比如服务端server_fd的accept连接请求就是一个事件)。

rblist是一个双向链表用来存储已发生的事件。

事件的结构体如下所示:

typedef union epoll_data {
  void *ptr;
  int fd;  /*事件对应的文件描述符*/
  uint32_t u32;
  uint64_t u64;
}epoll_data_t;

struct epoll_event {
  uint32_t events;    /* Epoll events */
  epoll_data_t data;      /* User data variable */
};

一个事件应该对应至少着一个文件描述符加I/O操作,代表这个事件对应的文件描述符和它是读事件还是写事件。

对应的I/O操作再epoll_event结构体中是同一个变量events,通过"按位或"操作可以同时添加关心读和写事件,"按位与"操作把它读取。

而它对应的文件描述符不直接在epoll_event结构体下,而是epoll_eventd的data union体下的fd。

2. 第二步,epoll_ctl操作红黑树

当用户调用epoll_ctl向结构体加入event时,会把事件挂在到红黑树rbr中。

而所有添加到epoll中的事件都会与低层接口(设备、网卡驱动程序)建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。

这个回调方法在内核中叫ep_poll_callback,这个过程中,因为用户关心的事件挂载在红黑树上,所以查找效率高只有O(ln(n))的事件复杂度。

然后它会将发生的事件添加到rdlist双链表中。红黑树加上函数回调的机制造就了它的高效。

 epoll_event示意图

3. 第三步,epoll_wait检查双向链表

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。

如果rdlist不为空,则把发生的事件复制到用户态(把内核的双向链表拷贝成一个struct epoll_event数组),同时返回文件描述符的数量。

用户只需要用这个数组去接收就可以。

为什么这里要用双向链表而不是单链表?

就绪列表引用着就绪的Socket,所以它应能够快速的插入数据。程序可能随时调用 epoll_ctl 添加监视Socket,也可能随时删除。

当删除时,若该Socket 已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。

双向链表就是这样一种数据结构,Epoll 使用双向链表来实现就绪队列。

三、编程实例

具体实现可以用一个进程去处理多客户端请求,而不用

1. 使用的头文件

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/epoll.h>  /* epoll模型api */

2. 函数声明

#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.1.7"
#define QUIT "quit"  /*用户退出指令*/
#define BACKLOG 5  /*监听的最大等待连接队列*/
#define EPOSIZE 100  /*接收已发生事件的最大数量*/

/* 套接字初始化的封装 */ int sock_init(int fd, struct sockaddr_in *sin); /* epoll_wait获得已发生的事件集合之后,具体的业务逻辑 */ void handle_events(int epoll_fd, struct epoll_event *events, int num, int accept_fd); /* 具体操作1,接收客户端连接 */ void do_accpet(int epoll_fd, int accept_fd); /* 具体操作2,读操作 */ void do_read(int epoll_fd, int fd, char *buff); /* 具体操作3,写操作 */ void do_write(int epoll_fd, int fd, char *buff); /*把epoll_ctl函数的操作再封装*/ void event_ctl(int epoll_fd, int fd, int flag, int state); /* argument: flag EPOLL_CTL_ADD 添加事件 EPOLL_CTL_DEL 删除事件 EPOLL_CTL_MOD 修改事件 argument: state EPOLLIN input事件 EPOLLOUT output事件 */

3. demo实现

#include "server.h"
int main()
{
    int ret = -1;
    int accept_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(accept_fd < 0)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in sin;
    ret = sock_init(accept_fd, &sin);
    if(ret < 0)
    {
        perror("sock_init");
        return 2;
    }

    int epoll_fd = epoll_create(EPOSIZE);
    struct epoll_event events[EPOSIZE];/*用户空间数组去接收内核的双向链表*/
    event_ctl(epoll_fd, accept_fd, EPOLL_CTL_ADD, EPOLLIN);//先把server_fd accept input事件加入红黑树

    while(1)
    {
        ret = epoll_wait(epoll_fd, events, EPOSIZE, -1);/*参数4,超时时间,特别的-1为阻塞等待,详见linux api: man 2 epoll_wait*/
        handle_events(epoll_fd, events, ret, accept_fd);
    }

    close(epoll_fd);
    close(accept_fd);
}
int sock_init(int fd, struct sockaddr_in *sin) { bzero(sin,sizeof(*sin)); sin->sin_family = AF_INET; sin->sin_port = htons(SERV_PORT); sin->sin_addr.s_addr = INADDR_ANY; if(bind(fd, (struct sockaddr*)sin, sizeof(*sin)) < 0) { perror("bind"); return -1; } if(listen(fd, BACKLOG) < 0) { perror("listen"); return -2; } return 0; }

void handle_events(int epoll_fd, struct epoll_event *events, int num, int accept_fd) { int i,fd; char buff[BUFSIZ]; for(i=0; i<num; i++) { fd = events[i].data.fd; if((fd == accept_fd) && events[i].events & EPOLLIN)/* 对events和EPOLLIN“与”操作,判断这个文件描述符是否有input事件*/ do_accpet(epoll_fd, fd); else if(events[i].events & EPOLLIN) do_read(epoll_fd, fd, buff); else if(events[i].events & EPOLLOUT) do_write(epoll_fd, fd, buff); } }
void do_accpet(int epoll_fd, int accept_fd) { int new_fd; struct sockaddr_in cin; socklen_t len; new_fd = accept(accept_fd, (struct sockaddr*)&cin, &len); if(new_fd < 0) { perror("accpet"); return; } printf("a new client connected!\n");

/*add_event input,
  client连接之后,把它的文件描述符的input事件加入到红黑树中*/
  */
event_ctl(epoll_fd, new_fd, EPOLL_CTL_ADD, EPOLLIN); }
void do_read(int epoll_fd, int fd, char *buff) { int ret = -1; bzero(buff, BUFSIZ); ret = read(fd, buff, BUFSIZ-1); if(ret == 0 || !strncmp(buff, QUIT, strlen(QUIT))) { printf("a client quit.\n"); close(fd); event_ctl(epoll_fd, fd, EPOLL_CTL_DEL, EPOLLIN);//delete_event in return; } if(ret < 0) { perror("read"); return; } printf("%s\n", buff); event_ctl(epoll_fd, fd, EPOLL_CTL_MOD, EPOLLOUT);//modif_event out } void do_write(int epoll_fd, int fd, char *buff) { int ret = -1; ret = write(fd, buff, strlen(buff)); if(ret <= 0) perror("write"); event_ctl(epoll_fd, fd, EPOLL_CTL_MOD, EPOLLIN);//modif_event in }
/*
参考的资料中把他分成几个操作,更直观
这里为了方便多加一个参数,增加事件/删除事件/修改事件/,把增删改集成到一个函数。
*/ void event_ctl(int epoll_fd, int fd, int flag, int state) { struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epoll_fd, flag, fd, &ev); }

关于epoll的水平触发LT和边缘触发ET还没研究清楚,

应该是类似驱动程序中检测硬件信号中,高/低电平触发,上升沿/下降沿触发。

* !!FIXME

参考资料:

https://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html

https://blog.csdn.net/u011063112/article/details/81771440

https://blog.csdn.net/armlinuxww/article/details/92803381


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK