实现I/O复用的传统方法包括:select函数、epoll函数。
select复用方法,无论如何优化程序性能也无法同时接入上百个客户端。这种局限性催生了Linux下的epoll诞生。
两点:
针对第2点,可以通过以下方式弥补:
仅向操作系统传递1次监视对象,监视范围或内容发生变化时,只通知发生变化的事项。
这样就无需每次调用select函数时都向操作系统传递监视对象信息。前提是操作系统支持这种处理方式。Linux支持的方式是epoll,Windows支持的是IOCP。
epoll是Linux下的支持方式,就是说,改进后的I/O复用模型不具有兼容性。不过大部分操作系统都支持select。
对于满足如下条件的,即使在Linux平台也不该拘泥于epoll:
epoll函数优点:
epoll服务器端实现中需要的3个函数:
epoll_create
:创建保存epoll文件描述符的空间(向操作系统申请的一块空间,由操作系统负责保存监视对象的文件描述符,而不是像select函数那样直接定义了一个fd_set函数)epoll_ctl
: 向空间注册并注销文件描述符(为了添加和删除监视对象文件描述符,select中需要FD_SET,FD_CLR,但epoll通过epoll_ctl函数请求操作系统完成)epoll_wait
: 与select函数类似,等待文件描述符发生变化(select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),epoll方式中通过epoll_event结构体将发生变化的文件描述符单独集中到一起)struct epoll_event{
__uint32_t events;
epoll_data_t data;
}
typedef union epoll_data{
void * ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
Linux内核2.5.44版本开始引入epoll。使用epoll之前需要验证Linux内核版本。
cat /proc/sys/kernel/osrelease
实际测试了我的云端Linux系统:
[root@VM_0_10_centos ~]# cat /proc/sys/kernel/osrelease
3.10.0-1062.9.1.el7.x86_64
#include
int epoll_create(int size);
size epoll实例的大小
成功时返回epoll文件描述符,失败-1
通过size传递的值决定epoll例程的大小,但该值只是向操作系统提的建议。最终多大还是由操作系统决定。在Linux 2.6.8之后的内核将完全忽略传入epoll_create函数的size参数,因为内核会根据情况调整epoll例程的大小。
在epoll例程生成后,应在其内部注册监视对象文件描述符:
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epfd 用于注册监视对象的epoll例程的文件描述符
op 用于指定监视对象的添加、删除、更改等操作(EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD)
fd 需要注册的监视对象文件描述符
event 监视对象的事件类型
返回值 成功返0,失败-1
epoll_ctl(A, EPOLL_CTL_ADD, B, C); 在epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件。
epoll_ctl(A, EPOLL_CTL_DEL, B, NULL); 在epoll例程A中删除文件描述符B(一般Linux2.6.9之前的内核不允许传递NULL,应传递一个epoll_event结构体变量的地址值,这个地址值会被忽略)
struct epoll_event event;
...
event.events = EPOLLIN;//发生需要读取数据的情况(事件)时
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
...
events中可以保存的常量及所指事件类型:
常量 | 事件 |
---|---|
EPOLLIN | 需要读取数据的情况 |
EPOLLOUT | 输出缓冲为空,可以立即发送数据的情况 |
EPOLLPRI | 收到OOB数据的情况? |
EPOLLRDHUP | 断开连接或半关闭的情况,在边缘触发方式下非常有用 |
EPOLLERR | 发生错误的情况 |
EPOLLLET | 以边缘出发的方式得到事件通知 |
EPOLLONESHOT | 发生一次事件后,相应的文件描述符不再收到事件通知 |
#include
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
epfd 表示事件发生监视范围的epoll例程的文件描述符
events 保存发生时间的文件描述符集合的结构体地址值
maxevents 第二个参数中可以保存的最大事件数
timeout 以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件
int event_cnt;
struct epoll_event* ep_events;
...
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);//EPOLL_SIZE是宏常量
...
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
...
[root@VM_0_10_centos epoll]# cat echo_epollserv.c
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char * buf);
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc != 2){
printf("Usage : %s \n" , argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)& serv_adr, sizeof(serv_adr)) == -1){
error_handling("bind() error");
}
if(listen(serv_sock, 5) == -1){
error_handling("listen() error");
}
epfd = epoll_create(EPOLL_SIZE);
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);
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){
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);
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void error_handling(char * buf){
fputs(buf, stderr);
fputc('\n',stderr);
exit(1);
}
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char * buf);
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc != 2){
printf("Usage : %s \n" , argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)& serv_adr, sizeof(serv_adr)) == -1){
error_handling("bind() error");
}
if(listen(serv_sock, 5) == -1){
error_handling("listen() error");
}
epfd = epoll_create(EPOLL_SIZE);
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);
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);
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){
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);
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void error_handling(char * buf){
fputs(buf, stderr);
fputc('\n',stderr);
exit(1);
}
#include
#int fcntl(int filedes, int cmd, ...);
成功时返回cmd参数相关值,失败时返回-1
filedes 属性更改目标的文件描述符
cmd 表示函数调用的目的
int flag = fcntl(fd, F_GETFL, 0); 获取之前设置的属性信息
fcntl(fd, F_SETEL, flag | O_NONBLOCK); 在此基础上添加非阻塞O_NONBLOCK标志,这样,调用read & write函数时,无论是否存在数据,都会形成非阻塞文件(套接字)
需要用errno分辨错误的原因:边缘触发方式中,接收数据时仅注册1次该事件。因此,一旦发生输入相关事件,就要读取输入缓冲区中的全部数据。所以要验证输入缓冲区是否为空。read函数返回-1,变量errno
中的值为EAGAIN
时,说明没有数据可读。
边缘触发方式中,以阻塞方式工作的read和write函数有可能引起服务器端的长时间停顿。因此,边缘触发方式中一定要采用非阻塞read和write函数。
#include
#include
#include
#include
#include
#include
#include
#include // here
#include // here
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char * buf);
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc != 2){
printf("Usage : %s \n" , argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)& serv_adr, sizeof(serv_adr)) == -1){
error_handling("bind() error");
}
if(listen(serv_sock, 5) == -1){
error_handling("listen() error");
}
epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
setnonblockingmode(serv_sock);// 设置非阻塞状态
event.events = EPOLLIN | EPOLLET; // 这里修改成边缘触发
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
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){ // 边缘触发方式中,发生事件时需要读取输入缓冲区中的所有数据,因此需要循环调用read函数。上面的条件触发回声服务器端就不需要有这个while循环
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len == 0){
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 if(str_len<0){
if(erron == EAGAIN){//是因为缓冲区中没有可读取的内容而退出的(此时,read函数返回-1,且errno值为EAGAIN)
break;
}
}
else{
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
//将文件描述符或套接字文件描述符修改成非阻塞状态
void setnonblockingmode(int fd){
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETEL, flag | O_NONBLOCK);
}
void error_handling(char * buf){
fputs(buf, stderr);
fputc('\n',stderr);
exit(1);
}
边缘触发可以做到:分离接收数据和处理数据的时间点!
即使服务器端的输入缓冲区中接收到数据(注册相应的事件),服务器端也能决定读取和处理这些数据的时间点,这样就给服务器端的实现带来了巨大的灵活性。
条件触发中,也不是无法区分数据接收和处理。但是,在输入缓冲收到数据后,如果不读取(选择延时处理),则每次调用epoll_wait函数的时候都会产生相应事件,而且事件数也会累加,服务器端无法承受,这十分不现实。
参考:https://blog.csdn.net/nail_candy/article/details/88957542
(1)利用select函数实现服务器端时,代码层面存在的2个缺点是?
(2)无论是select方式还是epoll方式,都需要将监视对象文件描述符信息通过函数调用传递给操作系统。请解释传递该信息的原因
select和epoll是系统函数,准确地说,是要求观察套接字变化的方式的。套接字是受操作系统进行管理的。即,select和epoll是一个由操作系统执行的函数。因此,应该将监视对象的文件描述符传递给操作系统
(3)select方式和epoll方式的最大差异在于监视对象文件描述符传递给操作系统的方式。请说明具体的差异,并解释为何存在这种差异。
epoll不同于select的地方是只要将监视对象文件描述符的信息传递一个给操作系统就可以了。因此epoll方式克服了select方式的缺点,体现在在linux内核上保存监视对象信息的方式。
(4)虽然epoll是select的改进方式,但select也有自己的优点。在何种情况下使用select方式更合理
如果连接服务器的人数不多(不需要高性能),而且需要在多种操作系统(windows和linux)下进行操作,在兼容性方面,使用select会比epoll更合理
(5)epoll以条件触发或边缘触发方式工作。二者有何区别?从输入缓冲的角度说明这2种方式通知事件的时间点的差异
在条件触发方式中,只要输入缓冲有数据,就会持续进行事件通知;而在边缘触发中,接收数据时仅注册1次该事件,并且只有当输入缓冲数据为空时才进行通知
(6)采用边缘触发时可以分离数据的接收和处理时间点。说明其原因及优点。
如果使用边缘触发方式,在输入缓冲中接收数据时,只会发生一次事件通知,而且输入缓冲中仍有数据时,不会进行通知,因此可以在数据被接收后,在想要的时间内处理数据。而且,如果分离数据的接收和处理时间点,在服务器中会有更大的灵活性
(7)实现聊天服务器,条件触发和边缘触发两种epoll方式实现
/**********************************char_EPLTserv.c*************************/
#include
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 100
#define MAX_CLNT 256
#define EPOLL_SIZE 50
void error_handling(char *buf);
void send_msg(char * msg, int len);
int clnt_cnt=0;
int clnt_socks[MAX_CLNT];
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc!=2) {
printf("Usage : %s \n" , argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE);
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);
while(1)
{
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if(event_cnt==-1)
{
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);
clnt_socks[clnt_cnt++]=clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else
{
str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
if(str_len==0)
{
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);
for(i=0; i<clnt_cnt; i++)
{
if(clnt_sock==clnt_socks[i])
{
while(i++<clnt_cnt-1)
clnt_socks[i]=clnt_socks[i+1];
break;
}
}
clnt_cnt--;
}
else
{
send_msg(buf, str_len);
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void send_msg(char * msg, int len) // send to all
{
int i;
for(i=0; i<clnt_cnt; i++)
write(clnt_socks[i], msg, len);
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
/**********************************char_EPETserv.c*************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 100
#define MAX_CLNT 256
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf);
void send_msg(char * msg, int len);
int clnt_cnt=0;
int clnt_socks[MAX_CLNT];
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc!=2) {
printf("Usage : %s \n" , argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
epfd=epoll_create(EPOLL_SIZE);
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
setnonblockingmode(serv_sock);
event.events=EPOLLIN;
event.data.fd=serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
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);
clnt_socks[clnt_cnt++]=clnt_sock;
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);
for(i=0; i<clnt_cnt; i++)
{
if(ep_events[i].data.fd==clnt_socks[i])
{
while(i++<clnt_cnt-1)
clnt_socks[i]=clnt_socks[i+1];
break;
}
}
clnt_cnt--;
printf("closed client: %d \n", ep_events[i].data.fd);
break;
}
else if(str_len<0)
{
if(errno==EAGAIN)
break;
}
else
{
send_msg(buf, str_len);
}
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void send_msg(char * msg, int len) // send to all
{
int i;
for(i=0; i<clnt_cnt; i++)
write(clnt_socks[i], msg, len);
}
void setnonblockingmode(int fd)
{
int flag=fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}