并发程序设计3:多路IO复用技术(2)

  上一节(https://www.cnblogs.com/yuanwebpage/p/12362876.html)记录了多路IO复用的第一种方式select函数,以及其相应的缺点。本节记录多路IO复用的第二种方式epoll(在windows系统下叫IOCP)。

1. epoll相关函数

  epoll函数克服了select函数的相关缺点,其优点如下:

(1) 只需向OS注册一次文件描述符集合,不用每次循环传递;

(2) epoll函数会将发生变化的文件描述符单独集中起来,这样每次遍历时只需要遍历发生变化的文件描述符。

(3) 相对于select同时监听的数量有限制,epoll监听数量一般远大于select,这对于多连接的服务器至关重要。

epoll用来集中通知变化的文件描述符结构体如下:

struct epoll_event
{
    __uint32_t events;  //用来注册是什么事件需要关注,如输入/输出
    epoll_data_t data;
}

typedef union epoll_data
{
    void* ptr;
    int fd; //发生变化的文件描述符
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

//可以看到,常用的为events, fd两个

epoll相关的函数总共有3个:

#include <sys/epoll.h>

int epoll_create(int size); //向OS申请创建管理所有文件描述符的epoll例程,返回该例程的文件描述符
size:可能注册的最大监视事件,仅供OS参考

int epoll_ctl(int epfd, int op, struct epoll_event* event); //成功返回0,失败-1,用于田间/删除/修改某个文件描述符
epfd:epoll_create返回的该epoll例程的描述符
op:具体的操作,如添加/删除,常用以下三种模式
    EPOLL_CTL_ADD:将fd的描述符注册到epfd,等价于FD_SET
    EPOLL_CTL_DEL:将fd的描述符从epfd移出,等价于FD_CLR
    EPOLL_CTL_MOD:修改fd所指描述符的监听类型
event:struct epoll_event结构体,内有一个event变量,指明需要监听的具体类型
    EPOLLiN:输入事件
    EPOLLOUT:输出事件
    EPOLLET:以边缘触发方式接收事件通知(稍后详述)

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); //类似于select,调用后监听发生变化的描述符,成功时返回发生事件的个数
epfd:epoll例程描述符
events:动态申请的结构体数组,用于保存,通知发生变化的文件描述符
maxevents:监视的最大事件数目,即events数组的大小
timeout:设置超时,单位为ms。-设置-1为无限等待

使用完了epoll例程后记得调用close()关闭。

有了以上epoll相关函数就很容易将之前两节的回声服务器服务器端改写为epoll形式的IO复用,完整代码如下:

 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 #include <string.h>
 4 #include <sys/socket.h>
 5 #include <arpa/inet.h>
 6 #include <unistd.h>
 7 #include <sys/epoll.h>
 8 #define EPOLL_SIZE 30 //定义监视事件的数组数量
 9
10 void error_handle(const char* msg)
11 {
12     fputs(msg,stderr);
13     fputc(‘\n‘,stderr);
14     exit(1);
15 }
16
17 int main(int argc,char* argv[])
18 {
19     //服务器建立连接
20     int servsock,clntsock;
21     struct sockaddr_in servaddr,clntaddr;
22     char message[50];
23     socklen_t clntlen;
24
25     if(argc!=2)
26         error_handle("Please input port number");
27
28     servsock=socket(PF_INET,SOCK_STREAM,0);  //1.建立套接字
29
30     memset(&servaddr,0,sizeof(servaddr));
31     servaddr.sin_family=AF_INET;
32     servaddr.sin_addr.s_addr=htonl(INADDR_ANY);  //默认本机IP地址
33     servaddr.sin_port=htons(atoi(argv[1]));
34
35     if(bind(servsock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
36         error_handle("bind error");  //2.建立连接
37
38     if(listen(servsock,10)==-1) //3.监听建立
39         error_handle("listen() error");
40
41     //到此的代码为socket创建过程,与前两节相同
42
43     //关于epoll的相关函数
44     epoll_event event;
45     event.events=EPOLLIN; //输入监听
46     event.data.fd=servsock;
47     int epfd=epoll_create(20); //创建epoll例程
48     epoll_ctl(epfd,EPOLL_CTL_ADD,servsock,&event); //向epfd添加fd的输入监听事件
49     struct epoll_event* events;
50     events=(epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //申请存放发生事件的数组
51     while(1)
52     {
53         int event_cnt=epoll_wait(epfd,events,EPOLL_SIZE,-1); //设置无限等待
54         if(event_cnt==-1)
55             printf("epoll_wait() error");
56         for(int i=0;i<event_cnt;i++)
57         {
58             if(events[i].data.fd==servsock)  //说明是新的客户端请求
59             {
60                 clntlen=sizeof(clntaddr);
61                 clntsock=accept(servsock,(struct sockaddr*)&clntaddr,&clntlen);
62                 if(clntsock==-1) //accept错误
63                 {
64                     close(clntsock);
65                     continue;
66                 }
67                 event.events=EPOLLIN;
68                 event.data.fd=clntsock;
69                 epoll_ctl(epfd,EPOLL_CTL_ADD,clntsock,&event); //将新申请的连接加入epfd
70                 printf("connecting\n");
71             }
72             else  //客户端发来消息
73             {
74                 int strlen=read(events[i].data.fd,message,sizeof(message));
75                 if(strlen==0){  //关闭连接请求
76                     epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL); //从epfd中删除
77                     close(events[i].data.fd);
78                 }
79                 else
80                     write(events[i].data.fd,message,strlen);
81             }
82         }
83     }
84     close(epfd); //关闭epoll例程
85     close(servsock);
86     return 0;
87 }

基于epoll的回声服务器服务器端代码

注,代码中用malloc申请的events数组,也可以直接申请struct epoll_event events[EPOLL_SIZE];

2. 条件触发和边缘触发

  上面在提到的在epoll_ctl加入新的描述符时,有一个选项:EPOLLET,即以边缘触发方式响应,1中代码的方式是条件触发,那么条件触发和边缘触发有什么区别呢?条件触发只要输入缓冲中有数据,epoll_wait就能检测到并将其注册到通知描述符的events数组中;边缘触发只在接收缓冲进入数据时在events数组中注册一次,此后缓冲区的数据即时没读完,仍不再注册。举个例子,假设输入缓冲中来了20个字节的数据,每次读取4个字节,那么条件触发方式在循环时每次调用epoll_wait()都向events数组注册输入事件,边缘触发只在数据进入时触发一次,此后不再触发。如果按照这种方式工作,边缘触发将使输入缓冲的数据不断累积,造成溢出(PS:select函数其实可以算作条件触发)。

  那么怎么检测输入缓冲中是否有数据呢?Linux在<error.h>中声明了全局变量errno,read函数发现输入缓冲中没有数据时返回-1,同时在errno中保存EAGAIN常量。那么怎么设置为边缘触发模式呢?这里要用到fcntl函数:

#include <fcntl.h>
void setnonblock(int fd){  int flag=fcntl(fd, F_GETFL, 0); //获取描述符fd的属性  fcntl(fd, F_SETFL, flag|O_NONBLOCK); //添加非阻塞属性}

为了将条件触发改成边缘触发,在1中完整的代码上进行以下改动

头文件中添加
#include <fcntl.h>
#include <error.h>

45行:
event.events=EPOLLIN|EPOLLET;
在46行之后添加:
setnonblock(servsock);
67行:
event.events=EPOLLIN|EPOLLET;
68行之后添加:
setnonblock(clntsock);

此后在接收客户端消息的代码如下:
else  //接收客户端消息
{
    while(1) //循环读写直到输入缓冲为空
    {
        int strlen=read(events[i].data.fd,message,sizeof(message));  //read函数此时不再阻塞
        if(strlen==0){
            epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL); //从epfd中删除
            close(events[i].data.fd);
        }
        else if(strlen<0)
        {
            if(errno==EAGAIN) break;
        }
        else
            write(events[i].data.fd,message,strlen);
    }
}

修改补充代码

  关于边缘触发和条件触发的优缺点和应用场景,目前还没有发现比较好的资料。

原文地址:https://www.cnblogs.com/yuanwebpage/p/12365123.html

时间: 2024-08-30 08:38:28

并发程序设计3:多路IO复用技术(2)的相关文章

Redis03——Redis之单线程+多路IO复用技术

Redis 是单线程+多路IO复用技术 多路复用:使用一个线程来检查多个文件描述符的就绪状态 如果有一个文件描述符就绪,则返回 否则阻塞直到超时 得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(线程池) 阻塞lO:给女神发一条短信, 说我来找你了,然后就默默的一直等着 女神下楼,这个期间除了等待你不会做其他事情,属于备胎做法. 非阻塞IO:给女神发短信,如果不回,接着再发,一直 发到女神下楼,这个期间你除了发短信等待不会做其他事情,属于专-做法. IO多路复用:是找一个宿

Libevent的IO复用技术和定时事件原理

Libevent 是一个用C语言编写的.轻量级的开源高性能网络库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大:源代码相当精炼.易读:跨平台,支持 Windows. Linux. *BSD 和 Mac Os:支持多种 I/O 多路复用技术, epoll. poll. dev/poll. select 和 kqueue 等:支持 I/O,定时器和信号等事件:注册事件优先级. 1 Libevent中的epoll Libevent重

一次读懂 Select、Poll、Epoll IO复用技术

我们之前采用的多进程方式实现的服务器端,一次创建多个工作子进程来给客户端提供服务.其实这种方式是存在问题的. 可以打个比方:如果我们先前创建的几个进程承载不了目前快速发展的业务的话,是不是还得增加进程数?我们都知道系统创建进程是需要消耗大量资源的,所以这样就会导致系统资源不足的情况. 那么有没有一种方式可以让一个进程同时为多个客户端端提供服务? 接下来要讲的IO复用技术就是对于上述问题的最好解答. 对于IO复用,我们可以通过一个例子来很好的理解它.(例子来自于<TCP/IP网络编程>) 某教室

Linux网络编程-IO复用技术

IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提供了select.poll.epoll三种接口函数来实现IO复用. 1.select函数 #include <sys/select.h> #include <sys/time.h> int select(int nfds, fd_set *readfds, fd_set *writef

基于select类型多路IO复用,实现简单socket并发

还有很多缺限,如客户断开无限重复 以下转至老师博客: server: #!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = "alex" import select import socket import sys import queue server = socket.socket() server.setblocking(0) server_addr = ('localhost',5000) print('start

3高并发服务器:多路IO之epoll

 1 epoll epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并.发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了. 目前epell是linux大规模并发网络程序中的热门首选模型. epoll除了提供s

3高并发server:多路IO之epoll

 1 epoll epoll是Linux下多路复用IO接口select/poll的增强版本号,它能显著提高程序在大量并.发连接中仅仅有少量活跃的情况下的系统CPU利用率,由于它会复用文件描写叙述符集合来传递结果而不用迫使开发人员每次等待事件之前都必须又一次准备要被侦听的文件描写叙述符集合,还有一点原因就是获取事件的时候,它无须遍历整个被侦听的描写叙述符集,仅仅要遍历那些被内核IO事件异步唤醒而增加Ready队列的描写叙述符集合即可了. 眼下epell是linux大规模并发网络程序中的热门首选

linux的IO复用技术:select、pool、epool的区别以及epool的原理和使用

select.poll.epoll都是IO多路复用的机制,但是他们的机制有很大的区别 1.select select机制刚开始的时候,需要把fd_set从用户空间拷贝到内核空间,并且检测的fd数是有限制的,由FD_SETSIZE设置,一般是1024. 检测的时候,根据timeout,遍历fd_set表,把活跃的fd(可读写或者错误),拷贝到用户空间, 再在用户空间依次处理相关的fd. 这个机制是linux内核很早的版本,epool是根据select,pool基础上优化的,缺点比较多. 缺点: 1

Reactor模式,或者叫反应器模式 - 为什么用多路io复用提供吞吐量

Reactor这个词译成汉语还真没有什么合适的,很多地方叫反应器模式,但更多好像就直接叫reactor模式了,其实我觉着叫应答者模式更好理解一些.通过了解,这个模式更像一个侍卫,一直在等待你的召唤,或者叫召唤兽. 并发系统常使用reactor模式,代替常用的多线程的处理方式,节省系统的资源,提高系统的吞吐量. 先用比较直观的方式来介绍一下这种方式的优点,通过和常用的多线程方式比较一下,可能更好理解. 以一个餐饮为例,每一个人来就餐就是一个事件,他会先看一下菜单,然后点餐.就像一个网站会有很多的请