linux网络编程学习笔记之六 -----I/O多路复用服务端

来源:互联网 发布:淘宝店铺转让后 编辑:程序博客网 时间:2024/06/03 07:20

多进程和多线程的目的是在于最大限度地利用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 32typedef 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 调用返回将修改fdsetretval = 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) //markcontinue;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 90int 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;}elseprintf("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;}


1 0
原创粉丝点击