Loading

I/O模型整理

I/O读写的基础原理

用户态进程与内核态进程

为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel Space),另一部分是用户空间(User Space);在Linux 系统中,内核模块运行在内核空间,对应的进程处于内核态;而用户程序运行在用户空间,对应的进程处于用户态;

操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内核空间(内核空间总是驻留在内存中,它是为操作系统的内核保留的),也有访问底层硬件设备的权限;

应用程序是不允许直接在内核空间区域进行读写,也不容许直接调用内核 代码定义的函数;每个应用程序进程都有一个独立的用户空间,对应的进程处于用户态,用户态进程不能访问内核空间中的数据,也不能直接调用内核函数,因此应用程序需要进行系统调用的时候,这时需要将进程切换到内核态才能进行;

 

用户态进程如何进行系统调用

内核态进程可以执行任意命令,调用系统的一切资源,而用户态进程只能执行简单的运算,不能直接调用系统资源;当用户态进程需要执行系统调用的时候,用户态进程必须通过System Call的接口,才能向内核发出指令,完成调用系统资源之类的操作;

用户程序进行I/O的读写,依赖于底层的I/O读写,基本上会用到底层的两大系统调用:sys_readsys_write;虽然在不同的操作系统中, sys_readsys_write两大系统调用的名称和形式可能不完全一样,但是它们的基本功能是一样的;

操作系统层面的sys_read系统调用,并不是直接从物理设备把数据读取到应用的内存中;sys_write系统调用,也不是直接把数据写入到物理设备;上层应用无论是调用操作系统的sys_read,还是调用操作系统的sys_write,都会涉及缓冲区,具体表现如下:

  • 上层应用通过操作系统的sys_read系统调用,把数据从内核缓冲区复制到应用程序的进程缓冲区;
  • 上层应用通过操作系统的sys_write系统调用,把数据从应用程序的进程缓冲区复制到操作系统内核缓冲区;

换句话说,应用程序的I/O操作,实际上不是物理设备级别的读写,而是缓存的复制;sys_readsys_write两大系统调用,都不负责数据在内核缓冲区和物理设备(如磁盘、网卡等)之间的交换;这项底层的读写交换操作,是由操作系统内核来完成的;

因此,应用程序中的I/O操作(包括Socket的I/O操作,文件的I/O操作等)都属于上层应用的开发,它们的在输入和输出维度上的执行流程,都是类似地在内核缓冲区和进程缓冲区之间的进行数据交换;

 

内核缓存区与进程缓冲区

缓冲区的目的

缓冲区的目的,是为了减少频繁地与设备之间的物理交换;

计算机的外部物理设备与内存与 CPU相比,有着非常大的差距,外部设备的直接读写,涉及操作系统的中断;发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息;为了减少底层系统的频繁中断所导致的时间损耗、性能损耗,于是出现了内核缓冲区;

当有了内核缓冲区,操作系统会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行I/O设备的中断处理,集中执行物理设备的实际I/O操作,通过这种机制来提升系统的性能;至于具体在什么时候执行系统中断(包括读中断、写中断),则由操作系统的内核来决定,应用程序不需要关心;

内核缓冲区与应用缓冲区在数量上也不同,在Linux系统中,操作系统内核只有一个内核缓冲区;而每个用户程序(进程)则有自己独立的缓冲区,叫做用户缓冲区或者进程缓冲区;Linux系统中的用户程序的 IO读写程序,在大多数情况下,并没有进行实际的I/O操作,而是在用户缓冲区和内核缓冲区之间直接进行数据的交换;

 

系统调用sys_read与sys_write的执行流程

系统调用sys_readsys_write,并不是使数据在内核缓冲区和物理设备之间的交换;sys_read调用把数据从内核缓冲区复制到应用的用户缓冲区, sys_write调用把数据从应用的用户缓冲区复制到内核缓冲区;两个系统调用的大致的流程,如下图;

客户端和服务器端之间完成一次Socket请求和响应(包括sys_readsys_write)的数据交换,其完整的流程如下:

  • 客户端发送请求

客户端程序通过sys_write系统调用,将数据复制到内核缓冲区, Linux将内核缓冲区的请求数据通过客户端器的网卡发送出去;

  • 服务端系统接收数据

在服务端,这份请求数据会被服务端操作系统通过DMA硬件, 从接收网卡中读取到服务端机器的内核缓冲区;

  • 服务端获取数据

