Linux快问快答之———IO!被拷打了!

同学,知道什么是IO吗?

InputOutput,输入输出。 IO就是从磁盘(IO设备或者网络)读到内存里,从内存写到磁盘(IO设备或者网络)里。到底什么是IO?一文解析IO基础概念 - 知乎 (zhihu.com)

那你知道Linux是怎么读写数据的吗?

首先在操作系统中,内核态(Kernel Mode)是运行操作系统程序、操作硬件的状态,具有最高权限;而用户态(User Mode)是运行用户程序的状态,其权限受到限制。

当用户程序需要操作系统帮助完成某些它没有权力和能力完成的工作时,就会通过系统调用陷入到内核态,由操作系统来完成相应的工作。一文搞懂操作系统的用户态与内核态

而IO操作比较底层,用户程序不能直接进行IO操作,而是调用操作系统内核提供的接口,来完成IO操作。

我们知道在内存中,分为内核空间和用户空间,也就对应内核态和用户态,内核空间里就是操作系统内核代码运行使用的内存空间,用户空间就是用户程序代码运行使用的内存空间。

当用户程序需要进行IO操作时,会进行系统调用,从用户态陷入内核态,让内核来完成IO操作,以读数据为例。陷入内核态后,内核根据用户指令,把数据从磁盘(IO设备或网络)通过DMA(Direct Memory Access,直接内存访问)传输到内核空间的缓冲区,再拷贝到用户程序的进程缓冲区,供用户程序使用,之后返回用户态,用户进程开始继续执行。

为什么设置那么多的缓冲区,直接读写不是更简单吗?

 缓冲区在计算机系统中的核心目的主要有以下几点:

  1. 解决速度差异:由于CPU和内存的速度远高于外部物理设备(如硬盘、网络设备等),通过设立缓冲区,可以缓解两者之间速度不匹配的问题,避免CPU等待外部设备完成I/O操作而造成空闲。

  2. 减少中断次数:外部设备直接读写涉及操作系统的中断处理,每次中断都需要保存和恢复进程上下文,这是一个耗时的过程。通过缓冲区,可以积累一批数据后才引发一次中断,从而减少中断次数,降低上下文切换带来的性能损失。

  3. 批量处理:操作系统通过监控内核缓冲区的状态,等到缓冲区数据达到一定量时再集中执行物理设备的I/O操作,这样有利于提高I/O操作的效率,尤其是针对顺序读写操作。

  4. 内核与用户空间的数据交换:当应用程序发起sys_readsys_write这样的系统调用时,实际上是数据在内核缓冲区与用户进程缓冲区之间进行转移。读操作时,数据是从内核缓冲区复制到用户进程缓冲区;写操作则是将用户进程缓冲区的数据复制到内核缓冲区,待内核合适时机再写入物理设备。

  5. 缓冲区架构:在Linux系统中,内核维护单一的内核缓冲区用于全局的I/O操作,而每个用户进程都有各自独立的用户缓冲区或进程缓冲区,这样可以确保不同进程的I/O操作互不干扰,同时保证数据的安全性。

总的来说,缓冲区的设计极大提升了系统整体的I/O性能,降低了系统开销,并使得应用程序在进行输入输出操作时无需详细了解底层硬件细节,只需关注与内核缓冲区的数据交换,从而简化编程模型,提高开发效率。

为什么会中断?操作系统怎么处理中断?

 IO操作时,当外部设备完成了一次数据传输,就会触发系统中断,CPU就会放下手头的工作,接着,CPU会跳转到预设好的中断服务程序(Interrupt Service Routine, ISR)或设备驱动程序。这个程序是专门用来处理中断请求的,它会根据中断类型识别中断源,并进行相应的处理。

对于I/O中断,ISR会负责检查中断的原因(如读操作完成、写操作完成等),并进行相应的数据传输操作,如将数据从设备控制器的缓冲区转移到内核缓冲区,或者从内核缓冲区转移到设备控制器的缓冲区以供设备进行下一步操作。

如果是输入中断,ISR可能负责将外部设备送来的数据从硬件缓冲区移动到内核空间的缓冲区;如果是输出中断,则确认数据已经成功从内核缓冲区转移到了外部设备。

