从基础讲起,IO的原理和模型是隐藏在编程知识底下的,是开发人员必须掌握的基础原理,是基础的基础,更是通关大公司面试的必备知识。 本章从操作系统的底层原理入手。通过图文并茂的方式,为大家深入剖析高并发I0的底层原理,并介绍如何通过设置来让操作系统支持高并发。

1.1 IO读写的基础原理

大家知道,用户程序进行I0的读写,依赖于底层的IO读写,基本上会用到底层的mead&write两大系统调用。在不同的操作系统中,IO读写的系统调用的名称可能不完全一样,但是基本功能是一样的。

这里涉及一个基础的知识:read系统调用,并不是直接从物理设备把数据读取到内存中; write系统调用,也不是直接把数据写入到物理设备。上层应用无论是调用操作系统的read,还是调用操作系统的 write,都会涉及缓冲区。具体来说,调用操作系统的read,是把数据从内核缓冲区复制到进程缓冲区;而 write系统调用,是把数据从进程缓冲区复制到内核缓冲区

也就是说,上层程序的IO操作,实际上不是物理设备级别的读写,而是缓存的复制,read&write两大系统调用,都不负责数据在内核缓冲区和物理设备(如磁盘)之间的交换,这项底层的读写交换,是由操作系统内核( Kermel)来完成的。注:内核即指操作系统

在用户程序中,无论是 Socket的IO、还是文件IO操作,都属于上层应用的开发,它们的输入(Input)和输出(Ouput)的处理,在编程的流程上,都是一致的。

1.1.1 内核缓冲区与进程缓冲区

为什么设置那么多的缓冲区,为什么要那么麻烦呢?缓冲区的目的,是为了减少频繁地与设备之间的物理交换,大家都知道,外部设备的直接读写,涉及操作系统的中断。发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息。为了减少这种底层系统的时间损耗、性能损耗,于是出现了内存缓冲区。

有了内存缓冲区,上层应用使用read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(进程缓冲区);上层应用使用wite系统调用时,仅仅把数据从进程缓冲区复制到内核缓冲区中。底层操作会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行I0设备 的中断处理,集中执行物理设备的实际IO操作,这种机制提升了系统的性能。至于什么时候中断(读中断、写中断),由操作系统的内核来决定,用户程序则不需要关心。

从数量上来说,在 Linux系统中,操作系统内核只有一个内核缓冲区。而每个用户程序(进程),有自己独立的缓冲区,叫作进程缓冲区。所以,用户程序的IO读写程序,在大多数情况下,并没有进行实际的I0操作,而是在进程缓冲区和内核缓冲区之间直接进行数据的交换。

1.1.2详解典型的系统调用流程

前面讲到,用户程序所使用的系统调用read&write,它们不等价于数据在内核缓冲区和磁盘之间的交换。read把数据从内核缓冲区复制到进程缓冲区, write把数据从进程缓冲区复制到内核缓冲区,具体的流程,如图2-1所示。

2.1 系统调用read&write的流程

这里以read系统调用为例,先看下一个完整输入流程的两个阶段:

  • 等待数据准备好

  • 从内核向进程复制数据

如果是read一个 socket(套接字),那么以上两个阶段的具体处理流程如下:

  • 第一个阶段,等待数据从网络中到达网卡。当所等待的分组到达时,它被复制到内核中的某个缓冲区。这个工作由操作系统自动完成,用户程序无感知。

  • 第二个阶段,就是把数据从内核缓冲区复制到应用进程缓冲区。

再具体一点,如果是在Java服务器端,完成一次 socket请求和响应,完整的流程如下:

  • 客户端请求:Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区

  • 获取请求数据:Java服务器通过read系统调用,从 Linux内核缓冲区读取数据,再送入Java进程缓冲区

  • 服务器端业务处理:Java服务器在自己的用户空间中处理客户端的请求

  • 服务器端返回数据:Java服务器完成处理后,构建好的响应数据,将这些数据从用户缓冲区写入内核缓冲区。这里用到的是wre系统调用

  • 发送给客户端: Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,会将数据发送给目标客户端

