Linux 系统编程导读 - 文件I/O

按照书籍的简介,对文件I/O的介绍流程为:文件I/O的相关概念、C标准的I/O接口、更高级专门化的I/O接口、文件和目录操作四个部分。

文件I/O基础

这章主要介绍Linux是如何管理文件的读写操作的,从打开文件到文件关闭中途的一系列对文件的操作过程。这里注意的是4点:

  1. 文件通过文件描述符来操作,即打开一个文件,就会返回一个文件描述符;然后Linux Kernel为每一个进程维护一张打开文件表,按照文件描述符对打开的文件进行索引。当然最终还是索引到inode。
  2. 每个进程一开始启动默认打开的三个文件描述符,0表示标准输入,1表示标准输出,2表示标准错误;因此我们打开一个文件,文件描述符一般都是从3开始。
  3. 文件的概念,既包括普通文件,也用于访问设备文件、管道、目录等等。遵循一切皆文件的理念,任何能够读写的东西都可以用文件描述符来访问。
  4. 关于子进程,默认会从父进程获得一份打开文件表的拷贝;子进程中文件的变化不会影响父进程(拷贝的目的? 为了效率?)

打开文件

打开文件包括打开已有文件以及打开不存在的文件(即创建文件)。返回值是文件描述符,失败的话返回-1.
比较重要的2点:

  1. 若新建文件,那么这个文件所属启动该进程的用户;用户组的话存在两个标准,详细可查看相关资料。
  2. 新建文件,涉及到权限问题,在打开的接口中设置。
    两个接口函数,open、creat(注意没有e zZ…)
    1
    2
    3
    4
    5
    6
    7
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    int open (const char *name, int flags);
    int open (const char *name, int flags, mode_t mode);
    int creat (const char *name, mode_t mode);

    相关可以查看手册说明,需要注意的是creat创建新的文件和open创建没有区别;mode表示的设置新建文件权限,只有在新建文件时才生效。

读文件

读文件是常见的接口,read

1
2
#include <unistd.h>
ssize_t read (int fd, void *buf, size_t len);

其中fd就是文件描述符了,要读哪个文件就传入该文件的描述符即可了。我们比较常用的是0,也就是从shell命令行中输入了,对应于前面的介绍,0就是标准输入。len为最大长度,试了一下若len为0或者负数的话就直接结束了,不会阻塞等待输入。
关于阻塞和非阻塞,我们比较常见的是阻塞了,也是默认的情况,就是一直挂起直到read结束或者错误产生;非阻塞就相反了。
一般判断read的状态就通过返回值了,书上对可能的结果描述的很详细;总结几点就是:

  1. 常见的话,就返回实际读入的长度
  2. 错误返回-1,具体错误信息需要进一步判断
  3. 返回0,标志着EOF,没有可读的数据了
    read返回值

写文件

写文件常用的就是write了

1
2
#include <unistd.h>
ssize_t write (int fd, const void *buf, size_t count);

和read类似了,只是我们常用的fd为1,也就是标准输出了。
write写入文件的话,最终是写入磁盘;值得一提的是,为了提高处理效率,内核会将需要写入的数据复制到内核缓冲区中,然后等到空闲或者其他条件满足时,将多个缓冲区的数据一并写入磁盘中。也称为延迟写入。这样既保证了程序执行的效率,即调用write就能够快速执行结束;同时下一次调用read读取写入的数据时可以不用从磁盘中读取,而是直接从缓冲区读入。当然也存在风险,在写入磁盘前系统崩了,数据就丢失了。

I/O 同步