服务端程序通过sys_read系统调用,从Linux内核缓冲区复制 数据,复制到用户缓冲区;

  • 服务器端业务处理

服务器在自己的用户空间中,完成客户端的请求所对应的业务处理;

  • 服务器端返回数据

服务器程序完成处理后,构建好的响应数据,将这些数据从用户缓冲区写入内核缓冲区,这里用到的是sys_write系统调用,操作系统会负责将内核缓冲区的数据发送出去;

  • 服务端系统发送数据

服务端Linux系统将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,会将数据发送给目标客户端;

 

注:由于不同的操作系统,或者同一个操作系统的不同版本,在具体实现上都有差异; 如在C程序中使用的read库函数会调用到的系统调用为sys_readsys_read完成内核空间的数据读取;而在C程序中使用的write库函数会调用到的系统调用为sys_writesys_write完成内核空间的数据写入;

 

I/O模型

根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型;

前置概念

同步与异步

同步与异步可以看成是发起I/O请求的两种方式,它们的区别体现在应用程序与内核的交互过程的方向

同步I/O操作由用户空间发起的I/O操作(发起方是用户空间的进程或线程,接收方是系统内核),并阻塞等待或者轮询的I/O操作是否完成;

异步I/O操作由应用程序提前注册完成回调函数,之后用户进程继续执行,I/O操作交给系统内核来处理,当系统内核完成I/O操作以后,启动用户进程的回调函数 ;

 

阻塞与非阻塞

阻塞与非阻塞,表现在用户进程在I/O过程中的等待状态

阻塞I/O是需要内核I/O操作彻底完成后,才返回到用户空间执行用户程序的操作指令,也就是发起I/O请求的进程或线程的执行状态是阻塞的(在Java中,默认创建的Socket都属于阻塞I/O);

非阻塞I/O是用户空间的进程或线程不需要等待内核I/O操作彻底完成,可以立即返回用户空间去执行后续的指令;也就是发起I/O请求的用户进程或线程处于非阻塞状态,与此同时,内核会立即返回给用户一个I/O的状态值

注:

  • 同步阻塞I/O、同步非阻塞I/O、I/O多路复用都是同步I/O;
  • 异步I/O必定是非阻塞的,所以不存在异步阻塞和异步非阻塞的说法;

 

recvfrom系统函数

recvfrom()函数用来从一个套接字接收消息,它与只能在连接的流套接字或绑定的数据报套接字上使用的recv()函数调用不同,recvfrom()函数可用于在套接字上接收数据,无论它是否已连接;

参考:https://en.wikipedia.org/wiki/System_call

   https://www.mkssoftware.com/docs/man3/recvfrom.3.asp

 

信号驱动I/O模型(SIGIO、Signal Driven I/O)

在信号驱动I/O模型中,用户线程通过向核心注册I/O事件的回调函数,来避免I/O查询时的阻塞;具体的做法是,用户进程预先在内核中设置一个回调函数,当某个事件发生时,内核使用信号(SIGIO)通知进程运行回调函数; 然后用户线程会继续执行,在信号回调函数中调用I/O读写操作来进行实际的I/O请求操作;

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

 

异步I/O模型(Asynchronous I/O)

异步IO模型基本流程:用户线程通过系统调用,向内核注册某个I/O操作;内核在整个I/O操作(包括数据准备、数据复制)完成后,通知用户进程,用户进程执行后续的业务操作;

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

异步I/O模型与信号驱动I/O模型区别:

信号驱动I/O由内核通知用户进程何时可以开始一个I/O操作;而异步I/O模型由内核通知用户进程I/O操作何时完成;

 

同步阻塞I/O模型(BIO、Blocking I/O)

最常用的I/O模型就是阻塞I/O模型,缺省情况下,所有文件操作都是阻塞的阻塞指的是用户程序(发起I/O请求的进程或者线程)的执行状态是阻塞的;

同步阻塞I/O指的是用户空间(或者线程)主动发起,需要等待内核 I/O操作彻底完成后,才返回到用户空间的I/O操作,I/O操作过程中,发起I/O请求的用户进程(或者线程)处于阻塞状态;

以套接字为例,在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或发送错误时才返回,在此期间一直会等待,进程在从调用recvfrom开始到它返回的整段时间内都是阻塞的;

 

  • 同步阻塞I/O服务端通信模型(一客户端一线程)

由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁;