操作系统的中断处理是计算机系统中非常重要的一部分,它指的是当CPU在执行当前任务时,因响应内外部发生的某些事件而暂时停止当前程序的执行,转而去执行一段专门处理该事件的程序,这个过程称为中断处理。

中断的发生原因主要包括以下几类:

  1. 外部中断(硬件中断):

    • I/O中断:当外部设备完成了一次数据传输,例如键盘输入、鼠标点击、硬盘读写完成等,会向CPU发出中断请求。
    • 定时中断(时钟中断):由系统时钟定期发出的中断,用于操作系统实现时间片轮转调度、定时任务等功能。
    • 其他外部设备的中断请求,如网络数据包到达、硬件错误等。
  2. 内部中断(异常或陷阱):

    • 程序异常:如除数为零、地址越界、运算溢出、非法指令、特权指令在非特权模式下执行等情况。
    • 系统调用:当用户程序主动请求操作系统服务(如读写文件、创建进程等)时,会通过软中断(软件中断指令)的方式进入内核态,这也是一种形式的中断。

中断处理的基本流程通常包括以下几个阶段:

  1. 中断检测:CPU检测到中断请求信号,判断中断源和优先级。
  2. 保存现场:CPU保存当前进程的上下文信息(如程序计数器、寄存器状态等)到相应的位置(如任务状态段TSS或者当前进程的内核栈)。
  3. 中断处理:CPU跳转到对应的中断服务例程(ISR,Interrupt Service Routine)进行处理。对于硬件中断,ISR可能直接处理I/O请求,对于系统调用,ISR会调用相应的系统调用处理程序。
  4. 中断结束:ISR执行结束后,CPU恢复之前被中断的进程的上下文,然后继续执行被中断的程序。
  5. 中断嵌套处理:操作系统需要支持中断的嵌套处理,即在处理一个中断的过程中,若有更高优先级的中断到来,应能暂停当前中断处理,转而处理新的中断。

通过中断处理机制,操作系统得以及时响应各种突发事件,实现设备驱动程序的执行、进程间的切换、错误处理以及系统调用功能,从而保证了计算机系统的并发性和可靠性。此外,中断也是实现多任务和实时操作系统的基石之一。

那你知道有哪些常见的IO模型?

 IO模型主要有几种区别:阻塞、非阻塞、多路复用、同步、异步、BIO、NIO、AIO 一文搞定 - 知乎 (zhihu.com)

同步/异步:同步与异步可以看成是发起IO请求的两种方式。同步IO是指用户空间(进程或者线程)是主动发起IO请求的一方,系统内核是被动接受方。异步IO则反过来,系统内核主动发起IO请求的一方,用户空间是被动接受方。

阻塞/非阻塞:阻塞是指用户进程(或者线程)一直在等待,而不能干别的事情;非阻塞是指用户进程 (或者线程)拿到内核返回的状态值就返回自己的空间,可以去干别的事情。

常见的IO模型有五种:

同步阻塞I/O(Blocking I/O):在这种模型下,应用程序发起I/O操作后,会一直阻塞,直到该操作完成。例如,当进程调用read()从网络或磁盘读取数据时,如果数据尚未准备好,进程会被挂起,直到数据准备好并完成数据拷贝到进程缓冲区后,read()函数才会返回。

 在Java中发起一个socket的sys_read读操作的系统调用,流程大致如下:

(1)从Java进行IO读后发起sys_read系统调用开始,用户线程(或者线程)就进入阻塞状态。

(2)当系统内核收到sys_read系统调用,就开始准备数据。一开始,数据可能还没有到达内核缓冲区(例如,还没有收到一个完整的socket数据包),这个时候内核就要等待。

(3)内核一直等到完整的数据到达,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到用户缓冲区中的字节数)。

(4)直到内核返回后,用户线程才会解除阻塞的状态,重新运行起来。

同步非阻塞I/O(Non-blocking I/O)

在非阻塞模式下,进程调用I/O操作后不会阻塞,而是立刻返回。若数据未准备好,函数会返回一个错误指示(如EAGAIN或EWOULDBLOCK)。进程需要不断地轮询检查数据是否已准备就绪,然后才能执行真正的读写操作。

发起一个非阻塞socket的sys_read读操作的系统调用,流程如下:

(1)在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。所以,为了读取到最终的数据,用户进程(或者线程)需要不断地发起IO系统调用。

