linux网络编程学习笔记之六 -----I/O多路复用服务端
多进程和多线程的目的是在于最大限度地利用CPU资源,当某个进程不需要占用太多CPU资源,而是需要I/O资源时,可以采用I/O多路复用,基本思路是让内核把进程挂起,直到有I/O事件发生时,再把控制返回给程序。这种事件驱动模型的高效之处在于,省去了进程和线程上下文切换的开销。整个程序运行在单一的进程上下文中,所有的逻辑流共享整个进程的地址空间。缺点是,编码复杂,而且随着每个逻辑流并发粒度的减小,编码复杂度会继续上升。
I/O多路复用典型应用场合(摘自UNP6.1)
select的模型就是这样一个实现,把每个客户的请求放入事件队列中,主线程通过非阻塞的I/O来处理他们。
select详细的用法和fd_set结构见:UNP的CH6
几个Tips
1、select在等待期间会被进程捕获的信号中断,从严谨的角度出发,应处理好EINTR错误
2、内核实际支持的时间分辨率比timeval结构的微秒级粗糙
3、select每次返回的是已就绪的总的描述位数,并把未就绪的位清0(三个fd_set参数都是值-结果参数)因此每次重新调用select时需重新对所有集合置1
一个服务器端程序的例子:
#include "simon_socket.h" #define SERV_PORT 12345 #define FDSET_SIZE 32 typedef struct Clientinfo{ int fd; struct sockaddr_in addr; }Clientinfo; typedef struct Clientpool{ int count; Clientinfo cinfo_set[FDSET_SIZE]; }Clientpool; void init_clientpool(Clientpool *pool) { int i; pool->count = 0; memset(pool->cinfo_set, 0, sizeof(pool->cinfo_set)); for (i = 0; i < FDSET_SIZE; i++) (pool->cinfo_set[i]).fd = -1; } void add_clientinfo(Clientpool *pool, int newfd, struct sockaddr_in client) // change { int i; for (i = 0; i < FDSET_SIZE; i++) { if (pool->cinfo_set[i].fd < 0) { pool->cinfo_set[pool->count].fd = newfd; memcpy((char*)&(pool->cinfo_set[pool->count].addr), (char*)&client, sizeof(struct sockaddr_in)); pool->count++; break; } } } int process_cli(Clientinfo cli) { int recv_bytes, send_bytes; if ((recv_bytes = recv(cli.fd, recv_buf, MAX_BUF_SIZE, 0)) < 0) { perror("Fail to recieve data"); } else if (!recv_bytes) return -1; printf("Success to recieve %d bytes data from %s:%d\n%s\n", recv_bytes, inet_ntoa(cli.addr.sin_addr), ntohs(cli.addr.sin_port), recv_buf); if ((send_bytes = send(cli.fd, recv_buf, recv_bytes, 0)) < 0) { perror("Fail to send data"); } printf("Success to send %d bytes data to %s:%d\n%s\n", recv_bytes, inet_ntoa(cli.addr.sin_addr), ntohs(cli.addr.sin_port), recv_buf); return 0; } int main() { int sockfd, retval, connfd, i, maxfd; size_t addr_len; struct sockaddr_in client_addr; fd_set fdset, watchset; Clientpool cpool; addr_len = sizeof(struct sockaddr); init_clientpool(&cpool); sockfd = init_tcp_psock(SERV_PORT); FD_ZERO(&fdset); FD_SET(sockfd, &fdset); maxfd = sockfd; for (; ;) { watchset = fdset; //select 调用返回将修改fdset retval = select(maxfd+1, &watchset, NULL, NULL, NULL); //两个同时连接,会不会排队? if (retval < 0) { perror("Select error"); continue; } else { while (retval--) { if (FD_ISSET(sockfd, &watchset)) { if ((connfd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len)) == -1) { perror("Fail to accept the connection"); continue; } printf("Get a connetion from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); FD_SET(connfd, &fdset); add_clientinfo(&cpool, connfd, client_addr); if ( connfd > maxfd ) maxfd = connfd; //mark } else { for (i = 0; i < cpool.count; i++) { if (cpool.cinfo_set[i].fd < 0) //mark continue; if (FD_ISSET(cpool.cinfo_set[i].fd, &watchset)) { if (process_cli(cpool.cinfo_set[i]) < 0) { printf("%s:%d quit the connection\n", inet_ntoa(cpool.cinfo_set[i].addr.sin_addr), ntohs(cpool.cinfo_set[i].addr.sin_port)); FD_CLR(cpool.cinfo_set[i].fd, &fdset); close(cpool.cinfo_set[i].fd); cpool.count--; cpool.cinfo_set[i].fd = -1; } } } } } } } return 0; }
由于select每次遍历地对描述字集合进行监测,当集合较大时,效率会受到极大的影响(随在线人数的线性递增呈二次乃至三次方下降)
Epoll的出现是作为 select的升级版,linux2.6以上都支持。它采用事件响应的方法,只遍历那些被内核I/O事件异步唤醒而加入Ready队列的描述符集合。因而显著减少了大量连接而只有少量活跃用户情况的系统CPU利用率。
epoll的接口函数很简单,只有三个:
#include<sys/epoll.h> int epoll_create(int size); int epoll_ctl(int epfd, int op, intfd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);详情可以参考:
epoll的工作原理是,如果想进行I/O操作,先向epoll查询是否可读或者可写,如果处于可读或可写状态,epoll会调用epoll_wait函数进行通知,此时再进一步recv或send。
epoll仅仅是一个异步事件通知机制,其本身并不进行任何的I/O读写操作,它只负责通知是不是可读或可写了,而具体的读写操作将由应用层自己来做。这种方式保证了事件通知和I/O操作之间彼此的独立性。
epoll的两种模式:ET(边缘触发)和LT(水平触发)
采用ET模式,仅当状态发生变化时内核才会通知,而采用LT模式,类似与select,只要还有没处理的事件内核就会一直通知。因此,ET模式是通过减少系统调用来达到提高并行效率的目的的。另一方面,ET模式对编程要求高,需要细致地处理每个请求,否则容易发生事件丢失的情况。比如:对ET而言,accept调用返回时,除了建立当前这个连接外,不能马上就epoll_wait,还要继续循环accept,直到返回-1,且errno==EAGAIN,才不继续accept。LT在服务编写上的表现就对编码要求低一些:只要数据没有被获取,内核就会不断地进行通知,因此不必担心事件丢失的情况。如果调用accept时,有返回就可以马上建立这个连接,再调用epoll_wait等待下次通知,和select类似。
一个服务器端程序的例子:
#include"simon_socket.h" #include<fcntl.h> #include<sys/epoll.h> #define SERV_PORT 12345 #define MAX_EPOLLFD 100 #define EVENT_SIZE 90 int set_fd_nonblocking(int fd) { int flag; flag = fcntl(fd, F_GETFL, 0); if (flag == -1) { perror("fcntl error: "); return -1; } flag |= O_NONBLOCK; if (fcntl(fd, F_SETFD, flag) == -1) { perror("fcntl error: "); return -1; } return 0; } int main() { int i, listenfd, contfd, epfd, readyfd, curfd = 1, recv_bytes; struct sockaddr_in cli_addr; struct epoll_event ev_tmp, events[EVENT_SIZE]; size_t addr_len = sizeof(struct sockaddr); epfd = epoll_create(MAX_EPOLLFD); listenfd = init_tcp_psock(SERV_PORT); ev_tmp.data.fd = listenfd; ev_tmp.events = EPOLLIN | EPOLLET; if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev_tmp) == -1) { perror("Add event failed: "); return 1; } printf("Epoll server startup at port %5d\n", SERV_PORT); while(1) { readyfd = epoll_wait(epfd, events, EVENT_SIZE, -1); for (i = 0; i < readyfd; i++) { if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP)) { perror("Epoll error: "); close(events[i].data.fd); continue; } else if (events[i].data.fd == listenfd) { if ((contfd = accept(listenfd, (struct sockaddr *)&cli_addr, &addr_len)) == -1) { perror("Accept request failed: "); return 1; } else printf("Get a connection from %s:%5d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); if (curfd > EVENT_SIZE) { printf("Too many connections, more than %d\n", EVENT_SIZE); continue; } set_fd_nonblocking(contfd); ev_tmp.data.fd = contfd; ev_tmp.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_ADD, contfd, &ev_tmp); curfd++; continue; } else if (events[i].events & EPOLLIN) { if ((recv_bytes = recv(events[i].data.fd, recv_buf, MAX_BUF_SIZE, 0)) <= 0) { epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL); getpeername(events[i].data.fd, (struct sockaddr *)&cli_addr, &addr_len); printf("%s:%5d quit the connection\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port)); close(events[i].data.fd); curfd--; } else { ev_tmp.data.fd = events[i].data.fd; ev_tmp.events = EPOLLOUT | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &ev_tmp); } } else if (events[i].events & EPOLLOUT) { send(events[i].data.fd, recv_buf, recv_bytes, 0); ev_tmp.data.fd = events[i].data.fd; ev_tmp.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &ev_tmp); } } } close(listenfd); return 0; }
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。