关于io:什么是IO阻塞

一、什么是IO?咱们都晓得unix世界里、所有皆文件、而文件是什么呢?文件就是一串二进制流而已、不论socket、还是FIFO、管道、终端、对咱们来说、一切都是文件、一切都是流、在信息替换的过程中、咱们都是对这些流进行数据的收发操作、简称为I/O操作(input and output)、往流中读出数据、零碎调用read、写入数据、零碎调用write、不过话说回来了、计算机里有这么多的流、我怎么晓得要操作哪个流呢?做到这个的就是文件描述符、即通常所说的fd、一个fd就是一个整数、所以对这个整数的操作、就是对这个文件(流)的操作、咱们创立一个socket、通过零碎调用会返回一个文件描述符、那么剩下对socket的操作就会转化为对这个描述符的操作、不能不说这又是一种分层和形象的思维。 二、IO交互通常用户过程中的一个残缺IO分为两个阶: 三、IO模式1.阻塞IO(blocking I/O)A拿着一支鱼竿在河边钓鱼,并且始终在鱼竿前等,在等的时候不做其余的事件,非常分心。只有鱼上钩的时,才完结掉等的动作,把鱼钓上来。在内核将数据筹备好之前,零碎调用会始终期待所有的套接字,默认的是阻塞形式。 2.非阻塞IO(noblocking I/O)B也在河边钓鱼,然而B不想将本人的所有工夫都破费在钓鱼上,在等鱼上钩这个时间段中,B也在做其余的事件(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事件的时候,每隔一个固定的工夫查看鱼是否上钩。一旦查看到有鱼上钩,就停下手中的事件,把鱼钓上来。 3.信号驱动IO(signal blocking I/O)C也在河边钓鱼,但与A、B不同的是,C比拟聪慧,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,C就会将鱼钓上来。 4.IO多路复用(I/O multiplexing)D同样也在河边钓鱼,然而D生存程度比拟好,D拿了很多的鱼竿,一次性有很多鱼竿在等,D一直的查看每个鱼竿是否有鱼上钩。减少了效率,缩小了期待的工夫。 5.异步IO(asynchronous I/O)E也想钓鱼,但E有事件,于是他雇来了F,让F帮他期待鱼上钩,一旦有鱼上钩,F就打电话给E,E就会将鱼钓上去。 阻塞水平:阻塞IO>非阻塞IO>多路转接IO>信号驱动IO>异步IO,效率是由低到高的。

September 18, 2023 · 1 min · jiezi

关于io:度加剪辑App的MMKV应用优化实践

作者 | 我爱吃海米 导读  挪动端开发中,IO密集问题在很多时候没有失去短缺的器重和解决,贸然的把IO导致的卡顿放到异步线程,可能会导致真正的问题被覆盖,前人挖坑前人踩。其实首先要想的是,数据存储形式是否正当,数据的应用形式是否正当。本文介绍度加剪辑对MMKV的应用和优化。 全文14813字,预计浏览工夫38分钟。 01 所有皆文件-挪动端IO介绍挪动端的App程序很多状况是IO密集型,比如说聊天信息的读取和发送、短视频的下载和缓存、信息流利用的图文缓存等。 绝对于计算密集,IO密集场景更加多样,比方零碎SharedPreferences和NSUserDefault自带的一些问题、Android中忙碌的binder通信、文件磁盘读取和写入、文件句柄泄露、主线程操作Sqlite导致的卡顿等,解决起来相当烫手。 IO不忙碌的状况下,主线程低频次的调用IO函数是没什么问题的。然而在IO忙碌时,IO性能急剧进化,任何IO操作都可能是压死骆驼的最初一根稻草。在平时开发测试中很难遇到IO卡顿,到了线上后才会裸露进去,iOS/Android双端根本都是如此:罕用的open零碎调用,线下测试只须要4ms,线上大把用户执行工夫超过10秒;就连获取文件长度、查看文件是否存在这种惯例操作,居然也能卡顿。 以Android线上抓到的卡顿为例(>5秒): at libcore.io.Linux.access(Native Method)at libcore.io.ForwardingOs.access(ForwardingOs.java:128)at libcore.io.BlockGuardOs.access(BlockGuardOs.java:76)at libcore.io.ForwardingOs.access(ForwardingOs.java:128)at android.app.ActivityThread$AndroidOs.access(ActivityThread.java:8121)at java.io.UnixFileSystem.checkAccess(UnixFileSystem.java:281)at java.io.File.exists(File.java:813)at com.a.b.getDownloaded(SourceFile:2)at libcore.io.Linux.stat(Native Method)at libcore.io.ForwardingOs.stat(ForwardingOs.java:853)at libcore.io.BlockGuardOs.stat(BlockGuardOs.java:420)at libcore.io.ForwardingOs.stat(ForwardingOs.java:853)at android.app.ActivityThread$AndroidOs.stat(ActivityThread.java:8897)at java.io.UnixFileSystem.getLength(UnixFileSystem.java:298)at java.io.File.length(File.java:968)具体源码能够参考 : https://android.googlesource.com/platform/libcore/+/master/lu...\_io\_Linux.cpp 最终是在C++中发动了零碎调用access()和stat()。 IO问题在很多时候被鄙视,贸然的把IO导致的卡顿放到异步线程,可能会导致真正的问题被覆盖,前人挖坑前人踩。其实首先要想的是,数据存储形式是否正当,数据的应用形式是否正当。 作为一款视频剪辑工具,度加剪辑在内存、磁盘、卡顿方面有大量的技术挑战,同时也积攒了大量的技术债。我从隔壁做图片丑化工具的团队那失去了双端的IO卡顿数据,能够说是难兄难弟,不分伯仲:有卧龙的中央,十步以内必有凤雏。 上面简略介绍度加剪辑App中对文件磁盘IO这部分的应用和优化,本文是无关MMKV。 (广告工夫:度加剪辑是一款音视频剪辑软件,针对口播用户开发了很多贴心性能,比如说疾速剪辑,各类素材也比拟丰盛,比方贴纸、文字模板等,欢送下载应用。) 02 高性能kv神器-MMKVMMKV是基于mmap的高性能通用key-value组件,性能极佳,让咱们在主线程应用kv成为了可能,堪称挪动端的Redis,实际上这两者在设计上也能找到类似的影子。 mmap是应用极其宽泛的内存映射技术,对内存的读写约等于对磁盘的读写,内存中的字节与文件中的字节相映成趣,一一对应。像Kafka和RocketMQ等消息中间件都应用了mmap,防止了数据在用户态跟内核态大量的拷贝切换, 所谓零拷贝。 为了进步性能,度加逐步从SharedPreferences向MMKV迁徙,对于Sp的卡顿逐步隐没,性能晋升成果非常哇塞。 然而,MMKV仍然有不少IO操作产生在主线程,这些函数在用户缓冲区都没有buffer(比照fread和fwrite等f打头的带有缓冲的函数),且磁盘绝对是低速设施,同步时效率较低,有时难免会呈现性能问题。 度加剪辑作为MMKV的重度甚至变态用户,随着应用越来越频繁,陆续发现了线上很多和MMKV相干的乏味问题,上面抛砖引玉简略介绍。 03 setX/encodeX卡顿-占度加剪辑总卡顿的1.2%at com.tencent.mmkv.MMKV.encodeString(Native Method)at com.tencent.mmkv.MMKV.encode(Proguard:8)通过剖析,卡顿根本都产生IO忙碌时刻。度加App在应用中充斥了大量的磁盘IO,在编辑页面会读取大量的视频文件、贴纸、字体等各种文件,像降噪、语音转文字等大量场景都须要本地写入;导出页面会在短时间内写入上G的视频到磁盘中:为了保障输入视频的清晰度,度加App设置了极高的视频和音频码率。 不可避免,当磁盘处于大规模写入状态,在视频合成导出、视频文件读取和下载、各类素材的下载过程中很容易发现MMKV卡顿的身影;通过减少研发打点数据以及其余辅助伎俩后,我大体演绎了两种卡顿产生的典型场景。 1、存储较长的字符串,例如云控json这个卡顿大部分是MMKV的重写和扩容机制引起,首先简略介绍MMKV的数据存储布局。_(https://github.com/Tencent/MMKV/wiki/design)_ MMKV在创立一个ID时,例如默认的mmkv.default,会为这个ID独自创立两个4K大小(操作系统pagesize值)的文件,寄存内容的文件和CRC校验文件。 每次插入新的key-value,以append模式在内容文件的尾部追加,取值以最初插入的数据为准,即便是已有雷同的key-value,也间接append在文件开端;key与value交替存储,与Redis的AOF非常相似。 便于了解不便,省去了key长度和value长度等其余字段: 此时MMKV的dict中有两对无效的key=>value数据: {"key1":"val3", "key2", "val2"} 重写:Append模式有个问题,当一个雷同的key一直被写入时,整个文件有局部区域是被节约掉的,因为后面的value会被前面的代替掉,只有最初插入的那组kv对才无效。所以当文件不足以寄存新增的kv数据时,MMKV会先尝试对key去重,重写文件以重整布局升高大小,相似Redis的bgrewriteaof。(重写后实际上是key2在前key1在后。) 扩容:在重写文件后,如果空间还是不够,会一直的以2倍大小扩容文件直到满足需要:JAVA中ArrayList的扩容系数是1.5,GCC中std::vector扩容系数是2,MMKV的扩容系数也是2。 size_t oldSize = fileSize;do { fileSize *= 2;} while (lenNeeded + futureUsage >= fileSize);重写和扩容都会波及到IO相干的零碎调用,重写会调用msync函数强制同步数据到磁盘;而扩容时逻辑更为简单,零碎调用次数更多: ...

August 24, 2023 · 5 min · jiezi

关于io:新一代异步IO框架-iouring-|-得物技术

1.Linux IO 模型分类 相比于kernel bypass 模式须要联合具体的硬件撑持来讲,native IO是日常工作中接触到比拟多的一种,其中同步IO在较长一段时间内被宽泛应用,通常咱们接触到的IO操作次要分为网络IO和存储IO。在大流量高并发的明天,提到网络IO,很容易想到赫赫有名的epoll  以及reactor架构。然而epoll并不属于异步IO的领域。实质上是一个同步非阻塞的架构。对于同步异步,阻塞与非阻塞的概念区别这里做简要概述: 什么是同步指过程调用接口时须要期待接口解决完数据并相应过程能力继续执行。这里重点是数据处理活逻辑执行实现并返回,如果是异步则不用期待数据实现,亦能够继续执行。同步强调的是逻辑上的秩序性; 什么是阻塞当过程调用一个阻塞的零碎函数时,该过程被 置于睡眠(Sleep)状态,这时内核调度其它过程运行,直到该过程期待的事件产生了(比 如网络上接管到数据包,或者调用sleep指定的睡眠工夫到了)它才有可能持续运行。与睡眠状态绝对的是运行(Running)状态,在Linux内核中,处于运行状态的过程分为两种状况,一种是过程正在被CPU调度,另一种是处于就绪状态随时可能被调度的过程;阻塞强调的是函数调用下过程的状态。 2.Linux常见文件操作形式2.1   open/close/read/write基本操作API 如下: #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> // 返回值:胜利返回新调配的文件描述符,出错返回-1并设置errno int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);// 返回值:胜利返回0,出错返回-1并设置errno int close(int fd); // 返回值:胜利返回读取的字节数,出错返回-1并设置errno,如果在调read之前已达到文件开端,则这次read返回0 ssize_t read(int fd, void *buf, size_t count); // 返回值:胜利返回写入的字节数,出错返回-1并设置errno ssize_t write(int fd, const void *buf, size_t count);在关上文件时能够指定为,只读,只写,读写等权限,以及阻塞或者非阻塞操作等;具体通过open函数的flags 参数指定 。这里以关上一个读写文件为例,同时定义了写文件的形式为追加写,以及应用间接IO模式操作文件,具体什么是间接IO下文会细述。open("/path/to/file", O\_RDWR|O\_APPEND|O_DIRECT);flags 可选参数如下: Flag 参数含意O_CREATE创立文件时,如果文件存在则出错返回O_EXCL如果同时指定了O_CREAT,并且文件已存在,则出错返回。O_TRUC把文件截断成0O_RDONLY只读O_WRONLY只写O_RDWR读写O_APPEND追加O_NONBLOCK非阻塞标记O_SYNC每次读写都期待物理IO操作实现O_DIRECT提供最间接IO反对通常读写操作的数据首先从用户缓冲区进入内核缓冲区,而后由内核缓冲区实现与IO设施的同步: 2.2   Mmap// 胜利执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],// error被设为以下的某个值:// 1 EACCES:拜访出错// 2 EAGAIN:文件已被锁定,或者太多的内存已被锁定// 3 EBADF:fd不是无效的文件形容词// 4 EINVAL:一个或者多个参数有效// 5 ENFILE:已达到系统对关上文件的限度// 6 ENODEV:指定文件所在的文件系统不反对内存映射// 7 ENOMEM:内存不足,或者过程已超出最大内存映射数量// 8 EPERM:权能有余,操作不容许// 9 ETXTBSY:已写的形式关上文件,同时指定MAP_DENYWRITE标记//10 SIGSEGV:试着向只读区写入//11 SIGBUS:试着拜访不属于过程的内存区void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);// 胜利执行时,munmap()返回0。失败时,munmap返回-1,error返回标记和mmap统一;// 该调用在过程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;int munmap( void * addr, size_t len )// 过程在映射空间的对共享内容的扭转并不间接写回到磁盘文件中,往往在调用munmap()后才执行该操作。// 如果冀望内存的数据变动可能立即反馈到磁盘上,能够通过调用msync()实现。int msync( void *addr, size_t len, int flags )Mmap 是一种内存映射办法,通过将文件映射到内存的某个地址空间上,在对该地址空间的读写操作时,会触发相应的缺页异样以及脏页回写操作,从而实现文件数据的读写操作; ...

April 14, 2023 · 6 min · jiezi

关于io:IO阻塞和非阻塞同步和异步

阻塞和非阻塞阻塞的时候线程会被挂起阻塞: 当数据还没筹备好时,调用了阻塞的办法,则线程会被挂起,会让出CPU工夫片,此时是无奈解决过去的申请,须要期待其余线程来进行唤醒,该线程能力进行后续操作或者解决其余申请。 非阻塞: 意味着,当数据还没筹备好的时候,即使我调用了阻塞办法,该线程也不会被挂起,后续的申请也可能被解决。 同步同步和异步跟串行和并行十分形似。假如在一个场景下:实现一个大工作须要4个小工作。 同步的做法:须要顺次4个步骤,留神这里是顺次,也就是说实现这个步骤,须要先实现前置步骤,也就是说下一个步骤是要看上一个步骤的执行后果。 异步的做法:能够同时进行4个步骤,无需期待其余步骤的执行后果。 阻塞和同步的最实质差异在于: 即使是同步,在期待的过程中,线程是不会被挂起,也不须要让出CPU工夫片的,在IO中的体现网络编程的根本模型是:Client/Server模型两个过程之间要互相通信,其中服务端须要提供地位信息,让客户端找到本人。服务端提供IP地址和监听的端口。 客户端拿着这些信息去向服务端发动建设连贯申请,通过三次握手胜利建设连贯后,客户端就能够通过socket向服务器发送和承受音讯。 BIOBIO通信模型采纳的是典型的:一申请一应答通信模型采纳BIO通信模型的服务端,通常会由一个独立的Acceptor线程负责监听客户端的连贯。 他不负责解决申请,他只是起到一个委派工作的作用,当他接管到申请之后,会为每个客户端创立一个新的线程进行链路解决。 解决完之后,通过输入流,返回应答给客户端,而后线程被销毁,资源被回收。 该模型的最大问题就是不足弹性伸缩能力,服务端的线程个数和客户端的并发拜访数是1:1的关系。 因为线程是Java虚拟机十分贵重的资源,当线程书收缩之后,零碎的性能会随着并发量减少呈反比的趋势降落。 而且会有OOM的危险,当没有内存空间创立线程时,就无奈解决客户端申请,最终导致过程宕机或卡死,无奈对外提供服务。 最大的问题就是:每当有一个客户端申请接入时,就会创立一个线程来解决申请。 为了改良这个一线程一连贯模型,前面又演进出通过: 线程池音讯队列来实现1个或者多个线程解决N个客户端的模型。 在这里,无论是线程池和音讯队列,都是解决内存空间,线程的问题,并没有实质性地扭转同步阻塞通信实质问题所以这种优化版本的BIO也被称为是伪异步。 伪异步IO采纳线程池和工作队列能够实现一种:伪异步的IO通信 将客户端的申请封装成一个Task(该工作实现java.lang.Runnable接口),投递到音讯队列中。 如果通过线程池保护一堆解决线程,去生产队列中的音讯。 处理完毕之后,再去通过客户端就能够了,他的资源是可控的,无论客户端的申请量是多少,也不会发生变化,同样这也是他的毛病之一。 建设连贯的accpet办法、读取数据的read办法都是阻塞。 这就意味着,如果有一方解决申请或者发出请求的比较慢,或者是网络传输比较慢,那么都会影响对方。 当调用OutputStream的write办法写输入流的时候,它将会被阻塞,直到所有要发送的字节全副写入结束,或者产生异样。 在TCP/IP中,当音讯的接管方解决迟缓的时候,因为音讯滑动窗口的存在,那么它的接管窗口就会变小,就是那个TCP window size。 如果这里采纳同步阻塞IO,并且write操作被阻塞很久,直到TCP window size 大于0或者产生IO异样了。 那么通信对方返回应答工夫过长会引起的级联故障: 线程问题:如果所有的可用线程都被故障服务器阻塞,那么后续所有的IO音讯都将被队列中排队。队列问题:如果队列采纳的是有界队列,队列满了之后那么就会无奈后续解决申请;如果采纳的是无界队列,那么会有OOM危险。NIONIO,官网叫法是new IO,因为它绝对于之前出的java.io包是新增的 然而之前老的IO库都是阻塞的,New IO类库指标就是为了让Java反对非阻塞IO,所有更多的人称为Non-Block IO 缓冲区BufferBuffer是一个对象,通常是ByteBuffer类型 任何时候操作NIO中的数据,都须要通过缓冲区。 在NIO库里,所有数据操作是用缓冲区解决的。 读取数据时,是间接读到缓冲区中(这里并没有间接读到某个中央,而是都放到缓冲区中)写入数据时,写入到缓冲区缓冲区本质上是一个数组,通常是一个字节数组ByteBuffer,本身还须要保护读写地位,能够用指针或者偏移量来实现。 除了ByteBuffer还有其余根本类型缓冲区: CharBuffer:字符缓冲区ShortBuffer:短整型缓冲区IntBuffer:整形缓冲区LongBuffer:长整型缓冲区DoubleBuffer:双精度缓冲区通常是用ByteBuffer通道Channel网络数据通过Channel读取和写入Channel通道和Stream流最大的区别在于: Channel的数据流向是双向的Stream的数据流向是单向的 这就意味着:应用Channel,能够同时进行读和写,他是全双工模型。(能够联想到HTTP1.1 HTTP2.0 HTTP3.0 `websocket`) 多路复用器SelectorSelector是NIO编程的根底Selector会一直轮询注册在其上的Channel。 如果某个Channel产生读写事件,就代表这个Channel是就绪状态,会被Selector轮询进去。 而后依据SelectionKey能够获取就绪Channel的汇合,进行后续IO操作。 一个Selector能够轮询多个Channel,JDK是基于epoll代替传统的select,所以不受句柄fd的限度。 意味着,一个线程负责Selector的轮询千万个客户端, AIONIO2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现通过java.util.concurrent.Future类来示意异步操作的后果。在执行异步操作的时候传入一个java.nio.channelsCompletionHandler接口的实现类作为操作实现的回调 NIO2.0的异步socket通道是真正的异步非阻塞IO。 同步socket channel:SocketServerChannel异步socket channel:AsynchronousServerSocketChannel它不须要通过多路复用器(selector)对注册到外面的通过进行轮询操作,就能够实现异步读写。 AIO和NIO最大的区别在于:异步Socket Channel是被动执行对象NIO须要咱们把channel注册到selector上进行程序扫描、轮询AIO则是通过Future类,实现回调办法:completed、failed4种IO比照IO模型次要是探讨2个维度: ...

February 26, 2023 · 1 min · jiezi

关于io:io的基本原理nio

I/O(Input/Output)是计算机科学中指计算机和外部设备进行数据交换的过程。I/O模型是指用于治理过程和设施之间数据传输的技术。 io读写的基本原理操作系统将内存(虚拟内存)划分为两局部:一部分是内核空间(Kernel-Space),另一部分是用户空间(User-Space)应用程序不容许间接在内核空间区域进行读写,也不容许间接调用内核代码定义的函数。每个应用程序过程都有一个独自的用户空间,对应的过程处于用户态,用户态过程不能拜访内核空间中的数据,也不能间接调用内核函数,因而须要将过程切换到内核态能力进行零碎调用。 上面一个read或者一次write指令的大抵流程:通过零碎调用,抉择不同的内核函数进行状态切换,而后把内核缓冲区的数据复制到用户缓冲区中,(内核缓冲区是惟一的) io的模型1.同步阻塞IO首先,解释一下阻塞与非阻塞。阻塞IO指的是须要内核IO操作彻底实现后才返回到用户空间执行用户程序的操作指令。“阻塞”指的是用户程序(发动IO申请的过程或者线程)的执行状态。能够说传统的IO模型都是阻塞IO模型,并且在Java中默认创立的socket都属于阻塞IO模型。其次,解释一下同步与异步。简略来说,能够将同步与异步看成发动IO申请的两种形式。同步IO是指用户空间(过程或者线程)是被动发动IO申请的一方,零碎内核是被动接管方。异步IO则反过来,零碎内核是被动发动IO申请的一方,用户空间是被动接管方。同步阻塞IO(Blocking IO)指的是用户空间(或者线程)被动发动,须要期待内核IO操作彻底实现后才返回到用户空间的IO操作。在IO操作过程中,发动IO申请的用户过程(或者线程)处于阻塞状态。 2. 同步非阻塞IO非阻塞IO(Non-Blocking IO,NIO)指的是用户空间的程序不须要期待内核IO操作彻底实现,能够立刻返回用户空间去执行后续的指令,即发动IO申请的用户过程(或者线程)处于非阻塞状态,与此同时,内核会立刻返回给用户一个IO状态值。阻塞和非阻塞的区别是什么呢?阻塞是指用户过程(或者线程)始终在期待,而不能做别的事件;非阻塞是指用户过程(或者线程)取得内核返回的状态值就返回本人的空间,能够去做别的事件。在Java中,非阻塞IO的socket被设置为NONBLOCK模式。阐明同步非阻塞IO也能够简称为NIO,然而它不是Java编程中的NIO。Java编程中的NIO(New IO)类库组件所归属的不是根底IO模型中的NIO模型,而是IO多路复用模型。同步非阻塞IO指的是用户过程被动发动,不须要期待内核IO操作彻底实现就能立刻返回用户空间的IO操作。在IO操作过程中,发动IO申请的用户过程(或者线程)处于非阻塞状态。 3. IO多路复用为了进步性能,操作系统引入了一种新的零碎调用,专门用于查问IO文件描述符(含socket连贯)的就绪状态。在Linux零碎中,新的零碎调用为select/epoll零碎调用。通过该零碎调用,一个用户过程(或者线程)能够监督多个文件描述符,一旦某个描述符就绪(个别是内核缓冲区可读/可写),内核就可能将文件描述符的就绪状态返回给用户过程(或者线程),用户空间能够依据文件描述符的就绪状态进行相应的IO零碎调用。IO多路复用(IO Multiplexing)属于一种经典的Reactor模式实现,有时也称为异步阻塞IO,Java中的Selector属于这种模型。 4. 异步IO异步IO(Asynchronous IO,AIO)指的是用户空间的线程变成被动接收者,而内核空间成为被动调用者。在异步IO模型中,当用户线程收到告诉时,数据曾经被内核读取结束并放在了用户缓冲区内,内核在IO实现后告诉用户线程间接应用即可。异步IO相似于Java中典型的回调模式,用户过程(或者线程)向内核空间注册了各种IO事件的回调函数,由内核去被动调用。Java AIO 也被称为 NIO2.0,提供了异步I/O的形式,用法和规范的I/O有十分大的差别。 5.半同步半阻塞半异步IO目前在Thrift中有所体现(ThreadedSelectorServer)半同步半阻塞半异步I/O是一种计算机网络通信模型,是一种折衷的解决方案,旨在均衡同步I/O、阻塞I/O和异步I/O的优缺点。在半同步半阻塞半异步I/O模型中,服务器端通过阻塞形式期待客户端的连贯申请,一旦建设连贯,服务器端将该连贯转换为异步形式解决申请。这样既能够解决同步I/O的性能问题,又能够保障客户端的申请不会被服务器端长时间阻塞。半同步半阻塞半异步I/O模型在性能和可靠性方面较好的均衡了同步I/O和异步I/O的优缺点,因而在许多网络系统中失去了宽泛的利用。 nio是什么?NIO是“New I/O”的缩写,是一组Java API,提供非阻塞、可扩大和高性能的I/O操作。NIO在Java 1.4中被引入,作为传统的阻塞I/O(BIO)模型的替代品,用于解决高性能和高并发利用。NIO提供了若干要害个性,如通道、缓冲区、选择器和非阻塞I/O操作,使得Java应用程序可能更无效、高效和可扩大地解决I/O操作。NIO宽泛用于各种类型的应用程序,包含服务器、代理和其余网络应用程序。 NIO 的外围原理:**缓冲区:NIO 应用缓冲区(Buffer)作为数据容器,数据读写时都是通过缓冲区进行的。通道:NIO 应用通道(Channel)作为数据的读写入口,所有的数据读写操作都是通过通道实现的。选择器:NIO 应用选择器(Selector)来治理多个通道,当通道筹备好读写操作时,选择器可能疾速地发现并告诉相应的线程。** 在 NIO 中,线程不再须要阻塞期待 I/O 操作实现,而是在读写操作实现时由选择器告诉线程,这大大提高了零碎的效率。 java版代码import java.io.IOException;import java.net.InetSocketAddress;import java.nio.ByteBuffer;import java.nio.channels.SelectionKey;import java.nio.channels.Selector;import java.nio.channels.ServerSocketChannel;import java.nio.channels.SocketChannel;import java.util.Iterator;public class NioServer { public static void main(String[] args) throws IOException { Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open(); serverSocket.bind(new InetSocketAddress("localhost", 8080)); serverSocket.configureBlocking(false); serverSocket.register(selector, SelectionKey.OP_ACCEPT); // 示意示通道感兴趣的事件类型 SelectionKey.OP_ACCEPT,从而将通道注册到选择器中。 while (true) { selector.select(); Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey key = keys.next(); keys.remove(); if (!key.isValid()) { continue; } if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel client = server.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); // 将一个通道注册到选择器中 } else if (key.isReadable()) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(256); int read = client.read(buffer); if (read == -1) { client.close(); key.cancel(); continue; } buffer.flip(); client.write(buffer); } } } }}cpp版本#include <iostream>#include <string>#include <chrono>#include <thread>#include <vector>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <unistd.h>#include <fcntl.h>using namespace std;int set_nonblock(int fd) { int flags;#if defined(O_NONBLOCK) if (-1 == (flags = fcntl(fd, F_GETFL, 0))) { // 获取文件描述符 fd 的标记 flags = 0; } return fcntl(fd, F_SETFL, flags | O_NONBLOCK); // flags 和 O_NONBLOCK 标记进行按位或运算,从而将 O_NONBLOCK 标记增加到原有的标记中。设置文件描述符 fd 为非阻塞模式#else flags = 1; return ioctl(fd, FIOBIO, &flags); //文件描述符设置为非阻塞模式。ioctl 操作通常用于特定的设施驱动程序#endif}int main(int argc, char **argv) { int MasterSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); vector<int> SlaveSockets; sockaddr_in SockAddr; SockAddr.sin_family = AF_INET; SockAddr.sin_port = htons(12345); SockAddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(MasterSocket, (sockaddr *)&SockAddr, sizeof(SockAddr)); set_nonblock(MasterSocket); // 将MasterSocket设置成非阻塞状态 listen(MasterSocket, SOMAXCONN); while (true) { fd_set Set; FD_ZERO(&Set); /*将set清零使汇合中不含任何fd*/ FD_SET(MasterSocket, &Set); /*将MasterSocket退出set汇合*/ for (int Slave : SlaveSockets) { FD_SET(Slave, &Set); /*将Slave注册到选择器中*/ } int Max = max(MasterSocket, *max_element(SlaveSockets.begin(), SlaveSockets.end())); select(Max + 1, &Set, NULL, NULL, NULL); // 监督的最大文件描述符加1 if (FD_ISSET(MasterSocket, &Set)) { //*MasterSocket是否在set汇合中*/ int Slave = accept(MasterSocket, 0, 0); // 期待连贯 set_nonblock(Slave); //设置非阻塞 SlaveSockets.push_back(Slave); } for (int Slave : SlaveSockets) { if (FD_ISSET(Slave, &Set)) { static char Buffer[1024]; int RecvSize = recv(Slave, Buffer, 1024, MSG_NOSIGNAL); // 从而从套接字中接收数据并将其存储到 Buffer 缓冲区中。 if ((RecvSize == 0) && (errno != EAGAIN)) { shutdown(Slave, SHUT_RDWR); close(Slave); SlaveSockets.erase(remove(SlaveSockets.begin(), SlaveSockets.end(), Slave)); } else if (RecvSize > 0) { send(Slave, Buffer, RecvSize, MSG_NOSIGNAL); // 将 } } } } return 0;}摘要:java高并发编程 ...

February 12, 2023 · 2 min · jiezi

关于io:IO优化1

我的项目须要解决上游180g的gz文件,读取文件内容过滤去重后仍以gz格局传给上游,上面是几种解决思路. 1.间接应用linux less命令绕过压缩/解压缩,实测效率极差,放弃. 2.Java api GZIPOutputStream,待测试,预计体现不会太好 3.单线程bash gzip/gunzipgunzip耗时16分钟gzip耗时18分钟 2022-08-01 13:21 started2022-08-01 13:55 finished4.多线程 bash gzip/gunzipio操作的瓶颈应该在磁盘,感觉多线程效率不会高,决定做个测试.(脱敏伪代码) public class CopyFileMain { static Integer callShell(String command) { try { Process p = Runtime.getRuntime().exec(command); return p.waitFor(); } catch (Exception e) { // } return -1; } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); int fileSize = 3; String command = "bash gunzip xx.gz"; List<Future<?>> list = new ArrayList<>(); for (int i = 0; i < fileSize; i++) { Future<?> result = executorService.submit(() -> callShell(command)); list.add(result); } while (true) { boolean finished = false; for (Future<?> future : list) { if(!future.isDone()){ finished=false; break; } finished=true; } if(finished){ System.out.println("finished"); break; } } } shutdown();}gunzip耗时:13分钟临时得出后果: 瓶颈在磁盘io ...

August 2, 2022 · 1 min · jiezi

关于io:高级IO模型之kqueue和epoll

简介任何一个程序都离不开IO,有些是很显著的IO,比方文件的读写,也有一些是不显著的IO,比方网络数据的传输等。那么这些IO都有那些模式呢?咱们在应用中应该如何抉择呢?高级的IO模型kqueue和epoll是怎么工作的呢?一起来看看吧。 block IO和nonblocking IO大家先来理解一下IO模型中最简略的两个模型:阻塞IO和非阻塞IO。 比方咱们有多个线程要从一个Socket server中读取数据,那么这个读取过程其实能够分成两个局部,第一局部是期待socket的数据筹备结束,第二局部是读取对应的数据进行业务解决。对于阻塞IO来说,它的工作流程是这样的: 一个线程期待socket通道数据筹备结束。当数据筹备结束之后,线程进行程序处理。其余线程期待第一个线程完结之后,持续上述流程。为什么叫做阻塞IO呢?这是因为当一个线程正在执行的过程中,其余线程只能期待,也就是说这个IO被阻塞了。 什么叫做非阻塞IO呢? 还是下面的例子,如果在非阻塞IO中它的工作流程是这样的: 一个线程尝试读取socket的数据。如果socket中数据没有筹备好,那么立刻返回。线程持续尝试读取socket的数据。如果socket中的数据筹备好了,那么这个线程继续执行后续的程序处理步骤。为什么叫做非阻塞IO呢?这是因为线程如果查问到socket没有数据,就会立即返回。并不会将这个socket的IO操作阻塞。 从下面的剖析能够看到,尽管非阻塞IO不会阻塞Socket,然而因为它会始终轮询Socket,所以并不会开释Socket。 IO多路复用和selectIO多路复用有很多种模型,select是最为常见的一种。实时不论是netty还是JAVA的NIO应用的都是select模型。 select模型是怎么工作的呢? 事实上select模型和非阻塞IO有点类似,不同的是select模型中有一个独自的线程专门用来查看socket中的数据是否就绪。如果发现数据曾经就绪,select能够通过之前注册的事件处理器,抉择告诉具体的某一个数据处理线程。 这样的益处是尽管select这个线程自身是阻塞的,然而其余用来真正解决数据的线程却是非阻塞的。并且一个select线程其实能够用来监控多个socket连贯,从而进步了IO的解决效率,因而select模型被利用在多个场合中。 为了更加具体的理解select的原理,咱们来看一下unix下的select办法: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);先来解释一下这几个参数的含意,咱们晓得unix零碎中,所有的对象都是文件,所以这里的fd示意的就是file descriptor ,也就是文件描述符。 fds示意的是 file descriptor sets,也就是文件描述符汇合。 nfds是一个整数值,示意的是文件描述符汇合中最大值+1. readfds是要查看的文件读取的描述符汇合。 writefds是要查看的文件写入的描述符汇合。 errorfds是要查看的文件异样描述符汇合。 timeout是超时工夫,示意的是期待抉择实现的最大距离。 其工作原理是轮询所有的file descriptors,而后找到要监控的那些文件描述符, pollpoll和select类很相似,只是形容fd汇合的形式不同. poll次要是用在POSIX零碎中。 epoll实时上,select和poll尽管都是多路复用IO,然而他们都有些毛病。而epoll和kqueue就是对他们的优化。 epoll是linux零碎中的系统命令,能够将其看做是event poll。首次是在linux外围的2.5.44版本引入的。 次要用来监控多个file descriptors其中的IO是否ready。 对于传统的select和poll来说,因为须要一直的遍历所有的file descriptors,所以每一次的select的执行效率是O(n) ,然而对于epoll来说,这个工夫能够晋升到O(1)。 这是因为epoll会在具体的监控事件产生的时候触发告诉,所以不须要应用像select这样的轮询,其效率会更高。 epoll 应用红黑树 (RB-tree) 数据结构来跟踪以后正在监督的所有文件描述符。 epoll有三个api函数: int epoll_create1(int flags);用来创立一个epoll对象,并且返回它的file descriptor。传入的flags能够用来管制epoll的体现。 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);这个办法用来对epoll进行管制,能够用来监控具体哪些file descriptor和哪些事件。 ...

March 21, 2022 · 1 min · jiezi

关于io:性能分析之IO分析从IO高到具体文件

IO的性能剖析始终是性能剖析的重点之一,剖析的思路是:在代码的逻辑清晰的状况下,是齐全能够晓得哪些文件是频繁读写的。然而对性能剖析人员来说,通常是面对一个不是本人编写的零碎,有时还是多个团队单干产生的零碎。这时就会呈现很多的推诿和争执。如果能够迅速地把问题到一个段具体的代码,到一个具体的文件,那就能够进步沟通的效率。 通常状况在linux 环境下,通过 vmstat 或者 iostat 命令能够发现磁盘IO的异样,能够看到零碎级的磁盘读写量及CPU占用率,但无奈明确定位到是什么过程在作怪,装置iotop 后,能够定位到过程,但并不知道改过程在操作什么文件。本文是思考从零碎级的工具来实现这个操作,比拟具备通用性。在这之前须要先了解一下文件的一个重要的属性:inode。什么是inode呢?先来看一个示意图:磁盘上最小的存储单元是扇区sector,每8个扇区组成一个块block(4096字节)。如下所示:[root@7DGroup2 ~]# tune2fs -l /dev/vda1|grep BlockBlock count: 10485504Block size: 4096Blocks per group: 32768[root@7DGroup2 ~]#文件的存储就是由这些 块组成的,当块多了之后就成了如下这样(其实磁盘上的块比这个图中多得多,这里只是示意图):其中红色的这部分是存储的文件,咱们通常在文件系统中间接ls或者用其余命令操作文件的时候是依据门路来操作的,那些是下层的命令。当咱们执行了一个命令之后,操作系统会来找到这些文件做相应的操作,怎么找到这些文件呢,那就须要inode了。Inode用来存储这些文件的元信息,也就是索引节点,它包含的信息有:· 字节数· User ID· Group ID· 读、写、执行权限· 工夫戳,共有三个:ctime指inode上一次变动的工夫,mtime指文件内容上一次变动的工夫,atime指文件上一次关上的工夫· 链接数,有多少文件名指向这个inode· 文件数据block的地位通过这些信息,咱们能力实现对文件的操作。这个inode其实也是存储在磁盘上的,也须要占用一些空间,如上图中的绿色局部所示。当咱们在零碎级看到IO过高的时候,比方下图所示:从上图能够看到,这零碎简直所有的CPU都在等IO。这时怎么办?就用咱们后面提到的剖析的思路,查看过程级和线程级的IO,进而找到具体的文件。上面咱们来具体实现。这里咱们用的是systemtap,这个工具7Dgroup之前的文章中提到的,但没有开展说。前面如果有可能咱们再多写些相似的工具原理和应用办法。Systemtap的逻辑图如下:从逻辑图上看,它工作在内核层面,不是shell的层面。SystemTap为咱们开启了一扇通往零碎内核的大门,SystemTap 自带的examples中提供一些磁盘IO相干的监控例子。以 iotop.stp 为例,源码如下: #!/usr/bin/stapglobal reads, writes, total_ioprobe vfs.read.return {reads[execname()] += bytes_read}probe vfs.write.return {writes[execname()] += bytes_written}# print top 10 IO processes every 5 secondsprobe timer.s(5) {foreach (name in writes) total_io[name] += writes[name]foreach (name in reads) total_io[name] += reads[name]printf ("%16s\t%10s\t%10s\n", "Process", "KB Read", "KB Written")foreach (name in total_io- limit 10) printf("%16s\t%10d\t%10d\n", name, reads[name]/1024, writes[name]/1024)delete readsdelete writesdelete total_ioprint("\n")}执行的后果是:每隔5秒打印读写总量排前10位的过程。该脚本有两个问题:依照过程名字统计,存在统计误差,过程名统一,但PID不一样的过程,都统计到一起;咱们仍然不能晓得过程操作了什么文件。通过对probe点的剖析(sudo stap -L'vfs.{write,read}'),咱们能够晓得,vfs.read,vfs.write有局部变量 ino 能够利用,ino 是文件的inode,这样咱们就能够明确的探测到读写量最多的过程及文件。$ sudo stap -L 'vfs.{write,read}'vfs.read file:long pos:long buf:long bytes_to_read:long dev:long devname:string ino:long name:string argstr:string $file:struct file* $buf:char $count:size_t $pos:loff_tvfs.write file:long pos:long buf:long bytes_to_write:long dev:long devname:string ino:long name:string argstr:string $file:struct file* $buf:char const $count:size_t $pos:loff_t扩大过的脚本如下: ...

June 18, 2021 · 2 min · jiezi

关于io:6-io

一个过程的地址空间分为用户空间user space和内核空间kernel space。 利用程序运行在用户空间。内核空间进行零碎态级别的资源无关的操作,比方文件治理,过程通信,内存治理。 用户空间的程序不能间接拜访内核空间,想执行io操作的时候,只能发动零碎调用(io调用)申请操作系统的内核来执行io操作。 应用程序发动io调用后,1.内核期待io设施筹备好数据 2.内核将数据从内核空间拷贝到用户空间。 5种常见io模型 同步阻塞io 同步非阻塞io io多路复用 信号驱动io 异步io同步非阻塞:内核筹备数据、数据就绪时重复调用read,不阻塞。拷贝数据时阻塞。轮询非常耗费cpu资源。 java中常见的三种iobio blocking io属于同步阻塞io,应用程序发动read调用后,会始终阻塞,直到内核把数据拷贝到用户空间。nio non-blocking io属于io多路复用,线程先发动select调用,询问内核数据是否准备就绪,等内核把数据筹备好了,用户线程再发动read调用。read调用的过程还是阻塞的。目前反对 IO 多路复用的零碎调用,有 select,epoll 等等。IO 多路复用模型,通过缩小有效的零碎调用,缩小了对 CPU 资源的耗费。Java 中的 NIO ,有一个十分重要的选择器 ( Selector ) 的概念,也能够被称为 多路复用器。通过它,只须要一个线程便能够治理多个客户端连贯。当客户端数据到了之后,才会为其服务。 AIO (Asynchronous I/O)就是 NIO 2,是异步 IO 模型,基于事件和回调机制实现,利用操作之后会间接返回,不会梗塞在那里,当后盾解决实现,操作系统会告诉相应的线程进行后续的操作。

June 3, 2021 · 1 min · jiezi

关于io:彻底搞懂-IO-底层原理

武侠小说里有很多的“心法”和“招式”。计算机技术里的“心法”和“招式”呢,咱们能够简称为“道”和“术”; “道”  最根底的计算机实践,暗藏于表象之下,十分形象、艰涩难懂,须要用具象化的事物加以了解;“术” 具体的技能,它有可能是一门语言,比方:python 出手见效快; 咱们明天要给大家讲的底层的IO就属于“道”的领域,看上去简略,实则形象。并且在它之上衍生出了语言层面用于实战的技术,比方咱们相熟的java语言中的NIO或者像Netty这样的框架。 一、凌乱的 IO 概念IO是Input和Output的缩写,即输出和输入。狭义上的围绕计算机的输入输出有很多:鼠标、键盘、扫描仪等等。而咱们明天要探讨的是在计算机外面,次要是作用在内存、网卡、硬盘等硬件设施上的输入输出操作。 谈起IO的模型,大多数人脑子里必定是一坨凌乱的概念,“阻塞”、“非阻塞”,“同步”、“异步”有什么区别?很多同学傻傻分不清,有尝试去搜寻相干材料去探索假相,后果又被吞没在茫茫的概念之中。 这里尝试简略地去解释下为啥会呈现这种景象,其中一个很重要的起因就是大家看到的材料对概念的解释都站在了不同的角度,有的站在了底层内核的视角,有的间接在java层面或者Netty框架层面给大家介绍API,所以给大家造成了肯定水平的困扰。 所以在开篇之前,还是要说下本文所站的视角,咱们将会从底层内核的层面给大家解说下IO。因为万变不离其宗,只有理解了底层原理,不论语言层面如何花里胡哨,咱们都能以不变应万变。 二、用户空间和内核空间为了便于大家了解简单的IO以及零拷贝相干的技术,咱们还是得花点工夫在回顾下操作系统相干的常识。这一节咱们重点看下用户空间和内核空间,基于此前面咱们能力更好地聊聊多路复用和零拷贝。 硬 件 层(Hardware)  包含和咱们熟知的和IO相干的CPU、内存、磁盘和网卡几个硬件;内核空间(Kernel Space)  计算机开机后首先会运行内核程序,内核程序占用的一块公有的空间就是内核空间,并且可反对拜访CPU所有的指令集(ring0 - ring3)以及所有的内存空间、IO及硬件设施;用户空间(User Space) 每个一般的用户过程都有一个独自的用户空间,用户空间只能拜访受限的资源(CPU的“保护模式”)也就是说用户空间是无奈间接操作像内存、网卡和磁盘等硬件的;如上所述,那咱们可能会有疑难,用户空间的过程想要去拜访或操作磁盘和网卡该怎么办呢? 为此,操作系统在内核中开拓了一块惟一且非法的调用入口“System Call Interface”,也就是咱们常说的零碎调用,零碎调用为下层用户提供了一组可能操作底层硬件的API。这样一来,用户过程就能够通过零碎调用拜访到操作系统内核,进而就可能间接地实现对底层硬件的操作。这个拜访的过程也即用户态到内核态的切换。常见的零碎调用有很多,比方:内存映射mmap()、文件操作类的open()、IO读写read()、write()等等。 三、IO模型1、 BIO(Blocking IO)咱们先看一下大家都相熟的BIO模型的 Java 伪代码: ServerSocket serverSocket = new ServerSocket(8080); // step1: 创立一个ServerSocket,并监听8080端口while(true) { // step2: 主线程进入死循环 Socket socket = serverSocket.accept(); // step3: 线程阻塞,开启监听 BufferedReader reader = new BufferedReader(nwe InputStreamReader(socket.getInputStream())); System.out.println("read data: " + reader.readLine()); // step4: 数据读取 PrintWriter print = new PrintWriter(socket.getOutputStream(), true); print.println("write data"); // step5: socket数据写入}这段代码能够简略了解成一下几个步骤: ...

November 30, 2020 · 2 min · jiezi

关于io:Linux服务器开发监控之-IO

简介能够通过如下命令查看与 IO 相干的零碎信息。 tune2fs -l /dev/sda7 ← 读取superblock信息 # blockdev --getbsz /dev/sda7 ← 获取block大小tune2fs -l /dev/sda7 | grep "Block size" ← 同上 # dumpe2fs /dev/sda7 | grep "Block size" ← 同上 # stat /boot/ | grep "IO Block" ← 同上 # fdisk -l ← 硬盘的扇区大小(Sector Size) 在 WiKi 中的定义:A “block”, a contiguous number of bytes, is the minimum unit of memory that is read from and written to a disk by a disk driver。 ...

November 18, 2020 · 7 min · jiezi

关于io:IQKeyboardManager-源代码看看

IQKeyboardManager 三步走大家都用 IQKeyboardManager, IQKeyboardManager 引入,就治理好了 第 1 步,注册零碎告诉,取得键盘事件从键盘事件中,失去输出文本框对象, UITextField / UITextView 的实例 IQKeyboardManager 初始化的时候,就实现了这些 第 2 步,计算出以后文本框的地位, 并挪动有了文本框,要找到他以后的地位,frame 就要从文本框溯源,找到他的根视图控制器 而后计算出以后文本框在哪个地位适合, 挪动过来,就好了 2.1 , 计算出适合的地位先算出,该文本框在根视图的地位 再算出,该文本框在以后窗口, KeyWindow, 中的适合地位 2.2,键盘呈现,与键盘隐没开始编辑,键盘呈现,挪动地位 完结编辑,键盘隐没,还原地位 3,状况判断UIView 上搁置几个 UITextField / UITextView ,好解决 UIView 上搁置 UITableView, UITableView 上的一个 cell,下面摆放 UITextField / UITextView,就简单了一些 3.1 非凡类解决,对于 UIAlertController 的输入框,不必解决 比拟非凡的,还有 UITableViewController、UISearchBar、 _UIAlertControllerTextFieldViewController 0, 键盘治理,很简略对于一个输入框 UITextField , 搁置在 UIView 上, 键盘进去了,这个 UITextField 的地位,要适当, 通过两个告诉解决掉, 个别状况下,键盘进去,把 UITextField 地位放高一点, 键盘隐没,把 UITextField 地位放回原处 ...

November 8, 2020 · 5 min · jiezi

关于io:网络的5种IO模型以及零拷贝

一、什么是IOIO是输出input输入output的首字母缩写模式,直观意思是计算机输入输出,它形容的是计算机的数据流动的过程,因而IO第一大特色是有数据的流动;另外,对于一次IO,它到底是输出还是输入,是针对不同的主体而言的,不同的主体有不同的形容。然而对于一个Java程序员来说,咱们个别把程序当做IO的主体,也能够了解为内存中的过程。那么对于IO的整个过程大体上分为2个局部,第一个局部为IO的调用,第二个过程为IO的执行。IO的调用指的就是零碎调用,IO的执行指的是在内核中相干数据的处理过程,这个过程是由操作系统实现的,与程序员无关。 二、一些基本概念阻塞IO:申请过程始终期待IO准备就绪。非阻塞IO:申请过程不会期待IO准备就绪。同步IO操作:导致申请过程阻塞,晓得IO操作实现。异步IO操作:不导致申请过程阻塞。 举个小例子来了解阻塞,非阻塞,同步和异步的关系,咱们晓得编写一个程序能够有多个函数,每一个函数的执行都是互相独立的,然而 对于一个程序的执行过程,每一个函数都是必须的,那么如果咱们须要期待一个函数的执行完结而后返回一个后果(比方接口调用),那么咱们说该函数的调用是阻塞的,对于至多有一个函数调用阻塞的程序,在执行的过程中,必然存在阻塞的一个过程,那么咱们就说该程序的执行是同步的,对于异步天然就是所有的函数执行过程都是非阻塞的。这里的程序就是一次残缺的IO,一个函数为IO在执行过程中的一个独立的小片段。 咱们晓得在Linux操作系统中内存分为内核空间和用户空间,而所有的IO操作都得取得内核的反对,然而因为用户态的过程无奈间接进行内核的IO操作,所以内核空间提供了零碎调用,使得处于用户态的过程能够间接执行IO操作,IO调用的目标是将过程的外部数据迁徙到内部即输入,或将内部数据迁徙到过程外部即输出。而在这里探讨的数据通常是socket过程外部的数据。 三、5种IO模型1、首先咱们来看看一次网络申请中服务端做了哪些操作。在上图中,每一个客户端会与服务端建设一次socket连贯,而服务端获取连贯后,对于所有的数据的读取都得通过操作系统的内核,通过零碎调用内核将数据复制到用户过程的缓冲区,而后才实现客户端的过程与客户端的交互。那么依据零碎调用的形式的不同分为阻塞和非阻塞,依据零碎解决利用过程的形式不同分为同步和异步。 2、阻塞式IO每一次客户端产生的socket连贯实际上是一个文件描述符fd,而每一个用户过程读取的实际上也是一个个文件描述符fd,在该期间的零碎调用函数会期待网络申请的数据的达到和数据从内核空间复制到用户过程空间,也就是说,无论是第一阶段的IO调用还是第二阶段的IO执行都会阻塞,那么就像图中所画的一样,对于多个客户端连贯,只能开拓多个线程来解决。 3、非阻塞IO模型对于阻塞IO模型来说最大的问题就体现在阻塞2字上,那么为了解决这个问题,零碎的内核因而产生了扭转。在内核中socket反对了非阻塞状态。既然这个socket是不阻塞的了,那么就能够应用一个过程解决客户端的连贯,该过程外部写一个死循环,一直的询问每一个连贯的网络数据是否曾经达到。此时轮询产生在用户空间,然而该过程仍然须要本人解决所有的连贯,所以该期间为同步非阻塞IO期间,也即为NIO。 4、IO多路复用在非阻塞IO模型中,尽管解决了IO调用阻塞的问题,然而产生了新的问题,如果当初有1万个连贯,那么用户线程会调用1万次的零碎调用read来进行解决,在用户空间这种开销太大,那么当初须要解决这个问题,思路就是让用户过程缩小零碎调用,然而用户本人是实现不了的,所以这就导致了内核产生了进一步变动。在内核空间中帮忙用户过程遍历所有的文件描述符,将数据筹备好的文件描述符返回给用户过程。该形式是同步阻塞IO,因为在第一阶段的IO调用会阻塞过程。 4.1、select/poll为了让内核帮忙用户过程实现文件描述符的遍历,内核减少了零碎调用select/poll(select与poll实质上没有什么不同,就是poll缩小了文件描述符的个数限度),当初用户过程只须要调用select零碎调用函数,并且将文件描述符全副传递给select就能够让内核帮忙用户过程实现所有的查问,而后将数据筹备好的文件描述符再返回给用户过程,最初用户过程顺次调用其余零碎调用函数实现IO的执行过程。 4.2、epoll在select实现的多路复用中仍然存在一些问题。 1、用户过程须要传递所有的文件描述符,而后内核将数据筹备好的文件描述符再次传递回去,这种数据的拷贝升高了IO的速度。2、内核仍然会执行复杂度为O(n)的被动遍历操作。对于第一个问题,提出了一个共享空间的概念,这个空间为用户过程和内核过程所共享,并且提供了mmap零碎调用,实现用户空间和内核空间到共享空间的映射,这样用户过程就能够将1万个文件描述符写到共享空间中的红黑树上,而后内核将准备就绪的文件描述符写入共享空间的链表中,而用户过程发现链表中有数据了就间接读取而后调用read执行IO即可。 对于第二个问题,内核引入了事件驱动机制(相似于中断),不再被动遍历所有的文件描述符,而是通过事件驱动的形式被动告诉内核该文件描述符的数据筹备结束了,而后内核就将其写入链表中即可。 对于epoll来说在第一阶段的epoll_wait仍然是阻塞的,故也是同步阻塞式IO。 5、信号驱动式IO在IO执行的数据筹备阶段,不会阻塞用户过程。当用户过程须要期待数据的时候,会向内核发送一个信号,通知内核须要数据,而后用户过程就持续做别的事件去了,而当内核中的数据筹备好之后,内核立马发给用户过程一个信号,用户过程收到信号之后,立马调用recvfrom,去查收数据。该IO模型应用的较少。 6、异步IO(AIO)利用过程通过 aio_read 告知内核启动某个操作,并且在整个操作实现之后再告诉利用过程,包含把数据从内核空间拷贝到用户空间。信号驱动 IO 是内核告诉咱们何时能够启动一个 IO 操作,而异步 IO 模型是由内核告诉咱们 IO 操作何时实现。是真正意义上的无阻塞的IO操作,然而目前只有windows反对AIO,linux内核临时不反对。 四、总结前四种模型的次要区别于第一阶段,因为他们的第二阶段都是一样的:在数据从内核拷贝到利用过程的缓冲区期间,过程都会阻塞。相同,异步 IO 模型在这两个阶段都不会阻塞,从而不同于其余四种模型。 五、间接内存与零拷贝间接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机标准中农定义的内存区域。间接内存申请空间消耗更高的性能,间接内存IO读写的性能要优于一般的堆内存,对于java程序来说,零碎内核读取堆类的对象须要依据代码段计算其偏移量来获取对象地址,效率较慢,不太适宜网络IO的场景,对于间接内存来说更加适宜IO操作,内核读取寄存在间接内存中的对象较为不便,因为其地址就是袒露的过程虚拟地址,不须要jvm翻译。那么就能够应用mmap开拓一块间接内存mapbuffer和内核空间共享,并且该间接内存能够间接映射到磁盘上的文件,这样就能够通过调用本地的put而不必调用零碎调用write就能够将数据间接写入磁盘,RandomAccessFile类就是通过开拓mapbuffer实现的读写磁盘。 以音讯队列Kafka来说,有生产者和消费者,对于生产者,从网络发来一个音讯msg并且被拷贝到内核缓冲区,该音讯通过Kafka调用recvfrom将内核中的msg读到队列中,而后加上音讯头head,再将该音讯写入磁盘。如果没有mmap的话,就会调用一个write零碎调用将该音讯写入内核缓冲区,而后内核将该音讯再写入磁盘。在此过程中呈现一次80中断和2次拷贝。但实际上Kafka应用的是mmap开拓了间接内存到磁盘的映射,间接应用put将音讯写入磁盘。实际上也是通过内核拜访该共享区域将该音讯写入的磁盘。同时在Kafka中有一个概念叫segment,个别为1G大小。它会充分利用磁盘的程序性,只追加数据,不批改数据。而mmap会间接开拓1G的间接内存,并且间接与segment造成映射关系,在segment满了的时候再开拓一个新的segment,清空间接内存而后在与新的segment造成映射关系。 零拷贝形容的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的工作,这通常用于通过网络传输一个文件时以缩小CPU周期和内存带宽。 在Kafka的消费者读取数据的时候,如果以后消费者想读取的数据是不是以后间接内存所映射的segment怎么办?如果没有零拷贝的话,过程会先去调用read读取,而后数据会从磁盘被拷贝到内核,而后内核再拷贝到Kafka队列,过程再调用write将数据拷贝到内核缓冲区,最初再发送给消费者。实际上能够发现,数据没有必要读到Kafka队列,间接读到内核的缓冲区的时候发送给消费者就行了。实际上,linux内核中有一个零碎调用就是实现了这种形式读取数据——sendfile,它有2个参数,一个是infd(读取数据的文件描述符),一个是outfd(客户端的socket文件描述符).消费者只需调用该函数,通知它须要读取那个文件就能够不通过Kafka间接将数据读到内核,而后由内核写到消费者过程的缓冲区中。

October 6, 2020 · 1 min · jiezi

Go使用压缩文件优化io-二

原文链接: https://blog.thinkeridea.com/... 上一篇文章《使用压缩文件优化io (一)》中记录了日志备份 io 优化方案,使用文件流数据压缩方案优化 io 性能,效果十分显著。这篇文章记录数据分析前置清洗、格式化数据的 io 优化方案,我们有一台专用的日志前置处理服务器,所有业务日志通过这台机器从 OSS 拉取回来清洗、格式化,最后进入到数据仓储中便于后续的分析。 随着业务扩展这台服务器压力越来越大,高峰时数据延迟越来越厉害,早期也是使用 Python 脚本 + awk 以及一些 shell 命令完成相关工作,在数据集不是很大的时候这种方案很好,效率也很高,随着数据集变大,发现服务器负载很高,经过分析是还是 io 阻塞,依旧采用对数据流进行处理的方案优化io,以下记录优化的过程。 背景介绍服务器配置:4 核 8G; 磁盘:1T分析前置服务会根据业务不同分为十分钟、一小时两个阶段拉取分析日志,每隔一个阶段会去 OSS 拉取日志回到服务器进行处理,处理过程因 io 阻塞,导致 CPU 和 load 异常高,且处理效率严重下降,这次优化主要就是降低 io 阻塞,提升 CPU 利用率 (处理业务逻辑而不是等待 io) 和处理效率。 后文中会详细描述优化前后的方案,并用 go 编写测试,使用一台 2 核4G的服务器进行测试,测试数据集大小为: 文件数量:432个压缩文件:17G解压后文件:63G压缩方案:lzoGoroutine 数量:20优化前优化前日志处理流程: 获取待处理文件列表拉取 OSS 日志到本地磁盘 (压缩文件)解压缩日志文件读取日志数据业务处理……导入到数据仓储中导致 io 阻塞的部分主要是: 拉取 OSS 日志、解压缩日志文件及读取日志数据,优化也主要从这三块着手。 这里有一段公共的日志读取方法,该方法接收一个 io.Reader, 并按行读取日志,并简单切分日志字段,并没有实质的处理日志数据,后面的优化方案也将使用这个方法读取日志。 package mainimport ( "bufio" "bytes" "io" "github.com/thinkeridea/go-extend/exbytes")func Read(r io.Reader) { rawBuffer := make([]byte, 512) buf := bufio.NewReader(r) for { line, ok, err := readLine(buf, rawBuffer) if err == io.EOF { return } if err != nil { panic(nil) } if ok { rawBuffer = line } c := bytes.Count(line, []byte{'\x01'}) if c != 65 { panic("无效的行") } }}func readLine(r *bufio.Reader, rawBuffer []byte) ([]byte, bool, error) { var ok bool line, err := r.ReadSlice('\n') if (err == bufio.ErrBufferFull || len(line) < 3 || exbytes.ToString(line[len(line)-3:]) != "\r\r\n") && err != io.EOF { rawBuffer = append(rawBuffer[:0], line...) for (err == bufio.ErrBufferFull || len(line) < 3 || exbytes.ToString(line[len(line)-3:]) != "\r\r\n") && err != io.EOF { line, err = r.ReadSlice('\n') rawBuffer = append(rawBuffer, line...) } line = rawBuffer ok = true } if len(line) > 0 && err == io.EOF { err = nil } return line, ok, err}日志按 \r\r\n 分隔行,使用 \x01 切分字段,读取方法使用 bufio.ReadSlice 方法,避免内存分配,且当 bufio 缓冲区满之后使用 rwaBuffer 作为本地可扩展缓冲,每次扩展之后会保留最大的扩展空间,因为业务日志每行大小差不多,这样可以极大的减少内存分配,效率是 bufio.ReadLine 方法的好几倍。 ...

July 8, 2019 · 6 min · jiezi

Go使用压缩文件优化io-一

原文连接:https://blog.thinkeridea.com/... 最近遇到一个日志备份 io 过高的问题,业务日志每十分钟备份一次,本来是用 Python 写一个根据规则扫描备份日志问题不大,但是随着业务越来越多,单机上的日志文件越来越大,文件数量也越来越多,导致每每备份的瞬间 io 阻塞严重, CPU 和 load 异常的高,好在备份速度很快,对业务影响不是很大,这个问题会随着业务增长,越来越明显,这段时间抽空对备份方式做了优化,效果十分显著,整理篇文章记录一下。 背景说明服务器配置:4 核 8G; 磁盘:500G每十分钟需要上传:18 个文件,高峰时期约 10 G 左右 业务日志为了保证可靠性,会先写入磁盘文件,每10分钟切分日志文件,然后在下十分钟第一分时备份日志到 OSS,数据分析服务会从在备份完成后拉取日志进行分析,日志备份需要高效快速,在最短的时间内备份完,一般备份均能在几十秒内完成。 备份的速度和效率并不是问题,足够的快,但是在备份时 io 阻塞严重导致的 CPU 和 load 异常,成为业务服务的瓶颈,在高峰期业务服务仅消耗一半的系统资源,但是备份时 CPU 经常 100%,且 iowait 可以达到 70 多,空闲资源非常少,这样随着业务扩展,日志备份虽然时间很短,却成为了系统的瓶颈。 后文中会详细描述优化前后的方案,并用 go 编写测试,使用一台 2 核4G的服务器进行测试,测试数据集大小为: 文件数:336原始文件:96G压缩文件:24G压缩方案:lzoGoroutine 数量:4优化前优化前日志备份流程: 根据备份规则扫描需要备份的文件使用 lzop 命令压缩日志上传压缩后的日志到 OSS下面是代码实现,这里不再包含备份文件规则,仅演示压缩上传逻辑部分,程序接受文件列表,并对文件列表压缩上传至 OSS 中。 .../pkg/aliyun_oss 是我自己封装的基于阿里云 OSS 操作的包,这个路径是错误的,仅做演示,想运行下面的代码,OSS 交互这部分需要自己实现。 package mainimport ( "bytes" "fmt" "os" "os/exec" "path/filepath" "sync" "time" ".../pkg/aliyun_oss")func main() { var oss *aliyun_oss.AliyunOSS files := os.Args[1:] if len(files) < 1 { fmt.Println("请输入要上传的文件") os.Exit(1) } fmt.Printf("待备份文件数量:%d\n", len(files)) startTime := time.Now() defer func(startTime time.Time) { fmt.Printf("共耗时:%s\n", time.Now().Sub(startTime).String()) }(startTime) var wg sync.WaitGroup n := 4 c := make(chan string) // 压缩日志 wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() for file := range c { cmd := exec.Command("lzop", file) cmd.Stderr = &bytes.Buffer{} err := cmd.Run() if err != nil { panic(cmd.Stderr.(*bytes.Buffer).String()) } } }() } for _, file := range files { c <- file } close(c) wg.Wait() fmt.Printf("压缩耗时:%s\n", time.Now().Sub(startTime).String()) // 上传压缩日志 startTime = time.Now() c = make(chan string) wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() for file := range c { name := filepath.Base(file) err := oss.PutObjectFromFile("tmp/"+name+".lzo", file+".lzo") if err != nil { panic(err) } } }() } for _, file := range files { c <- file } close(c) wg.Wait() fmt.Printf("上传耗时:%s\n", time.Now().Sub(startTime).String())}程序运行时输出: ...

June 30, 2019 · 6 min · jiezi

go-bufio-缓冲读写详解级实例

go在提供了io包的同时也提供了bufio包来实现有缓存的读写操作以提高读写性能。为什么bufio性能比io高呢? 缓冲读写缓冲读// 默认缓冲区大小const ( defaultBufSize = 4096)// 最小缓冲区大小 自定义小于次阈值将会被覆盖const minReadBufferSize = 16// 使用默认缓冲区大小bufio.NewReader(rd io.Reader)// 使用自定义缓冲区大小bufio.NewReaderSize(rd io.Reader, size int)缓冲读的大致过程如下,设定好缓冲区大小buf_size后,读取的字节数为rn,缓冲的字节数为bn: 如果缓冲区为空,且 rn >= buf_size,则直接从文件读取,不启用缓冲。如果缓冲区为空,且 rn < buf_size,则从文件读取buf_size 字节的内容到缓冲区,程序再从缓冲区中读取rn字节的内容,此时缓冲区剩余bn = buf_size - rn字节。如果缓冲区不为空,rn < bn,则从缓冲区读取rn字节的内容,不发生文件IO。如果缓冲区不为空,rn >= bn,则从缓冲区读取bn字节的内容,不发生文件IO,缓冲区置为空,回归1/2步骤。缓冲读通过预读,可以在一定程度上减少文件IO次数,故提高性能。 代码演示: package mainimport ( "bufio" "fmt" "strings")func main() { // 用 strings.Reader 模拟一个文件IO对象 strReader := strings.NewReader("12345678901234567890123456789012345678901234567890") // go 的缓冲区最小为 16 byte,我们用最小值比较容易演示 bufReader := bufio.NewReaderSize(strReader, 16) // bn = 0 但 rn >= buf_size 缓冲区不启用 发生文件IO tmpStr := make([]byte, 16) n, _ := bufReader.Read(tmpStr) // bufReader buffered: 0, content: 1234567890123456 fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n]) // bn = 0 rn < buf_size 缓冲区启用 // 缓冲区从文件读取 buf_size 字节 发生文件IO // 程序从缓冲区读取 rn 字节 // 缓冲区剩余 bn = buf_size - rn 字节 tmpStr = make([]byte, 15) n, _ = bufReader.Read(tmpStr) // bufReader buffered: 1, content: 789012345678901 fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n]) // bn = 1 rn > bn // 程序从缓冲区读取 bn 字节 缓冲区置空 不发生文件IO // 注意这里只能读到一个字节 tmpStr = make([]byte, 10) n, _ = bufReader.Read(tmpStr) // bufReader buffered: 0, content: 2 fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n]) // bn = 0 rn < buf_size 启用缓冲读 发生文件IO // 缓冲区从文件读取 buf_size 字节 // 程序从缓冲区读取 rn 字节 // 缓冲区剩余 bn = buf_size - rn 字节 tmpStr = make([]byte, 10) n, _ = bufReader.Read(tmpStr) // bufReader buffered: 6, content: 3456789012 fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n]) // bn = 6 rn <= bn // 则程序冲缓冲区读取 rn 字节 不发生文件IO tmpStr = make([]byte, 3) n, _ = bufReader.Read(tmpStr) // bufReader buffered: 3, content: 345 fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n]) // bn = 3 rn <= bn // 则程序冲缓冲区读取 rn 字节 不发生文件IO tmpStr = make([]byte, 3) n, _ = bufReader.Read(tmpStr) // bufReader buffered: 0, content: 678 fmt.Printf("bufReader buffered: %d, content: %s\n", bufReader.Buffered(), tmpStr[:n])}要注意的是当缓冲区中有内容时,程序的此次读取都会从缓冲区读,而不会发生文件IO。只有当缓冲区为空时,才会发生文件IO。如果缓冲区的大小足够,则启用缓冲读,先将内容载入填满缓冲区,程序再从缓冲区中读取。如果缓冲区过小,则会直接从文件读取,而不使用缓冲读。 ...

June 15, 2019 · 3 min · jiezi

BIO与AIO模型在JDK实现Netty序章

BIO编程回顾下Linux下阻塞IO模型: 再看看Java的BIO编程模型: /** * 类说明:客户端 */public class BioClient { public static void main(String[] args) throws InterruptedException, IOException { //通过构造函数创建Socket,并且连接指定地址和端口的服务端 Socket socket = new Socket(DEFAULT_SERVER_IP,DEFAULT_PORT); System.out.println("请输入请求消息:"); //启动读取服务端输出数据的线程 new ReadMsg(socket).start(); PrintWriter pw = null; //允许客户端在控制台输入数据,然后送往服务器 while(true){ pw = new PrintWriter(socket.getOutputStream()); pw.println(new Scanner(System.in).next()); pw.flush(); } } //读取服务端输出数据的线程 private static class ReadMsg extends Thread { Socket socket; public ReadMsg(Socket socket) { this.socket = socket; } @Override public void run() { //负责socket读写的输入流 try (BufferedReader br = new BufferedReader( new InputStreamReader(socket.getInputStream()))){ String line = null; //通过输入流读取服务端传输的数据 //如果已经读到输入流尾部,返回null,退出循环 //如果得到非空值,就将结果进行业务处理 while((line=br.readLine())!=null){ System.out.printf("%s\n",line); } } catch (SocketException e) { System.out.printf("%s\n", "服务器断开了你的连接"); } catch (Exception e) { e.printStackTrace(); } finally { clear(); } } //必要的资源清理工作 private void clear() { if (socket != null) try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } }}/** * 类说明:bio的服务端主程序 */public class BioServer { //服务器端必须 private static ServerSocket server; //线程池,处理每个客户端的请求 private static ExecutorService executorService = Executors.newFixedThreadPool(5); private static void start() throws IOException{ try{ //通过构造函数创建ServerSocket //如果端口合法且空闲,服务端就监听成功 server = new ServerSocket(DEFAULT_PORT); System.out.println("服务器已启动,端口号:" + DEFAULT_PORT); while(true){ Socket socket= server.accept(); System.out.println("有新的客户端连接----" ); //当有新的客户端接入时,打包成一个任务,投入线程池 executorService.execute(new BioServerHandler(socket)); } }finally{ if(server!=null){ server.close(); } } } public static void main(String[] args) throws IOException { start(); }}/** * 类说明: */public class BioServerHandler implements Runnable{ private Socket socket; public BioServerHandler(Socket socket) { this.socket = socket; } public void run() { try(//负责socket读写的输出、输入流 BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true)){ String message; String result; //通过输入流读取客户端传输的数据 //如果已经读到输入流尾部,返回null,退出循环 //如果得到非空值,就将结果进行业务处理 while((message = in.readLine())!=null){ System.out.println("Server accept message:"+message); result = response(message); //将业务结果通过输出流返回给客户端 out.println(result); } }catch(Exception e){ e.printStackTrace(); }finally{ if(socket != null){ try { socket.close(); } catch (IOException e) { e.printStackTrace(); } socket = null; } } }}过程: ...

June 9, 2019 · 5 min · jiezi

Linux网络IO模型

Linux网络IO模型 阻塞IO模型 1.当应用程序发起网络IO的请求时会调用操作系统recvfrom函数2.等待系统内核准备数据报(获取网络数据:网卡把数据拷贝到内核)3.数据报准备好后将数据从内核拷贝到用户空间,即应用程序空间非阻塞IO模型 一般很少用这个非阻塞模型,因为反复调用消耗CPU IO复用模型IO复用模型是linux下用的最多的,也就是JDK中的NIO。 特点: 对于某一个应用进程而言,或者说在某一次网络通信,它还是阻塞的,但是可以同时服务于多个网络通信。多个select函数即多个socket请求,会挂在内核中,操作系统会轮询检查数据报是否准备好再返回可读条件即就绪,应用程序再发起recefrom函数拷贝内核中的数据.select和epoll;对一个socket,两次调用,两次返回,比阻塞IO并没有什么优越性; 关键是能实现同时对多个socket进行处理。 信号驱动IO---(相对少用) 异步IO模型 当一个异步过程调用发出后,系统直接返回,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。 五种IO模型的比较: 除异步IO模型,前面四种IO模型第二阶段都是相同的,阻塞于recefrom调用。 参考书籍:《UNIX网络编程》

June 5, 2019 · 1 min · jiezi

java-中-IO-操作总结

代码java IO 操作分析package jdk.util.sourceCode;import java.io.*;/** * 经常会遇到各种 IO 流操作,IO 流操作一般分为两类:字符流和字节流。 * 以 "Reader" 结尾都是字符流,操作的都是字符型的数据 * 以 "Stream" 结尾的都是字节流,操作的都是 byte 类型的数据 * 二者的区别: * 字节流没有缓冲区,是直接输出的;而字符流是先输出到缓冲区,然后在调用 close() 方法后再输出信息 * 处理对象不同,字节流能处理所有类型的数据,但是字符流只能处理字符类型的数据(只要是处理纯文本数据,就优先考虑使用字符流,除此之外都使用字节流) * java byte -> short -> int -> long 1byte -> 2byte -> 4byte -> 8byte * * * * InputStream 和 OutputStream 是各种输入输出字节流的基类,所有字节流都继承于这两个基类 * * * FileInputStream 和 FileOutputStream 这两个从字面意思很容易理解,是对文件的字节流操作,也是最常见的 IO 操作流 * * * 非流式文件类 -- File 类 * 从定义来看,File 类是 Object 的直接子类,同时它继承了 Comparable 接口可以进行数组的排序 * File 类的操作包括文件的创建,删除,重命名,得到文件/文件夹的路径,创建时间等 * File 类是对文件系统中文件以及文件夹进行封装的一个对象,可以通过对象的思想来操作文件和文件夹 * //** * @author: util.you.com@gmail.com * @date: 2019/5/25 15:40 * @description: * @version: 1.0 * @className: TestIO */public class TestIO { public static void main(String[] args){ // 1.调用 新建文件// createFile("F:\\github\\util.you.com@gmail.com\\jdk\\src\\main\\java\\jdk\\util\\sourceCode\\", "测试io.txt"); // 2.调用删除文件// deleteFile("F:\\github\\util.you.com@gmail.com\\jdk\\src\\main\\java\\jdk\\util\\sourceCode\\","测试io.txt"); // 3.调用创建文件夹// createFolder("F:\\github\\util.you.com@gmail.com\\jdk\\src\\main\\java\\jdk\\util\\sourceCode\\", "测试io文件夹"); // 4.列出指定目录下面的所有文件,包括隐藏文件// listFiles("F:\\github\\util.you.com@gmail.com\\jdk\\src\\main\\java\\jdk\\util\\sourceCode\\"); // 5.判断指定的 文件夹是否是一个 目录(即是否是一个 文件夹)// isFolder("F:\\\\github\\\\util.you.com@gmail.com\\\\jdk\\\\src\\\\main\\\\java\\\\jdk\\\\util\\\\sourceCode\\", "测试io文件夹"); // 6. 向指定的文件中(需要在文件名中给出路径和文件名,我这里是为了简便这样写了)通过 字节流 写入数据 (这里前提:认为该文件已经存在,不需要再创建)// writeFileByByte("F:\\github\\util.you.com@gmail.com\\jdk\\src\\main\\java\\jdk\\util\\sourceCode\\测试io.txt"); // 7.从指定的文件中读取内容// readFileByByte("F:\\github\\util.you.com@gmail.com\\jdk\\src\\main\\java\\jdk\\util\\sourceCode\\测试io.txt"); // 8. 从 指定文件读取内容并写入到 目标文件 readWriteFile("F:\\game\\xx.mp4", "E:\\github-project\\jdk\\src\\main\\java\\jdk\\util\\sourceCode\\测试io.txt"); } /** * 因为 io 流基本是与 File(文件/文件夹) 操作密不可分的,因此 io 的操作,只要涉及到文件,文件夹的都必须使用 File 类 * 在指定的路径下,新建一个 指定文件名的 文件 * @param path 文件路径 * @param fileName 文件名 */ public static void createFile(String path, String fileName){ // 因为是在 操作 文件,所以用到 File 对象【记住:所有与文件/文件夹操作相关的内容,都必须第一时间想到要用 File 对象】 File file = new File(path+fileName); // 实例化一个 file 操作对象 try { file.createNewFile(); // 调用 file 文件/文件夹 实例对象的 方法,来新建文件 System.out.println("目标文件已存在: " + path + fileName); } catch (IOException e) { e.printStackTrace(); } } /** * 删除一个指定路径下的 文件 * @param path 该文件的路径 * @param fileName 该文件的文件名 */ public static void deleteFile(String path, String fileName){ File file = new File(path+fileName); if(file.exists()){ file.delete(); System.out.println("目标文件已删除"); }else{ System.out.println("要删除的目标文件不存在"); } } /** * 新建一个 文件夹 * @param path 路径 * @param folderName 文件夹名 */ public static void createFolder(String path, String folderName){ File file = new File(path+folderName); file.mkdir(); System.out.println("该文件夹已经存在于: " + path + folderName); } /** * 列出指定目录下面的所有文件 * @param path 目录的路径名 */ public static void listFiles(String path){ File file = new File(path); if (file.isDirectory()){ File[] fileArray = file.listFiles(); for (int i = 0; i < fileArray.length; i++){ System.out.println( "该目录下的文件: " + fileArray[i]); System.out.println( "该目录下的文件或文件夹的名字: " + fileArray[i].getName()); } }else{ System.out.println(path + " 目录不存在"); } } /** * 判断给定的 文件夹 是否是一个目录 * @param path */ public static void isFolder(String path, String folderName){ File file = new File(path + folderName); if (file.isDirectory()){ System.out.println(path + folderName + " 是一个目录"); }else{ System.out.println(path + folderName + " 不是一个目录"); } } /** * 通过 字节流 向 指定文件 写入内容 * @param fileName 文件名,这里为了简化,文件名中带上 路径 */ public static void writeFileByByte(String fileName){ File file = new File(fileName); OutputStream outputStream = null; // 从内存中 写入内容 到 文件中,这是输出流,因此要用 输出流 // FileOutputStream 的构造器大体上有两类:一类是 传入一个带有文件名和文件路径的字符串;另一类是 传入一个 File 文件/文件夹对象 try { outputStream = new FileOutputStream(file, true); // 给 file 文件对象 构造一个字节输出流 } catch (FileNotFoundException e) { e.printStackTrace(); } // 这里穿插一个小知识点,即我们 给一个 int 参数,但是我们要让 outputStream 以 byte[] 的形式写入,接下来就看 int 转 byte[] 吧 int a = 12345678; // 为什么这样呢?因为 一个 int 是 4个byte,所以一个 int 转成 byte[] 后,一定是里面包含4个byte元素的 byte[] 数组 byte[] b = new byte[]{ (byte) ((a >> 24) & 0xFF), (byte) ((a >> 16) & 0xFF), (byte) ((a >> 8) & 0xFF), (byte) ((a ) & 0xFF) }; try { outputStream.write(b); // 这里还有一个问题没解决:写入的时候,选择编码格式(稍后解决) outputStream.close(); }catch (IOException e) { e.printStackTrace(); } } /** * 通过 字节流 从 指定文件 读取输出内容 * @param fileName 文件名,这里为了简化,文件名中带上 路径 */ public static void readFileByByte(String fileName){ File file = new File(fileName); InputStream inputStream = null; // 从 硬盘中 读取内容 到 内存中,这是 输入流,因此声明 输入流 对象 try { inputStream = new FileInputStream(file); // inputStream 读取内容有5个方法 read():默认读取一个byte,readBytes(byte b[], int off, int len) // 这里我们采用 read(byte b[], int off, int len) 方法 byte[] byter = new byte[1024]; // 所以先实例化一个 byte[] int len = inputStream.read(byter); inputStream.close(); // 最后我们输出一下读取到的内容 System.out.println(new String(byter, 0, len)); } catch (Exception e) { e.printStackTrace(); } } /** * @author: util.you.com@gmail.com * @param: [sourceFile, desFile] * @return: void * @date: 2019/5/25 18:04 * @version: 1.0 * @description: 最后来一个 从 指定文件中 读取内容 到 指定目标文件中 */ public static void readWriteFile(String sourceFile, String desFile){ File inputFile = new File(sourceFile); File outputFile = new File(desFile); InputStream inputStream = null; OutputStream outputStream = null; try { inputStream = new FileInputStream(inputFile); byte[] byter = new byte[1024]; inputStream.read(byter); outputStream = new FileOutputStream(outputFile, true); outputStream.write(byter); outputStream.close(); inputStream.close(); System.out.println("操作完成"); }catch (Exception e){ System.out.println(e.getMessage()); } }}声明原创手敲,by 拉丁小毛

May 25, 2019 · 4 min · jiezi

PHP并发IO编程之路

并发 IO 问题一直是服务器端编程中的技术难题,从最早的同步阻塞直接 Fork 进程,到 Worker 进程池/线程池,到现在的异步IO、协程。PHP 程序员因为有强大的 LAMP 框架,对这类底层方面的知识知之甚少,本文目的就是详细介绍 PHP 进行并发 IO 编程的各种尝试,最后再介绍 Swoole 的使用,深入浅出全面解析并发 IO 问题。多进程/多线程同步阻塞最早的服务器端程序都是通过多进程、多线程来解决并发IO的问题。进程模型出现的最早,从 Unix 系统诞生就开始有了进程的概念。最早的服务器端程序一般都是 Accept 一个客户端连接就创建一个进程,然后子进程进入循环同步阻塞地与客户端连接进行交互,收发处理数据。多线程模式出现要晚一些,线程与进程相比更轻量,而且线程之间是共享内存堆栈的,所以不同的线程之间交互非常容易实现。比如聊天室这样的程序,客户端连接之间可以交互,比聊天室中的玩家可以任意的其他人发消息。用多线程模式实现非常简单,线程中可以直接向某一个客户端连接发送数据。而多进程模式就要用到管道、消息队列、共享内存,统称进程间通信(IPC)复杂的技术才能实现。代码实例:多进程/线程模型的流程是创建一个 socket,绑定服务器端口(bind),监听端口(listen),在PHP中用stream_socket_server一个函数就能完成上面3个步骤,当然也可以使用更底层的sockets扩展分别实现。进入while循环,阻塞在accept操作上,等待客户端连接进入。此时程序会进入睡眠状态,直到有新的客户端发起connect到服务器,操作系统会唤醒此进程。accept函数返回客户端连接的socket主进程在多进程模型下通过fork(php: pcntl_fork)创建子进程,多线程模型下使用pthread_create(php: new Thread)创建子线程。下文如无特殊声明将使用进程同时表示进程/线程。子进程创建成功后进入while循环,阻塞在recv(php: fread)调用上,等待客户端向服务器发送数据。收到数据后服务器程序进行处理然后使用send(php: fwrite)向客户端发送响应。长连接的服务会持续与客户端交互,而短连接服务一般收到响应就会close。当客户端连接关闭时,子进程退出并销毁所有资源。主进程会回收掉此子进程。这种模式最大的问题是,进程/线程创建和销毁的开销很大。所以上面的模式没办法应用于非常繁忙的服务器程序。对应的改进版解决了此问题,这就是经典的 Leader-Follower 模型。代码实例:它的特点是程序启动后就会创建N个进程。每个子进程进入 Accept,等待新的连接进入。当客户端连接到服务器时,其中一个子进程会被唤醒,开始处理客户端请求,并且不再接受新的TCP连接。当此连接关闭时,子进程会释放,重新进入 Accept ,参与处理新的连接。这个模型的优势是完全可以复用进程,没有额外消耗,性能非常好。很多常见的服务器程序都是基于此模型的,比如 Apache 、PHP-FPM。多进程模型也有一些缺点。这种模型严重依赖进程的数量解决并发问题,一个客户端连接就需要占用一个进程,工作进程的数量有多少,并发处理能力就有多少。操作系统可以创建的进程数量是有限的。启动大量进程会带来额外的进程调度消耗。数百个进程时可能进程上下文切换调度消耗占CPU不到1%可以忽略不计,如果启动数千甚至数万个进程,消耗就会直线上升。调度消耗可能占到 CPU 的百分之几十甚至 100%。另外有一些场景多进程模型无法解决,比如即时聊天程序(IM),一台服务器要同时维持上万甚至几十万上百万的连接(经典的C10K问题),多进程模型就力不从心了。还有一种场景也是多进程模型的软肋。通常Web服务器启动100个进程,如果一个请求消耗100ms,100个进程可以提供1000qps,这样的处理能力还是不错的。但是如果请求内要调用外网Http接口,像QQ、微博登录,耗时会很长,一个请求需要10s。那一个进程1秒只能处理0.1个请求,100个进程只能达到10qps,这样的处理能力就太差了。有没有一种技术可以在一个进程内处理所有并发IO呢?答案是有,这就是IO复用技术。IO复用/事件循环/异步非阻塞其实IO复用的历史和多进程一样长,Linux很早就提供了 select 系统调用,可以在一个进程内维持1024个连接。后来又加入了poll系统调用,poll做了一些改进,解决了 1024 限制的问题,可以维持任意数量的连接。但select/poll还有一个问题就是,它需要循环检测连接是否有事件。这样问题就来了,如果服务器有100万个连接,在某一时间只有一个连接向服务器发送了数据,select/poll需要做循环100万次,其中只有1次是命中的,剩下的99万9999次都是无效的,白白浪费了CPU资源。直到Linux 2.6内核提供了新的epoll系统调用,可以维持无限数量的连接,而且无需轮询,这才真正解决了 C10K 问题。现在各种高并发异步IO的服务器程序都是基于epoll实现的,比如Nginx、Node.js、Erlang、Golang。像 Node.js 这样单进程单线程的程序,都可以维持超过1百万TCP连接,全部归功于epoll技术。IO复用异步非阻塞程序使用经典的Reactor模型,Reactor顾名思义就是反应堆的意思,它本身不处理任何数据收发。只是可以监视一个socket句柄的事件变化。Reactor有4个核心的操作:add添加socket监听到reactor,可以是listen socket也可以使客户端socket,也可以是管道、eventfd、信号等set修改事件监听,可以设置监听的类型,如可读、可写。可读很好理解,对于listen socket就是有新客户端连接到来了需要accept。对于客户端连接就是收到数据,需要recv。可写事件比较难理解一些。一个SOCKET是有缓存区的,如果要向客户端连接发送2M的数据,一次性是发不出去的,操作系统默认TCP缓存区只有256K。一次性只能发256K,缓存区满了之后send就会返回EAGAIN错误。这时候就要监听可写事件,在纯异步的编程中,必须去监听可写才能保证send操作是完全非阻塞的。del从reactor中移除,不再监听事件callback就是事件发生后对应的处理逻辑,一般在add/set时制定。C语言用函数指针实现,JS可以用匿名函数,PHP可以用匿名函数、对象方法数组、字符串函数名。Reactor只是一个事件发生器,实际对socket句柄的操作,如connect/accept、send/recv、close是在callback中完成的。具体编码可参考下面的伪代码:Reactor模型还可以与多进程、多线程结合起来用,既实现异步非阻塞IO,又利用到多核。目前流行的异步服务器程序都是这样的方式:如Nginx:多进程ReactorNginx+Lua:多进程Reactor+协程Golang:单线程Reactor+多线程协程Swoole:多线程Reactor+多进程Worker协程是什么协程从底层技术角度看实际上还是异步IO Reactor模型,应用层自行实现了任务调度,借助Reactor切换各个当前执行的用户态线程,但用户代码中完全感知不到Reactor的存在。PHP并发IO编程实践PHP相关扩展Stream:PHP内核提供的socket封装Sockets:对底层Socket API的封装Libevent:对libevent库的封装Event:基于Libevent更高级的封装,提供了面向对象接口、定时器、信号处理的支持Pcntl/Posix:多进程、信号、进程管理的支持Pthread:多线程、线程管理、锁的支持PHP还有共享内存、信号量、消息队列的相关扩展PECL:PHP的扩展库,包括系统底层、数据分析、算法、驱动、科学计算、图形等都有。如果PHP标准库中没有找到,可以在PECL寻找想要的功能。PHP语言的优劣势PHP的优点:第一个是简单,PHP比其他任何的语言都要简单,入门的话PHP真的是可以一周就入门。C++有一本书叫做《21天深入学习C++》,其实21天根本不可能学会,甚至可以说C++没有3-5年不可能深入掌握。但是PHP绝对可以7天入门。所以PHP程序员的数量非常多,招聘比其他语言更容易。PHP的功能非常强大,因为PHP官方的标准库和扩展库里提供了做服务器编程能用到的99%的东西。PHP的PECL扩展库里你想要的任何的功能。另外PHP有超过20年的历史,生态圈是非常大的,在Github可以找到很多代码。PHP的缺点:性能比较差,因为毕竟是动态脚本,不适合做密集运算,如果同样的 PHP 程序使用 C/C++ 来写,PHP 版本要比它差一百倍。函数命名规范差,这一点大家都是了解的,PHP更讲究实用性,没有一些规范。一些函数的命名是很混乱的,所以每次你必须去翻PHP的手册。提供的数据结构和函数的接口粒度比较粗。PHP只有一个Array数据结构,底层基于HashTable。PHP的Array集合了Map,Set,Vector,Queue,Stack,Heap等数据结构的功能。另外PHP有一个SPL提供了其他数据结构的类封装。所以PHPPHP更适合偏实际应用层面的程序,业务开发、快速实现的利器PHP不适合开发底层软件使用C/C++、JAVA、Golang等静态编译语言作为PHP的补充,动静结合借助IDE工具实现自动补全、语法提示PHP的Swoole扩展基于上面的扩展使用纯PHP就可以完全实现异步网络服务器和客户端程序。但是想实现一个类似于多IO线程,还是有很多繁琐的编程工作要做,包括如何来管理连接,如何来保证数据的收发原子性,网络协议的处理。另外PHP代码在协议处理部分性能是比较差的,所以我启动了一个新的开源项目Swoole,使用C语言和PHP结合来完成了这项工作。灵活多变的业务模块使用PHP开发效率高,基础的底层和协议处理部分用C语言实现,保证了高性能。它以扩展的方式加载到了PHP中,提供了一个完整的网络通信的框架,然后PHP的代码去写一些业务。它的模型是基于多线程Reactor+多进程Worker,既支持全异步,也支持半异步半同步。Swoole的一些特点:Accept线程,解决Accept性能瓶颈和惊群问题多IO线程,可以更好地利用多核提供了全异步和半同步半异步2种模式处理高并发IO的部分用异步模式复杂的业务逻辑部分用同步模式底层支持了遍历所有连接、互发数据、自动合并拆分数据包、数据发送原子性。Swoole的进程/线程模型:Swoole程序的执行流程:使用PHP+Swoole扩展实现异步通信编程实例代码在https://github.com/swoole/swo… 主页查看。TCP服务器与客户端异步TCP服务器:在这里new swoole_server对象,然后参数传入监听的HOST和PORT,然后设置了3个回调函数,分别是onConnect有新的连接进入、onReceive收到了某一个客户端的数据、onClose某个客户端关闭了连接。最后调用start启动服务器程序。swoole底层会根据当前机器有多少CPU核数,启动对应数量的Reactor线程和Worker进程。异步客户端:客户端的使用方法和服务器类似只是回调事件有4个,onConnect成功连接到服务器,这时可以去发送数据到服务器。onError连接服务器失败。onReceive服务器向客户端连接发送了数据。onClose连接关闭。设置完事件回调后,发起connect到服务器,参数是服务器的IP,PORT和超时时间。同步客户端:同步客户端不需要设置任何事件回调,它没有Reactor监听,是阻塞串行的。等待IO完成才会进入下一步。异步任务:异步任务功能用于在一个纯异步的Server程序中去执行一个耗时的或者阻塞的函数。底层实现使用进程池,任务完成后会触发onFinish,程序中可以得到任务处理的结果。比如一个IM需要广播,如果直接在异步代码中广播可能会影响其他事件的处理。另外文件读写也可以使用异步任务实现,因为文件句柄没办法像socket一样使用Reactor监听。因为文件句柄总是可读的,直接读取文件可能会使服务器程序阻塞,使用异步任务是非常好的选择。异步毫秒定时器这2个接口实现了类似JS的setInterval、setTimeout函数功能,可以设置在n毫秒间隔实现一个函数或 n毫秒后执行一个函数。异步MySQL客户端swoole还提供一个内置连接池的MySQL异步客户端,可以设定最大使用MySQL连接数。并发SQL请求可以复用这些连接,而不是重复创建,这样可以保护MySQL避免连接资源被耗尽。异步Redis客户端异步的Web程序程序的逻辑是从Redis中读取一个数据,然后显示HTML页面。使用ab压测性能如下:同样的逻辑在php-fpm下的性能测试结果如下:WebSocket程序swoole内置了websocket服务器,可以基于此实现Web页面主动推送的功能,比如WebIM。有一个开源项目可以作为参考。https://github.com/matyhtf/ph…

April 8, 2019 · 1 min · jiezi

linux重定向标准输入后,再重新打开标准输入为什么会失效?

首先来看一段代码:#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int main(){ int fd1,fd2; fd2 = open(“p1.py”,O_RDONLY,0); dup2(fd2,0); char c; while( read(0,&c,1) > 0) printf("%c",c); close(fd2); fd1 = open("/dev/stdin",O_RDONLY); printf("%d\n",fd1); while( read(fd1, &c, 1) > 0) printf("%c",c); return 0;}先讲一下文件描述符是什么。linux进程每打开一个文件都会返回一个文件描述符(整数)。这个描述符实际是打开的文件在该进程的描述符表上的偏移值。比如说p是描述符表,1是描述符,那么p[1]就能够索引到1描述符对应的打开文件。有了这个偏移值(文件描述符)就能够快速的找到并操作文件。(当然实际的情况是这个文件描述符能够索引到打开文件表表项,然后再通过打开文件表表项索引到对应的V-node节点表表项,而这个v-node节点表表项才代表真正的文件。不过只从逻辑上来看不需要理解这个括号里的说明。)解释一下这段程序:1、首先打开了一个叫做p1.py的文件。2、然后用dup2这个函数使得文件描述符0这个位置的指针指向文件描述符fd2这个位置的指针指向的文件。也就是说本来是这样:p[0] = &fiel1,p[fd2] = &file2,现在p[0] = p[fd2] = &file2。而我们都知道文件描述符0这个位置对应的文件file1是标准输入文件/dev/stdin。那么这个函数的意义就是把标准输入重定向到了p1.py这个文件里了。之后再用标准输入比如说scanf(),来读,那么都是从p1.py这个文件里读了。3、然后把这个文件里的东西读出来并输出到屏幕上。4、打开/dev/stdin这个文件,这个文件是标准输入。5、把这个文件里的东西读出来并打印到屏幕上。预期结果是什么?执行到第5步应该停下来,等待键盘输入。然后把输入的东西打印到屏幕。实际结果?没有等待键盘输入,它直接把p1.py的文件内容输出到屏幕上,也就是说和上面的输出是一样的!!!why???how???我打开了一个文件,应该读取文件里的内容。而这个文件是标准输入,那么既然是标准输入(从键盘输入),我还没输入呢,怎么就输出结果了呢???而且结果还很奇特。。。这是因为,标准输入这个文件/dev/stdin是个链接文件!!!它存放的是别的文件的地址!!!如果文件描述符指向的文件是个普通文件,那么把这个文件描述符指向别的文件,就是真的指向了别的文件。而这里的文件描述符指向的是个链接文件,那么把这个文件描述符指向别的文件,意味着什么???意味着它把这个链接文件里的内容(地址)更改了,而它仍然指向这个文件。只不过它知道这是个链接文件,因此它会访问的是这个链接文件中的地址对应的文件。理解了上面的操作就可以说得通了,dup2对于链接文件只是修改了文件中的地址,它并没有真正指向别的文件,这也导致了一个问题,那就是它把这个链接文件给修改了,如果进程再次打开这个链接文件,那么链接文件之前存的地址就没有了,因此执行fd1 = open("/dev/stdin",O_RDONLY);这个的时候,实际上是又一次打开了p1.py这个文件!!!因为/dev/stdin这个文件里存放的地址已经是p1.py这个文件的地址了。。。

March 25, 2019 · 1 min · jiezi

PHP socket初探 --- 关于IO的一些枯燥理论

[原文地址:https://blog.ti-node.com/blog…]要想更好了解socket编程,有一个不可绕过的环节就是IO.在Linux中,一切皆文件.实际上要文件干啥?不就是读写么?所以,这句话本质就是"IO才是王道".用php的fopen打开文件关闭文件读读写写,这叫本地文件IO.在socket编程中,本质就是网络IO.所以,在开始进一步的socket编程前,我们必须先从概念上认识好IO.如果到这里你还对IO没啥概念,那么我就通过几个词来给你一个大概的印象:同步,异步,阻塞,非阻塞,甚至是同步阻塞,同步非阻塞,异步阻塞,异步非阻塞.是不是晕了?截至到目前为止,你可以简单地认为只要搞明白这几个名词的含义以及区别,就算弄明白IO了,至少了可以继续往下看了.先机械记忆一波儿:IO分为两大种,同步和异步.同步IO:阻塞IO非阻塞IOIO多路复用(包括select,poll,epoll三种)信号驱动IO异步IO那么如何理解区别这几个概念呢?尤其是同步和阻塞,异步和非阻塞,看起来就是一样的.我先举个例子结合自己的理解来说明一下:你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,你自己看着点儿!".于是你就站在旁边只等馒头.此时的你,是阻塞的,是同步的.阻塞表现在你除了等馒头,别的什么都不做了.同步表现在等馒头的过程中,阿梅不提供通知服务,你不得不自己要等到"馒头出炉"的消息.你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,你自己看着点儿!".于是你就站在旁边发微信,然后问一句:"好了没?",然后发QQ,然后再问一句:"好了没?".此时的你,是非阻塞的,是同步的.非阻塞表现在你除了等馒头,自己还干干别的时不时会主动问问馒头好没好.同步表现在等馒头的过程中,阿梅不提供通知服务,你不得不自己要等到"馒头出炉"的消息.你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,蒸好了我打电话告诉你!".但你依然站在旁边只等馒头,此时的你,是阻塞的,是异步的.阻塞表现在你除了等馒头,别的什么都不做了.异步表现在等馒头的过程中,阿梅提供电话通知"馒头出炉"的消息,你只需要等阿梅的电话.你去甜在心馒头店买太极馒头,阿梅说:"暂时没,正在蒸呢,蒸好了我打电话告诉你!".于是你就走了,去买了双新球鞋,看了看武馆,总之,从此不再过问馒头的事情,一心只等阿梅电话.此时的你,是非阻塞的,是异步的.非阻塞表现在你除了等馒头,自己还干干别的时不时会主动问问馒头好没好.异步表现在等馒头的过程中,阿梅提供电话通知"馒头出炉"的消息,你只需要等阿梅的电话.如果你仔细品过上面案例中的每一个字,你就能慢慢体会到之所以异步和非阻塞,同步和阻塞容易混淆,仅仅是因为二者的表现形式稍微有点儿相似而已.阻塞和非阻塞关注的是:在等馒头的过程中,你在干啥.同步和异步关注的是:等馒头这件事,你是一直等到"馒头出炉"的结果,还是立即跑路等阿梅告诉你的"馒头出炉".重点的是你是如何得知"馒头出炉"的.所以现实世界中,最傻的人才会采用异步阻塞的IO方式去写程序.其余三种方式,更多的人都会选择同步阻塞或者异步非阻塞.同步非阻塞最大的问题在于,你需要不断在各个任务中忙碌着,导致你的大脑混乱,非常累.[原文地址:https://blog.ti-node.com/blog…]

September 2, 2018 · 1 min · jiezi