一、非阻塞IO
阻塞其实就是进入了休眠状态,交出了
CPU
控制权。
普通文件的读写操作是不会阻塞的,不管读写多少个字节数据,
read()
或
write()
一定会在有限的时间内返回,所以普通文件一定是以非阻塞的方式进行 I/O
操作,这是普通文件本质上决定的;但是对于某些文件类型,譬如上面所介绍的管道文件、设备文件等,它们既可以使用阻塞式 I/O
操作,也可以使用非阻塞式
I/O进行操作。
阻塞
I/O
与非阻塞
I/O
读文件
在调用
open()
函数打开文件时,为参数 flags
指定
O_NONBLOCK
标志,
open()
调用成功后,后续的
I/O
操作将以非阻塞式方式进行;这就是非阻塞 I/O
的打开方式,如果未指定
O_NONBLOCK
标志,则默认使用阻塞式
I/O
进行操作。
阻塞
I/O
的优点与缺点
当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式
I/O
会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;而对于非阻塞 I/O
,应用程序不会被挂起,而是会立即返回,它要么一直轮训等待,直到数据可读,要么直接放弃!
所以阻塞式
I/O
的优点在于能够提升
CPU
的处理效率,当自身条件不满足时,进入阻塞状态,交出
CPU资源,将 CPU
资源让给别人使用;而非阻塞式则是抓紧利用
CPU
资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的 CPU
使用率。
使用非阻塞
I/O
实现并发读取
如果要先读鼠标,在接着读键盘,在实际测试当中,需要先动鼠标在按键盘(按下键盘上的按键、按完之后按下回车),这样才能既成功读取鼠标、又成功读取键盘,程序才能够顺利运行结束。因为 read 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得
不到执行。若采用轮询的非阻塞读取则可以解决这个问题。
但由于程序当中使用轮训方式,故而会使得该程序的 CPU
占用率特别高,终归还是不太安全,会对整个系统产生很大的副作用
二、I/O复用
I/O
多路复用(
IO multiplexing
)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行 I/O
操作时,能够通知应用程序进行相应的读写操作。
I/O 多路复用技术是为了解决:在并发式 I/O 场景中进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用。
此可知,
I/O
多路复用一般用于并发式的非阻塞
I/O
,也就是多路非阻塞
I/O
,譬如程序中既要读取鼠标、又要读取键盘,多路读取。 我们可以采用两个功能几乎相同的系统调用来执行 I/O 多路复用操作,分别是系统调用
select()和 poll()
。这两个函数基本是一样的,细节特征上存在些许差别。
I/O
多路复用存在一个非常明显的特征:外部阻塞式,内部轮询监视多路
I/O
。
在使用
select()
或
poll()
时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的 I/O
操作,以清除该状态,否则该状态将会一直存在;
三、异步IO
在
I/O
多路复用中,进程通过系统调用
select()
或
poll()
来主动查询文件描述符上是否可以执行
I/O
操作。
而在异步
I/O
中,当文件描述符上可以执行
I/O
操作时,进程可以请求内核为自己发送一个信号。之后进程就可以执行任何其它的任务直到文件描述符可以执行 I/O
操作为止,此时内核会发送信号给进程。
要使用异步
I/O
,程序需要按照如下步骤来执行:
⚫
通过指定
O_NONBLOCK
标志使能非阻塞
I/O
。
⚫
通过指定
O_ASYNC
标志使能异步
I/O
。
⚫
设置异步
I/O
事件的接收进程。也就是当文件描述符上可执行
I/O
操作时会发送信号通知该进程, 通常将调用进程设置为异步 I/O
事件的接收进程。
⚫
为内核发送的通知信号注册一个信号处理函数。默认情况下,异步
I/O
的通知信号是
SIGIO
,所以 内核会给进程发送信号 SIGIO
。
⚫
以上步骤完成之后,进程就可以执行其它任务了,当
I/O
操作就绪时,内核会向进程发送一个
SIGIO 信号,当进程接收到信号时,会执行预先注册好的信号处理函数,我们就可以在信号处理函数中进行 I/O
操作。
四、优化异步 I/O
在一个需要同时检查大量文件描述符(譬如数千个)的应用程序中,例如某种类型的网络服务端程序,与 select()
和
poll()
相比,异步
I/O
能够提供显著的性能优势。 之所以如此,原因在于:对于异步 I/O
,内核可以“记住”要检查的文件描述符,且仅当这些文件描述符上可执行 I/O
操作时,内核才会向应用程序发送信号。
而对于
select()
或
poll()
函数来说,内部实现原理其实是通过轮训的方式来检查多个文件描述符是否可执行 I/O
操作,所以,当需要检查的文件描述符数量较多时,随之也将会消耗大量的
CPU
资源来实现轮训检查操作。当需要检查的文件描述符并不是很多时,使用 select()
或
poll()
是一种非常不错的方案。
现在要对上述的异步
I/O
进行优化,既然要对其进行优化,那必然存在着一些缺陷,如下所示:
⚫
默认的异步
I/O
通知信号
SIGIO
是非排队信号。
SIGIO
信号是标准信号(非实时信号、不可靠信号),所以它不支持信号排队机制,譬如当前正在执行 SIGIO
信号的处理函数,此时内核又发送 多次 SIGIO
信号给进程,这些信号将会被阻塞,只有当信号处理函数执行完毕之后才会传递给进 程,并且只能传递一次,而其它后续的信号都会丢失。
⚫ 无法得知文件描述符发生了什么事件。
优化方法:
使用实时信号替换默认信号
SIGIO ;
使用
sigaction()
函数注册信号处理函数;
五、存储映射 I/O
存储映射
I/O
(
memory-mapped I/O
)是一种基于内存区域的高级
I/O
操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read
操作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write
操作)。这样就可以在不使用基本 I/O
操作函数
read()
和
write()
的情况下执行
I/O 操作。
mmap()
和
munmap()
函数
munmap()
解除映射
mprotect()
函数
msync()
函数
普通 I/O 与存储映射 I/O 比较
普通
I/O
方式的缺点
普通
I/O
方式一般是通过调用
read()
和
write()
函数来实现对文件的读写,使用
read()
和
write()
读写文件 时,函数经过层层的调用后,才能够最终操作到文件,中间涉及到很多的函数调用过程,数据需要在不同的 缓存间倒腾,效率会比较低。同样使用标准 I/O
(库函数
fread()
、
fwrite()
)也是如此,本身标准
I/O
就是对普通 I/O
的一种封装。
那既然效率较低,为啥还要使用这种方式呢?原因在于,只有当数据量比较大时,效率的影响才会比较明显,如果数据量比较小,影响并不大,使用普通的 I/O
方式还是非常方便的。
存储映射
I/O
的优点
存储映射
I/O
的实质其实是共享,与
IPC
之内存共享很相似。譬如执行一个文件复制操作来说,对于普通 I/O
方式,首先需要将源文件中的数据读取出来存放在一个应用层缓冲区中,接着再将缓冲区中的数据写入到目标文件中
而对于存储映射
I/O
来说,由于源文件和目标文件都已映射到了应用层的内存区域中,所以直接操作映 射区来实现文件复制,如下所示:
首先非常直观的一点就是,使用存储映射
I/O
减少了数据的复制操作,所以在效率上会比普通
I/O
要高,其次上面也讲了,普通 I/O
中间涉及到了很多的函数调用过程,这些都会导致普通
I/O
在效率上会比存储映射 I/O
要低。
存储映射 I/O
的实质其实是共享,如何理解共享呢?其实非常简单,我们知道,应用层与内核
层是不能直接进行交互的,必须要通过操作系统提供的系统调用或库函数来与内核进行数据交互,包括操作硬件。通过存储映射 I/O
将文件直接映射到应用程序地址空间中的一块内存区域中,也就是映射区;直接将磁盘文件直接与映射区关联起来,不用调用 read()
、
write()
系统调用,直接对映射区进行读写操作即可操作磁盘上的文件,而磁盘文件中的数据也可反应到映射区中,这就是一种共享,可以认为映射区就是应用层与内核层之间的共享内存。
存储映射 I/O 的不足
存储映射 I/O 方式并不是完美的,它所映射的文件只能是固定大小
因为文件所映射的区域已经在调用mmap()函数时通过
length
参数指定了。
另外,文件映射的内存区域的大小必须是系统页大小的整数倍
譬 如映射文件的大小为 96
字节,假定系统页大小为
4096
字节,那么剩余的
4000
字节全部填充为
0
,虽然可以通过映射地址访问剩余的这些字节数据,但不能在映射文件中反应出来,由此可知,使用存储映射 I/O
在进行大数据量操作时比较有效;对于少量数据,使用普通 I/O
方式更加方便。
存储映射 I/O 的应用场景
由上面介绍可知,存储映射
I/O
在处理大量数据时效率高,对于少量数据处理不是很划算,所以通常来说,存储映射 I/O
会在视频图像处理方面用的比较多,譬如在
Framebuffer
编程,通俗点说就是 LCD
编程,就会使用到存储映射
I/O
。
六、文件锁
对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行 I/O 操作,在这段时间内不允许其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux 系统提供了文件锁机制。
前面学习过互斥锁、自旋锁以及读写锁,文件锁与这些锁一样,都是内核提供的锁机制,锁机制实现用于对共享资源的访问进行保护;只不过互斥锁、自旋锁、读写锁与文件锁的应用场景不一样,互斥锁、自旋锁、读写锁主要用在多线程环境下,对共享资源的访问进行保护,做到线程同步。
而文件锁,顾名思义是一种应用于文件的锁机制,当多个进程同时操作同一文件时,我们怎么保证文件 数据的正确性,linux
通常采用的方法是对文件上锁,来避免多个进程同时操作同一文件时产生竞争状态。 譬如进程对文件进行 I/O
操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程)可以对其进行读写操作。
一个文件既然可以被多个进程同时操作,那说明文件必然是一种共享资源,所以由此可知,归根结底,文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁,来避免访问共享资源产生竞争状态。