大数跨境
0
0

网络通信基础重难点解析 :Linux epoll 模型

网络通信基础重难点解析 :Linux epoll 模型 CppGuide
2019-05-17
1
导读:详解 Linux epoll 模型重难点。

Linux epoll 模型


综合 select 和 poll 的一些优缺点,Linux 从内核 2.6 版本开始引入了更高效的 epoll 模型,本节我们来详细介绍 epoll 模型。


要想使用 epoll 模型,必须先需要创建一个 epollfd,这需要使用 epoll_create 函数去创建:

1#include <sys/epoll.h>
2
3int epoll_create(int size);

参数 size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。epoll_create 函数调用成功返回一个非负值的 epollfd,调用失败返回 -1。


有了 epollfd 之后,我们需要将我们需要检测事件的其他 fd 绑定到这个 epollfd 上,或者修改一个已经绑定上去的 fd 的事件类型,或者在不需要时将 fd 从 epollfd 上解绑,这都可以使用 epoll_ctl 函数:

1int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

参数说明:


参数 epfd 即上文提到的 epollfd;


参数 op,操作类型,取值有 EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL,分别表示向 epollfd 上添加、修改和移除一个其他 fd,当取值是 EPOLL_CTL_DEL,第四个参数 event 忽略不计,可以设置为 NULL;


参数 fd,即需要被操作的 fd;


参数 event,这是一个 epoll_event 结构体的地址,epoll_event 结构体定义如下:

1struct epoll_event
2{

3    uint32_t     events;      /* 需要检测的 fd 事件,取值与 poll 函数一样 */
4    epoll_data_t data;        /* 用户自定义数据 */
5};

epoll_event 结构体的 data 字段的类型是 epoll_data_t,我们可以利用这个字段设置一个自己的自定义数据,它本质上是一个 Union 对象,在 64 位操作系统中其大小是 8 字节,其定义如下:

1typedef union epoll_data
2{
3    void*         ptr;
4    int          fd;
5    uint32_t     u32;
6    uint64_t     u64;
7epoll_data_t;

函数返回值:epoll_ctl 调用成功返回 0,调用失败返回 -1,你可以通过 errno 错误码获取具体的错误原因。


创建了 epollfd,设置好某个 fd 上需要检测事件并将该 fd 绑定到 epollfd 上去后,我们就可以调用 epoll_wait 检测事件了,epoll_wait 函数签名如下:

1int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

参数的形式和 poll 函数很类似,参数 events 是一个 epoll_event 结构数组的首地址,这是一个输出参数,函数调用成功后,events  中存放的是与就绪事件相关 epoll_event 结构体数组;参数 maxevents 是数组元素的个数;timeout 是超时时间,单位是毫秒,如果设置为 0,epoll_wait 会立即返回。


当 epoll_wait 调用成功会返回有事件的 fd 数目;如果返回 0 表示超时;调用失败返回 -1。

epoll_wait 使用示例如下:

 1while (true)
2{
3    epoll_event epoll_events[1024];
4    int n = epoll_wait(epollfd, epoll_events, 10241000);
5    if (n < 0)
6    {
7        //被信号中断
8        if (errno == EINTR)
9            continue;
10
11        //出错,退出
12        break;
13    }
14    else if (n == 0)
15    {
16        //超时,继续
17        continue;
18    }
19
20    for (size_t i = 0; i < n; ++i)
21    {
22        // 处理可读事件
23        if (epoll_events[i].events & POLLIN)
24        {
25        }
26        // 处理可写事件
27        else if (epoll_events[i].events & POLLOUT)
28        {
29        }
30        //处理出错事件
31        else if (epoll_events[i].events & POLLERR)
32        {
33        }
34    }
35}

epoll_wait 与 poll 的区别

通过前面介绍 poll 与 epoll_wait 函数的介绍,我们可以发现:


epoll_wait 函数调用完之后,我们可以直接在 event 参数中拿到所有有事件就绪的 fd,直接处理即可(event 参数仅仅是个出参);而 poll 函数的事件集合调用前后数量都未改变,只不过调用前我们通过 pollfd 结构体的 events 字段设置待检测事件,调用后我们需要通过 pollfd 结构体的 revents 字段去检测就绪的事件( 参数 fds 既是入参也是出参)。


举个生活中的例子,某人不断给你一些苹果,这些苹果有生有熟,调用 epoll_wait 相当于:

11. 你把苹果挨个投入到 epoll 机器中(调用 epoll_ctl);
22. 调用 epoll_wait 加工,你直接通过另外一个袋子就能拿到所有熟苹果。

调用 poll 相当于:

11. 把收到的苹果装入一个袋子里面然后调用 poll 加工;
22. 调用结束后,拿到原来的袋子,袋子中还是原来那么多苹果,只不过熟苹果被贴上了标签纸,你还是需要挨个去查看标签纸挑选熟苹果。

当然,这并不意味着,poll 函数的效率不如 epoll_wait,一般在 fd 数量比较多,但某段时间内,就绪事件 fd 数量较少的情况下,epoll_wait 才会体现出它的优势,也就是说 socket 连接数量较大时而活跃连接较少时 epoll 模型更高效。


LT 模式和 ET 模式


与 poll 的事件宏相比,epoll 新增了一个事件宏 EPOLLET,这就是所谓的边缘触发模式(Edge Trigger,ET),而默认的模式我们称为 水平触发模式(Level Trigger,LT)。这两种模式的区别在于:


对于水平触发模式,一个事件只要有,就会一直触发;

对于边缘触发模式,只有一个事件从无到有才会触发。

这两个词汇来自电学术语,你可以将 fd 上有数据认为是高电平,没有数据认为是低电平,将 fd 可写认为是高电平,fd 不可写认为是低电平。那么水平模式的触发条件是状态处于高电平,而边缘模式的触发条件是新来一次电信号将当前状态变为高电平,即:


水平模式的触发条件

11. 低电平 => 高电平
22. 处于高电平状态

边缘模式的触发条件

11. 低电平 => 高电平

说的有点抽象,以 socket 的读事件为例,对于水平模式,只要 socket 上有未读完的数据,就会一直产生 POLLIN 事件;而对于边缘模式,socket 上每新来一次数据就会触发一次,如果上一次触发后,未将 socket 上的数据读完,也不会再触发,除非再新来一次数据。对于 socket 写事件,如果 socket 的 TCP 窗口一直不饱和,会一直触发 POLLOUT 事件;而对于边缘模式,只会触发一次,除非 TCP 窗口由不饱和变成饱和再一次变成不饱和,才会再次触发 POLLOUT 事件。


socket 可读事件水平模式触发条件:

11socket上无数据 => socket上有数据
22socket处于有数据状态

socket 可读事件边缘模式触发条件:

11socket上无数据 => socket上有数据
22socket又新来一次数据

socket 可写事件水平模式触发条件:

11socket可写   => socket可写
22socket不可写 => socket可写

socket 可写事件边缘模式触发条件:

11socket不可写 => socket可写

也就是说,如果对于一个非阻塞 socket,如果使用 epoll 边缘模式去检测数据是否可读,触发可读事件以后,一定要一次性把 socket 上的数据收取干净才行,也就是说一定要循环调用 recv 函数直到 recv 出错,错误码是EWOULDBLOCK(EAGAIN 一样)(此时表示 socket 上本次数据已经读完);如果使用水平模式,则不用,你可以根据业务一次性收取固定的字节数,或者收完为止。边缘模式下收取数据的代码写法示例如下:

 1bool TcpSession::RecvEtMode()
2{
3    //每次只收取256个字节
4    char buff[256];
5    while (true)
6    {       
7        int nRecv = ::recv(clientfd_, buff, 2560);
8        if (nRecv == -1)
9        {
10            if (errno == EWOULDBLOCK)
11                return true;
12            else if (errno == EINTR)
13                continue;
14
15            return false;
16        }
17        //对端关闭了socket
18        else if (nRecv == 0)
19            return false;
20
21        inputBuffer_.add(buff, (size_t)nRecv);
22    }
23
24    return true;
25}

下面我们来看一个具体的例子来比较一下 LT 模式与 ET 模式的区别,代码如下:

  1/** 
2 * 验证epoll的LT与ET模式的区别, epoll_server.cpp
3 * zhangyl 2019.04.01
4 */

5#include<sys/types.h>
6#include<sys/socket.h>
7#include<arpa/inet.h>
8#include<unistd.h>
9#include<fcntl.h>
10#include<sys/epoll.h>
11#include<poll.h>
12#include<iostream>
13#include<string.h>
14#include<vector>
15#include<errno.h>
16#include<iostream>
17
18int main()
19
{
20    //创建一个监听socket
21    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
22    if (listenfd == -1)
23    {
24        std::cout << "create listen socket error" << std::endl;
25        return -1;
26    }
27
28    //设置重用ip地址和端口号
29    int on = 1;
30    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
31    setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on));
32
33
34    //将监听socker设置为非阻塞的
35    int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
36    int newSocketFlag = oldSocketFlag | O_NONBLOCK;
37    if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1)
38    {
39        close(listenfd);
40        std::cout << "set listenfd to nonblock error" << std::endl;
41        return -1;
42    }
43
44    //初始化服务器地址
45    struct sockaddr_in bindaddr;
46    bindaddr.sin_family = AF_INET;
47    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
48    bindaddr.sin_port = htons(3000);
49
50    if (bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1)
51    {
52        std::cout << "bind listen socker error." << std::endl;
53        close(listenfd);
54        return -1;
55    }
56
57    //启动监听
58    if (listen(listenfd, SOMAXCONN) == -1)
59    {
60        std::cout << "listen error." << std::endl;
61        close(listenfd);
62        return -1;
63    }
64
65
66    //创建epollfd
67    int epollfd = epoll_create(1);
68    if (epollfd == -1)
69    {
70        std::cout << "create epollfd error." << std::endl;
71        close(listenfd);
72        return -1;
73    }
74
75    epoll_event listen_fd_event;
76    listen_fd_event.data.fd = listenfd;
77    listen_fd_event.events = EPOLLIN;
78    //取消注释掉这一行,则使用ET模式
79    //listen_fd_event.events |= EPOLLET;
80
81    //将监听sokcet绑定到epollfd上去
82    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1)
83    {
84        std::cout << "epoll_ctl error" << std::endl;
85        close(listenfd);
86        return -1;
87    }
88
89    int n;
90    while (true)
91    {
92        epoll_event epoll_events[1024];
93        n = epoll_wait(epollfd, epoll_events, 10241000);
94        if (n < 0)
95        {
96            //被信号中断
97            if (errno == EINTR) 
98                continue;
99
100            //出错,退出
101            break;
102        }
103        else if (n == 0)
104        {
105            //超时,继续
106            continue;
107        }
108        for (size_t i = 0; i < n; ++i)
109        {
110            //事件可读
111            if (epoll_events[i].events & EPOLLIN)
112            {
113                if (epoll_events[i].data.fd == listenfd)
114                {
115                    //侦听socket,接受新连接
116                    struct sockaddr_in clientaddr;
117                    socklen_t clientaddrlen = sizeof(clientaddr);
118                    int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen);
119                    if (clientfd != -1)
120                    {
121                        int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
122                        int newSocketFlag = oldSocketFlag | O_NONBLOCK;
123                        if (fcntl(clientfd, F_SETFD, newSocketFlag) == -1)
124                        {
125                            close(clientfd);
126                            std::cout << "set clientfd to nonblocking error." << std::endl;
127                        }
128                        else
129                        {
130                            epoll_event client_fd_event;
131                            client_fd_event.data.fd = clientfd;
132                            client_fd_event.events = EPOLLIN;
133                            //取消注释这一行,则使用ET模式
134                            //client_fd_event.events |= EPOLLET; 
135                            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1)
136                            {
137                                std::cout << "new client accepted,clientfd: " << clientfd << std::endl;
138                            }
139                            else
140                            {
141                                std::cout << "add client fd to epollfd error" << std::endl;
142                                close(clientfd);
143                            }
144                        }
145                    }
146                }
147                else
148                {
149                    std::cout << "client fd: " << epoll_events[i].data.fd << " recv data." << std::endl;
150                    //普通clientfd
151                    char ch;
152                    //每次只收一个字节
153                    int m = recv(epoll_events[i].data.fd, &ch, 10);
154                    if (m == 0)
155                    {
156                        //对端关闭了连接,从epollfd上移除clientfd
157                        if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
158                        {
159                            std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl;
160                        }
161                        close(epoll_events[i].data.fd);
162                    }
163                    else if (m < 0)
164                    {
165                        //出错
166                        if (errno != EWOULDBLOCK && errno != EINTR)
167                        {
168                            if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
169                            {
170                                std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl;
171                            }
172                            close(epoll_events[i].data.fd);
173                        }
174                    }
175                    else
176                    {
177                        //正常收到数据
178                        std::cout << "recv from client:" << epoll_events[i].data.fd << ", " << ch << std::endl;
179                    }
180                }
181            }
182            else if (epoll_events[i].events & POLLERR)
183            {
184                // TODO 暂不处理
185            }
186        }
187    }
188
189    close(listenfd);
190    return 0;
191}