该模型最大的问题缺乏弹性的伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数暴涨后,系统性能将会急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出,创建新线程失败等问题(注:系统开辟线程数是有上限的),并最终导致进程宕机或僵死,不能对外提供服务;

 

  • 采用线程池和任务队列实现的伪异步I/O模型

当有新的客户端接入的时候,将客户端的Socket封装成一个Task(该任务实现 java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列(即线程池配置的阻塞队列)和N个活跃线程(即线程池配置的核心线程数)对任务队列中的任务进行处理;

 

上述的伪异步I/O模型对之前的一客户一线程的模型做了简单的优化,但是它无法解决BIO在通信阻塞上的问题;如服务端处理缓慢,导致响应超时;

 

阻塞IO的优点

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

 

阻塞I/O的缺点

  • 每个请求都需要创建独立的线程,与对应的客户端进行数据读,业务处理进行数据写;
  •  当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大;

  •  连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在读操作上,造成线程资源浪费;

 

非阻塞I/O模型(NIO、None Blocking I/O)

recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来;

上图中前两次调用recvfrom时没有数据可返回,因此内核会立即返回一个EWOULDBLOCK错误,当第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,应用调用recvfrom成功返回;

应用进程对一个非阻塞fd循环调用recvfrom,即为轮询;应用进程持续轮询内核,以查看fd是否就绪,这样会耗费大量CPU资源;

 

同步非阻塞I/O的优点

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

 

同步非阻塞I/O的缺点

  • 不断地轮询内核,这将占用大量的 CPU时间,效率低下;

 

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

I/O多路复用是一种同步I/O模型,同一个线程可以监听多个文件句柄(即fd),处理多个I/O事件;I/O多路指的是多个文件句柄(即fd),复用指的是该过程的多个I/O事件由同一个线程处理;

Linux提供select/poll函数调用,进程通过将一个或多个fd传递给select或poll调用,阻塞在select/poll调用上,可侦测一个或多个fd是否处于就绪状态(即等待数据报套接字变为可读)当select/poll返回套接字可读状态时,应用进程调用recvfrom把数据报复制到应用进程缓冲区;而select/poll是顺序扫描fd的

Linux还提供一种epoll函数调用(epoll是在 Linux 2.6内核中提出的,它是select系统调用的 Linux增强版本),epoll使用事件驱动的方式替代顺序扫描fd,因此性能更高当fd就绪时,立即回调返回;

 

I/O多路复用模型的优点

  • 当应用进程使用I/O多路复用时,应用进程可以在同一个线程内监控多个fd(等待多个fd就绪),处理多个I/O事件,用户进程不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销;这是一个线程维护一个连接的BIO模式相比,使用多路I/O复用模型的最大优势;

 

I/O模型的比较

 

Java NIO

select、poll、epoll区别

  select poll epoll(JDK1.5_update10及以上)
操作方式 遍历 每次调用都进行 线性遍历,时间 复杂度为O(n) 回调
底层实现 数组 链表 哈希表
IO效率 每次调用都进行线 性遍历,时间复杂 度为O(n) 每次调用都进行 线性遍历,时间 复杂度为O(n) 事件通知方式,每当有IO事件 就绪,系统注册的回调函数就 会被调用,时间复杂度O(1)
最大连接 有上限,1024个fd 无上限 无上限

 

epoll的改进

epoll与select的原理比较类型,为了克服select的缺点,epoll做了很多重大改进,如下:

  • 支持一个进程打开的socket描述符(fd)不受限制(仅受限于操作系统的最大文件句柄数)
  • I/O效率不会随着fd数目的增加而线性下降;传统的select/poll的一个致命弱点,当应用程序中有一个很大的socket集合,由于网络延迟或链路空闲,任一时刻只有少部分的socket是就绪状态的,但是select/poll每次调用都会线性扫描全部集合,导致效率呈线性下降;
  • 使用mmap加速内核与用户空间的消息传递;无论是select/poll/epoll都需要内核把fd消息通知给用户空间,epoll是通过内核和用户空间mmap同一块内存实现;

 

注:

  • 在JDK1.4推出Java NIO之前,Java的Socket通信采用的是同步阻塞模式,即BIO;
  • JDK1.4版本提供了新的NIO类库,这里Java的NIO(从Java的应用层面看,N是New的意思,New I/O是一个新的I/O体系,有Channel,Buffer,Selector这些新的组件;从操作系统内核的层面看,N是Non Blocking,非阻塞的意思)类库组件,所归属的不是基础I/O模型中的NIO(None Blocking I/O)模型,而是另外的一种模型,叫做 I/O 多路复用模型( IO Multiplexing); 
  • 和NIO模型相似,多路复用I/O也需要轮询;负责 select/poll/epoll状态查询调用的线程,需要不断地进行select/poll/epoll轮询,查找出达到I/O操作就绪的fd;

  

Java NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器);

 

  • Channel

