计算机网络学习笔记六、IO多路复用

IO多路复用

  从本篇文章开始总结IO多路复用相关的内容,IO多路复用相关的知识点主要分为select、poll、epoll三部分内容。从内容上来说,IO应该属于操作系统范畴的内容,但是IO多路复用多用于解决高性能服务器的并发问题(Redis、Nginx底层就是用epoll实现)。于是,将该部分内容划分到计算机网络的范畴进行记录。


1. 高性能服务器

1.1. 多线程并发服务器

  阻塞情况下,服务端发现有socket请求连接,服务端服务器进程就开一个线程,来处理该socket所有流的IO事件,这就是多线程方式来处理并发。每来一个连接就创建一个线程,多线程处理并发时涉及到线程创建、切换和销毁,造成系统开销过大。

  可以使用线程池的方式来避免线程的频繁创建和销毁,所谓线程池,就是提前创建若干线程,当有新连接建立时,就将已连接的Socket添加到一个队列中,线程池里的线程负责从队列中取出已连接的Socket进行处理。需要注意:该队列是一个全局队列,线程操作该队列时,必须加锁。

  在高性能服务器中有一个经典的C10K问题,即支持客户端1万请求的并发量,也就意味着一台机器维护1万个连接,也就是说服务器进程要开辟1万个线程,操作系统是扛不住这么大开销的。

  有没有只使用一个线程,来维护多个Socket的解决方案?I/O多路复用技术。

1.2. IO多路复用

  通常情况下,对文件描述符进行读写操作时,函数会将某一个文件描述符作为参数,即单个函数操作一个文件。很多应用场景下,如单线程服务器,需要某个函数实现监听多个文件描述符的功能。通俗来说,多路复用就是一个进程监听多个文件描述符。

  select、poll、epoll是内核提供给用户态的系统调用,进程(用户态)可以通过系统调用函数,从内核中获取某个socket文件描述符是否有读写事件。那select、poll、epoll如何获取网络事件的呢?进程(用户态)先把socket文件描述符传给内核,再由内核返回产生了事件的socket,最后 由用户态处理这些socket对应的请求。

2. select

  不使用select的情况下,当socket连接中有读写操作时,说明客户端有数据请求。问题是怎么知道流中有没有IO事件呢?轮询。

	while true {
		for i in fds[] {
			if i has data {
				read until unavaliable
			}
		}
	}

   这种轮询方式,会导致CPU的大量资源用来处理轮询,而非IO事件,这样如果轮询所有流,全都没有IO,CPU资源就白白浪费了。为了避免cpu空转,引入一层机制,如果某个socket有IO才轮询,没有IO就一直阻塞,这层机制就是select。

2.1. select示例

计算机网络学习笔记六、IO多路复用_第1张图片
select示例

  select系统调用,有一个参数fd_set结构体(上述代码中的rset),可以看作是一个文件描述符集合,fd_set是一个位图,每一个bit代表一个描述符,如果一个socket建立连接,对于bit置为1。select使用固定长度的BitsMap,表示文件描述符集合,Linux系统中有内核FD_SETSIZE限制,默认最大值为1024。

  • 问题一:select流程
  1. 调用select会先把fd_set从用户空间拷贝到内核空间。
  2. 内核遍历fd_set,监听是否有socket有IO操作,
  3. 一旦监听到有IO,对应fd_set对应的socket会置位,把fd_set拷贝用户空间,select返回
  4. 进程(用户态)遍历fd_set,找出有IO的socket,并处理(读写)。

  • 问题二:select缺点
  1. select内部,2次文件描述符集合拷贝操作
  2. 进行了2次文件描述符集合遍历操作。

2.2. select的API

  select是Linux提供的一个系统调用,主要包含五个函数,一个位图。select函数会接收位图作为参数,由内核对发生IO的fd进行置位,再将置位后的位图拷贝到用户态。

	// 把位图中fd位置0
	void FD_CLR(int fd, fd_set *set)
	// 判断位图中fd位是否为0
	int FD_ISSET(int fd, fd_set *set)
	// 把位图中fd位置1
	void FD_SET(int fd, fd_set *set)
	// 把位图全部置0
	void FD_ZERO(fd_set *set)
	
	// select函数,返回发生IO的socket个数
	int select(int nfds, fd_set *readfds, fds_set *writefds, fd_set *exceptfds, struct timeval *timeout)

2.3. select其他用途

  在UNIX中,一切皆文件,文件就是一串二进制流,socket、管道、终端、设备等都是文件。在信息交换过程中,就是对这些流进行数据收发操作,简称IO操作。系统调用read指从流中读数据,系统调用write指向流中写数据。

  select是IO复用函数,同时监听多个文件描述符的读写,除了网络通信,还可用于文件、管道、终端和设备等操作。

3. poll

  poll本质上和select是一样的,select中每个fd_set结构体最多只能标识1024个描述符,poll中去掉了这种限制。另外,pollfd中增加了reevents字段来记录对应fd是否有输入,不需要像select那样对位图进行置位修改。

3.1. poll示例

  poll使用一个结构体pollfd来指定一个需要监听的描述符,调用poll时一般会传入一个pollfd的结构体数组,数组中的元素个数表示监控的描述符个数。

计算机网络学习笔记六、IO多路复用_第2张图片
poll示例

