再看
select
代码
while(1){
cpy_reads=reads;
timeout.tv_sec=5;
timeout.tv_usec=5000;
if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)
break;
if(fd_num==0)
continue;
for(i=0; i<fd_max+1; i++)
{
if(FD_ISSET(i, &cpy_reads))
{
if(i==serv_sock) // connection request!
{
adr_sz=sizeof(clnt_adr);
clnt_sock=
accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max<clnt_sock)
fd_max=clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else // read message!
{
str_len=read(i, buf, BUF_SIZE);
if(str_len==0) // close request!
{
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}
else{
write(i, buf, str_len); // echo!
}
}
}
}
}
select
函数采用轮询机制,调用select
函数后需要遍历所以文件描述符以找到发生事件的文件描述符。- 每次调用
select
函数时都需要向该函数传递监视对象信息 。
在调用select
函数后并不是把发生变化的文件描述符集中到一起。而是通过观察作为监视对象的fd_set变量的变化,找出发生变化的文件描述符。而且,作为监视对象的fd_set变量会发生改变。调用select函数前应复制并保存原有信息并在每次调用select函数时传递新的监视对象信息 。
最大缺点:每次调用 select函数时向操作系统传递监视对象信息 。
发生数据从用户态到内核态的拷贝。
epoll
仅向操作系统传递 1 次监视对象监视范围或内容发生变化时只通知发生变化的事项 。
无需编写以监视状态变化为目的的针对所有文件描述符的循环语句 。
调用对应于seleot函数的epoll_wait函数时无需每次传递监视对象信息 。
epoll_create: 创建保存epoll文件描述符的空间 。
epoll_ctl: 向空间注册并注销文件描述符。
epoll_wait: 与 select函数类似等待文件描述符发生变化 。
epoll由操作系统保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create。此外,为了添加和删除监视对象文件描述符,select方式中需要FD_SET FD_CLR函数。 但在epoll方式中,通过epoll_ctl函数请求操作系统完成。最后,select方式下调用 select函数等待文件描述符的变化,而epoll中调用 epoll_wait函数 。 还有,select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),而epoll方式中通过如下结构体epoll_event将发生变化的 (发生事件的)文件描述符单独集中到一起。
struct epoll_event{
__uint32_t event;
epoll_data_t data;
};
typedef union epoll_data
{
void * ptr;
int fd;
__uint32_t u32;
uint64_t u64;
} epoll_data_t;
声明足够大的epoll_even结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符信息将被填入该数组 。 因此,无需像select函数那样针对所有文件描述符进行循环 。
int epoll_create(int size);
// 成功时返回epoll文件描述描述符,失败返回-1
// epoll实例的大小
//创建的资源与套接字相同,也由操作系统管理。
//生成epoll例程后,应在其内部注册监视对象文件描述符,
int epoll_ctl(int epfd, int op, int fd, struct epol1_event * event);
// epfd epoll历程的文件描述符
// op 指定对象的添加、删除或更改操作
// fd 需要注册的监视对象文件描述符。
// event 监视对象的事件类型。
// op的选项
// EPOLL_CTL_ADD: 将文件描述符注册到 epoll例程 。
// EPOLL_CTL_DEL: 从epoll例程中删除文件描述符 。
// EPOLL_CTL_MOD: 更改注册的文件描述符的关注事件发生情况 。
int epoll_wait(int epfd, struct epoll_event * events, ïnt maxevents, int timeout);
// events 保存发生事件的文件描述符集合的结构体地址值 。所指缓冲需要动态分配 。
// 调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲内保存发生事件
// 的文件描述符集合。 因此,无需像select那样插入针对所有文件描述符的循环。
基于epoll的回声服务端
epfd=epoll_create(EPOLL_SIZE);//注册epoll历程
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events=EPOLLIN;
event.data.fd=serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);//向epoll历程中注册服务端套接字
while(1)
{
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);//等待事件发生
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
for(i=0; i<event_cnt; i++)//处理发生的事件
{
if(ep_events[i].data.fd==serv_sock)//新的请求
{
adr_sz=sizeof(clnt_adr);
clnt_sock=
accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
event.events=EPOLLIN;
event.data.fd=clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d \n", clnt_sock);
}
else//用户发送数据
{
str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len==0) // close request!
{
epoll_ctl(
epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
}
else
{
write(ep_events[i].data.fd, buf, str_len); // echo!
}
}
}
}
条件触发和边缘触发
水平触发(Lever trigger, LT),边缘触发(Edge Trigger, ET)。epoll默认是水平触发。
条件触发和边缘触发的区别在于发生事件的时间点
条件触发方式中只要输入缓冲有数据就会一直通知该事件。服务器端输入缓冲收到 50字节的数据时。服务器端操作系统将通知该事件 (注册到发生变化的文件描述符)。 但服务器端读取20字节后还剩30字节的情况下,仍会注册事件 。 也就是说, 条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册 。
边缘触发中输入缓冲收到数据时仅注册 1次该事件 。 即使输入缓冲中还留有数据,也不会再进行注册。
边缘触发的服务器端实现中必知的两点
通过errno变量验证错误原因
为了完成非阻塞( Non-blocking ) I/O , 更改套接字特性 。
Linux 的 套接字相关函数一般通过返回 -1通知发生了错误 。 虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因 。 因此,为了在发生错误时提供额外的信息, Linux声明了如下全局变量 :
int errno
另外每种函数发生错误时,保存到 errno变量中的值都不同 ,没必要记住所有可能的值。
read函数发现输入缓冲中没有数据可读时返回-1,同时在 errno中保存EAGAIN常量 。
Lin以提供更改或读取文件属性的如下方法
int fcntl(int fd, int cmd, ...)
// 成功时返回 cmd 参数相关值,失败时返回-1
从上述声明中可以看到, fcntl具有可变参数的形式 。 如果向第二个参数传递F_GETFL ,可以获得第一个参数所指的文件描述符属性 ( int型 )。 反之,如果传递F_SETFL ,可以更改文件描述符属性 。 若希望将文件( 套接字)改为非阻塞模式,需要如下2条语句 。
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志 。调用 read& write函数时,无论是否存在数据,都会形成非阻塞文件 (套接字 )。
首先说明为何需要通过errno确认错误原因 。"边缘触发方式中 ,接收数据时仅注册 1 次该事件 。 "就因为这种特点,一旦发生输入相关事件,就应该读取输入缓冲中的全部数据。因此需要验证输入缓冲是否为空 。"read函数返回-1, 变量erron 中的值 EAGAIN 时,说明没有数据可读。既然如此,为何还需要将套接字变成非阻塞模式?边缘触发方式下,以阻塞方式工作的read& write函数有可能引起服务器端的长时间停顿 。 因此 边缘触发方式中一定要采用非阻塞read&write 函数。
while(1)
{
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
puts("return epoll_wait");
for(i=0; i<event_cnt; i++)
{
if(ep_events[i].data.fd==serv_sock)
{
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
setnonblockingmode(clnt_sock);
event.events=EPOLLIN|EPOLLET;
event.data.fd=clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d \n", clnt_sock);
}
else
{
while(1)//边缘触发,循环读取所有数据
{
str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len==0) // close request!
{
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
break;
}
else if(str_len<0)//返回-1说明没有数据
{
if(errno==EAGAIN)
break;
}
else
{
write(ep_events[i].data.fd, buf, str_len); // echo!
}
}
}
}
}
边缘触发方式下可以做到如下这点 :"可以分离接收数据和处理数据的时间点!"即使输入缓冲收到数据(注册相应事件),服务器端也能决定读取和处理这些数据的时间点,这样就给服务器端的实现带来巨大的灵活性。 在水平触发条件下有数据就会产生时间,当客户端连接时数很大时,服务器难以承受。