(2)内核数据到达后,用户进程(或者线程)发起系统调用,用户进程(或者线程) 阻塞(大家一定要注意,此处用户进程的阻塞状态)。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区,然后内核返回结果(例如返回复制到的用户缓冲区的字节数)。

(3)用户进程(或者线程)在读数据时,没有数据会立即返回而不阻塞,用户空间需 要经过多次的尝试,才能保证最终真正读到数据,而后继续执行。 

 I/O多路复用(I/O Multiplexing)

此模型通过select()poll() 或者更高效的 epoll() 系统调用,允许单个进程监视多个文件描述符(通常是网络套接字),一旦某个文件描述符准备好进行I/O操作(如读或写),进程就会解除阻塞并进行相应的处理。虽然进程在等待数据准备期间仍处于阻塞状态,但它可以同时监听多个描述符,所以相比于单独处理每个描述符的阻塞I/O,I/O多路复用能够更好地利用CPU资源。

 发起一个多路复用IO的sys_read读操作的系统调用,流程如下:

(1)选择器注册。在这种模式中,首先,将需要sys_read操作的目标文件描述符(socket 连接),提前注册到Linux的select/epoll选择器中,在Java中所对应的选择器类是Selector类。 然后,才可以开启整个IO多路复用模型的轮询流程。

(2)就绪状态的轮询。通过选择器的查询方法,查询所有的提前注册过的目标文件描 述符(socket连接)的IO就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列 表。当任何一个注册过的socket中的数据准备好或者就绪了,就是内核缓冲区有数据了,内 核就将该socket加入到就绪的列表中,并且返回就绪事件。

(3)用户线程获得了就绪状态的列表后,根据其中的socket连接,发起sys_read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。

(4)复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

 信号驱动I/O(Signal-driven I/O,a.k.a. Asynchronous I/O via signals)

进程通过注册一个信号处理函数来处理I/O操作。当数据准备好时,内核会发送一个信号给进程,而不是进程去轮询或阻塞。不过,这种模型在现代编程实践中并不常用。

信号驱动IO的基本流程是:用户进程通过系统调用,向内核注册SIGIO信号的owner进程和以及进程内的回调函数。内核IO事件发生后(比如内核缓冲区数据就位)后,通知用户程序,用户进程通过sys_read系统调用,将数据复制到用户空间,然后执行业务逻辑。

发起一个异步IO的sys_read读操作的系统调用,流程如下:

(1)设置SIGIO信号的信号处理回调函数。

(2)设置该套接口的属主进程,使得套接字的IO事件发生时,系统能够将SIGIO信号 传递给属主进程,也就是当前进程。

(3)开启该套接口的信号驱动I/O机制,通常通过使用fcntl方法的F_SETFL操作命令, 使能(enable)套接字的 O_NONBLOCK非阻塞标志和O_ASYNC异步标志完成。

完成以上三步,用户进程就完成了事件回调处理函数的设置。当文件描述符上有事件发 生时,SIGIO 的信号处理函数将被触发,然后便可对目标文件描述符执行 I/O 操作。

异步I/O(Asynchronous I/O,a.k.a. AIO)

在异步I/O模型中,进程发起I/O请求后立即返回,不需等待数据准备或数据复制完成。内核在完成I/O操作后,通过回调函数或事件通知机制告知进程。这意味着在整个I/O操作的生命周期中,进程都不会被阻塞,可以执行其他任务。

AIO的基本流程是:用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后, 通知用户程序,用户执行后续的业务操作。