1.2 四种主要的IO模型

服务器端编程,经常需要构造高性能的网络应用,需要选用高性能的IO模型,这也是通关大司面试必备的知识。

本章从最为基础的模型开始,为大家揭秘IO模型。常见的IO模型有四种:

  • 同步阻塞IO(Blocking IO)

首先,解释一下这里的阻塞与非阻塞:

阻塞IO,指的是需要内核IO操作彻底完成后,才返回到用户空间执行用户的操作。阻塞指的是用户空间程序的执行状态。传统的IO模型都是同步阻塞IO。在Java中,默认创建的 socke都是阻塞的。

其次,解释一下同步与异步:

同步IO,是一种用户空间与内核空间的IO发起方式。同步IO是指用户空间的线程是主动发起IO请求的一方,内核空间是被动接受方。异步IO则反过来,是指系统内核是主动发起IO请求的一方,用户空间的线程是被动接受方。

  • 同步非阻塞IO(Non- blocking IO)

非阻塞IO,指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间执行用户的操作,即处于非阻塞的状态,与此同时内核会立即返回给用户一个状态值。

简单来说:阻塞是指用户空间(调用线程)一直在等待,而不能干别的事情:非阻塞是指用户空间(调用线程)拿到内核返回的状态值就返回自己的空间,IO操作可以干就干,不可以干,就去干别的事情。

非阻塞IO要求 socket被设置为 NONBLOCK。

强调一下,这里所说的NIO(同步非阻塞IO)模型,并非Java的NIO(New IO)库。

  • 3.1 IO多路复用(IO Multiplexing)

即经典的 Reactor反应器设计模式,有时也称为异步阻塞IO,Java中的 Selector选择器和Limu中的 epoll都是这种模型。

  • 4.异步IO( Asynchronous IO)

异步1O,指的是用户空间与内核空间的调用方式反过来。用户空间的线程变成被动接受者,而内核空间成了主动调用者。这有点类似于Java中比较典型的回调模式,用户空间的线程向内核 空间注册了各种IO事件的回调函数,由内核去主动调用。

1.2.1 同步阻塞IO( Blocking IO)

在Java应用程序进程中,默认情况下,所有的 socket连接的IO操作都是同步阻塞IO(Blocking IO)。

在阻塞式IO模型中,Java应用程序从IO系统调用开始,直到系统调用返回,在这段时间内Java进程是阻塞的。返回成功后,应用进程开始处理用户空间的缓存区数据。

同步阻塞IO的具体流程,如图2-2所示:

图2-2同步阻塞10的流程

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

  • (1)从Java启动IO读的read系统调用开始,用户线程就进入阻塞状态。

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

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

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

总之,阻塞IO的特点是:在内核进行IO执行的两个阶段,用户线程都被阻塞了。

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

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

Linux c/c++服务器开发高阶视频学习资料+qun720209036获取

内容包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒体,P2P,K8S,Docker,TCP/IP,协程,DPDK多个高级知识点。

关注VX公众号:Linux C后台服务器开发

1.2.2 同步非阻塞NIO( None Blocking IO)

socket连接默认是阻塞模式,在 Linux系统下,可以通过设置将 socket变成为非阻塞的模式(Non- Blocking)。使用非阻塞模式的IO读写,叫作同步非阻塞Io( None Blocking I0),简称为NIO模式。在NIO模型中,应用程序一旦开始IO系统调用,会出现以下两种情况:

  • (1)在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。

  • (2)在内核缓冲区中有数据的情况下,是阻塞的,直到数据从内核缓冲复制到用户进程缓冲。复制完成后,系统调用返回成功,应用进程开始处理用户空间的缓存数据。

同步非阻塞IO的流程,如图2-3所示。

图2-3 同步非阻塞I0的流程

