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;
}


linux网络编程学习笔记之六 -----I/O多路复用服务端,古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。