目录
在linux的IO多路复用中有水平触发,边缘触发两种模式
LT模式(默认方式)
水平触发图例
ET模式
总结:
epoll 水平触发代码:
epoll 边缘触发代码
这两种模式的区别如下:
水平触发:
边缘触发:
写过单片机的人可以从另一方理解水平触发和边缘触发的区别:
水平触发:
边缘触发:
大家可能还不能完全了解这两种模式的区别,我们可以举例说明:
在linux的IO多路复用中有水平触发,边缘触发两种模式
LT模式(默认方式)
LT模式即Level Triggered工作模式。
与ET模式不同的是,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll,无论后面的数据是否被使用。
LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
123
水平触发图例
ET模式
ET(edge-triggered):ET是高速工作方式,只支持no-block socket。
在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
总结:
水平触发,是相对比较安全的,因为当内核有事件被唤醒的时候,linux系统就会将被唤醒的事件拷贝到用户态,让用户对被唤醒的事件进行处理,如果该事件没有处理,那么下一次等待中,linux内核依旧会拷贝到用户态中,保证每一次事件都能抵达用户态。
边缘触发,只会在内核被唤醒事件从无到有的那一刻,才会将事件拷贝给用户态,虽然它减少了linux内核拷贝到用户态的次数,但带来的后果有可能在linux中部分事件已经被唤醒,但是没有被获取得到。
epoll 水平触发代码:
#include "../common.h"
int set_NonBlock(int fd )
{
int flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd , F_SETFL, flags);
return 0;
}
int main()
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = 0;
int ret = bind(fd ,(struct sockaddr*)&addr, sizeof(addr));
if (ret != 0)
{
perror("bind");
close(fd);
return ret;
}
ret = listen(fd, 1024);
if (ret != 0)
{
perror("listen");
close(fd);
return ret;
}
set_NonBlock(fd);
int epfd = epoll_create(1024);
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
if (ret != 0)
{
perror("epoll_ctl");
close(fd);
return 0;
}
while ( 1 )
{
struct epoll_event ev[8];
ret = epoll_wait(epfd, ev, 8, 5000);
if (ret != 0)
{
if (errno == EINTR)
continue;
}
for (int i = 0 ; i < ret ; i++)
{
int newfd;
if (ev[i].data.fd == fd)
{
//socket fd
newfd = accept(ev[i].data.fd,NULL,NULL);
event.data.fd = newfd;
event.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &event);
if (ret != 0)
{
perror("epoll_ctl");
close(newfd);
close(fd);
return -1;
}
}
else
{
int connectfd = ev[i].data.fd;
char buf[1024] = {0};
if (read( connectfd, buf,sizeof(buf)) > 0)
{
printf("recv buf:%s\n",buf);
}
else
{
close(connectfd);
}
}
}
}
return 0;
}
epoll 边缘触发代码
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
void sig_handle(int sig)
{
printf("recv signal :%d\n",sig);
}
int main(int argc, char * argv[])
{
signal(SIGPIPE,sig_handle);
if (argc<2)
{
printf("usage:%s + [count]\n",argv[0]);
return 0;
}
unlink("dbg.txt");
int dbg = open("dbg.txt",O_CREAT|O_APPEND|O_RDWR,0666);
int count = atoi(argv[1]);
int fd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(9988);
int ret = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
if (ret ==-1)
{
perror("bind");
return 0;
}
listen(fd,250);
int is_child_process = 0;//判断在哪个进程中,父进程0,子进程1
for (int i = 0 ; i < count ; i++)
{
pid_t pid = fork();
if (pid==0)
{
is_child_process = 1;
break;
}
}
struct epoll_event ev;
ev.events = EPOLLIN|EPOLLET;
ev.data.fd = fd;
int epfd = epoll_create(1024);//建立epfd的描述符
int flags = fcntl(fd,F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd,F_SETFL,flags);
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev);
while (1)
{
struct epoll_event evs[10];
int process_count = epoll_wait(epfd,evs,10,5000);
if (process_count == 0) continue;//如果监听的进程都没有事件产生,则再次进入循环,继续监听
for (int i = 0 ; i < process_count ;i++)
{
if (evs[i].data.fd == fd)
{
//当进程中的socket描述符是server的socket本身时候,则accept否则就直接操作
int ret = accept(evs[i].data.fd,NULL,NULL);
if (ret == -1)
{
printf("errno:%s",strerror(errno));
//其他错误,直接exit
break;
}
ev.data.fd = ret;
epoll_ctl(epfd,EPOLL_CTL_ADD,ret,&ev);
}
else
{
//read or write
char buf[1024];
int ret = read(evs[i].data.fd,buf,sizeof(buf));
if (ret == -1)
{
perror("read");
if (errno == EINTR)
break;
exit(0);
}
else if (ret == 0)
{
//normal exit
close(evs[i].data.fd);
break;
}
//printf("recv data %s from pid:%d\n",buf,getpid());
write(dbg,"1",1);
}
}
}
if (!is_child_process)
{
for (int i = 0 ; i < count; i ++)
{
wait(NULL);//等待所有的子进程退出为止
}
}
return 0;
}
这两种模式的区别如下:
水平触发:
如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态.select,poll就属于水平触发.
边缘触发:
如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述符.信号驱动式IO就属于边缘触发.
写过单片机的人可以从另一方理解水平触发和边缘触发的区别:
水平触发:
就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.上面提到的只要有数据可读(描述符就绪)那么水平触发的epoll就立即返回.
边缘触发:
只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即使有数据可读,但是io状态没有变化epoll也不会立即返回.
epoll既可以采用水平触发,也可以采用边缘触发.
大家可能还不能完全了解这两种模式的区别,我们可以举例说明:
一个管道收到了1kb的数据,epoll会立即返回,此时读了512字节数据,然后再次调用epoll.这时如果是水平触发的,epoll会立即返回,因为有数据准备好了.如果是边缘触发的不会立即返回,因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来,直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取).所以当我们写epoll网络模型时,如果我们用水平触发不用担心数据有没有读完因为下次epoll返回时,没有读完的socket依然会被返回,但是要注意这种模式下的写事件,因为是水平触发,每次socket可写时epoll都会返回,当我们写的数据包过大时,一次写不完,要多次才能写完或者每次socket写都写一个很小的数据包时,每次写都会被epoll检测到,因此长期关注socket写事件会无故cpu消耗过大甚至导致cpu跑满,所以在水平触发模式下我们一般不关注socket可写事件而是通过调用socket write或者send api函数来写socket,说到这我们可以看到这种模式在效率上是没有边缘触发高的,因为每个socket读或者写可能被返回两次甚至多次,所以有时候我们也会用到边缘触发但是这种模式下在读数据的时候一定要注意,因为如果一次可写事件我们没有把数据读完,如果没有读完,在socket没有新的数据可读时epoll就不回返回了,只有在新的数据到来时,我们才能读取到上次没有读完的数据。