进程、线程、内存和IO模型的概念详解
- 1 进程与线程
- 1.1 进程
- 1.1.1 进程分类
- 1.1.2 进程的状态和转换
- 1.1.3 僵尸进程和孤儿进程的区别
- 1.1.4 进程之间的通信
- 1.1.5 用户态和内核态
- 1.1.6 用户空间和内核空间
- 1.2 线程
- 1.2.1 线程的状态和转换
- 1.2.2 进程与线程的区别
- 1.3 多进程和多线程
- 1.3.1 并行和并发
- 1.3.2 python中的GIL锁
- 1.3.3 CPU密集型和IO密集型
- 2 内存管理
- 2.1 物理内存和虚拟内存
- 2.2 页高速缓存与页写回机制
- 2.3 Swap Space
- 2.4 页面缺失和页面调度
- 3 I/O模型
- 3.1 IO的分类
- 3.1.1 磁盘IO和网络IO
- 3.1.2 同步IO和异步IO
- 3.1.3 阻塞IO和非阻塞IO
- 3.1.4 联系
- 3.2 同步IO模型
- 3.2.1 阻塞IO模型
- 3.2.2 非阻塞IO模型
- 3.2.3 多路复用IO模型
- 3.2.3.1 select
- 3.2.3.2 poll
- 3.2.3.3 epoll
- 3.2.4 信息驱动IO模型
- 3.3 异步IO模型
- 3.4 IO模型总结
1 进程与线程
1.1 进程
进程:进程是具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。可以理解为是程序的实际执行实例,当程序在计算机上运行时,操作系统会为它创建一个进程,分配资源(如内存、CPU时间、文件描述符等),并在计算机上执行程序的指令。
1.1.1 进程分类
操作系统分类:
- 协作式多任务:早期 windows 系统使用,即一个任务得到了 CPU 时间,除非它自己放弃使用CPU,否则将完全霸占 CPU ,所以任务之间需要协作——使用一段时间的 CPU ,主动放弃使用。
- 抢占式多任务:Linux内核,CPU的总控制权在操作系统手中,操作系统会轮流询问每一个任务是否需要使用 CPU ,需要使用的话就让它用,不过在一定时间后,操作系统会剥夺当前任务的 CPU使用权,把它排在询问队列的最后,再去询问下一个任务。
进程类型:
- 守护进程: daemon,在系统引导过程中启动的进程,和终端无关进程(后台执行,ps aux中TTY为?的)
- 前台进程:跟终端相关,通过终端启动的进程(占用终端资源)
- 注意:两者可相互转化
按进程资源使用的分类:
- CPU-Bound:CPU 密集型,非交互
- IO-Bound:IO 密集型,交互
1.1.2 进程的状态和转换
基本状态
- 创建状态:进程在创建时会首先完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行。在这个状态下,进程只是被初始化,但尚未分配 CPU 资源。
- 就绪状态:进程已准备好,已分配到所需资源,只要分配到CPU就能够立即运行,其实就是进入了任务队列,在就绪状态下,进程等待操作系统的调度以获得 CPU 时间片来执行。
- 运行状态:当操作系统调度进程并将其分配到 CPU 时,进程进入运行状态。在运行状态下,进程将会执行其指令和代码。
- 阻塞状态:如果一个进程在执行过程中需要等待某些事件(I/O操作完成,申请缓存区失败,等待资源释放)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用,在阻塞状态下,进程不会消耗CPU时间,直到等待的事件发生。
- 终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行,在终止状态下,进程的所有资源被释放。
其他状态
- 睡眠态:分为两种,可中断:interruptable,不可中断:uninterruptable
- 可中断睡眠态的进程在睡眠状态下等待特定事件发生,即使特定事件没有产生,也可以通过其它手段唤醒该进程,比如,发信号,释放某些资源等。
- 不可中断睡眠态的进程在也是在睡眠状态下等待特定事件发生,但其只能被特定事件唤醒,发信号或其它方法都无法唤醒该进程。
- 停止态:stopped,暂停于内存,但不会被调度,除非手动启动
- 僵死态:zombie,僵尸态。父进程结束前,子进程关闭,杀死父进程可以关闭僵死态的子进程
状态转换
-
运行——>就绪
- 主要是进程占用CPU的时间过长,而系统分配给该进程占用CPU的时间是有限的;
- 在采用抢先式优先级调度算法的系统中,当有更高优先级的进程要运行时,该进程就被迫让出CPU,该进程便由执行状态转变为就绪状态
-
就绪——>运行:运行的进程的时间片用完,调度就转到就绪队列中选择合适的进程分配CPU
-
运行——>阻塞:正在执行的进程因发生某等待事件而无法执行,则进程由执行状态变为阻塞状态,如发生了I/O请求
-
阻塞——>就绪:进程所等待的事件已经结束,就进入就绪队列
以下两种状态是不可能发生的:
-
阻塞——>运行:即使给阻塞进程分配CPU,也无法执行,操作系统在进行调度时不会从阻塞队列进行挑选,而是从就绪队列中选取
-
就绪——>阻塞:就绪态根本就没有执行,谈不上进入阻塞态
1.1.3 僵尸进程和孤儿进程的区别
僵尸进程是子进程已经结束,但父进程可能处于停止状态,未回收其资源导致的状态,所以这个子进程被称为"僵尸"。僵尸进程不占用系统资源,但在系统中存在时,会占用进程号(PID)等资源,影响系统性能。僵尸进程通常发生在子进程已经结束运行
孤儿进程是指子进程的父进程自己先提前退出或异常终止,导致子进程失去了父进程。这时候,孤儿进程会被操作系统的init进程(通常具有PID 1)接管。init进程会成为孤儿进程的新父进程,负责收养和管理它们。以避免它们变成僵尸进程。
1.1.4 进程之间的通信
-
管道(pipe):单向传输,只能用于父子进程(有亲缘关系)之间的通信,随进程的创建而建立,随进程的结束而销毁。
-
命名管道(FIFO):允许无亲缘关系进程间的通信,不适合进程间频繁地交换数据。
-
消息队列(MessageQueue):
- 解决了FIFO的缺点。消息队列实际上是链表,链表的每个节点都是一条消息。每一条消息都有自己的消息类型,用整数来表示,而且必须大于 0,但不适合比较大的数据的传输。
- 读取和写入的过程,都会发生用户态与内核态之间的消息拷贝过程。
-
共享存储(SharedMemory):
- 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
- 每个进程都会维护一个从内存地址到虚拟内存页面之间的映射关系。尽管每个进程都有自己的内存地址,不同的进程可以同时将同一个内存页面映射到自己的地址空间中,从而达到共享内存的目的。
- 比如a把数据全都丢到共享内存里面,b去共享内存拿数据,而且b可以按需选择拿哪些数据。
-
信号(sinal): 用于通知接收进程某个事件已经发生。
-
信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
-
套接字(Socket):
- 套接字文件,双工通信,网络进程间通信,比如A与B要通信,需要A将数据发生给套接字文件,B从套接字文件接收,或者B发A收。
- 它既解决了管道只能在相关进程间单向通信的问题,又解决了网络上不同主机之间无法通信的问题。
1.1.5 用户态和内核态
用户态:当程序运行在用户态时,不能直接使用系统资源,也不能改变 CPU 的工作状态,并且只能访问这个用户程序自己的存储空间,所以用户态不能直接创建进程。
内核态:系统中既有操作系统的程序,也有普通用户程序。为了安全性和稳定性,操作系统的程序不能随便访问,这就是内核态。即需要执行操作系统的程序就必须转换到内核态才能执行,它可以访问计算机的所有资源。
为什么要区分用户态和内核态?
在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。
所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。
如此设计的本质意义是进行权限保护。 限定用户的程序不能乱搞操作系统,如果人人都可以任意读写任意地址空间软件管理便会乱套。
1.1.6 用户空间和内核空间
用户空间:指的是操作系统为用户程序分配的内存区域。每个运行在用户空间的应用程序都有其独立的地址空间,这有助于保护系统免受恶意或错误代码的影响。用户空间的应用程序不能直接访问硬件资源或关键的操作系统数据结构。当程序以用户态运行时,它只能访问属于自己的那部分用户空间内存,而不能直接访问内核空间或其他进程的空间。
内核空间:是操作系统内核使用的内存区域。这里存放了内核代码、设备驱动程序、以及所有需要直接访问硬件资源的数据结构等。内核空间允许直接操作硬件资源和管理整个系统的状态。
1.2 线程
在早期的操作系统中并没有线程的概念,后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
线程是程序执行的最小单位,是进程内的一个执行单元,一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间。
1.2.1 线程的状态和转换
状态 | 含义 |
---|---|
就绪(Ready) | 线程能够运行,但在等待被调度。可能线程刚刚创建启动,或刚刚从阻塞中恢 复,或者被其他线程抢占 |
运行 (Running) | 线程正在运行 |
阻塞 (Blocked) | 线程等待外部事件发生而无法运行,如I/O等待操作 |
终止 (Terminated) | 线程完成,或退出,或被取消 |
1.2.2 进程与线程的区别
在概念上:进程是操作系统分配资源的最小单位;线程是程序执行的最小单位;一个进程由一个或多个线程组成。
在资源分配上:进程是独立的资源拥有单位,每个进程都有独立的地址空间和系统资源,某进程内的线程在其它进程不可见;而线程是共享所属进程的资源,多个线程共享同一个地址空间和系统资源,包括内存、文件和其他系统资源。
在创建和销毁上:进程的开销通常比线程大,因为进程需要为其分配独立的内存空间和资源;而线程的创建和销毁开销相对较小,因为它们共享了进程的资源。
在通信上,也是在并发性上:线程之间的通信和同步相对容易,并发性较高,因为它们共享同一地址空间;而进程之间的通信和同步则需要额外的机制,如管道、消息队列等,并发性较低,因为它们通常是相互独立的。
1.3 多进程和多线程
多进程:多进程是指在一个应用程序中同时运行多个进程,每个进程都有独立的地址空间和资源,可以较充分地利用多处理器。
多线程:顾名思义,多个线程,一个进程中如果有多个线程运行,就是多线程,实现一种并发。
1.3.1 并行和并发
并行:指的是多个任务真正地在同一时刻执行,通常需要多个处理器核心来实现。
并发:指的是多个任务在同一时间段内交替执行,但并不一定是同时执行。操作系统通过时间片轮转等调度机制使得多个任务看起来像是同时进行的。
单核CPU:无论是多进程还是多线程,都是通过时间片轮转的方式实现并发,不能实现真正的并行执行。
多核CPU:可以实现真正的并行执行,即多个进程或线程可以在不同的核心上同时执行,从而提高系统的性能和效率。
1.3.2 python中的GIL锁
GIL 保证CPython进程中,只有一个线程执行字节码。甚至是在多核CPU的情况下,也只允许同时只能有一个CPU核心上运行该进程的一个线程。所以python中的多线程是假并行。
1.3.3 CPU密集型和IO密集型
CPU密集型任务:
- 指那些需要大量CPU计算时间的任务,例如视频编码、图像处理、科学计算等。这类任务主要消耗的是CPU资源。
- 适用模型:多进程
- 因为其主要依赖于CPU计算能力,用多进程可以充分利用多核CPU的优势,实现真正的并行计算;而使用多线程可能不会带来显著的性能提升,因为在Python中有GIL(全局解释器锁)。
IO密集型任务:
- 指那些花费大量时间等待外部资源响应的任务,例如读写文件、网络请求、数据库查询等。这类任务大部分时间都在等待I/O操作完成,而非占用CPU资源。
- 适用模型:多线程或多协程
- 当一个线程执行 I/O 操作(如网络请求、文件读写等)时,CPython 会检测到这是一个 I/O 操作,并允许该线程暂时释放 GIL。这使得其他线程有机会获取 GIL 并执行它们的 Python 字节码。一旦 I/O 操作完成,线程重新获取 GIL 并继续执行后续代码。这种机制对于 I/O 密集型任务非常重要,因为它允许在等待 I/O 的时候让出 CPU 给其他线程使用,从而提高程序的整体效率和响应速度。
协程是一种用户级别的轻量级线程,由程序员显式控制其执行流程。协程可以在特定点暂停(yield),然后在之后恢复执行,而且无需内核态与用户态之间的转换,这使得它非常适合于异步编程模型。例如可以使用Python中的asyncio模块来实现。
2 内存管理
2.1 物理内存和虚拟内存
物理内存是计算机系统中实际存在的硬件内存,通常是 RAM(随机访问存储器)的形式
虚拟内存是为了满足物理内存不足而提出的策略,通过将内存地址空间划分为固定大小的块(通常称为页或页帧),并在需要时将这些页面从磁盘交换到物理内存中来工作。这使得每个进程都有自己的独立地址空间,而不需要直接管理物理内存的分配。
当运行某个大程序、大游戏,需要的内存超过空闲内存但小于物理内存总量时,会暂时把内存里这些数据放到磁盘上的虚拟内存里,空出物理内存运行游戏。等退出游戏后,又会把虚拟内存里的东西读出来,放回物理内存。所以,虚拟内存并不是用来虚拟物理内存的,而是暂存数据的。所以虚拟内存不是代替物理内存来运行程序的。
2.2 页高速缓存与页写回机制
在当今的计算机系统中,处理器的运行速度是非常快的,但 RAM 和磁盘并没有质的飞跃(尤其是磁盘读写速度),这就导致了系统整体性能并没有因为处理器速度的提升而提升。于是就使用到了缓存技术(其实就是内存缓存的技术),通过缓存机制解决了处理器和磁盘直接速度的不平衡。
页高速缓存通常以页面的单位来存储数据,因此被称为"页"高速缓存。页高速缓存是操作系统在物理内存中维护的一个缓存,用于存储磁盘上的文件数据的副本。Linux 系统中当一个文件的数据(内容)被读取时,操作系统将数据从磁盘读取到页高速缓存中,以便后续的读取操作可以直接去内存中读取数据,更快速地访问数据。
因此页高速缓存提高了文件的读取性能,因为它允许频繁访问的数据保留在快速的内存中,而不是每次都从慢速的磁盘中读取。
页写回机制是一种优化技术,用于减少文件写入操作对性能的影响,提高磁盘I/O的性能。当文件数据被修改并需要写回磁盘时,操作系统通常不会立即将数据写回磁盘,而是将数据标记为"脏",并将其保留在页高速缓存中。操作系统通过一种策略,例如延迟写回或按需写回,决定何时将脏数据写回磁盘。这允许操作系统将多个写操作合并,以减少磁盘写入的次数,提高性能。
页写回机制可以防止频繁的磁盘写入操作对系统性能造成明显的影响,因为它允许系统在更高效的时间进行磁盘写入,而不是在每个写操作之后立即进行。
2.3 Swap Space
Swap Space(交换空间)是 Linux 中虚拟内存的一个实现方式,除了填补因物理内存不足的空缺外,还将会在适当的时候将物理内存中不经常读写的数据块自动交换到 Swap 交换空间(这个交换的操作是由 Linux 内核来执行的),从而侧面将经常读写的数据保留在了物理内存。
说白了就是,它为什么叫交换空间,就是要实现数据的交换(将不常用数据交换到作为逻辑内存的磁盘空间,而保留常用数据在真正的物理内存空间中)。这个交换的策略由 Linux 系统内核定时执行,目的就是为了保持尽可能多的空闲物理内存。
那什么叫做在适当的时候会进行数据交换,我们所说的适当时间其实是 Linux 内核根据“最近最经常使用”算法(LRU 算法),定时地将一些不经常使用的页面文件(其实就是文件数据,因为内存是以页面存储数据的)交换到 Swap。
有时候会发现:Linux 物理内存还有很多,而 Swap 的数据占用却很大,这是什么原因呢?其实这是正常现象。如果一个内存占用很大的进程正在运行,必然就会耗费大量的内存资源,此时 Linux 内核就会将一些不常用的页面文件交换到 Swap Space 中。当该进程终止后,Linux 就会释放该进程占用的大量内存资源,而此时被交换出去的页面文件数据并不会自动又交换到物理内存中来(除非有这个必要),那此时看到的就是物理内存空间空闲,Swap Space 占用较大的现象了。
2.4 页面缺失和页面调度
程序要读虚拟内存中的某个页面数据时,而恰好这个页面数据位于 Swap Space 中。那此时的流程就是:交换空间(Swap Space)中的数据在被读取时通常会首先被交换到物理内存,然后才能被程序访问。
页面缺失(Page Fault):当程序尝试访问一个在物理内存中不存在的内存页时,会触发一个页面缺失。这可能是因为该页已经被交换到了交换空间中,或者是因为程序首次访问该页。在任何情况下,操作系统会注意到页面缺失。
页面调度(Page Scheduling):操作系统负责页面调度,决定哪些页面从交换空间加载到物理内存中以满足程序的需求。通常,操作系统会使用一种页面替换算法(例如LRU - 最近最少使用)来选择哪些页从交换空间中加载,以便最大限度地减少性能开销。一旦数据加载到物理内存中,操作系统会更新进程的页表,以指示这些页面现在位于物理内存中,程序可以访问它们。页表是一个数据结构,用于映射虚拟地址到物理地址。
3 I/O模型
3.1 IO的分类
3.1.1 磁盘IO和网络IO
磁盘I/O处理过程
- 进程向内核发起系统调用,请求磁盘上的某个资源比如是html 文件或者图片
- 然后内核通过相应的驱动程序将目标文件加载到内核的内存空间
- 加载完成之后把数据从内核内存再复制给进程内存,如果是比较大的数据也需要等待时间。
网络I/O 处理过程
- 获取请求数据,客户端与服务器建立连接发出请求,服务器接受请求
- 构建响应,当服务器接收完请求,并在用户空间处理客户端的请求,直到构建响应完成
- 返回数据,服务器将已构建好的响应再通过内核空间的网络 I/O 发还给客户端
不论磁盘和网络I/O,每次I/O,都要经由两个阶段:
- 第一步:将数据从文件先加载至内核内存空间(缓冲区),等待数据准备完成,时间较长
- 第二步:将数据从内核缓冲区复制到用户空间的进程的内存中,时间较短
3.1.2 同步IO和异步IO
函数或方法被调用的时候,调用者是否得到最终结果的。
-
直接得到最终结果结果的,就是同步调用;
-
不直接得到最终结果的,就是异步调用。
3.1.3 阻塞IO和非阻塞IO
函数或方法调用的时候,是否立刻返回。
-
立即返回就是非阻塞调用;
-
不立即返回就是阻塞调用。
阻塞I/O (Blocking I/O)
- 当一个进程发起一个I/O请求时,它会暂停执行,直到I/O操作完成。
- 这是最常见的I/O模型,例如当你打开一个文件进行读取时。
非阻塞I/O (Non-blocking I/O)
- 在非阻塞模式下,当进程尝试发起I/O操作时,如果操作不能立即完成,则会立即返回一个错误或特定值,而不是等待。
- 进程需要不断地轮询检查I/O操作的状态,这称为“忙等”(busy-waiting)。
3.1.4 联系
同步阻塞,我啥事不干,就等你打饭打给我。打到饭是结果,而且我啥事不干一直等,同步加阻塞。
同步非阻塞,我等着你打饭给我,饭没好,我不等,但是我无事可做,反复看饭好了没有。打饭是结果,但是我不一直等。
异步阻塞,我要打饭,你说等叫号,并没有返回饭给我,我啥事不干,就干等着饭好了你叫我。例如,取了号什么不干就等叫自己的号。
异步非阻塞,我要打饭,你给我号,你说等叫号,并没有返回饭给我,我去看电视、玩手机,饭打好了叫我。
3.2 同步IO模型
3.2.1 阻塞IO模型
进程等待(阻塞),直到读写完成。(全程等待)
read() write() recv() send() accept() connect()
等…都是阻塞IO
当用户调用了read()
,kernel 就开始了IO的第一个阶段:准备数据。对于网络IO来说,很多时候数据在一开始还没有达到(比如,还没有收到一个完整的数据包),这个时候kernel 就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel 一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来
所以,阻塞IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被阻塞了。这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够
3.2.2 非阻塞IO模型
进程调用recvfrom操作,如果IO设备没有准备好,立即返回ERROR,进程不阻塞。用户可以再次发起系统调用(可以轮询),如果内核已经准备好,就阻塞,然后复制数据到用户空间。可防止进程阻塞在IO操作上,需要轮询。
第一阶段数据没有准备好,可以先忙别的,等会再来看看。检查数据是否准备好了的过程是非阻塞的。
第二阶段是阻塞的,即内核空间和用户空间之间复制数据是阻塞的。
淘米、蒸饭我不阻塞等,反复来询问,一直没有拿到饭。盛饭过程我等着你装好饭,但是要等到盛好饭才算完事,这是同步的,结果就是盛好饭。
所以,在非阻塞式 IO 中,用户进程其实是需要不断的主动询问(轮询) kernel 数据准备好了没有。 轮询的时间不好把握,这里是要猜多久之后数据才能到。等待时间设的太长,程序响应延迟就过大;设的太短,就会造成过于频繁的重试,干耗CPU而已,是比较浪费CPU的方式。
3.2.3 多路复用IO模型
以前一个应用程序需要对多个网络连接进行处理,传统的方法是使用多线程或多进程模型,为每个连接创建一个线程或进程进行处理。这种方法存在一些问题,例如线程或进程的创建和销毁需要消耗大量的系统资源,且容易导致线程或进程的数量过多,进而导致系统崩溃或运行缓慢。所以便出现了IO多路复用。
多路复用思想是将操作的所有文件描述符保存在一张文件描述符表中,然后将文件描述符表交给内核,让内核检测当前是否有准备就绪的文件描述符(例如有数据可读或可写),如果有则通知应用程序,操作就绪的文件描述符。这种方式可以让一个进程处理多个并发的IO操作,而不需要为每个IO操作创建一个独立的线程或进程。
以select为例,当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
相比非阻塞IO
- 在非阻塞IO模型中,如果一个文件描述符还没有准备好进行读写操作,应用程序需要不断地轮询该描述符的状态(即不断调用
read()
或write()
)。这会导致较高的CPU使用率,因为即使在没有数据的情况下,程序也需要不断地执行这些系统调用。 - 相比之下,使用IO多路复用(如
select()
,poll()
, 或epoll()
),程序只需要在一个调用中指定多个文件描述符,然后等待至少有一个描述符准备就绪。这减少了CPU的轮询开销。
3.2.3.1 select
- 这是最早的多路复用函数之一,它可以监控多个文件描述符,并且在任何一个描述符上有事件发生时返回。但是它的最大限制是文件描述符的数量受限于系统定义的最大值。
- 通过将已连接的Socket放入一个文件描述符集合中,然后调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生。然而,select存在一些缺点,如每次调用select都需要将fd集合从用户态拷贝到内核态,且仅仅返回可读文件描述符的个数,具体哪个可读还需要用户自己以轮询的方式线性扫描,效率不高。
3.2.3.2 poll
- 与select()类似,但没有文件描述符数量的限制。poll()使用链表来跟踪描述符的状态变化。
- 修复了select的一些问题,不再使用BitsMap来存储所关注的文件描述符,改用动态数组,以链表形式来组织,突破了select的文件描述符个数限制。然而,poll和select并没有太大的本质区别,都是使用线性结构存储进程关注的Socket集合,因此都需要遍历文件描述符集合来找到可读或可写的Socket。
3.2.3.3 epoll
- 这是Linux系统提供的一个更为高效的多路复用接口,它可以高效地处理大量的文件描述符。epoll使用事件驱动的方式,只有当事件发生时才会通知应用程序。
- 它针对select和poll的缺点进行了优化。epoll在内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分。内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步I0事件唤醒。这使得epoll在处理大量并发连接时具有更高的性能和效率。
3.2.4 信息驱动IO模型
进程在IO访问时,先通过sigaction系统调用,提交一个信号处理函数,立即返回,进程不阻塞。当内核准备好数据后,产生一个SIGIO信号并投递给信号处理函数(由内核通知,发送信号)。可以在此函数中调用recvfrom函数操作数据从内核空间复制到用户空间,这段过程进程阻塞。
此模型的优势在于等待数据报到达期间进程不被阻塞。用户主程序可以继续执行,只要等待来自信号处理函数的通知。但是信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。
3.3 异步IO模型
进程发起异步IO请求,立即返回。内核完成IO的两个阶段,内核给进程发一个信号。异步I/O允许进程发起一个I/O操作后继续执行其他任务,当I/O操作完成时,操作系统会通知进程。所以IO两个阶段,进程都是非阻塞的。
异步I/O 与 信号驱动I/O最大区别在于,信号驱动是内核通知用户进程何时开始一个I/O操作,而异步I/O是由内核通知用户进程I/O操作何时完成。
但是Linux的 aio 的系统调用,内核是从版本2.6开始支持,只用在磁盘IO读写操作,不用于网络IO。