文件I/O与系统编程

 

文件IO与系统编程本文是作者阅读TLPI(TheLinuxProgramerInterface的总结)...



文件IO与系统编程

本文是作者阅读TLPI(The Linux Programer Interface的总结),为了突出重点,避免一刀砍,我不会过多的去介绍基本的概念和用法,我重点会去介绍原理和细节。因此对于本文的读者,至少要求读过APUE,或者是实际有写过相关代码的程序员,因为知识有点零散,所以我会尽可能以FAQ的形式呈现给读者。

系统编程概览

如何确定glibc的版本?

可以分为下面两种方式:

第一种就是直接查看,先通过ldd来定位glibc的位置,然后通过直接运行glibc库就可以查看到其版本号了.



如上图,可以看到glibc的版本是2.17,如果我们需要在我们的源代码中检测glibc的版本,因为我们的代码可能使用了一些高版本的glibc库函数。因此你可以使用下面这种方式第二种方式是通过__GLIBC__和__GLIBC_MINOR__这两个常量,是编译时常量,可以借助#ifdef预处理指令来测试,也可以到运行时借助if来判断,因为这是两个常量,那么就会存在一个问题,如果在A系统上进行了编译,拿到B系统上运行,那么这两个常量就没有任何用处了,因为编译期就已经确定了其数值,除非在B系统上再次编译一次。为了应对这种可能,程序可以调用gnu_get_libc_version这个函数来确定运行时所采用的glibc版本。代码如下:



需要包含gnu/lib-version.h这个头文件,__GLIBC__是主版本号,__GLIBC_MINOR__是次版本号,除了可以使用gnu_get_libc_version函数外,还可以使用glibc特有的函数来获取glibc的版本号。

如何打印系统数据类型值?

对于C语言中的基本类型来说,很方便就可以通过printf来打印,但是linux系统通过typedef重新定义了很多系统数据类型,对于这些类型来说除非我们知道这个类型是对何种基本类型的typedef,否则很难正确的通过printf来打印,往往会导致打印的时候出现很多编译器的警告。例如下面这种情况。



常见的应对策略是强制转换为long型,再使用%ld。但是有一个例外在一些编译环境中off_t的大小和long long相当,所以对于off_t类型的数据应该强转为long long使用%lld打印。



到此为此,解决了打印系统数据类型出现Warnning的问题了。其实在C99标准中提供了%z用来打印size_t和ssize_t类型,但是并不是所有的UNIX实现都支持。

文件I/O: 通用I/O模型

如何查看进程打开的所有文件描述符的信息?

通过/proc/PID/fdinfo 可以查看到进程号为PID的进程的打开文件描述符信息,如下:



如上图显示的是pid为854的进程,其打开的文件描述符号,通过读取上面的文件,还可以知道更多关于该描述符的信息。



分别打开了描述符3,描述4,描述符5,其中每个描述符显示的信息都不一样,对于3来说就是一个普通的文件描述符,输出的信息显示其文件偏移量,文件的打开标志信息,文件的挂载点id,通过cat /proc/854/mountinfo可以查看每个挂载点id对应的挂载路径信息。对于描述符4来说,额外多出了一个eventfd-count,这表明这是一个通过eventfd系统调用产生的描述符描述符5则是一个inotify产生的描述符,如果是IO复用机制产生的fd,那么会多出一个tfd的字段,还有signalfd产生的描述符,会多出一个sigmask字段。

O_ASYNC标志的作用是什么? 

O_ASYNC称为信号驱动I/O机制,仅对特定类型的文件有效,诸如终端,FIFOS,以及socket等,当文件描述符可以实施IO操作的时候(数据准备就绪,已经存储在内核缓冲区中),系统会产生一个信号通知进程。然后进程开始读取数据(把数据从内核缓冲区拷贝到用户缓冲区)。在Linux中在打开文件的时候指定O_ASYNC没有任何实质效果,必须调用fcntl的F_SETFL,代码如下:



上图是对标准输入设置O_ASYNC机制,那么用户输入数据就会导致系统发送信号通知进程。

O_EXCL和O_CREAT的妙用? 

通过查看open的man文档你会发现,对于O_EXCL标志的作用的解释就是,结合参数_CREAT参数使用,专门用户创建文件,如果要创建的文件存在就返回错误。这个标志最大的作用在于将检查文件是否存在和创建文件这两步变成一个原子操作。首先我们看下使用O_CREAT的情况下,如何创建文件。



上面是不使用O_EXCL标志的情况下,如何判断一个文件是否存在,不存在则打开这个文件。如果在NFS场景下,或者是多进程,多线程场景下,首先access判断文件是否不存在,发现不存在那么开始open打开这个文件,但是在open未调用之前,其它进程或线程创建了这个文件。然后开始执行open,那么这就存在问题了,open打开的是一个已经存在的文件。为了避免这个问题必须要给这两步加锁,除此之外你就可以使用O_EXCL标志配合O_CREAT标志,将上面两个步骤原子化。

什么是文件空洞,其作用是什么? 