主要是对针对前面的write操作,因为write默认是将数据放在内核缓冲区中,并不写入磁盘之中。当然有些需求可能需要数据实时同步到磁盘中,尤其是一些很重要的数据;这里的话Linux也提供了相应的操作选项。

  1. 针对单个文件同步接口
    1
    2
    3
    #include <unistd.h>
    int fsync (int fd);
    int fdatasync (int fd);

    这两个接口函数用于对指定文件进行一次同步,即将fd对应的缓冲区数据写入磁盘之中。区别在于fsync将所有数据写入,fdatasync仅写入普通数据,并不包括元数据(任何文件系统中的数据包括两部分,一是普通数据,一是元数据,简单描述为普通数据就是文件中的实际数据,元数据就是描述文件特征的系统数据。详细参考http://www.cnblogs.com/wspblog/p/4967339.html)。
    调用成功返回0,失败返回-1.
    上面是对单个文件的,当然也有对全部文件的,即将所有缓冲区(暂存文件数据的)中数据同步到磁盘
  2. 针对所有文件同步接口
    1
    2
    #include <unistd.h>
    void sync (void);

    当然除了接口之外,在打开文件时也可以通过设置参数来实现同步
  3. 通过设置参数
    1
    2
    //O_SYNC 
    fd = open (file, O_WRONLY | O_SYNC);

    O_SYNC标志打开文件使得所有在该文件上的I/O操作都会同步到磁盘
    O_DSYNC 和 O_RSYNC标志,O_DSYNC标志使用普通数据会同步,元数据不同步;O_RSYNC标志要求读操作像写操作一样同步(当然主要是元数据,比如最后打开文件的时间更新之类的,因为读文件并不会修改基本数据……)。

直接I/O

有时候用户并不需要内核的I/O管理,而是倾向于实现一套属于自己的I/O管理。
在open中使用O_DIRECT标志会使内核最小化I/O管理的影响。使用该标志,I/O操作将会忽略页缓存机制,直接对用户空间缓冲区和设备进行初始化。所有I/O将是同步的,操作在完成之前不会返回。

关闭文件

常用的close接口

1
2
#include <unistd.h>
int close (int fd);

执行成功返回0,失败返回-1

lseek查找

1
2
3
4
#include <sys/types.h>
#include <unistd.h>

off_t lseek (int fd, off_t pos, int origin);

相关参数定义参考:http://www.cnblogs.com/ly01/p/4597926.html
该接口主要是为了满足在指定位置读写的需求。这么说吧,假设我们调用read函数进行读取数据,read (int fd, void *buf, size_t len); 其中传入的是一个文件标识符fd,用于找到文件;其实这中间还隐藏了另一个参数,那就是文件偏移,默认我们的操作偏移都是0,所以我们打开文件然后读取就会从文件的开始之处读,而lseek就是为了改变这个偏移值,使得从指定的位置开始读入。
写了一个小例子,其中1234.txt中的内容为1234567890
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
char buf[100] ;
int fd = open("1234.txt", O_RDWR) ;

off_t t = lseek(fd, 5, SEEK_CUR) ;
read(fd, buf, 1) ;

printf("%s\n", buf) ;

return 0 ;
}

设置了偏移为当前位置偏移5,那就是开始读入为6,因此输出为6.
这是一种方式,但是已经改变了文件偏移,另外两个接口就是直接指定位置读写,而且不会改变原始偏移
1
2
3
#include <unistd.h>
ssize_t pread (int fd, void *buf, size_t count, off_t pos);
ssize_t pwrite (int fd, const void *buf, size_t count, off_t pos);

pos是绝对偏移

文件截断

就是把一个指定文件设置为指定长度,长了截断,短了补0

1
2
3
4
5
#include <unistd.h>
#include <sys/types.h>

int ftruncate (int fd, off_t len);
int truncate (const char *path, off_t len);

两个接口,一个操作一个可写的fd,一个直接操作文件路径

I/O多路复用 (IO multiplexing)

正常情况下我们要进行多个I/O操作的时候,比如说read,阻塞等待数据到来;一种很常规的方法就是多线程监听等待数据。I/O多路复用说简单点就是使用一个进程来对多个I/O进行管理,基本原理是该进程不断查询所负责I/O,当某个I/O有数据达到时,就通知用于进程进行处理。
对该方法的实现,提供了3个接口:select、poll、epoll,这个接口也是依次优化。这种方法常用于WebServer的处理,一般都要处理不同用户的请求,如nginx中使用了epoll。
简单介绍参考:https://segmentfault.com/a/1190000003063859#articleHeader9
详细描述参考:https://www.zhihu.com/question/32163005 (包含对三个接口的比较)

内核内幕

这部分主要介绍Linux内核是如何实现I/O的
** 虚拟文件系统 (VFS) **
可以看成是对不同文件系统(如NTFS等)的一个封装,然后提供接口给上层程序员调用;程序员不需要关心文件所在的文件系统或者介质,通过统一接口处理即可。
** 页缓存 **
简单说是利用局部性原理,将最近访问的数据存入到内存之中,提高下一次的访问速率(当然是基于假设最近访问的数据再次访问的概率较大)
** 页回写 **
也就是前面write中的延迟写,满足以下条件时会将内存中的数据写入磁盘:

  1. 当空闲内存小于设定的阈值时
  2. 当一个脏的缓冲区(存储将要写入磁盘中数据的缓冲区称之为脏缓冲区)寿命超过设定的阈值时
    回写入磁盘由pdflush内核线程操作,当出现上述条件之一,pdflush线程将被唤醒,并开始回写。
Linux 系统编程概述 Linux 系统编程导读 - 简介
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×