标准访问文件的方式
Linux 中,这种访问文件的方式是通过两个系统调用实现的:read()和 write()。当应用程序调用 read()系统调用读取一块数据的时候,如果该块数据已经在内存中了,那么就直接从内存中读出该数据并返回给应用程序;如果该块数据不在内存中,那么数据会被从磁盘上读到页高缓存中去,然后再从页缓存中拷贝到用户地址空间中去。如果一个进程读取某个文件,那么其他进程就都不可以读取或者更改该文件;对于写数据操作来说,当一个进程调用了 write()系统调用往某个文件中写数据的时候,数据会先从用户地址空间拷贝到操作系统内核地址空间的页缓存中去,然后才被写到磁盘上。但是对于这种标准的访问文件的方式来说,在数据被写到页缓存中的时候,write()系统调用就算执行完成,并不会等数据完全写入到磁盘上。Linux在这里采用的是我们前边提到的延迟写机制( deferred writes)。
Diect IO
凡是通过直接 I/O方式进行数据传输,数据均直接在用户地址空间的缓冲区和磁盘之间直接进行传输,完全不需要页缓存的支持。操作系统层提供的缓存往往会使应用程序在读写数据的时候获得更好的性能,但是对于某些特殊的应用程序,比如说数据库管理系统这类应用,他们更倾向于选择他们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。
直接I/O 的优点
直接 I/O 最主要的优点就是通过减少操作系统内核缓冲区和应用程序地址空间的数据拷贝次数,降低了对文件读取和写入时所带来的CPU 的使用以及内存带宽的占用。这对于某些特殊的应用程序,比如自缓存应用程序来说,不失为一种好的选择。如果要传输的数据量很大,使用直接I/O 的方式进行数据传输,而不需要操作系统内核地址空间拷贝数据操作的参与,这将会大大提高性能。
直接I/O 潜在可能存在的问题
直接 I/O 并不一定总能提供令人满意的性能上的飞跃。设置直接 I/O 的开销非常大,而直接 I/O 又不能提供缓存 I/O 的优势。缓存 I/O 的读操作可以从高速缓冲存储器中获取数据,而直接I/O 的读数据操作会造成磁盘的同步读,这会带来性能上的差异, 并且导致进程需要较长的时间才能执行完;对于写数据操作来说,使用直接I/O 需要write() 系统调用同步执行,否则应用程序将会不知道什么时候才能够再次使用它的I/O 缓冲区。与直接I/O 读操作类似的是,直接I/O 写操作也会导致应用程序关闭缓慢。所以,应用程序使用直接I/O 进行数据传输的时候通常会和使用异步I/O 结合使用。
Linux IO机制演变
一般来说,recv函数将会阻塞如果当前没有数据可用,同样,send函数将会拥塞当socket的出口队列没有足够的空间来传送信息。这些都会改变当我们处于非拥塞模式,这样的话,如果遇到拥塞就会失败,可以使用poll或者select函数来决定何时能接收或传输数据。
Socket机制有其特殊的处理异步I/O的方法,但是并不是标准的。有时称作是“signal-based I/O”来区别于实时扩展的异步I/O。可以使用SIGIO信号来告知我们:从一个socket上读数据和何时socket的写队列是可用的。分为以下两步:
1 建立socket的所有者,这样信号就知道发送给哪个进程
2 当I/O操作不再拥塞时通知这个socket
处理fcntl和ioctl函数,加上参数
Linux的I/O机制经历了一下几个阶段的演进:
1.同步阻塞I/O:用户进程进行I/O操作,一直阻塞到I/O操作完成为止。
2.同步非阻塞I/O:用户程序可以通过设置文件描述符的属性O_NONBLOCK,I/O操作可以立即返回,但是并不保证I/O操作成功。
3.异步事件阻塞I/O:用户进程可以对I/O事件进行阻塞,但是I/O操作并不阻塞。通过select/poll/epoll等函数调用来达到此目的。
4.异步时间非阻塞I/O: 也叫异步I/O(AIO),用户程序可以通过向内核发出I/O请求命令,不用等待I/O事件真正发生,可以继续做另外的事情,等I/O操作完成,内核会通过函数回调或者信号机制通知用户进程。这样很大程度提高了系统吞吐量。
相关文档:
http://www.fsl.cs.sunysb.edu/~vass/linux-aio.txt
ceph的新bluestore组件,自己实现
bluefs、blockdevice驱动层
启动blockdevice用到的是aio库
5、system io 和stream io
System io调用系统api,进入内核空间
Stream io: 调用c标准库的api,数据在应用层的buffer中,不会调用系统的io api.
文件IO原子操作
在多个进程同时写一个文件的时候,在没有O_APPEND参数时,
只好用lseek和write两个系统调用来实现这个功能现在假设进程A和B分别写文件file,
A在offset为1000处写入100字节,B在offset为1000处写入50字节。
执行A得到文件尾的偏移量为1000,此时内核转而执行进程B,在偏移量为1000(文件尾)写入50字节,
现在的偏移量为1050(文件的属性也扩展了)。再当系统反过来执行进程A的时候,
这时A的写入的偏移量依然为1000,写入100字节,这时会将进程B所写入的50字节覆盖。
内核很有可能在两个系统调用之间暂停这个进程,从而进入下一个进程。
这样任何非单一系统调用都不能被称之为原子操作。就以上的例子中,当我们打开文件的时候加上参数O_APPEND,那么在每次写之前都会自动的定位到文文件尾,
这样我们不必在每一次write操作之前调用lseek操作。
这里列举pread和pwrite函数,
pread函数相当于在read函数之后加上lseek来定位偏移量。它们并不改变文件的指针
另外,如果我们要打开一个文件,同时使用O_CREAT和 O_EXCL两个参数在打开的文件已经存在的前提下会报错。检查文件是否存在和创建新的文件是一个原子操作。如果我们没有这个原子操作,那么可能的实现方法是
if ((fd =open(pathname,O_WRONLY)) < 0) {
if (errno == ENOENT) {
if ((fd= create(pathname,mode) < 0){
err_sys("create error ");
}
} else
err_sys(" open error ");
}
这里有一个问题,那就是在open和create两个操作之间内核中的其他进程或者创建了这个文件,或者往文件写入了内容,新的创建会把文件原来的内容给擦除掉,这样会导致文件的不一致性。
总体来讲,原子操作就是这个操作可能由多步组成,或者全做或者全不做
pwrite函数原型:
ssize_t pwrite(int fildes, const void *buf, size_tnbyte,off_t offset);
相比write,多了个lseek定位到offset指定位置的功能
sync函数和fsync函数
传统的UNIX系统的应用程序大部分的磁盘I/O都会经过buffer缓存或者页缓存。当我们写一个文件数据的时候,内核会将它写入其中的一个缓冲区排队,并等待在之后的某个时间写入磁盘,这就是通常所说的延迟写。
sync只是将所有修改的块都放到等待冲向磁盘的队列中,但是并不等到这些块写入磁盘。
fsync是将文件描述符fd标志的文件的缓冲区排队写入磁盘,并且等待写完后才返回。包括文件的元数据。
fdatasync只是将数据同步到磁盘,在必要的情况下才同步元数据,比如文件大小变化了。(目前linux不支持该功能)