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

你说的曾经没有我的故事 提交于 2020-02-26 13:09:08

  上一节(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);
    }
}
修改补充代码

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!