「linux」实例浅析epoll的LT和ET模式,ET模式为何要使用非阻塞IO

linux大本营 · · 1567 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一.概念

我们通俗一点讲:

Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!

Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!

阻塞IO:当你去读一个阻塞的文件描述符时,如果在该文件描述符上没有数据可读,那么它会一直阻塞(通俗一点就是一直卡在调用函数那里),直到有数据可读。当你去写一个阻塞的文件描述符时,如果在该文件描述符上没有空间(通常是缓冲区)可写,那么它会一直阻塞,直到有空间可写。以上的读和写我们统一指在某个文件描述符进行的操作,不单单指真正的读数据,写数据,还包括接收连接accept(),发起连接connect()等操作...

非阻塞IO:当你去读写一个非阻塞的文件描述符时,不管可不可以读写,它都会立即返回,返回成功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理。它不会像阻塞IO那样,卡在那里不动!!!

二.几种IO模型的触发方式

select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。

这里我们要探讨epoll()的水平触发和边缘触发,以及阻塞IO和非阻塞IO对它们的影响!!!下面称水平触发为LT,边缘触发为ET。

对于监听的socket文件描述符我们用sockfd代替,对于accept()返回的文件描述符(即要读写的文件描述符)用connfd代替。

我们来验证以下几个内容:

1.水平触发的非阻塞sockfd

2.边缘触发的非阻塞sockfd

3.水平触发的阻塞connfd

4.水平触发的非阻塞connfd

5.边缘触发的阻塞connfd

6.边缘触发的非阻塞connfd

以上没有验证阻塞的sockfd,因为epoll_wait()返回必定是已就绪的连接,设不设置阻塞accept()都会立即返回。例外:UNP里面有个例子,在BSD上,使用select()模型。设置阻塞的监听sockfd时,当客户端发起连接请求,由于服务器繁忙没有来得及accept(),此时客户端自己又断开,当服务器到达accept()时,会出现阻塞。本机测试epoll()模型没有出现这种情况,我们就暂且忽略这种情况!!!

需要C/C++ Linux服务器架构师学习资料加qun563998835(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

三.验证代码

文件名:epoll_lt_et.c

1#include

2#include

3#include

4#include

5#include

6#include

7#include

8#include

9#include

10#include

11

12/* 最大缓存区大小 */

13#define MAX_BUFFER_SIZE5

14/* epoll最大监听数 */

15#define MAX_EPOLL_EVENTS20

16/* LT模式 */

17#define EPOLL_LT0

18/* ET模式 */

19#define EPOLL_ET1

20/* 文件描述符设置阻塞 */

21#define FD_BLOCK0

22/* 文件描述符设置非阻塞 */

23#define FD_NONBLOCK1

24

25/* 设置文件为非阻塞 */

26intset_nonblock(intfd)

27{

28intold_flags = fcntl(fd, F_GETFL);

29fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);

30returnold_flags;

31}

32

33/* 注册文件描述符到epoll,并设置其事件为EPOLLIN(可读事件) */

34void addfd_to_epoll(intepoll_fd,intfd,intepoll_type,intblock_type)

35{

36structepoll_event ep_event;

37ep_event.data.fd = fd;

38ep_event.events = EPOLLIN;

39

40/* 如果是ET模式,设置EPOLLET */

41if(epoll_type == EPOLL_ET)

42ep_event.events |= EPOLLET;

43

44/* 设置是否阻塞 */

45if(block_type == FD_NONBLOCK)

46set_nonblock(fd);

47

48epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ep_event);

49}

50

51/* LT处理流程 */

52void epoll_lt(intsockfd)

53{

54char buffer[MAX_BUFFER_SIZE];

55intret;

56

57memset(buffer,0, MAX_BUFFER_SIZE);

58printf("开始recv()...\n");

59ret = recv(sockfd, buffer, MAX_BUFFER_SIZE,0);

60printf("ret = %d\n", ret);

61if(ret >0)

62printf("收到消息:%s, 共%d个字节\n", buffer, ret);

63else

64{

65if(ret ==0)

66printf("客户端主动关闭!!!\n");

67close(sockfd);

68}

69

70printf("LT处理结束!!!\n");

71}

72

73/* 带循环的ET处理流程 */

74void epoll_et_loop(intsockfd)

75{

76char buffer[MAX_BUFFER_SIZE];

77intret;

78

79printf("带循环的ET读取数据开始...\n");

80while (1)

81{

82memset(buffer,0, MAX_BUFFER_SIZE);

83ret = recv(sockfd, buffer, MAX_BUFFER_SIZE,0);

84if(ret ==-1)

85{

86if(errno == EAGAIN || errno == EWOULDBLOCK)

87{

88printf("循环读完所有数据!!!\n");

89break;

90}

91close(sockfd);

92break;

93}

94elseif(ret ==0)

95{

96printf("客户端主动关闭请求!!!\n");

97close(sockfd);

98break;

99}

100else

101            printf("收到消息:%s, 共%d个字节\n", buffer, ret);

102}

103printf("带循环的ET处理结束!!!\n");

104}

