1 阻塞与非阻塞,LT与ET概念
- 1)我们知道,文件描述符(例如管道,套接字等等)具有阻塞与非阻塞,例如套接字,当我们使用read去读取内容时,由于套接字默认是阻塞的,所以当没有内容时,read就会阻塞,这就是read读取阻塞的根本原因。所以阻塞就是卡住,非阻塞就是不卡住,这是最简单的理解。
- 2)而Level Triggered (LT)和Edge Triggered (ET) 两者,前者是水平出发,只要有数据就会读取。后者是边沿触发,只有电平改变才会触发,电平改变就是0变成1(上升沿)或者1变成0(下降沿)。例如服务器与客户端连接,只有客户端发消息到套接字的缓冲区了,服务器才会认为触发到满足事件,然后去读取套接字的缓冲区。否则即使你套接字还有内容,但是客户端没有发消息,服务器也不会去读取,这就是边沿触发。
- 3)注意,ET和LT是针对于epoll的,因为select,poll是不支持这两个模型的。
2 epoll的LT和ET详解
经过上面分析后,我们可以这样理解,阻塞非阻塞是文件描述符的属性,而描述符作为read(write不需要考虑,因为我们只需要分析读即可)的参1,该属性会影响到read,也就是说,描述符的阻塞非阻塞就是read的阻塞非阻塞。借此,我们进而可以深入分析epoll的LT和ET模型。首先先记住使用epoll的总结。
2.1 epoll的LT,ET模型是否阻塞和非阻塞总结
- 1)epoll的LT模型支持阻塞和非阻塞。
- 2)epoll的ET模型只支持非阻塞,不支持阻塞。
2.2 针对epoll的LT模型代码
为了方便,我们使用管道来测试。注意,select,poll,epoll都是可以在文件描述符中使用,文件描述符包括管道(即管道两个读写fd),mmap映射,网络套接字这些。这些IO复用函数虽然常用在网络套接字上,但不仅仅只能在网络套接字使用select,poll,epoll,也能用在管道等文件描述符。
案例:我们创建父子进程,然后父进程读,子进程写。子进程每次往管道写10个字节后睡眠5s,等待父进程读。父进程读,但是每次读5个,也就是说,读完5个后(睡眠1s让现象明显),管道中还有数据,若仍会触发epoll返回,去读取管道剩余的数据,就证明epoll默认是LT,LT模型是只有还有数据,我就会去读。
并且实际该例子也证明了epoll的LT模型是支持阻塞的。因为管道两个读写描述符默认就是阻塞的,程序没问题就表示支持。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h>
#define MAXLINE 10
int main(int argc, char *argv[])
{
int efd, i;
int pfd[2];
pid_t pid;
char buf[MAXLINE], ch = 'a';
pipe(pfd);
pid = fork();
if (pid == 0) {//子进程写
close(pfd[0]);
while (1) {
//aaaa\n
for (i = 0; i < MAXLINE/2; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//bbbb\n
for (; i < MAXLINE; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//aaaa\nbbbb\n
write(pfd[1], buf, sizeof(buf));
sleep(5);
}
close(pfd[1]);
} else if (pid > 0) {//父进程读
struct epoll_event event;
struct epoll_event resevent[10]; //epoll_wait就绪返回event
int res, len;
close(pfd[1]);
efd = epoll_create(10);
//event.events = EPOLLIN | EPOLLET; // ET 边沿触发
event.events = EPOLLIN; // LT 水平触发 (默认)
event.data.fd = pfd[0];
epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
while (1) {
res = epoll_wait(efd, resevent, 10, -1);
//printf("res %d\n", res);
if (resevent[0].data.fd == pfd[0]) {
len = read(pfd[0], buf, MAXLINE/2);
write(STDOUT_FILENO, buf, len);
sleep(1);
}
}
close(pfd[0]);
close(efd);
} else {
perror("fork");
exit(-1);
}
return 0;
}
结果:
1)首先父进程会先读取5个。然后因为是LT模型,所以会继续触发epoll返回,读取剩余的5个字节。
2)然后子进程睡5s后,继续写10个字节。父进程不断重复第一步读取。
所以,程序正常按照猜想执行,说明epoll的默认模型是LT,并且LT模型是支持阻塞的。关于LT支持非阻塞的案例这里不举例了,可以通过fcntl这个函数设置非阻塞测试,但是个人觉得没啥必要。我们只需要知道epoll的LT和ET即可。并且这个案例只是为了让大家理解epoll的这两种
模型,实际项目很少用到。
2.3 测试epoll的ET模型代码(项目中绝不允许使用,这里只是测试,看第四小点解释)
将上面代码的注释行改成ET边沿触发即可。
//event.events = EPOLLIN | EPOLLET; // ET 边沿触发
1)首先,子进程写了10个字节,然后父进程读取5个字节后,由于是ET模型,所以剩余的数据并不会被读取,只能等待下一次子进程写时才会触发。
2)接着,5s后子进程又发送10个字节,父进程将管道剩余的5字节读取即bbbb,但是本次的10个字节并未读取。
3)也就是说,子进程每隔5s写一次,父进程就会读一次,只有子进程写了,父进程才会去读。即使当管道还有剩余的数据,epoll也不会返回,让父进程去读。
4)看到这里,有人会问,这不是ET的阻塞模型吗?这程序不是可以运行吗,我们开始解释:我们使用的是read,因为本程序子进程不断写保证管道有数据,所以read不会阻塞,只会在epoll_wait函数阻塞。但是很多时候read并不会直接使用,很多公司会将read封装成readn,readn的作用是只有读取到一定字节数才会返回,封装阻塞。那么当readn阻塞了,客户端再发送信息,而由于程序阻塞在readn导致epoll_wait无法返回,那么程序就卡死了,无法进行任何处理。所以这里证明epoll的ET模式不支持阻塞(注意管道的文件描述符默认是阻塞)。
3 总结epoll的LT和ET
- 1)epoll的LT模型支持阻塞和非阻塞。
- 2)epoll的ET模型只支持非阻塞,不支持阻塞(看2.3的第4点)。
- 3)select,poll,epoll这些IO复用函数可以用在管道,mmap映射,套接字等文件描述符的场合。
好了,本篇就是我们想要讲述的epoll的LT和ET模型,说难不难,说简单也不易,多看几篇就熟。