在 linux 的 IO 多路复用中有水平触发, 边缘触发两种模式, 这两种模式的区别如下:
水平触发:如果文件描述符已经就绪可以非阻塞的执行 IO 操作了,此时会触发通知。允许在任意时刻重复检测 IO 的状态, 没有必要每次描述符就绪后尽可能多的执行 IO、select、poll 就属于水平触发。
边缘触发: 如果文件描述符自上次状态改变后有新的 IO 活动到来, 此时会触发通知. 在收到一个 IO 事件通知后要尽可能多的执行 IO 操作, 因为如果在一次通知中没有执行完 IO 那么就需要等到下一次新的 IO 活动到来才能获取到就绪的描述符。信号驱动式 IO 就属于边缘触发。
epoll 既可以采用水平触发, 也可以采用边缘触发.
我们可以举例说明:一个管道收到了 1kb 的数据,epoll 会立即返回, 此时读了 512 字节数据, 然后再次调用 epoll。这时如果是水平触发的,epoll 会立即返回,因为有数据准备好了。
如果是边缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来, 直到有新的数据到来 epoll 才会返回,此时老的数据和新的数据都可以读取到 (当然是需要这次你尽可能的多读取).
下面我们还从电平变化的角度来解释一下:
水平触发: 也就是只有高电平 (1) 或低电平 (0) 时才触发通知, 只要在这两种状态就能得到通知. 上面提到的只要有数据可读 (描述符就绪) 那么水平触发的 epoll 就立即返回.
边缘触发: 只有电平发生变化 (高电平到低电平, 或者低电平到高电平) 的时候才触发通知. 上面提到即使有数据可读, 但是没有新的 IO 活动到来, epoll 也不会立即返回.
/*************************************************************************
> File Name: t_select.c
> Author: liuxingen
> Mail: liuxingen@nsfocus.com
> Created Time: 2014年08月11日 星期一 21时22分32秒
************************************************************************/
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/time.h>
#include<sys/select.h>
#include<string.h>
#include<errno.h>
int main(int argc, char *argv[])
{
struct timeval timeout;
char buf[10];
fd_set readfds;
int nread, nfds, ready, fd;
while(1)
{
timeout.tv_sec = 20L;
timeout.tv_usec = 0;
fd = 0; //stdin
nfds = fd + 1;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
ready = select(nfds, &readfds, NULL, NULL, &timeout);
if(ready == -1 && errno == EINTR)
{
continue;
}else if(ready == -1)
{
fprintf(stderr, "select error:%s\n", strerror(errno));
}
for(fd = 0; fd < nfds; fd++)
{
if(FD_ISSET(fd, &readfds))
{
nread = read(fd, buf, 9);
buf[nread] = '\0';
puts(buf);
}
}
}
return 0;
}
上面的示例中每次最多读取9个字节,当我们一次输入了20个字节那么分三次调用select,每次都能立即读取到数据,这也就证明了水平触发中只要数据准备好了那么select都会立即返回.
lxg@remoter:~/station$ ./t_select
ni hao ma ,wo hen hao a ,ni ne ???
ni hao ma
,wo hen
hao a ,ni
ne ???
边缘触发
/*************************************************************************
> File Name: demo_sigio.c
> Author: liuxingen
> Mail: liuxingen@nsfocus.com
> Created Time: 2014年08月14日 星期四 21时32分03秒
************************************************************************/
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<ctype.h>
#include<signal.h>
#include<fcntl.h>
static int g_fd;
static void sigio_handler(int signum)
{
char buf[8] = {0};
if(read(g_fd, buf, 7) < 0)
{
fprintf(stderr, "read error:%s\n", strerror(errno));
}else
{
printf("sigio recv:%s\n", buf);
}
}
int main(int argc, char *argv[])
{
struct sigaction act;
int flags, i = 1, fds[2];
pid_t pid;
if(pipe(fds) < 0)
{
fprintf(stderr, "pipe error:%s\n", strerror(errno));
return 1;
}
if((pid = fork()) > 0)
{
close(fds[1]);
dup2(fds[0], g_fd);
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;
act.sa_handler = sigio_handler;
if(sigaction(SIGIO, &act, NULL) == -1)
{
fprintf(stderr, "sigaction error:%s\n", strerror(errno));
return 1;
}
if(fcntl(g_fd, F_SETOWN, getpid()) == -1)
{
fprintf(stderr, "fcntl F_SETOWN error:%s\n", strerror(errno));
return 1;
}
flags = fcntl(g_fd, F_GETFL);
if(fcntl(g_fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1)
{
fprintf(stderr, "fcntl F_GETFL error:%s\n", strerror(errno));
return 1;
}
while(1)
{
sleep(10);
}
}else
{
char buf[20] = {0};
close(fds[0]);
for(i = 0; i < 3; i++)
{
snprintf(buf, 20, "this is loop %d", i);
write(fds[1], buf, strlen(buf));
printf("loop %d\n", i);
sleep(3);
}
}
return 0;
}
因为信号驱动IO属于边缘触发,所以上面以信号驱动来举例.从下面的输出可以得知:我们一次写入14个字节,但是一次我们每次只读取7字节,除非等到下一次数据写入不然不会再触发SIGIO信号,并且上一次未读完的数据会在下次继续被读取.
lxg@remoter:~/station$ ./demo_sigio
loop 0
sigio recv:this is
loop 1
sigio recv: loop 0
loop 2
sigio recv:this is
sigio recv: loop 1