朴素、Select、Poll和Epoll网络编程模型实现和分析——模型比较

2019-01-16 15:15:29 浏览数 (1)

        经过之前四篇博文的介绍,可以大致清楚各种模型的编程步骤。现在我们来回顾下各种模型(转载请指明出于breaksoftware的csdn博客)

模型编程步骤对比

《朴素、Select、Poll和Epoll网络编程模型实现和分析——朴素模型》中介绍的是最基本的网络编程模型,我们使用单线程去实现,它的步骤很简单

        如同这幅流程图,它简单到非常单薄。这个模型暴露出来的问题是在一个线程中,它一次只能处理一个请求。当然我们可以通过多线程或者多进程的模式对该模型进行改进:主线程监听接入socket请求,将其保存到一段空间中。其他线程从该段空间中获取socket并处理。但是这种改善不是从网络编程模型的角度出发的,而是多线程/多进程的一种应用,这种应用也可以用于之后几个模型。所以我们暂且抛开这种优化来讨论。

《朴素、Select、Poll和Epoll网络编程模型实现和分析——Select模型》解决了朴素模型同步的执行的问题,而且还解决了一次只能处理一个请求的问题。

        可以见得这种模型稍微厚实了一点,但是它也有一个问题:最多只能同时处理1024个请求。致命的是这种限制是在内核代码级别的(详见之前文章的分析)。为了解决这个问题于是就有了poll模型。

《朴素、Select、Poll和Epoll网络编程模型实现和分析——Poll模型》中介绍的Poll模型结构和Select模型是非常相似的,但是它不再依赖于结构体的位数来限制最多处理的socket数,而是采用一个数组去保存数据。

        poll模型的问题是其效率随着同时连接的socket数而下降。因为它和Select模型有着相同的缺陷——每次只是知道有事件发生,但是不知道是哪个socket有事件发生,于是需要遍历所有的socket。这样socket如果越来越多,它的效率自然会下降。为了解决这个问题,于是有了Epoll模型。

《朴素、Select、Poll和Epoll网络编程模型实现和分析——Epoll模型》算是目前最优秀的解决多连接的模型。但是它也是最复杂的模型。

模型效率对比

        首先明确一个立场,没有实际测试数据的对比都是空谈。网上一般充斥的一种观点就是EPOLL模型效率最好,Poll和Select模型相当,朴素模型最差。但是这个观点是错误的。因为不同的模型在不同的使用场景下有着不同的效率。

        比如,在我们之前测试的场景下。朴素模型的平均处理能力大概是15000次/秒,Select的平均处理能力是7000次/秒,Poll的平均处理能力是7500次/秒,Epoll的平均处理能力是11000次/秒。为什么朴素模型的处理能力是最高的?因为我们这个是一个典型的短连接、一次性交互的场景,这种场景下如果得到一个请求马上去读和写,而不经过其他复杂的过程,自然是最快的。Poll模型和Select模型能力相当。Epoll是对Poll模型的优化,所以它的效率比Select和Poll要好一些。

        我们使用valgrind对上述四种模型的执行情况进行分析。

        首先我们看朴素模型

        朴素模型的main函数自身(即刨除下面列出的函数的其他执行时间)的执行时间占比是非常小的(5.45%),其主要的耗时操作是读和写操作(server_write占38.94%,server_read占27.64%)。

         我们再看下效率第二的Epoll模型

        可见Epoll模型中,server_write操作耗时占比最高(29.32%),其次是main函数自身耗时占(25.7%),再次是server_read(22.48%)。

        再看下效率倒数第二的Poll模型

        Poll模型中main函数自身耗时占比最高(46.93%),其次是server_write(21.44%),再次是server_read(16.44%)。我们看到main函数自身耗时提高非常多。

        最后我们看看效率最低的Select模型

        Select模型中,main函数自身耗时占比最多(97.99%),其次是server_write(0,83%),再次是server_read(0.64%)。那么main函数中什么是最耗时的呢?我对Select模型源码进行了细分,将之前循环遍历各个Socket的逻辑提炼出来

代码语言:javascript复制
void deal_socket(int index, int listen_sock, fd_set* active_fd_set, fd_set* read_fd_set) {
	if (listen_sock == index) {
		/* Connection request on original socket. */
		int new_sock;
		new_sock = accept(listen_sock, NULL, NULL);
		if (new_sock < 0) {
			perror("accept error");
			exit(EXIT_FAILURE);
		}
		request_add(1);
		FD_SET(new_sock, active_fd_set);
	} else {
		if (0 == server_read(index)) {
			server_write(index);
		}
		close(index);
		FD_CLR(index, active_fd_set);
	}
}

void deal_request(int listen_sock, fd_set* active_fd_set, fd_set* read_fd_set) {
	int index = 0;
	for (index = 0; index < FD_SETSIZE;   index) {
		if (FD_ISSET(index, read_fd_set)) {
			deal_socket(index, listen_sock, active_fd_set, read_fd_set);
		}
	}
}

        再进行测试,我们得出以下结果

        deal_request函数函数内部理论上最大调用deal_socket的次数是30万*1024=3072万,而实际调用deal_socket的次数只有60万。这说明平均一次调用deal_request只会有2个socket被命中从而调用deal_socket。也就是说,1次select操作只有2个socket会被处理,而其他1022次循环操作都是不被命中的比较。而恰恰最耗时的就是deal_request操作(deal_socket操作占用deal_request时间不足2%),所以我们可以得出select模型中最耗时的就是对socket的遍历操作。而其根本原因是select函数每次返回时可以被处理的socket数量太少——2个。

        那么epoll和poll模型的循环使用率是多少?我们对它们的代码做点改造,我们引入两个标记函数——total_loop_count和deal_sock_count,前者用于统计一共循环了多少次,后者用于统计需要处理的socket多少次。我们先看Poll模型的结果。

        可见Poll模型中对pollfd数组遍历的次数一共是60万次,有效处理socket也是60万次。使用率100%,而不像Select模型的使用率只有60/3072=2%。同样epoll的使用率也是100%。

        通过上述分析,我们可以看出在短连接、一次性读写完数据的情况下,朴素模型的效率是最高的,其次是Epoll、Poll、Select。其实Select模型性能和Poll是差不多的,在我们测试场景下产生的差距是因为我们实现Select模式的方式问题。我们对其可以参照Poll模型进行改造,使用一个int型数组保存接入的socket,同时动态记录可用socket的个数。这样我们每次遍历就只遍历连续的可用socket数组空间,而不用像例子中将整个数组都遍历,从而提高循环操作的有效率。

        那么什么时候才是该使用Select、Poll或者Epoll模型的呢?我们将在下一篇博文中对其进行分析。

0 人点赞