IO
IO和IO模型
IO 操作指的就是数据的输入输出操作;IO 过程可以分为两个步骤:等待 IO 就绪、数据拷贝
同步异步
同步 指进程触发IO操作后会阻塞用户进程,用户等待请求数据到达。调用线程会被阻塞,必须等待操作完成,这意味着在等待期间,无法进行其他操作。
异步 程序发出IO请求后立即继续执行,不需要等待该请求完成。请求的结果在后台处理,完成后可以通过回调、Future或Promise等机制获取结果。
异步IO的最大特点是非阻塞。当程序发出一个异步IO请求时,操作系统或运行时环境会立即返回控制权给程序主线程,而不必等待IO操作完成。这使得主线程能够继续执行后续的代码,而IO操作会在后台异步进行。
阻塞非阻塞
阻塞 指用户进程触发IO操作后,触发read()等待内核将数据copy到用户空间的过程(select 也是一种阻塞IO)。
非阻塞 指用户进程触发IO操作后,触发read(),系统内核会立刻返回成功,当read响应完成,系统内核会产生一个信号或者基于一个线程回调函数完成IO过程。
两者区别,触发read()操作后,系统内核是否会立刻返回状态还是等待, 所以阻塞非阻塞是针对操作系统内核。
概念不同
同步/异步:专注于程序控制流的顺序,程序是否需要等待IO完成才能继续。
阻塞/非阻塞:专注于调用线程的状态,线程是否在等待期间挂起。
一般常见情况都是阻塞同步IO。程序会在调用后等待操作完成,直到成功或失败。
还有,非阻塞异步IO,程序发出IO请求并立即返回控制权,随后程序可以进行其他工作,而当IO操作完成时,再通过回调或事件处理结果。
非阻塞的同步IO(特殊情况)
某些系统中可以通过轮询方式实现非阻塞的同步IO。例如,在网络编程中,某些系统支持使用同步调用,但使用轮询机制不断检查IO操作的状态,而不是阻塞调用线程。
IO处理方式(切换用户态和内核态)
应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态。
IO操作通常需要在用户态和内核态之间切换,在用户态,应用程序无法直接控制硬件设备。当应用程序请求IO操作时,它会通过系统调用(system call)将请求传递给内核。操作系统内核负责处理IO请求,与硬件设备通信。
系统调用:系统调用会将控制权从用户态切换到内核态,执行内核中的相关函数来处理IO请求。例如,读写文件、访问设备驱动等操作通常通过系统调用完成。
中断处理:当硬件设备完成一个IO操作时,它会向CPU发送一个中断信号。在中断处理过程中,CPU会从用户态切换到内核态,处理中断,然后根据需要执行相应的内核函数,如更新设备状态、文件系统状态等。处理完毕后,系统可能会将控制权返回到中断前的中断服务程序(ISR),或者重新切换回用户态执行后续操作。
文件描述符
文件描述符(File Descriptor,FD)是操作系统内核为进程分配的一个非负整数,用于标识进程正在访问的文件或I/O资源。文件描述符是操作系统提供的一种机制,允许进程与文件系统、网络套接字以及其他类型的I/O设备进行交互。
文件描述符和句柄的关系
文件描述符:
主要在Unix-like系统(如Linux、macOS、BSD等)中使用。 是一个非负整数,用于标识打开的文件、管道、网络连接等资源。
文件描述符在程序运行期间是动态分配的,通常从3开始(标准输出、标准输入、标准错误分别对应文件描述符0、1、2)。
文件描述符是操作系统层面的概念,不依赖于应用程序。
句柄:
在Windows操作系统中使用。 是一个唯一的标识符,用于访问操作系统资源,如文件、设备、进程等。 句柄通常是一个指针或四字节的整数。
句柄是应用程序层面的概念,应用程序通过句柄来访问操作系统资源。
IO多路转接(复用)
IO多路转接,也称为IO多路复用,是一种网络通信机制,它允许单个线程或进程同时监控多个文件描述符(例如网络套接字)的状态,以确定它们是否准备好进行读写操作。这种机制在单线程/进程的场景下实现了并发处理,从而提高了程序的性能和资源利用率。
IO多路转接的核心思想是,程序将一组文件描述符的状态检查委托给操作系统内核,内核负责检测这些文件描述符是否就绪(即是否可以读取数据或写入数据)。一旦有文件描述符就绪,内核会通知程序,程序再根据就绪的文件描述符进行相应的读写操作。
常见的IO多路转接方式包括:
-
select:这是最早的多路转接方法之一,它通过维护三个集合(读集合、写集合和异常集合)来跟踪文件描述符的状态。select函数会阻塞直到至少有一个文件描述符就绪。select的一个缺点是它对文件描述符的数量有限制,并且每次调用都需要重新传入所有文件描述符的状态。
-
poll:poll是对select的改进,它使用一个pollfd结构体数组来代替select的三个集合,从而克服了select的一些限制。poll没有文件描述符数量的限制,并且可以更高效地处理大量文件描述符。
-
epoll:epoll是Linux内核中的一种高效的多路转接机制,它使用基于事件机制。epoll通过维护一个事件表来跟踪文件描述符的状态,并且能够高效地处理大量并发连接。epoll支持两种模式:LT(Level Triggered)和ET(Edge Triggered),分别对应于水平触发和边缘触发。
IO多路转接在服务器端的应用中尤为重要,因为它允许服务器同时处理多个客户端连接,而不需要为每个连接创建一个单独的线程或进程。这大大减少了系统资源的消耗,并提高了程序的响应能力和吞吐量。
IO多路复用适合的场景
- 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
- 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
- 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
- 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
为什么单线程或者单进程下是用select或者epoll就能实现多个客户端的并发
在单线程或单进程环境下,使用select或epoll等IO多路复用技术能够实现多个客户端的并发处理,主要是因为这些技术允许单个线程或进程同时监控多个文件描述符(通常是网络套接字),并在这些文件描述符就绪时(例如有数据可读、可写或发生错误)通知程序。这样,程序就可以在单个线程或进程中顺序处理所有就绪的文件描述符,而不需要为每个客户端连接创建单独的线程或进程。
select和epoll效率
-
文件描述符的数量:select有一个硬限制,通常为1024个文件描述符,而epoll没有这个限制,它可以处理成千上万个文件描述符。
-
性能随文件描述符数量的变化:select的性能随着文件描述符数量的增加而显著下降,因为它需要线性扫描所有文件描述符来检查哪些已经就绪。epoll则不会随着文件描述符数量的增加而性能下降,因为它使用基于事件的通知机制,只处理那些已经就绪的文件描述符。
-
系统调用开销:select每次调用都需要将文件描述符集合从用户空间复制到内核空间,而epoll在初始化时创建了一个事件表,后续只需要更新这个表,减少了系统调用的开销。
-
事件通知机制:epoll提供了两种事件通知模式:水平触发(LT)和边缘触发(ET)。边缘触发模式只在事件发生时通知一次,而水平触发模式会持续通知直到事件被处理。边缘触发模式通常更高效,因为它减少了epoll_wait的调用次数。
-
可扩展性:epoll在处理大量并发连接时表现出更好的可扩展性,因为它不需要像select那样重复检查所有文件描述符。
综上所述,epoll在大多数情况下比select更高效,特别是在处理大量并发连接时。epoll的设计允许它以更低的资源消耗和更高的性能来处理更多的文件描述符。因此,对于需要处理大量并发连接的应用程序,epoll通常是更好的选择。
零拷贝技术
零拷贝技术是一种优化数据传输的技术,旨在减少或消除在数据传输过程中数据在用户空间和内核空间之间不必要的拷贝操作。在传统的数据传输过程中,数据通常需要从用户空间拷贝到内核空间,然后再从内核空间拷贝到用户空间,或者在网络传输中从内核空间拷贝到用户空间,再从用户空间拷贝到内核空间。这些拷贝操作不仅增加了CPU的负担,还可能导致性能瓶颈。
零拷贝技术通过以下几种方式实现:
-
减少数据拷贝次数:通过使用特定的系统调用,如Linux中的sendfile系统调用,可以直接在内核空间中传输数据,从而减少数据在用户空间和内核空间之间的拷贝次数。
-
使用内存映射文件:通过内存映射文件(Memory-mapped files),可以将文件直接映射到进程的地址空间,这样数据就可以直接在用户空间和内核空间之间传输,而不需要额外的拷贝操作。
-
使用DMA(直接内存访问):在某些情况下,可以使用DMA技术,让硬件直接在源地址和目标地址之间传输数据,而不需要CPU的介入。
-
使用特定的网络协议:例如,在某些网络协议中,数据可以直接在内核空间中处理和传输,从而避免了用户空间和内核空间之间的拷贝。
零拷贝技术的优势在于它可以显著减少CPU的负载,提高数据传输的效率,尤其是在处理大量数据传输时。然而,零拷贝技术也有其局限性,例如,它可能不适用于所有类型的数据传输,且在某些情况下,可能需要特定的硬件或软件支持。
服务器端维护100万个TCP长连接可能会遇到以下问题:
-
内存消耗:每个TCP连接都需要在服务器上分配内存来存储状态信息,如连接的上下文、缓冲区等。大量连接可能会导致内存消耗急剧增加,甚至耗尽服务器的内存资源。
-
CPU压力:处理和维护大量的TCP连接需要消耗CPU资源。例如,每个连接的建立、维护和关闭都可能涉及CPU密集型的操作,如加密、解密、序列化和反序列化等。
-
网络带宽:尽管TCP连接是长连接,但它们仍然需要网络带宽来传输数据。如果所有连接同时发送数据,可能会耗尽服务器的网络带宽,导致网络拥塞。
-
文件描述符限制:操作系统对进程可以打开的文件描述符数量有限制。在Linux系统中,可以通过
ulimit
命令查看和设置这个限制。如果服务器尝试打开超过限制数量的文件描述符,将会失败。 -
连接管理:管理大量的连接需要有效的连接管理策略。例如,如何高效地分配和回收连接,如何处理连接的异常情况,如何监控连接的健康状态等。
-
状态同步:在分布式系统中,如果多个服务器实例需要共享连接状态,那么如何同步这些状态将是一个挑战。
-
安全风险:大量的连接可能会增加安全风险,如DDoS攻击、恶意连接等。服务器需要有效的安全策略来保护自己不受这些威胁。
-
性能瓶颈:即使服务器硬件足够强大,也可能存在性能瓶颈,如磁盘I/O、数据库访问等,这些瓶颈可能会限制服务器的整体性能。
为了应对这些问题,服务器端可以采取以下措施:
- 使用高效的内存管理策略,如对象池、内存缓存等。
- 优化CPU使用,减少不必要的计算和上下文切换。
- 使用负载均衡技术,分散网络流量。
- 调整操作系统的文件描述符限制,确保服务器可以打开足够的连接。
- 实施有效的连接管理策略,如连接池、连接复用等。
- 使用分布式系统架构,分散连接和状态管理。
- 加强安全措施,如防火墙、入侵检测系统等。
- 优化性能瓶颈,如使用更快的存储、缓存数据库查询结果等。
通过这些措施,服务器可以更好地管理和维护大量的TCP长连接,提高系统的稳定性和性能。
协程
主要是面对高并发问题
协程(Coroutine)是一种轻量级的线程,它是在单个线程中通过暂停和恢复执行来模拟多线程的技术。协程的核心思想是利用程序的控制流,而不是操作系统资源,来实现类似多任务的效果。
协程的主要特点包括:
- 轻量级:协程是在单个线程中运行的,不涉及线程切换的开销,因此创建和销毁协程的开销非常小,适合处理大量短生命周期的任务。
- 暂停和恢复:协程在执行过程中可以被暂停,然后在需要时恢复执行。这通常通过yield关键字(如Python的yield)或者类似机制来实现,当协程遇到yield时,会暂停并返回控制权给调用者。
- 非阻塞I/O:当协程执行到I/O操作(如网络请求或文件读写)时,它会自动切换到其他任务,不会阻塞整个线程,从而提高程序的并发性能。
- 通信机制:协程之间通常通过共享数据或者通信机制(如事件、管道、队列等)来交换数据,而不是像进程间通信那样需要复杂的接口。
- 易于理解和调试:由于协程在同一个线程中运行,所以它们之间的交互和调试相对简单,不像多线程那样容易出现线程安全问题。
异步IO(AIO)
同样是面对高并发问题
异步IO(Asynchronous I/O)是一种I/O操作方式,在这种方式中,I/O操作不会阻塞调用它的线程或进程。当异步I/O操作被请求时,调用者会立即返回,而I/O操作在后台进行。一旦I/O操作完成,系统会通过某种机制(如回调函数、事件通知等)通知调用者。
- 事件循环:异步编程通常需要一个事件循环来监听和响应I/O事件。
- 回调函数:当异步I/O操作完成时,会调用一个回调函数来处理结果。
- Future/Promise:在异步编程中,通常使用Future或Promise对象来表示尚未完成的结果。
异步IO的关键特性包括:
- 非阻塞性:调用者不会因为等待I/O操作完成而被阻塞。
- 并发性:调用者可以在等待I/O操作完成的同时执行其他任务。
- 事件驱动:I/O操作通常由事件触发,而不是由程序主动轮询。
异步IO的优势在于可以提高程序的响应性和吞吐量,特别是在I/O密集型应用中。然而,异步IO编程模型通常比同步I/O编程模型更复杂,需要开发者有更深入的理解和更多的编程技巧。
异步编程
异步编程是一种编程范式,它利用异步IO来提高程序的并发性能。在异步编程中,程序可以同时执行多个任务,而不会因为等待某个I/O操作完成而阻塞。异步编程通常需要使用事件循环来监听和响应I/O事件,并使用回调函数或Future/Promise对象来处理异步操作的结果。
事件机制
事件机制通常被认为是一种设计模式,具体来说,它实现的是观察者模式(Observer Pattern)。观察者模式是一种行为设计模式,它定义了对象之间的一对多依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。
事件队列:用于存储待处理的事件。
事件循环:事件循环会持续监听和响应I/O事件,并检查事件队列,直到没有更多的事件需要处理。
事件处理器:负责处理事件队列中的事件。
事件监听:允许特定的对象或代码块注册为事件的监听者,当事件发生时,通知监听者。事件处理结束后,通过回调函数通知监听者。
异步处理:事件处理通常是非阻塞的,这意味着事件处理器可以继续执行其他任务,直到事件处理完成。
线程池:为了提高效率,应用程序可能会使用线程池来处理IO操作,这样可以在等待IO操作完成时执行其他任务。
例如,当服务器接收到一个HTTP请求时,事件循环会将这个事件添加到事件队列中,然后继续执行其他任务。当请求处理完成时,事件循环会调用相应的回调函数进行处理,从而实现了异步处理和高效响应。