摘要:本文记录epoll的相关知识和epoll服务器的开发过程。
三种多路复用的比较
IO多路复用是指,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
模型 | 文件描述符数量 | 工作方式 |
---|---|---|
select | 默认1024,可以通过宏定义修改 | 轮询方式扫描文件描述符,遍历数组等,用户和内核之间存在大量文件描述符拷贝,内核开销随文件描述符增加而线性增加 |
poll | 不受限 | 和select工作方式相同,poll是select更加底层的系统调用 |
epoll | 不受限 | I/O事件通知机制,只关心"活跃"连接,无需遍历文件描述符集,能够处理大量请求,一般百万级别 |
epoll的工作原理
- 多路复用epoll技术就像信号处理机制一样,我们先安装(注册)需要关心的文件描述符,然后对其进行监测,一旦有某个描述符被触发,epoll机制就会通知进程该描述符可以进行IO操作。因为socket服务器和socket客户端之间是两个描述符之间依赖TCP协议进行通信,所以我们可以使用epoll实现多并发服务器。
- epoll机制:红黑树 + 双链表数据结构 + TCP回调机制
- 调用epoll_create():建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源),在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket,再建立一个rdllist双向链表,用于存储准备就绪的事件。
- 调用epoll_ctl:向epoll对象中添加这100万个连接的套接字。
- 调用epoll_wait:观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。
- 所有添加到epoll对象中的事件都会与设备(如网卡)驱动程序建立回调关系(ep_poll_callback),把事件放到上面的rdllist双向链表中。
epoll API 简单说明
-
int epoll_create(int size):参数size指定了我们想要通过epoll实例来检查的文件描述符个数,出错返回-1。
-
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev):
epfd:是epoll_create()实例文件描述符。
op:用来指定需要执行的操作EPOLL_CTL_ADD:将描述符fd添加到epoll实例中的兴趣列表(红黑树)中 去。 EPOLL_CTL_MOD:修改描述符上设定的事件,需要用到由ev所指向的 结构体中的信息。 EPOLL_CTL_DEL:将文件描述符fd从epfd的兴趣列表中移除,该操作忽 略参数ev。
fd:指明了要修改兴趣列表中的哪一个文件描述符的设定。
ev:ev是指向结构体epoll_event的指针。
typedef union epoll_data
{
void *ptr; /* Pointer to user-defind data */
int fd; /* File descriptor */
uint32_t u32; /* 32-bit integer */
uint64_t u64; /* 64-bit integer */
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* epoll events(bit mask) */
epoll_data_t data; /* User data */
};
-
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout):
epfd:是epoll_create()的返回值; evlis:t所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数 组evlist的空间由调用者负责申请; maxevents:指定所evlist数组里包含的元素个数; timeout:用来确定epoll_wait()的阻塞行为,有如下几种: 如果timeout等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有 事件产生或者直到捕获到一个信号为止。 如果timeout等于0,执行一次非阻塞式地检查,看兴趣列表中的描述符上产 生了哪个事件。 如果timeout大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件 发生,或者直到捕获到一个信号为止
epoll服务器实现过程
遇到的问题
1、竟然是多并发服务器,多个客户端可以与之连接通信,那么服务器需要开多少个端口,还是一个端口可以连接多个客户端?
答、一个端口就可以连接多个客户端,端口只是服务器中某一程序的唯一标识。根据这一标
识,客户端可以找到该服务器程序,并与之通信。当客户端连接上来时,服务器端重新分配
一个文件描述符,epoll对其进行统一管理。
2、边缘触发服务器同时接入两个客户端后,断开其中一个,会导致服务器退出程序?
答、原因是客户端断开后,我没有将文件描述符关闭。
2网络epoll中:正常连接之后,再次循环到位置的时候,从连接描述符文件描述符读数据错误,文件描述符是负数。
3、client编程时,打算使用alarm()结合signal实现定时器,30秒上报一次,可是超时时,程序不跑进信号处理程序,why?
答、原因是我将alarm(3)定时在while循环中,在3秒未超时时,程序又跑到这个地方,再次
开始定时,所以一直循环下去,就不会超时,不产生超时信号SIGALARM。
4、客户端每各三秒进行一次采样和上报数据,客户端正常发送,然而服务器第一次接收正常外,后面几次出现数据接收不完全,丢包现象?
答:原因是文件io操作不当,我是这样写的:
客户端:read(fd,readBuff,sizeof(readBuff));//从服务器接收响应信息
write(fd,writeBuff,sizeof(writeBuff));//writeBuff暂存采集到温度
服务器端:read(fd,readBuff,sizeof(readBuff));//readBuff暂存接收到的温度数据
write(fd,writeSponsBuff,sizeof(writeSponsBuff));//writeSponsBuff暂存响应客
户端的信息,比如:"server received from client datas successfully! "
此程序中read和write的数据大小不一致,read读到的数据大小只是客户端发送的数据,数据
大小小于readBuff,而服务器write参数使用的是sizeof(writeSponsBuff),发送了整个buff的数
据,即便是空的,那么就会读写速度不一致,导致丢包或重包,所以我将客户端和服务器端
write的第三个参数改为strlen(writeBuff)和strlen(writeSponsBuff),重新运行程序,进行数据
接收和发送,呵呵,这回接收正常。
注:strlen():计算出字符串长度;
sizeof():计算出数组大小,有时sizeof是操作符,sizeof()可以用来计算出数据类型的大小。
5、为什么边缘触发必须使用非阻塞??
ET 模式是一种边沿触发模型,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件
通知的文件描述符,每于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直
读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉
事件。而如果你的文件描述符如果不是非阻塞的,那这个一直读或一直写势必会在最后一次
阻塞。
四、了解到的知识空缺
- 行缓冲和全缓冲
(1)标准出错是不带缓冲的;若是指向终端设备的流才是行缓冲的,否则是全缓冲的。
(2)注:printf会把东西送到缓冲区,而如果缓冲区不刷新到话,你便不会在屏幕上看到东西,而能导致缓冲区刷新到情况有这些:
①强制刷新 标准输出缓存fflush(stdout);
②放到缓冲区到内容中包含/n /r ;
③缓冲区已满;
④需要从缓冲区拿东西到时候,如执行scanf;
(3)一般C库函数写入文件是全缓冲的,而写入显示器是行缓冲
-
alarm()
alarm()是一个秒级的定时,当超时时,内核向进程发出SIGALARM信号。可以使用它结合信号量signal实现定时回调。 -
边缘触发要使用非阻塞IO:边缘触发处理效率要比水平触发高。
-
sleep: 通过调用alarm()来设定报警时间,调用sigsuspend()将进程挂起在信号SIGALARM上,sleep()只能精确到秒级上
nanosleep:通过使用定时器实现的; -
水平触发模式(LT)和边缘触发模式(ET)
LT:epoll机制默认触发模式是水平触发模式,LT模式下,只要没有数据被获取,内核就会不断通知进程,也就是只要缓冲区有数据就会一直触发,那么系统调用增多,内核开销增大。
ET:只有在缓冲区增加数据的那一刻才会触发。
代码实现:
server_main.c:
#include "server.h"
int main(int argc,char **argv)
{
int server_port;
int sockfd = -1;
int clienfd = -1;
int re_val = -1;
//参数解析
re_val = argument_parse(argc, argv, &server_port);
if(re_val < 0)
{
printf("参数解析错误!\n");
exit(0);
}
//套接字初始化
re_val = socket_init(server_port,&sockfd);
if(re_val < 0)
{
printf("套接字初始化错误!\n");
}
//epoll服务器开始工作
server_epoll_create(sockfd);
return 0;
}
完整代码:
服务器端
测试
client[5]:第一个客户端连接上服务器
client[6]:第二个客户端连接上服务器
client[7]:第三个客户端连接上服务器