举个例子。发起一个非阻塞 socket的read读操作的系统调用,流程如下:

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

  • (2)内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到的用户缓冲区的字节数)。

  • (3)用户线程读到数据后,才会解除阻塞状态,重新运行起来。也就是说,用户进程需要经过多次的尝试,才能保证最终真正读到数据,而后继续执行。

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

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

同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。

总体来说,在高并发应用场景下,同步非阻塞O也是不可用的。一般web服务器不使用这种IO模型,这种IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO这一特性。在Java的实际开发中,也不会涉及这种IO模型。

这里说明一下,同步非阻塞IO,可以简称为NIO,但是,它不是Java中的NO,虽然它们的英文缩写一样,希望大家不要混淆。Java的NIO(New IO),对应的不是四种基础IO模型中的NIO(None Blocking IO)模型,而是另外的一种模型,叫作IO多路复用模型(IO Multiplexing)。

1.2.3 IO多路复用模型(IO Multiplexing)

如何避免同步非阻塞IO模型中轮询等待的问题呢?这就是IO多路复用模型。

在IO多路复用模型中,引入了一种新的系统调用。查询IO的就绪状态。在 Linux系统中,对应的系统调用为 select/epoll系统调用。通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。

目前支持IO多路复用的系统调用,有 select、epoll等。 select系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。 epoll是在Lnux2.6内核中提出的,是 select系统调用的 Linux增强版本。

在IO多路复用模型中通过 select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的 socket连接,当某个或者某些 socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。

举个例子来说明IO多路复用模型的流程。发起一个多路复用O的read读操作的系统调用,流程如下:

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

  • (2)就绪状态的轮询。通过选择器的查询方法,查询注册过的所有 socket连接的就绪状态。通过查询的系统调用,内核会返回一个就绪的 socket列表。当任何一个注册过的 socket中的数据准备好了,内核缓冲区有数据(就绪)了,内核就将该 socket加入到就绪的列表中。 当用户进程调用了 select查询方法,那么整个线程会被阻塞掉。

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

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

IO多路复用模型的流程,如图2-4所示。

图2-4 IO多路复用模型的流程

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

和NO模型相似,多路复用IO也需要轮询。负责 select/epoll状态查询调用的线程,需要不断地进行 select/epoll轮询,查找出达到IO操作就绪的 socket连接。

IO多路复用模型与同步非阻塞IO模型是有密切关系的。对于注册在选择器上的每一个可以查询的 socket连接,一般都设置成为同步非阻塞模型。仅是这一点,对于用户程序而言是无感知的。

IO多路复用模型的优点:与一个线程维护一个连接的阻塞IO模式相比,使用 select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection),系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。

Java语言的NO(NewO)技术,使用的就是IO多路复用模型。在 Linux系统上,使用的是epoll系统调用。

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

如何彻底地解除线程的阻塞,就必须使用异步IO模型。

1.2.4 异步模型(Asynchronous IO)

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

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

异步IO模型的流程,如图2-5所示

图2-5异步1O模型的流程

举个例子。发起一个异步IO的read读操作的系统调用,流程如下:

  • (1)当用户线程发起了read系统调用,立刻就可以开始去做其他的事,用户线程不阻塞;

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

  • (3)内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。

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

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

异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。

理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。

就目前而言, Windows系统下通过IOCP实现了真正的异步IO。而在inx系统下,异步IO模型在2.6版本才引入,目前并不完善,其底层实现仍使用 epoll,与IO多路复用相同,因此在性能上没有明显的优势。

大多数的高并发服务器端的程序,一般都是基于 Linux系统的。因而,目前这类高并发网络应用程序的开发,大多采用IO多路复用模型。

大名鼎鼎的 Netty框架,使用的就是IO多路复用模型,而不是异步IO模型。

1.3通过合理配置来支持百万级并发连接

本章所聚焦的主题,是高并发IO的底层原理。前面已经深入浅出地介绍了高并发IO的模型。但是,即使采用了最先进的模型,如果不进行合理的配置,也没有办法支撑百万级的网络连接并发。

