600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > Linux网络编程(六)-高并发服务器03-I/O多路复用03:epoll工作方式【①水平触发LT(

Linux网络编程(六)-高并发服务器03-I/O多路复用03:epoll工作方式【①水平触发LT(

时间:2023-03-30 14:42:16

相关推荐

Linux网络编程(六)-高并发服务器03-I/O多路复用03:epoll工作方式【①水平触发LT(

epoll事件有两种模型:

Edge Triggered (ET) 边缘触发:只有数据第一次进入缓冲区时才触发(只触发一次),以后不再触发epoll_wait(),不管缓存区中是否还有数据;触发epoll_wait()后要将缓冲区内的数据读完之后才读下一批数据;Level Triggered (LT) 水平触发:只要监听的缓冲区有数据就会触发epoll_wait()。

epoll_wait() 的边缘触发、水平触发

思考如下步骤:

假定我们已经把一个用来从管道中读取数据的文件描述符(RFD)添加到epoll描述符。管道的另一端写入了2KB的数据调用epoll_wait,并且它会返回RFD,说明它已经准备好读取操作读取1KB的数据调用epoll_wait……

在这个过程中,有两种工作模式:

ET模式(边缘触发)即Edge Triggered工作模式。 如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 基于非阻塞文件句柄只有当read或者write返回EAGAIN(非阻塞读,暂时无数据)时才需要挂起、等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。LT模式(水平触发)即Level Triggered工作模式。 ​​​​与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).

水平触发 v.s. 边缘触发:

LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).

一、epoll监听socket实现高并发服务器【水平触发(默认)】

只要缓冲区有数据,就触发epoll_wait系统调用。

#include <stdio.h>#include <fcntl.h>#include "wrap.h"#include <sys/epoll.h>int main(int argc, char *argv[]){//创建套接字 绑定int lfd = tcp4bind(8000,NULL);//监听Listen(lfd,128);//创建树int epfd = epoll_create(1);//将lfd上树struct epoll_event ev,evs[1024];ev.data.fd = lfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);//while监听while(1){int nready = epoll_wait(epfd,evs,1024,-1);//监听printf("epoll wait _________________\n");if(nready <0)// 出错了{perror("");break;}else if( nready == 0)// 没有文件描述符变化{continue;}else//有文件描述符变化{for(int i=0;i<nready;i++) // nready为变化了的文件描述符数量,变化了的文件描述符保存在了evs数组中{//判断lfd变化,并且是“读”事件变化(变化的文件描述符也有可能是写事件)if(evs[i].events & EPOLLIN && evs[i].data.fd == lfd){struct sockaddr_in cliaddr;char ip[16]="";socklen_t len = sizeof(cliaddr);//提取新的连接int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);printf("new client ip=%s port =%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16), ntohs(cliaddr.sin_port));//将cfd上树ev.data.fd =cfd; // 可以重复使用之前定义的evev.events =EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);}else if(evs[i].events & EPOLLIN)//cfd 变化 ,而且是读事件变化{char buf[4]="";int n = read(evs[i].data.fd,buf,sizeof(buf));if(n < 0)//出错,cfd下树{//普通错误perror("");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);break;}else if(n == 0)//客户端关闭 ,{printf("client close\n");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);//下树break;}else{//printf("%s\n",buf);write(STDOUT_FILENO,buf,4);write(evs[i].data.fd,buf,n);}}}}}return 0;}

二、epoll监听socket实现高并发服务器【边缘触发】

默认的水平触发会经常触发系统调用epoll_wait(),会消耗系统资源;

因为设置为水平触发,只要缓存区有数据epoll_wai惭会被触发,epoll_wait是一个系统调用,尽量少调用;

所以尽量使用边沿触发,边沿出触发数据来一次只触发一次这个时候要求一次性将数据读完,所以while循环读,读到最后read默认带阻塞,不能让read阻塞,因为不能再去监听,设置cfd为非阻塞,read读到最后一次返回值为-1。判断errno的值为EAGAN代表数据读干净。

将cfd设置为边缘触发。

1、在 ev.events =EPOLLIN处再加上EPOLLET,此设置会形成阻塞,也没法一次读完缓冲区内的所有数据

//将cfd上树ev.data.fd =cfd; // 可以重复使用之前定义的evev.events =EPOLLIN | EPOLLET; // EPOLLET 边缘触发epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);

新写入1234567890之后,触发一次epoll_wait(),读取4个字节,缓冲区内还剩下数据:567890,然后阻塞

新写入abcdef之后,由于有新数据进入缓冲区,所以再次触发一次epoll_wait(),读取4个字节,这次读取的4个字节为5678,缓冲区内还剩下数据:90abcdef,然后阻塞

2、设置非阻塞,且一次性将缓冲区内的数据读完

2.1 设置cfd为非阻塞

//设置cfd为非阻塞int flags = fcntl(cfd,F_GETFL);//获取的cfd的标志位flags |= O_NONBLOCK;fcntl(cfd,F_SETFL,flags);

2.2 将下述代码用while(1){}进行包裹

char buf[4]="";int n = read(evs[i].data.fd,buf,sizeof(buf));if(n < 0)//出错,cfd下树{//普通错误perror("");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);break;}else if(n == 0)//客户端关闭 ,{printf("client close\n");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);//下树break;}else{//printf("%s\n",buf);write(STDOUT_FILENO,buf,4);write(evs[i].data.fd,buf,n);}

包裹之后:

while(1){char buf[4]="";int n = read(evs[i].data.fd,buf,sizeof(buf));if(n < 0)//出错,cfd下树{//普通错误perror("");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);break;}else if(n == 0)//客户端关闭 ,{printf("client close\n");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);//下树break;}else{//printf("%s\n",buf);write(STDOUT_FILENO,buf,4);write(evs[i].data.fd,buf,n);}}

2.3 如果缓冲区内的数据读取完毕,即n < 0时,不应该下树

将下述代码加到if(n < 0)中,使得如果缓冲区内的数据读取完毕后,不用下树,而应该跳出当前while(1)循环,继续监听;

//如果缓冲区读干净了,这个时候应该跳出while(1)循环,继续监听if(errno == EAGAIN){break;}

while(1){char buf[4]="";//如果读一个缓冲区,缓冲区没有数据,如果是带阻塞,就阻塞等待,如果是非阻塞,返回值等于-1,并且会将errno 值设置为EAGAINint n = read(evs[i].data.fd,buf,sizeof(buf));if(n < 0)//出错,cfd下树{//如果缓冲区读干净了,这个时候应该跳出while(1)循环,继续监听if(errno == EAGAIN){break;}//普通错误perror("");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);break;}else if(n == 0)//客户端关闭 ,{printf("client close\n");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);//下树break;}else{//printf("%s\n",buf);write(STDOUT_FILENO,buf,4);write(evs[i].data.fd,buf,n);}}

完整代码如下:

#include <stdio.h>#include <fcntl.h>#include "wrap.h"#include <sys/epoll.h>int main(int argc, char *argv[]){//创建套接字 绑定int lfd = tcp4bind(8000,NULL);//监听Listen(lfd,128);//创建树int epfd = epoll_create(1);//将lfd上树struct epoll_event ev,evs[1024];ev.data.fd = lfd;ev.events = EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);//while监听while(1){int nready = epoll_wait(epfd,evs,1024,-1);//监听printf("epoll wait _________________\n");if(nready <0)// 出错了{perror("");break;}else if( nready == 0)// 没有文件描述符变化{continue;}else//有文件描述符变化{for(int i=0;i<nready;i++) // nready为变化了的文件描述符数量,变化了的文件描述符保存在了evs数组中{//判断lfd变化,并且是“读”事件变化(变化的文件描述符也有可能是写事件)if(evs[i].events & EPOLLIN && evs[i].data.fd == lfd){struct sockaddr_in cliaddr;char ip[16]="";socklen_t len = sizeof(cliaddr);//提取新的连接int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);printf("new client ip=%s port =%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16), ntohs(cliaddr.sin_port));//将cfd上树ev.data.fd =cfd; // 可以重复使用之前定义的evev.events =EPOLLIN;epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);}else if(evs[i].events & EPOLLIN)//cfd 变化 ,而且是读事件变化{while(1){char buf[4]="";//如果读一个缓冲区,缓冲区没有数据,如果是带阻塞,就阻塞等待,如果是非阻塞,返回值等于-1,并且会将errno 值设置为EAGAINint n = read(evs[i].data.fd,buf,sizeof(buf));if(n < 0)//出错,cfd下树{//如果缓冲区读干净了,这个时候应该跳出while(1)循环,继续监听if(errno == EAGAIN){break;}//普通错误perror("");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);break;}else if(n == 0)//客户端关闭 ,{printf("client close\n");close(evs[i].data.fd);//将cfd关闭epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,&evs[i]);//下树break;}else{//printf("%s\n",buf);write(STDOUT_FILENO,buf,4);write(evs[i].data.fd,buf,n);}}}}}}return 0;}

三、工具函数

wrap.h

#ifndef __WRAP_H_#define __WRAP_H_#include <stdlib.h>#include <stdio.h>#include <unistd.h>#include <errno.h>#include <string.h>#include <sys/socket.h>#include <arpa/inet.h>#include <strings.h>void perr_exit(const char *s);int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);int Bind(int fd, const struct sockaddr *sa, socklen_t salen);int Connect(int fd, const struct sockaddr *sa, socklen_t salen);int Listen(int fd, int backlog);int Socket(int family, int type, int protocol);ssize_t Read(int fd, void *ptr, size_t nbytes);ssize_t Write(int fd, const void *ptr, size_t nbytes);int Close(int fd);ssize_t Readn(int fd, void *vptr, size_t n);ssize_t Writen(int fd, const void *vptr, size_t n);ssize_t my_read(int fd, char *ptr);ssize_t Readline(int fd, void *vptr, size_t maxlen);int tcp4bind(short port,const char *IP);#endif

wrap.c

#include <stdlib.h>#include <stdio.h>#include <unistd.h>#include <errno.h>#include <string.h>#include <sys/socket.h>#include <arpa/inet.h>#include <strings.h>void perr_exit(const char *s){perror(s);exit(-1);}int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr){int n;again:if ((n = accept(fd, sa, salenptr)) < 0) {if ((errno == ECONNABORTED) || (errno == EINTR))//如果是被信号中断和软件层次中断,不能退出goto again;elseperr_exit("accept error");}return n;}int Bind(int fd, const struct sockaddr *sa, socklen_t salen){int n;if ((n = bind(fd, sa, salen)) < 0)perr_exit("bind error");return n;}int Connect(int fd, const struct sockaddr *sa, socklen_t salen){int n;if ((n = connect(fd, sa, salen)) < 0)perr_exit("connect error");return n;}int Listen(int fd, int backlog){int n;if ((n = listen(fd, backlog)) < 0)perr_exit("listen error");return n;}int Socket(int family, int type, int protocol){int n;if ((n = socket(family, type, protocol)) < 0)perr_exit("socket error");return n;}ssize_t Read(int fd, void *ptr, size_t nbytes){ssize_t n;again:if ( (n = read(fd, ptr, nbytes)) == -1) {if (errno == EINTR)//如果是被信号中断,不应该退出goto again;elsereturn -1;}return n;}ssize_t Write(int fd, const void *ptr, size_t nbytes){ssize_t n;again:if ( (n = write(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again;elsereturn -1;}return n;}int Close(int fd){int n;if ((n = close(fd)) == -1)perr_exit("close error");return n;}/*参三: 应该读取固定的字节数数据*/ssize_t Readn(int fd, void *vptr, size_t n){size_t nleft; //usigned int 剩余未读取的字节数ssize_t nread; //int 实际读到的字节数char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ((nread = read(fd, ptr, nleft)) < 0) {if (errno == EINTR)nread = 0;elsereturn -1;} else if (nread == 0)break;nleft -= nread;ptr += nread;}return n - nleft;}/*:固定的字节数数据*/ssize_t Writen(int fd, const void *vptr, size_t n){size_t nleft;ssize_t nwritten;const char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {if (nwritten < 0 && errno == EINTR)nwritten = 0;elsereturn -1;}nleft -= nwritten;ptr += nwritten;}return n;}static ssize_t my_read(int fd, char *ptr){static int read_cnt;static char *read_ptr;static char read_buf[100];if (read_cnt <= 0) {again:if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {if (errno == EINTR)goto again;return -1;} else if (read_cnt == 0)return 0;read_ptr = read_buf;}read_cnt--;*ptr = *read_ptr++;return 1;}ssize_t Readline(int fd, void *vptr, size_t maxlen){ssize_t n, rc;char c, *ptr;ptr = vptr;for (n = 1; n < maxlen; n++) {if ( (rc = my_read(fd, &c)) == 1) {*ptr++ = c;if (c == '\n')break;} else if (rc == 0) {*ptr = 0;return n - 1;} elsereturn -1;}*ptr = 0;return n;}int tcp4bind(short port,const char *IP){struct sockaddr_in serv_addr;int lfd = Socket(AF_INET,SOCK_STREAM,0);bzero(&serv_addr,sizeof(serv_addr));if(IP == NULL){//如果这样使用 0.0.0.0,任意ip将可以连接serv_addr.sin_addr.s_addr = INADDR_ANY;}else{if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){perror(IP);//转换失败exit(1);}}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);// int opt = 1;//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));return lfd;}

Linux网络编程多路IO复用----epoll_城南花已开.jpg的博客-CSDN博客

Linux网络编程(六)-高并发服务器03-I/O多路复用03:epoll工作方式【①水平触发LT(默认;系统开销大;实际开发中不常用)】【②边缘触发ET+非阻塞=高速模式】

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。