多进程和多线程的目的是在于最大限度地利用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
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
#include
#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;
}