Channel是一个通道,可以通过它读取和写入数据,通道与流不同在于通道是双向的,流只能一个方向移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读,写或同时读写;

 

  • Buffer

Buffer(缓冲区)本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况;Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer;

 

  • Selector

Selector可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器);

Selector(选择器) 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理;这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求;

只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程;避免了多线程之间的上下文切换导致的开销;

Channel 会注册到 Selector 上,由 Selector 根据 Channel 读写事件的发生将其交由某个空闲的线程处理(也可以是单线程,如Redis6之前的线程模型是单线程I/O多路的,Redis6开始更新线程模型为多线程模型,如果是单线程Selector读取到一个大Key,由于读取处理时间较长,对于后面处理监听的事件会有影响,那么不推荐使用大Key的说法就能说通了);

 

SelectionKey,表示 Selector 和网络通道的注册关系;

int OP_ACCEPT:有新的网络连接可以 accept,值为  16
int OP_CONNECT:代表连接已经建立,值为  8
int OP_READ:代表读操作,值为  1
int OP_WRITE:代表写操作,值为  4
源码中:
public static final int OP_READ =  1 <<  0 ; 
public static final int OP_WRITE =  1 <<  2 ;
public static final int OP_CONNECT =  1 <<  3 ;
public static final int OP_ACCEPT =  1 <<  4 ;

ServerSocketChannel 在服务器端监听新的客户端 Socket 连接;

SocketChannel,网络 IO 通道,具体负责进行读写操作;NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区;

 

注:

在用户进程进行I/O 就绪事件的轮询时,需要调用了选择器的select查询方法,发起查询的用户进程或者线程是阻塞的;

当然,如果使用了查询方法的非阻塞的重载版本,发起查询的

 

测试demo

服务端

查看代码
 public class NIOSocketServer {

    //public static ExecutorService pool = Executors.newFixedThreadPool(10);
    public static void main(String[] args)  throws IOException {
        // 创建一个在本地端口进行监听的服务Socket通道.并设置为非阻塞方式
        ServerSocketChannel ssc = ServerSocketChannel.open();

        //必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
        ssc.configureBlocking( false );
        ssc.socket().bind( new InetSocketAddress( 9000 ));
        // 创建一个选择器selector
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        while ( true ) {
            System.out.println( "等待事件发生.." );
            // 轮询监听channel里的key,select是阻塞的,accept()也是阻塞的
            int select = selector.select();

            System.out.println( "有事件发生了.." );
            // 有客户端请求,被轮询监听到
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                //删除本次已处理的key,防止下次select重复处理
                it.remove();
                handle(key);
            }
        }
    }

    private static void handle(SelectionKey key)  throws IOException {
        if (key.isAcceptable()) {
            System.out.println( "有客户端连接事件发生了.." );
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            //NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
            //处理完连接请求不会继续等待客户端的数据发送
            SocketChannel sc = ssc.accept();
            sc.configureBlocking( false );
            //通过Selector监听Channel时对读事件感兴趣
            sc.register(key.selector(), SelectionKey.OP_READ);
        }  else if (key.isReadable()) {
            System.out.println( "有客户端数据可读事件发生了.." );
            SocketChannel sc = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate( 1024 );
            //NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
            int len = sc.read(buffer);
            if (len != - 1 ) {
                System.out.println( "读取到客户端发送的数据:" +  new String(buffer.array(),  0 , len));
            }
            ByteBuffer bufferToWrite = ByteBuffer.wrap( "HelloClient" .getBytes());
            sc.write(bufferToWrite);
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        }  else if (key.isWritable()) {
            SocketChannel sc = (SocketChannel) key.channel();
            System.out.println( "write事件" );
            // NIO事件触发是水平触发
            // 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
            // 在有数据往外写的时候再注册写事件
            key.interestOps(SelectionKey.OP_READ);
            sc.close();
        }
    }
}

  