在异步IO模型中,在整个内核的数据处理过程中,包括内核将数据从网络物理设备(网 卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。

发起一个异步IO的sys_read读操作的系统调用,流程如下:

(1)当用户线程发起了sys_read系统调用(可以理解为注册一个回调函数),立刻就可以开始去做其他的事,用户线程不阻塞。

(2)内核就开始了IO的第一个阶段:准备数据。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区。

(3)内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调方法, 告诉用户线程,sys_read系统调用已经完成了,数据已经读入到了用户缓冲区。

(4)用户线程读取用户缓冲区的数据,完成后续的业务操作。

知道它们的特点和优缺点吗?

1.同步阻塞I/O (Blocking I/O):

阻塞IO的特点是:在内核进行IO执行的两个阶段,发起IO请求的用户进程(或者线程) 被阻塞了。

阻塞IO的优点是:应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起, 用户线程基本不会占用CPU资源。

阻塞IO的缺点是:一般情况下,会为每个连接配备一个独立的线程,一个线程维护一 个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用 场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。在高并 发应用场景中,阻塞IO模型是性能很低的,基本上是不可用的。

2.同步非阻塞I/O (Non-blocking I/O):

同步非阻塞IO的特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。

同步非阻塞IO的优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。 用户线程不会阻塞,实时性较好。

同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。 总体来说,在高并发应用场景中,同步非阻塞IO是性能很低的,也是基本不可用的, 一般Web服务器都不使用这种IO模型。在Java的实际开发中,也不会涉及这种IO模型。但是此模型还是有价值的,其作用在于,其他IO模型中可以使用非阻塞IO模型作为基础,以实现其高性能。

3.I/O多路复用 (I/O Multiplexing):

IO多路复用模型的特点:IO多路复用模型的IO涉及两种系统调用,一种是IO操作的系统调用,另一种是select/epoll就绪查询系统调用。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。

和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。 IO多路复用模型与同步非阻塞IO模型是有密切关系的,具体来说,注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。只是这一点对于用户程序而言,是无感知的。

IO多路复用模型的优点:一个选择器查询线程,可以同时处理成千上万的网络连接, 所以,用户程序不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。 这是一个线程维护一个连接的阻塞IO模式相比,使用多路IO复用模型的最大优势。 通过JDK的源码可以看出,Java语言的NIO(New IO)组件,在Linux系统上,是使用的 是select系统调用实现的。所以,Java语言的NIO(New IO)组件所使用的,就是IO多路复用模型。

IO多路复用模型的缺点:本质上,select/epoll系统调用是阻塞式的,属于同步阻塞IO。 都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个事件的查询过程是阻塞的。

select/epoll系统调用是阻塞式的,属于同步阻塞IO。 都需要在读写事件就绪后,由系统调用本身负责进行读写?

 select 和 epoll 都是用来监听文件描述符(通常是套接字)上的读写事件,这两种系统调用在查询是否有事件就绪时,都存在阻塞的可能性:

  1. select

    • 当调用 select 函数时,如果没有任何一个文件描述符准备好读写操作,则调用会阻塞,直到至少有一个描述符变为可读、可写或发生异常。
    • select 需要传入三个集合,分别对应读、写和异常条件,它会等待直到这三个集合中的任意一个描述符准备就绪。
  2. epoll

    • epoll 是 Linux 内核提供的一种更高效的通知机制,它改进了 select 的一些限制,如最大监听描述符数量等。
    • 使用 epoll_create 创建 epoll 实例,epoll_ctl 添加或修改监听的文件描述符,然后调用 epoll_wait 监听事件。
    • 当调用 epoll_wait 时,如果没有就绪事件,它也会阻塞等待,直到有至少一个已注册的描述符发生了可读、可写或错误等事件。

虽然两者都涉及阻塞等待,但 epoll 提供了更高的性能和更好的可扩展性,因为它仅通知发生了状态改变的描述符,而不是像 select 那样每次都要遍历整个描述符集合。此外,epoll 支持水平触发和边缘触发两种模式,为开发者提供了更灵活的选择。不过,从根本上讲,它们在等待事件就绪时确实是同步阻塞的,一旦有事件发生,系统调用才会返回并告知程序可以进行读写操作。

4.信号驱动I/O (Signal-driven I/O):

信号驱动IO优势:用户进程在等待数据时,不会被阻塞,能够提高用户进程的效率。 具体来说:在信号驱动式I/O模型中,应用程序使用套接口进行信号驱动I/O,并安装一个信 号处理函数,进程继续运行并不阻塞。

信号驱动IO缺点:

1 在大量IO事件发生时,可能会由于处理不过来,而导致信号队列溢出。

2 对于处理UDP套接字来讲,对于信号驱动I/O是有用的。可是,对于TCP而言,由于致使SIGIO信号通知的条件为数众多,进行IO信号进一步区分的成本太高,信号驱动的I/O 方式近乎无用。

3 信号驱动IO可以看成是一种异步IO,可以简单理解为系统进行用户函数的回调。只是,信号驱动IO的异步特性,又做的不彻底。为什么呢? 信号驱动IO仅仅在IO事件的通知 阶段是异步的,而在第二阶段,也就是在将数据从内核缓冲区复制到用户缓冲区这个过程, 用户进程是阻塞的、同步的。

5.异步I/O (Asynchronous I/O):

异步IO模型的特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。 用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO。

异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给 了操作系统,也就是说,需要底层内核提供支持。 理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。

如何配置Linux来支持百万级并发连接?

在 Linux 环境中,任何事物都是用文件来表示,设备是文件,目录是文件,socket 也是文件。用来表示所处理对象的接口和唯一接口就是文件。应用程序在读/写一个文件时,首先需要打开这个文件,打开的过程其实质就是在进程与文件之间建立起连接,句柄的作用就是唯一标识此连接。此后对文件的读/写时,由这个句柄作为代表。最后关闭文件其实就是释放这个句柄的过程,也就是进程与文件之间的连接断开。

在生产环境Linux系统中, 基本上都需要解除文件句柄数的限制。原因是,Linux的系统默认值为1024,也就是说,一 个进程最多可以接受1024个socket连接。这是远远不够的。

什么是文件句柄?

文件句柄,也叫文件描述符。在Linux系统中,文件可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,它是一个非负整数(通常是小整数),用于指代被打开的文件。所有的IO系统调用,包括socket的读写调用,都是通过文件描述符完成的。 

在Linux下,通过调用ulimit命令,可以看到一个进程能够打开的最大文件句柄数量,这个命令的具体使用方法是:

ulimit -n #-n选项 用于引用或设置当前的文件句柄数量的限制值,Linux的系统默认值为1024。

一个高并发的应用, 面临的并发连接数往往是十万级、百万级、甚至像腾讯QQ一样的上亿级。 文件句柄数不够,会导致什么后果呢?当单个进程打开的文件句柄数量超过了系统配置的上限值时,就会发出“Socket/File:Can't open so many files”的错误提示。 所以,对于高并发、高负载的应用,就必须要调整这个系统参数,以适应处理并发处理 大量连接的应用场景。可以通过ulimit来设置这两个参数。方法如下

ulimit -n 1000000

在上面的命令中,n的设置值越大,可以打开的文件句柄数量就越大。建议以root用户来执行此命令。

使用ulimit命令有一个缺陷,该命令仅仅只能修改当前用户环境的一些基础限制,仅在当前用户环境有效。也即是说,在当前的终端工具连接当前shell期间,修改是有效的;一旦断开用户会话,或者说用户退出Linux后,它的数值就又变回系统默认的1024了。并且,系统重启后,句柄数量又会恢复为默认值。

ulimit命令只能用于临时修改,如果想永久地把最大文件描述符数量值保存下来,可以 编辑/etc/rc.local开机启动文件,在文件中添加如下内容:

ulimit -SHn 1000000

以上示例增加-S和-H两个命令选项。选项-S表示软性极限值,-H表示硬性极限值。硬性极限是实际的限制,就是最大可以是100万,不能再多了。软性极限值则是系统发出警告 (Warning)的极限值,超过这个极限值,内核会发出警告。

普通用户通过ulimit命令,可将软极限更改到硬极限的最大设置值。如果要更改硬极限, 必须拥有root用户权限。

终极解除Linux系统的最大文件打开数量的限制,可以通过编辑Linux的极限配置文件 /etc/security/limits.conf来解决,修改此文件,加入如下内容:

* soft nofile 1000000

* hard nofile 1000000

soft nofile表示软性极限,hard nofile表示硬性极限。

举个实际例子,在使用和安装目前非常流行的分布式搜索引擎——ElasticSearch时,基本上就必须去修改这个文件,用于增加最大的文件描述符的极限值。当然,在生产环境运行Netty时,最好是修改/etc/security/limits.conf文件,增加文件描述符数量的限制。

除了修改应用进程的文件句柄上限之外,还需要修改内核基本的全局文件句柄上限,通过修改 /etc/sysctl.conf 配置文件来更改,参考的配置如下:

fs.file-max = 2048000

fs.nr_open = 1024000

fs.file-max表示系统级别的能够打开的文件句柄的上限,可以理解为全局的句柄数上限。 是对整个系统的限制,并不是针对用户的。

fs.nr_open指定了单个进程可打开的文件句柄的数量限制,nofile受到这个参数的限制,nofile值不可用超过fs.nr_open值

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JAVA技术开发员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值