3.2. poll的API

  poll是Linux提供的一个系统调用,主要包含一个函数,一个结构体。

		struct pollfd {
		int fd;
		short int events; // 当前fd要监听的的事件
		short int revents; // 初始为0,是对events的回馈,内核如果监听到该fd有IO,会将它置位
	}
	int poll(struct pollfd *fds, nfds_t nfds, int timeout)

3.3. poll优势

  poll比select更灵活,体现在两个方面:(1)poll不再使用BitMap存储所关注的文件描述符,而是使用以链表形式来组织,突破了select文件描述符1024的限制(2)调用poll以后,不用像select那样需要重新对文件描述符初始化,因为poll返回的事件写在了reevets选项中。

  poll和select并没有太大的本质区别,都使用线性结构存储进程所关注的Socket集合,因此需要拷贝文件描述符集合,同时需要遍历文件描述符集合来监听哪个文件描述符发生了IO。

4. epoll

  selectpoll存在两方面的性能问题:拷贝和遍历。针对拷贝,epoll使用红黑树来组织进程所监听的文件描述符;针对轮询,epoll改用了通知机制。

4.1. epoll示例

计算机网络学习笔记六、IO多路复用_第3张图片
epoll示例

  epoll所支持的文件描述符上限是整个系统最大可以打开的文件数目,如1G内存大概是10万个左右。每个fd上面都注册了callback函数,只有活跃的文件描述符会主动去调用callback函数。

  某个socket有IO发生时,通过回调函数将该socket加入到就绪事件队列。当用户调用epoll_wait()时,会返回有IO的文件描述符的个数,不需要像select/poll那样遍历整个socket集合。

4.2. epoll的API

  epoll是Linux提供的一个系统调用,主要包含三个函数,一个结构体。它本身是一种异步IO。

	typedef union epoll_data {
		void *ptr;
		int fd;
		uint32_t u32;
		uint64_t u64;
	} epoll_data_t;
	
	// epoll_event中data指向一个共用结构体,可以用该共用体保存自定义参数,或指向被监控的文件描述符。
	struct epoll_event {
		uint32_t events;
		epoll_data_t data;
	};
	// 创建一个epoll实例并返回,该实例可用于监控_size个文件描述符(现在已经废弃)。即红黑树根节点。
	int epoll_create(int size)
	// 向epoll中注册事件函数,将待检测的文件描述符,加入到内核中的红黑树
	int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
	// 类似select中的select函数,poll中的poll函数,等待内核返回监听描述符的事件产生(依旧是拷贝),返回发生IO的socket个数。
	int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

  epoll在内核中使用红黑树来组织进程所关心的socket文件描述符,同时使用链表来维护就绪事件。epoll_ctl()会将待检测的文件描述符,加入到内核中的红黑树,就不需要像select/poll一样每次操作时,都传入整个socket集合;epoll使用时间驱动的机制,内核维护了一个链表来记录就绪事件,当红黑树中某个fd的通知到了,会将对应fd添加到就绪链表。。

  注意,关于对epoll就绪链表中的fd的操作,有说是用户态和内核态采用共享内存的方式实现,这种说法是错误的。事实上,在Linux源码中,仍然使用拷贝来实现。

  epoll的方式可以监听的socket数量很多的情况下,也不会大幅降低效率,完全能够解决C10K问题。

4.3. 水平触发和边缘触发

  epoll支持水平触发(level-triggered)和边缘触发(edge-triggered)两种事件触发模式,默认情况下是水平触发,具体可以根据应用场景设置为边缘触发模式(ev.events = EPOLLIN|EPOLLET)。select和poll只有水平触发。

  水平触发: 如果报告了fd后,事件没有被处理或数据没有被全部读取,epoll会立即再报告该fd。

  边缘触发: 如果报告了fd后,事件没有被处理或数据没有被全部读取,epoll会下次再报告该fd。

  一般来说,边缘触发的效率比水平触发高,因为边缘触发可以减少epoll_wait的系统调用次数,降低由于上下文切换带来的开销。

  使用I/O多路复用时,最好搭配非阻塞I/O一起使用。多路复用API返回的事件并不一定可读写,如果使用阻塞I/O,调用read/write时,会发生程序阻塞,因此最好搭配非阻塞I/O,以便应对特殊情况。

4.4. select、poll、epoll比较

  1. select和poll机制基本相同,poll没有select最大文件描述符的限制。
  2. select和poll缺点:

  每次调用select和poll,都需要将监听的fd_set或pollfd发给内核态,如果需要监听大量文件描述符,这样效率会很低。
  内核态中,每次需要对传入的文件描述符进行轮询,查询是否有对应的事件产生。

  1. epoll优势:

  epoll使用epoll_create创建一个epoll实例,调用epoll_ctl新增监听描述符,此时才将用户态描述符切换到内核态
  epoll_wait调用频率肯定比epoll_create频率高,所以epoll_wait无需发送任何描述符到用户态
  内核态中使用一个描述符就绪的链表。当描述符就绪时,会有回调函数将它添加到链表,调用epoll_wait时,就不需要遍历所有描述符,而是直接去查看链表是否为空。

5. 参考文献

[1] https://www.bilibili.com/video/BV1qJ411w7du?from=search&seid=11659769434400760394
[2] https://mp.weixin.qq.com/s/uJLw9tCUANNhWlFAIZfDIA
[3] https://www.bilibili.com/video/BV1pp4y1e7xN?p=10
[4] https://www.bilibili.com/video/BV1Yt4y1i7hf?t=4777

你可能感兴趣的:(计网,面试,epoll)