客户端

查看代码
public class NIOSocketClient {
    //通道管理器
    private Selector selector;

    /**
     * 启动客户端测试
     *
     * @throws IOException
     */
    public static void main(String[] args)  throws IOException {
        NIOSocketClient client =  new NIOSocketClient();
        client.initClient( "127.0.0.1" ,  9000 );
        client.connect();
    }

    /**
     * 获得一个Socket通道,并对该通道做一些初始化的工作
     *
     * @param ip   连接的服务器的ip
     * @param port 连接的服务器的端口号
     * @throws IOException
     */
    public void initClient(String ip,  int port)  throws IOException {
        // 获得一个Socket通道
        SocketChannel channel = SocketChannel.open();
        // 设置通道为非阻塞
        channel.configureBlocking( false );
        // 获得一个通道管理器
        this .selector = Selector.open();

        // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
        //用channel.finishConnect();才能完成连接
        channel.connect( new InetSocketAddress(ip, port));
        //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
        channel.register(selector, SelectionKey.OP_CONNECT);
    }

    /**
     * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
     *
     * @throws IOException
     */
    public void connect()  throws IOException {
        boolean flag =  false ;

        // 轮询访问selector
        while (!flag) {
            selector.select();
            // 获得selector中选中的项的迭代器
            Iterator<SelectionKey> it =  this .selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                // 删除已选的key,以防重复处理
                it.remove();
                // 连接事件发生
                if (key.isConnectable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    // 如果正在连接,则完成连接
                    if (channel.isConnectionPending()) {
                        channel.finishConnect();
                    }
                    // 设置成非阻塞
                    channel.configureBlocking( false );
                    //在这里可以给服务端发送信息哦
                    ByteBuffer buffer = ByteBuffer.wrap( "HelloServer".getBytes());
                    channel.write(buffer);
                    //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                    channel.register( this .selector, SelectionKey.OP_READ);  // 获得了可读的事件
                }  else if (key.isReadable()) {
                    read(key);
                    SelectableChannel channel = key.channel();
                    channel.close();
                    flag =  true ;
                }
            }
        }
    }

    /**
     * 处理读取服务端发来的信息 的事件
     *
     * @param key
     * @throws IOException
     */
    public void read(SelectionKey key)  throws IOException {
        //和服务端的read方法一样
        // 服务器可读取消息:得到事件发生的Socket通道
        SocketChannel channel = (SocketChannel) key.channel();
        // 创建读取的缓冲区
        ByteBuffer buffer = ByteBuffer.allocate( 512 );
        int len = channel.read(buffer);
        if (len != - 1 ) {
            System.out.println( "客户端收到信息:" +  new String(buffer.array(),  0 , len));
        }
    }
}

 

配置文件描述符

文件描述符( File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,它是一个非负整数(通常是小整数),用于指代被打开的文件;所有的I/O系统调用,包括socket的读写调用,都是通过文件描述符完成的;

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

ulimit -n

ulimit命令是用来显示和修改当前用户进程一 些基础限制的命令, -n选项用于引用或设置当前的文件句柄数量的限制值,Linux的系统默认值为 1024;

 

当单个进程打开的文件句柄数量超过了系统配置的上限值时,就会发出"Socket/File:Can't open so many files"的错误提示;

解决文件描述符不足的方式

ulimit -n [需要的数值]

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

 

编辑 /etc/rc.local开机启动文件

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

ulimit -SHn [需要的数值]

选项-S表示软性极限值,-H表示硬性极限值;硬性极限是实际的限制,即最大的限制;软性极限值则是系统发出警告(Warning)的极限值,超过这个极限值,内核会发出警告;

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

 

编辑Linux的极限配置文件/etc/security/limits.conf和控制内核相关配置文件/etc/sysctl.conf

可以通过编辑Linux的极限配置文件/etc/security/limits.conf来解决,修改此文件,加入如下内容:

soft nofile [需要的数值]
hard nofile [需要的数值]

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

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

fs.file-max=[需要的数值]
fs.nr_open=[需要的数值]

fs.file-max表示系统级别的能够打开的文件句柄的上限,它是对整个系统的限制,并不是针对用户的;

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

 

参考:

《Netty权威指南》

《UNIX网络编程卷1》

 

posted @ 2021-01-25 19:28  街头卖艺的肖邦  阅读(152)  评论(0编辑  收藏  举报