这里所涉及的配置,就是Linux操作系统中文件句柄数的限制。

顺便说下,在生产环境中,大家都使用 Linux系统,所以,后续文字中假想的生产操作系统都是 Linux系统。另外,由于大多数同学使用 Windows进行学习和工作,因此,后续文字中假想的开发所用的操作系统都是 Windows系统。

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

本书的原则是:从基础讲起。

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

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

ulimit -n

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

默认的数值为1024,对绝大多数应用(例如 Apache、桌面应用程序)来说已经足够了。但是,对于一些用户基数很大的高并发应用,则是远远不够的。一个高并发的应用,面临的并发连接数往往是十万级、百万级、千万级、甚至像腾讯QQ一样的上亿级。

文件句柄数不够,会导致什么后果呢?当单个进程打开的文件句柄数量,超过了系统配置的上限值时,就会发出“Socket/file:can' t open so many files”的错误提示。

对于高并发、高负载的应用,就必须要调整这个系统参数,以适应处理并发处理大量连接的应用场景。可以通过 ulimit来设置这两个参数。方法如下:

ulimit -n 1000000

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

然而,使用 ulimit命令来修改当前用户进程的一些基础限制,仅在当前用户环境有效。直白地说,就是在当前的终端工具连接当前 shell期间,修改是有效的;一旦断开连接,用户退出后,它的数值就又变回系统默认的1024了。也就是说,ulimit只能作为临时修改,系统重启后,句柄数量又会恢复为默认值。

如果想永久地把设置值保存下来,可以编辑/etc/rc.local开机启动文件,在文件中添加如下内容:

ulimit -SHn 1000000

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

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

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

soft nofile 1000000hard nofile 1000000

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

在使用和安装目前非常火的分布式搜索引擎 ElasticSearch,就必须去修改这个文件,增加最大的文件句柄数的极限值。

在服务器运行 Netty时,也需要去解除文件句柄数量的限制,修改/etc/security/limits.conf文件即可。

1.4 本章小结

本书的原则是:从基础讲起。本章彻底体现了这个原则。

本章聚焦的主题:一是底层IO操作的两个阶段,二是最为基础的四种IO模型,三是操作系统对高并发的底层的支持。

四种IO模型,基本上概括了当前主要的IO处理模型。理论上来说,从阻塞IO到异步IO,越往后,阻塞越少,效率也越优。在这四种IO模型中,前三种属于同步1O,因为真正的IO操作都将阻塞应用线程。

只有最后一种异步1O模型,才是真正的异步IO模型,可惜目前 Linux操作系统尚欠完善。

不过,通过应用层优秀框架如 Netty,同样能在IO多路复用模型的基础上,开发出具备支撑高并发(如百万级以上的连接)的服务器端应用。

最后强调一下,本章是理论课,比较抽象,但是一定要懂。理解了这些理论之后,再学习后面的章节就会事半功倍。