文件空洞其实就是文件偏移量超过了文件的结尾,然后再执行IO操作,那么文件偏移量到文件结尾这段空间就形成了所谓的空洞,是不占据磁盘空间的。直到后续需要写入数据的时候才会分配磁盘块存储数据,虚拟机的磁盘格式有一种就是稀疏存储,就是用户设置了一个固定的磁盘大小,但是实际上并没有占用这么大的空间,而是使用多少就占用多少,直到超过这个固定的大小,也就是文件空洞的结尾处。

如何分配磁盘空间? 

分配磁盘空间?没听错是分配磁盘空间,平时我们都知道分配内存空间,但是很少知道分配磁盘空间,那么为什么需要分配磁盘空间这样的概念呢?,其实我们在使用write的时候,就会触发磁盘空间的分配,如果磁盘满了,或者达到文件空洞的边界,会导致write失败,为此我们可以预先分配磁盘空间,如果分配失败就可以不进行write操作了。linux下提供了一个特殊的系统调用如下:



这个系统调用用于给fd,分配磁盘空间,POSIX标准提供了posix_fallocate。但是前者更高效。mode参数决定了是分配磁盘空间,还是回收磁盘空间,mode的取值如下:

FALLOC_FL_KEEP_SIZE 分配空间

FALLOC_FL_PUNCH_HOLE 释放空间

深入探究文件I/O

如何原子性向文件中追加数据?

如果只是谈论如何向文件中追加数据,那么我很容易想到下面两种解决方式:

lseek定位到文件某尾,然后开始write写入数据

打开文件的时候使用O_APPEND标志,然后开始write写入数据

但是如果问我如何原子性的向文件追加数据,那么只能是第二种方法,使用lseek定位的方法不是原子的,如果lseek定位到文件末尾,而另外一个进程向这个文件追加了数据,那么此时lseek返回的值已经不再是文件末尾了,这个时候写入数据存在问题。引起这个问题的原因就是lseek和write其实不是原子的。而是用O_APPEND标志不一样,加入这个标志后,每次write其实就是原子性的lseek和write操作。

如何原子性的读取文件特定pos的数据?

很容易我会想到lseek到特定pos,然后读取数据,很可惜这样不是原子的,因为多进程是共享文件的pos,所以很有可能在你开始写入数据之前pos被改动了。为此linux内核提供了一套原子性的系统调用,用于原子性的定位和读写,函数原型如下:



这两个系统调用基本等同于下面操作:



pread基本包含了上述几个操作,不过对于pread来说这些操作是原子性的,并且操作完成后,还会还原回原来的文件偏移量。除了这个原子性优势外单个pread的操作的系统调用成本要低于上述几个操作带来的开销。

什么是Scatter-Gather I/O?

这个术语在linux内核层面也有一个同样的概念,不过我们这里谈论的不是Linux内核里面的这个概念。我们平常在编程的过程中是否遇到过,需要一次性将分布在内存多处的地方的数据写入到文件或者网卡上呢?,有没有需要从文件描述符中读取数据,并将这些数据依次放在多个buf里面的需求呢,这样的需求在网络编程中经常遇到,我们知道网络协议中经常有一个概念就是包头和包体,通常包头是放在一个地方,包体放在另外一个地方,这样导致发送数据的时候需要发送多次才能将数据发送出去,但是有了Scatter-Gather I/O只需要一次操作就可以完成,避免了多次系统调用的开销。Linux为我们提供了这样的系统调用,如下:



struct iovec设置需要写入/读取的多个buf的位置和长度。linux同时也提供了preadv/pwritev用于原子性的定位Scatter-Gather I/O。

什么是非阻塞I/O?

我们平常读写文件的时候使用read/write的时候,好像没有阻塞的情况吧,其实不然,那是因为内核缓冲区保证了普通文件I/O不会陷入阻塞,因此打开普通文件一般会忽略阻塞标志,但是对于设备,管道和套接字文件来说是会发生阻塞的,比如,读取标准输入设备的时候,如果用户不输入数据,那么read会阻塞,读取套接字的时候,如果对端没有发送数据,那么read也会阻塞。有的时候我并不希望我的read操作是阻塞的,我希望在没有数据的时候可以立刻告诉我,这样我好去做其他事情,而不是浪费在等待上。因此有了非阻塞IO的概念,通过fcntl设置描述符的O_NONBLOCK标志就可以将描述符变为非阻塞的,此时如果发起read操作,内核没有就绪的数据那么就会立刻返回,并出现EAGAIN或EWOULDBLOCK错误。

如何创建临时文件?

有些程序需要创建一些临时文件,仅供其在运行期间使用,程序终止后立即删除,linux为我们提供了一系列的函数用于创建临时文件,其函数原型如下:



mkstemp需要用户提供一个文件名模板,模板中的最后六个字符必须是XXXXXX,这六个字符将会被替换以保证文件名的唯一性。但是需要注意的是,生成的临时文件名是通过template传回的,因此template要求应该是一个字符数组。mkstemp的返回值就是这个临时文件的文件描述符。tmpfile也是用于创建临时文件,不过其返回的是待缓冲的IO。


    关注 Linux内核之旅


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册