105

106

107/* 不带循环的ET处理流程,比epoll_et_loop少了一个while循环 */

108void epoll_et_nonloop(intsockfd)

109{

110char buffer[MAX_BUFFER_SIZE];

111intret;

112

113printf("不带循环的ET模式开始读取数据...\n");

114memset(buffer,0, MAX_BUFFER_SIZE);

115ret = recv(sockfd, buffer, MAX_BUFFER_SIZE,0);

116if(ret >0)

117{

118printf("收到消息:%s, 共%d个字节\n", buffer, ret);

119}

120else

121    {122if(ret ==0)

123printf("客户端主动关闭连接!!!\n");

124close(sockfd);

125}

126

127printf("不带循环的ET模式处理结束!!!\n");

128}

129

130/* 处理epoll的返回结果 */

131void epoll_process(intepollfd,structepoll_event *events,intnumber,intsockfd,intepoll_type,intblock_type)

132{

133structsockaddr_in client_addr;

134socklen_t client_addrlen;

135intnewfd, connfd;

136inti;

137

138for(i =0; i < number; i++)

139{

140newfd = events[i].data.fd;

141if(newfd == sockfd)

142{

143printf("=================================新一轮accept()===================================\n");

144printf("accept()开始...\n");

145

146/* 休眠3秒,模拟一个繁忙的服务器,不能立即处理accept连接 */

147printf("开始休眠3秒...\n");

148sleep(3);

149printf("休眠3秒结束!!!\n");

150

151client_addrlen = sizeof(client_addr);

152connfd = accept(sockfd, (structsockaddr *)&client_addr, &client_addrlen);

153printf("connfd = %d\n", connfd);

154

155/* 注册已链接的socket到epoll,并设置是LT还是ET,是阻塞还是非阻塞 */

156addfd_to_epoll(epollfd, connfd, epoll_type, block_type);

157printf("accept()结束!!!\n");

158}

159elseif(events[i].events & EPOLLIN)

160{

161/* 可读事件处理流程 */

162

163if(epoll_type == EPOLL_LT)

164{

165printf("============================>水平触发开始...\n");

166epoll_lt(newfd);

167}

168elseif(epoll_type == EPOLL_ET)

169{

170printf("============================>边缘触发开始...\n");

171

172/* 带循环的ET模式 */

173epoll_et_loop(newfd);

174

175/* 不带循环的ET模式 */

176//epoll_et_nonloop(newfd);

177            }

178        }

179        else

180            printf("其他事件发生...\n");

181    }

182 }

183

184 /* 出错处理 */

185 void err_exit(char *msg)

186 {

187    perror(msg);

188    exit(1);

189 }

190

191 /* 创建socket */

192 int create_socket(const char *ip, const int port_number)

193 {

194    struct sockaddr_in server_addr;

195    int sockfd, reuse = 1;

196

197    memset(&server_addr, 0, sizeof(server_addr));

198    server_addr.sin_family = AF_INET;

199    server_addr.sin_port = htons(port_number);

200

201    if (inet_pton(PF_INET, ip, &server_addr.sin_addr) == -1)

202        err_exit("inet_pton() error");

203

204    if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)

205        err_exit("socket() error");

206

207    /* 设置复用socket地址 */

208    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1)

209        err_exit("setsockopt() error");

210

211    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)

212        err_exit("bind() error");

213

214    if (listen(sockfd, 5) == -1)

215        err_exit("listen() error");

216

217    return sockfd;

218 }

219

220 /* main函数 */

221 int main(int argc, const char *argv[])

222 {

223    if (argc < 3)

224    {

225        fprintf(stderr, "usage:%s ip_address port_number\n", argv[0]);

226        exit(1);

227    }

228

229    int sockfd, epollfd, number;

230

231    sockfd = create_socket(argv[1], atoi(argv[2]));

232    struct epoll_event events[MAX_EPOLL_EVENTS];

233

234    /* linux内核2.6.27版的新函数,和epoll_create(int size)一样的功能,并去掉了无用的size参数 */

235    if ((epollfd = epoll_create1(0)) == -1)

236        err_exit("epoll_create1() error");

237

238    /* 以下设置是针对监听的sockfd,当epoll_wait返回时,必定有事件发生,

239      * 所以这里我们忽略罕见的情况外设置阻塞IO没意义,我们设置为非阻塞IO */

240

241    /* sockfd:非阻塞的LT模式 */

242    addfd_to_epoll(epollfd, sockfd, EPOLL_LT, FD_NONBLOCK);

243

244    /* sockfd:非阻塞的ET模式 */

245    //addfd_to_epoll(epollfd, sockfd, EPOLL_ET, FD_NONBLOCK);

246

247   

248    while (1)

249    {

250        number = epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1);

251        if (number == -1)

252            err_exit("epoll_wait() error");

253        else

254        {

255            /* 以下的LT,ET,以及是否阻塞都是是针对accept()函数返回的文件描述符,即函数里面的connfd */

256

257            /* connfd:阻塞的LT模式 */

258            epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_BLOCK);

259

260            /* connfd:非阻塞的LT模式 */

261            //epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_NONBLOCK);

262

263            /* connfd:阻塞的ET模式 */

264            //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_BLOCK);

265

266            /* connfd:非阻塞的ET模式 */

267            //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_NONBLOCK);

268        }

