Linux网络编程之epoll服务器

       epoll同上篇博客中的select一样,都是用于多路转接,但epoll被公认为Linux2.6下性能最好的多路I/O就绪通知方法。


一、epoll相关系统调用

epoll只有三个系统调用函数:

epoll_create:创建epoll模型

epoll_ctl:管理epoll模型

epoll_wait:等待I/O时间就绪


events可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里



二、epoll工作原理


       epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式,在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。


关于内存映射技术mmap:

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset );
参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它 从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射 到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。



三、epoll模型


(1)调用epoll_create创建epoll模型的时候,实际上是在内核区创建了一棵空的红黑树和一个空的队列;

(2)调用epoll_ctl的时候,实际上是在往红黑树中添加结点,结点描述的是文件描述符及其上的对应事件;

(3)当某文件描述符上的某事件就绪的时候操作系统会创造一个结点放在队列中(此结点表示此文件描述符上的此事件就绪),这个队列通过内存映射机制让用户看到。



四、epoll的优点


1.支持一个进程打开无限数目的fd

       select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过epoll没有这个限制,它所支持的fd上限是最大可以打来文件的数目,这个数字一般远大于1024,举个例子,1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。


2.I/O效率不随fd数目增加而线性下降

      传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"socket才会主动的去调用 callback函数,其他idle状态socket则不会,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。


3.使用mmap加速内核与用户控件的消息传递

    这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把fd消息通知给用户空间都需要内核把fd信息传递给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。

 


 五、epoll服务器

epoll_server.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <string.h>

static void usage(const char* proc)
{
	printf("Usage: [local_ip] [local_port] %s\n", proc);
}

int startup(const char* _ip, int _port)
{
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock < 0)
	{
		perror("socket");
		exit(2);
	}
	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(_port);
	local.sin_addr.s_addr = inet_addr(_ip);
	if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
	{
		perror("bind");
		exit(3);
	}
	if(listen(sock, 10) < 0)
	{
		perror("listen");
		exit(4);
	}
	return sock;
}

int main(int argc, char* argv[])
{
	if(argc != 3)
	{
		usage(argv[0]);
		return 1;
	}

	int listen_sock = startup(argv[1], atoi(argv[2]));
	
	int epfd = epoll_create(256);
	struct epoll_event ev;
	ev.events = EPOLLIN;
	ev.data.fd = listen_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
	int nums = -1;
	struct epoll_event revs[64];
	int timeout = 1000;

	while(1)
	{
		switch(nums = epoll_wait(epfd, revs, 64, timeout))
		{
			case -1:
				perror("epoll_wait");
				break;
			case 0:
				printf("timeout...\n");
			default:
				{
					int i = 0;
					for(; i<nums; ++i)
					{
						int sock = revs[i].data.fd;
						if(sock==listen_sock && (revs[i].events&EPOLLIN))
						{
							//listen_sock ready!!!
							struct sockaddr_in client;
							socklen_t len = sizeof(client);
							int new_sock = accept(listen_sock, 
									(struct sockaddr*)&client, &len);
							if(new_sock < 0)
							{
								perror("accept");
								continue;
							}
							ev.events = EPOLLIN;
							ev.data.fd = new_sock;
							epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev);
						}
						else if(sock != listen_sock)
						{
							if(revs[i].events & EPOLLIN)
							{
								//read event ready!!!
								char buf[1024];
								ssize_t s = read(sock, buf, sizeof(buf)-1);
								if(s >0)
								{
									buf[s] = 0;
									printf("client# %s\n", buf);
									ev.events = EPOLLOUT;
									epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);
								}
								else if(s == 0)
								{
									printf("client is quit...\n");
									close(sock);
									epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
								}
								else
								{
									perror("read");
									continue;
								}
							}
							else if(revs[i].events & EPOLLOUT)
							{
								const char* msg = "HTTP/1.0 OK 200\r\n\r\n 
									<html><h1>hello epoll!</h1></html>";
								write(sock, msg, strlen(msg));
								close(sock);
								epoll_ctl(epfd, EPOLL_CTL_DEL, sock, NULL);
							}
						}
					}
				}
		}
	}
}
运行结果:

浏览器运行结果:





猜你喜欢

转载自blog.csdn.net/if9600/article/details/73924537