我们先来看水平模式的行为,将代码 79 行和 134 行注释掉则使用 LT 模式,我们编译下程序并运行:

1[root@localhost testepoll]# g++ -g -o epoll_server epoll_server.cpp 
2[root@localhost testepoll]# ./epoll_server 

然后再另外开启一个 shell 窗口,使用 nc 命令模拟一个客户端,连接服务器成功后,我们给服务器发送一个消息"abcef":

1[root@localhost ~]# nc -v 127.0.0.1 3000
2Ncat: Version 7.50 ( https://nmap.org/ncat )
3Ncat: Connected to 127.0.0.1:3000.
4abcdef

此时服务器端输出:

 1[root@localhost testepoll]# ./epoll_server 
2new client accepted,clientfd: 5
3client fd: 5 recv data.
4recv from client:5, a
5client fd: 5 recv data.
6recv from client:5, b
7client fd: 5 recv data.
8recv from client:5, c
9client fd: 5 recv data.
10recv from client:5, d
11client fd: 5 recv data.
12recv from client:5, e
13client fd: 5 recv data.
14recv from client:5, f
15client fd: 5 recv data.
16recv from client:5

nc 命令实际发送了 a、b、c、d、e、f 和 \n 七个字符,由于服务器端使用的是 LT 模式,每次接收一个字符,只要 socket 接收缓冲区中仍有数据可读,POLLIN 事件就会一直触发,所以服务器一共有 7 次输出,直到 socket 接收缓冲区没有数据为止。


我们将代码 79 行和 134 行注释取消掉,使用 ET 模式再试一下,修改代码并重新编译,然后重新运行一下。再次使用 nc 命令模拟一个客户端连接后发送"abcef",服务器只会有一次输出,效果如下:

由于使用了 ET 模式,只会触发一次 POLLIN 事件,如果此时没有新数据到来,就再也不会触发。所以,如果我们继续给服务器发送一条新数据,如 123,服务器将再次触发一次 POLLIN 事件,然后打印出字母 b,效果如下:

所以如果使用 ET 模式,切记要将该次 socket 上的数据收完。


小结:

LT 模式和 ET 模式各有优缺点,无所谓孰优孰劣。使用 LT 模式,我们可以自由决定每次收取多少字节(对于普通 socket)或何时接收连接(对于侦听 socket),但是可能会导致多次触发;使用 ET 模式,我们必须每次都要将数据收完(对于普通 socket)或必须理解调用 accept 接收连接(对于侦听socket),其优点是触发次数少。


本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。

欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 


【声明】内容源于网络
0
0
CppGuide
专注于高质量高性能C++开发,站点:cppguide.cn
内容 1260
粉丝 0
CppGuide 专注于高质量高性能C++开发,站点:cppguide.cn
总阅读783
粉丝0
内容1.3k