269    }

270

271    close(sockfd);

272    return 0;

273 }

四.验证

1.验证水平触发的非阻塞sockfd,关键代码在242行。编译运行

代码里面休眠了3秒,模拟繁忙服务器不能很快处理accept()请求。这里,我们开另一个终端快速用5个连接连到服务器:

我们再看看服务器的反映,可以看到5个终端连接都处理完成了,返回的新connfd依次为5,6,7,8,9:

上面测试完毕后,我们批量kill掉那5个客户端,方便后面的测试:

1 $:foriin{1..5};dokill%$i;done

2.边缘触发的非阻塞sockfd,我们注释掉242行的代码,放开245行的代码。编译运行后,用同样的方法,快速创建5个客户端连接,或者测试5个后再测试10个。再看服务器的反映,5个客户端只处理了2个。说明高并发时,会出现客户端连接不上的问题:

3.水平触发的阻塞connfd,我们先把sockfd改回到水平触发,注释245行的代码,放开242行。重点代码在258行。

编译运行后,用一个客户端连接,并发送1-9这几个数:

再看服务器的反映,可以看到水平触发触发了2次。因为我们代码里面设置的缓冲区是5字节,处理代码一次接收不完,水平触发一直触发,直到数据全部读取完毕:

4.水平触发的非阻塞connfd。注释263行的代码,放开261行的代码。同上面那样测试,我们可以看到服务器反馈的消息跟上面测试一样。这里我就不再截图。

5.边缘触发的阻塞connfd,注释其他测试代码,放开264行的代码。先测试不带循环的ET模式(即不循环读取数据,跟水平触发读取一样),注释173行的代码,放开176行的代码。

编译运行后,开启一个客户端连接,并发送1-9这几个数字,再看看服务器的反映,可以看到边缘触发只触发了一次,只读取了5个字节:

我们继续在刚才的客户端发送一个字符a,告诉epoll_wait(),有新的可读事件发生:

再看看服务器,服务器又触发了一次新的边缘触发,并继续读取上次没读完的6789加一个回车符:

这个时候,如果继续在刚刚的客户端再发送一个a,客户端这个时候就会读取上次没读完的a加上次的回车符,2个字节,还剩3个字节的缓冲区就可以读取本次的a加本次的回车符共4个字节:

我们可以看到,阻塞的边缘触发,如果不一次性读取一个事件上的数据,会干扰下一个事件!!!

接下来,我们就一次性读取数据,即带循环的ET模式。注意:我们这里测试的还是边缘触发的阻塞connfd,只是换个读取数据的方式。

注释176行代码,放开173的代码。编译运行,依然用一个客户端连接,发送1-9。看看服务器,可以看到数据全部读取完毕:

细心的朋友肯定发现了问题,程序没有输出"带循环的ET处理结束",是因为程序一直卡在了83行的recv()函数上,因为是阻塞IO,如果没数据可读,它会一直等在那里,直到有数据可读。如果这个时候,用另一个客户端去连接,服务器不能受理这个新的客户端!!!

6.边缘触发的非阻塞connfd,不带循环的ET测试同上面一样,数据不会读取完。这里我们就只需要测试带循环的ET处理,即正规的边缘触发用法。注释其他测试代码,放开267行代码。编译运行,用一个客户端连接,并发送1-9。再观测服务器的反映,可以看到数据全部读取完毕,处理函数也退出了,因为非阻塞IO如果没有数据可读时,会立即返回,并设置error,这里我们根据EAGAIN和EWOULDBLOCK来判断数据全部读取完毕了,可以退出循环了:

这个时候,我们用另一个客户端去连接,服务器依然可以正常接收请求:

五.总结

1.对于监听的sockfd,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,网上有的方案是用while来循环accept()。

2.对于读写的connfd,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞。

3.对于读写的connfd,边缘触发模式下,必须使用非阻塞IO,并要一次性全部读写完数据。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:linux大本营

查看原文:「linux」实例浅析epoll的LT和ET模式,ET模式为何要使用非阻塞IO

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1567 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传