高并发IO的底层原理相关推荐

  1. 《Netty、Redis、Zookeeper高并发实战》2️⃣高并发IO的底层原理

    文章目录 1.IO读写的基础原理 1.1 内核缓冲区与进程缓存区 1.2 详解典型的系统调用流程 2.四种主要的IO模型 2.1 同步阻塞IO(Blocking IO) 2.2 同步非阻塞IO(Non ...

  2. 并发编程五:java并发线程池底层原理详解和源码分析

    文章目录 java并发线程池底层原理详解和源码分析 线程和线程池性能对比 Executors创建的三种线程池分析 自定义线程池分析 线程池源码分析 继承关系 ThreadPoolExecutor源码分 ...

  3. 冰河最新出版的《深入理解高并发编程:核心原理与案例实战》到底讲了些啥?(视频为证)

    大家好,我是冰河~~ 最近有很多小伙伴问我:<深入理解高并发编程:核心原理与案例实战>这本书有没有目录.我:安排!这不,我连夜录制了这本书的整体内容,希望能够为小伙伴们带来实质性的帮助,直 ...

  4. Java开发大型互联网高并发架构实战之原理概念分析

    JAVA大飞哥 2019-06-16 21:07:08 引言 高并发是指在同一个时间点,有很多用户同时访问URL地址,比如:淘宝的双11.双12,就会产生高并发.又如贴吧的爆吧,就是恶意的高并发请求, ...

  5. php抢红包 并发 超时,处理高并发 IO瓶颈解决红包程序

    解决高并发 io瓶颈解决红包程序 本程序模拟的红包抽奖模式.总金额100元,随机用户获得1-10元的红包,直到红包分发完. redis中luckMoneyMax需要提前设置 $r->set('l ...

  6. 【高并发】JUC底层工具类Unsafe

    1.概述 转载:添加链接描述 参考:[java]java的unsafe 参考:JUC原子类: CAS, Unsafe和原子类详解 1.1 本文主要内容 Unsafe基本介绍 获取Unsafe实例 Un ...

  7. IO多路复用底层原理及源码解析

    基本概念 1. 关于linux文件描述符 在Linux中,一切都是文件,除了文本文件.源文件.二进制文件等,一个硬件设备也可以被映射为一个虚拟的文件,称为设备文件.例如,stdin 称为标准输入文件, ...

  8. 【洁洁送书第一期】Python高并发与高性能编程: 原理与实践

    这里写目录标题 Python高并发.高性能 解决问题 书本介绍 关于作者 直播预告 Python高并发.高性能 Python成为时下技术革新的弄潮儿,全民Python的发展趋势让人们不再满足于简单地运 ...

  9. Java并发编程-synchronized底层原理

    synchronized底层原理与Monitor密切相关 1.Java对象头 以 32 位虚拟机为例 普通对象 对象的类型,如Student类型,Teacher类型等是由KlassWord来表示的,它 ...

最新文章

  1. 2017年高级二级计算机考试试题,2017年计算机二级高级Office考试试题操作题
  2. 移动的验证码安全问题告诉移动网站后......,1860意指一般人不会这样做.
  3. 艾伟_转载:[一步一步MVC]第五回:让TagBuilder丰富你的HtmlHelper
  4. 使用labelme制作自己的深度学习图像分割数据集
  5. routine tortoisesvn operations-1
  6. 解决Mac安装LightGBM报错LightGBM and gcc 8 in MacOS: Library not loaded
  7. Driver 初始化顺序
  8. 记某站被搜索引擎入侵
  9. 【手写数字识别】基于matlab GUI BP神经网络手写数字识别【含Matlab源码 868期】
  10. Effective Java 学习笔记之二
  11. 数学,远比你想象的重要!
  12. VB6 Socket编程
  13. 工程师总结:PCB设计中降低噪声与电磁干扰的方法
  14. 因果倒置的实验名称是“延迟实验”(Wheeler's delayed choice experiment)
  15. 人工智能一大技术:强化学习(RL)
  16. Mac上iPad Pro高清桌面动态壁纸
  17. hls简述(HTTP live Streaming)
  18. s5p4418 PWM亮度调节修改
  19. 如何制作 linux 系统 U盘启动盘
  20. 支持华为鸿蒙2.0的手机型号是,华为鸿蒙2.0系统支持的手机型号 华为鸿蒙2.0系统详解...

热门文章

  1. android Auto animate layout updates
  2. python 身份证号的有效性、生日(阴历阳历)、星座、生肖、性别、年龄、户籍所在地测试
  3. video 微信 标签层级过高_标签Tag在用户决策中的作用
  4. java集合-ArrayList学习笔记
  5. 11个常见的Linux基础面试问题
  6. 101. 对称二叉树
  7. 云服务器怎样杀毒软件,云服务器用什么杀毒软件
  8. 线程池ThreadPoolExecutor的使用
  9. 【第7期】云计算监控——Prometheus监控系统
  10. mysql 存储矢量数据_MongoDB集群环境存储空间矢量数据