Unix中的文件IO

说明

本文基于《Unix环境高级编程》第三版,大部分内容来源于此。


文件描述符

文件描述符是打开文件的引用,可以通过文件描述符来对打开文件进行 IO 相关操作。
文件描述符是一个非负整数(小整数),由于每个进程打开文件的个数有限,会根据系统来进行限制。目前不用担心进程中打开文件数目过多问题。


IO 操作函数

在IO 操作函数中,只关心 open, read, write, lseek 和 close 几个函数。这几个函数都是系统调用,并且不带缓冲。看下图就会很清楚:
Unix中的文件IO_第1张图片

open 和 openat 函数

#include 

int open(const char *path, int oflag, .../* mode_t mode */);
int openat(int fd, const char *path, int oflag, .../* mode_t mode */);
返回值:
  成功则返回文件描述符,错误返回 -1 。
参数:
  path: 打开文件的路径,可以是绝对路径和相对路径。
  oflag: 打开文件的标志
    O_RDONLY        只读
    O_WRONLY        只写
    O_RDWR          可读写
    O_APPEND        附加到文件尾
    O_CLOEXEC       执行时关闭
    O_CREAT         文件不存在则创建该文件
    O_TRUNC         若文件存在,并且只写或者可读写,将文件长度截断为 0。
  mode: 该参数是无符号32位整数。在新建文件时生效,表示权限位
    S_IRUSR     所有者拥有读权限
    S_IWUSR     所有者拥有写权限
    S_IXUSR     所有者拥有执行权限
    S_IRGRP     群组拥有读权限
    S_IWGRP     群组拥有写权限
    S_IXGRP     群组拥有执行权限
    S_IROTH     其他用户拥有读权限
    S_IWOTH     其他用户拥有写权限
    S_IXOTH     其他用户拥有执行权限

mode参数可用几个八进制数来表示,三个二进制可以表示出一个八进制数,看下图:

    ... 000 000 000 000
    ... ug- rwx rwx rwx
         S    U   G   O

S 表示特殊权限。
u  表示普通用户拥有可以执行 “只有root权限才能执行” 的特殊权限,也叫 setuid。
g  表示 setgid,和 setuid 同理。
-  表示粘贴位,只有文件拥有者和 root 用户能删除该文件,其他用户不能。 

U 表示用户权限
G 表示组用户权限
O 表示其他用户权限

说明:前面的省略号表示 0 的个数,mode 参数是32 位,前面还有很多 0省略掉了

看一个小例子:

int fd; 
fd = open(“abc.txt”, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) 
    ...

上面代码打开当前文件夹下abc.txt文件,如果文件存在,就将文件长度截断为0 (文件内容清零)。如果文件不存在,就创建abc.txt,创建时给予 rw- r– r– 权限。

创建文件时实际上涉及掩码(umask),它可以更改文件或者目录的默认权限。计算方法为:
真实权限 = 设置权限 & ~umask
例如umask值为:0002, 表示其他用户是没有写权限的。当执行open(“abc.txt”, O_CREAT, 0777)时会发现,abc.txt的真实权限为:rwx rwx r-x。
真实操作实际是 0777 & ~0002 = 0775。
mask的意思是掩码,umask就是掩码取反的意思。

creat函数可以用open函数进行替代,这里就不做介绍。

close 函数

#include 
int close(int fd);

返回值:
0 表示成功关闭,-1 表示失败。

程序结束,内核会自动关闭所有打开的文件。为了养成良好的习惯,打开文件之后记得调用 close 显式进行关闭。

lseek 函数

#include 

off_t lseek(int fd, off_t offset, int whence);

返回值:
成功则返回打开文件的当前偏移量,失败返回 -1 。

参数:
offset 依赖 whence,offset指定偏移量,而whence则指定开始计算偏移量的位置。
whence:
SEEK_SET 表示文件开始
SEEK_CUR 表示文件当前偏移量,offset可以为正和负,表示方向。
SEEK_END 表示文件结尾,offset可以为正和负,表示方向。

lseek函数的作用是什么?其实 lseek 并没有进行读写,就是修改了打开文件的偏移量,为下一次读写做准备。内核中拥有一个打开的文件表项,该数据结构上有一个字段表示打开文件的当前偏移量。

read 函数

#include 

ssize_t read(int fd, void *buf, size_t nbytes);

返回值:
返回读取的字节数,0 表示读至打开文件尾,-1 表示错误。

说明:nbytes 表示要读取的字节数,而返回的是读取到的字节数。很有可能nbytes大于读取到的字节数,例如一个文件只有 30 个字节,若从中读取 100 个字节,则 read 函数返回值可能是 30。

write 函数

#include 

ssize_t write(int fd, const void *buf, size_t nbytes);

返回值:
成功则返回写入的字节数,失败则返回 -1。

说明:通常nbytes和返回值相同,如果不同,则表示发成错误。

小结

IO 操作主要关心读写操作,本小结主要是快速看了函数的基本定义,以及参数和返回值的讲解。除了 open 和 openat 函数是 fcntl.h 头文件,其他的 close, read, write, lseek都是在 unistd.h 头文件中。
read 和 write 操作是无缓冲的,根据文件表项中的当前偏移量进行读写。在文本编辑器中,将文件内容先全部读入内存,然后操作实际上内存里面的数据,与真实文件内容无关,每次保存操作实际上是写入文件操作。


文件共享

Unix支持多个进程打开同一个文件。在了解的理论知识前,先看看数据结构是怎样的。
Unix中的文件IO_第2张图片
上图中,可以分为3部分(左-中-右)进行解读:
左:

每个进程有一个进程表项,里面含有文件描述符表项,可将其看做一个矢量或者是引用,指向打开的文件表项(在内核中)。

中:

内核中有一个打开的文件表项数组(为了便于分析,实际上不是一个数组),每个文件表项是数组中的一个元素。每个文件表项中存有:
a) 文件状态标志,表示读写,附加,同步,非阻塞等状态。
b) 当前文件的偏移量。
c) v-node 指针,指向v-node表项。

右:

每个打开文件(或设备)都有一个v-node 结构。v-node 结构存储了文件的类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点包含了该文件的 i节点(索引节点)。

说明:linux没有使用 v 节点,而是使用了通用的 i 节点。虽然实现有所不同,但是概念还是一样的。

下面介绍两个独立进程打开同一文件。
Unix中的文件IO_第3张图片
上图中两个进程都拥有自己的进程表项,虽然是打开同一文件,但是在内核中却有不同的文件表项,这样每个进程有自己打开文件的状态和的当前偏移量等信息。再看右边只有一个 v-node 表项,证明v-node 就是指向了真实的文件信息。看下面示意图:
Unix中的文件IO_第4张图片

接着讨论多个进程同时写入同一文件情况,因为每个进程都有自己的文件偏移量,那么同时写入有可能会导致发生数据错乱,这里就涉及到原子操作。


原子操作

什么是原子操作?原子操作一般是指由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么就一步也不执行,不可能只执行所有步骤的一个子集。

考虑下面情况,假设有两个独立的进程 A 和进程 B都对同一文件进行追加写操作。每个进程都已经打开了该文件,但是没有使用 O_APPEND 标志。假设进程A 调用 lseek,将该文件偏移量设置为1500。然后此时进行内核切换,进程B开始执行。进程B调用lseek,也将文件表项的文件偏移量设置为1500,然后写100个字节数据,此时进程B中对应的文件表项的偏移量就为1600。当内核又进行切换时,A开始执行,并且也向该文件写 100 字节数据,此时进程A对应的文件表项的偏移量还是1500,当写100字节时就覆盖了刚才进程B写的100字节数据。

Unix系统提供了原子操作方法,那就是打开文件时添加 O_APPEND 标志。使得内核每次写操作之前,都将进程的当前偏移量设置到该文件尾,于是就可以不使用lseek操作。


dup 和 dup2 操作

#include 

int dup(int fd);
int dup2(int fd, int fd2);

dup 和 dup2 函数用来复制一个文件描述符,通常被用作重定向进程的stdin, stdout 和 stderr。dup函数意味着两个描述符指向同一个文件表项。但是dup函数不支持指定文件描述符进行重定向,所以系统会指定一个当前可用的最小文件描述符。

dup2可以将文件描述符 fd “复制”给指定文件描述符 fd2 ,使 fd 和 fd2 都指向同一个文件表项。如果文件描述符fd2已经打开,首先将fd2关闭。如果fd2 等于 fd,则 dup2直接返回fd2 ,不关闭 fd2 。
Unix中的文件IO_第5张图片


fcntl 函数

#include 
#include 

int fcntl(int fd, int cmd, .../* int arg */);

fcntl函数的功能实际上修改文件描述符。
fcntl有五种功能:
(1) 复制一个已有的描述符( cmd = F_DUPFD 或 F_DUPFD_CLOEXEC)
(2) 获取/设置文件描述符标志( cmd = F_GETFD 或 F_SETFD )
(3) 获取/设置文件状态标志( cmd = F_GETCFL 或 F_SETFL )
(4) 获取/设置异步I/O所有权( cmd = F_GETOWN 或 F_SETOWN )
(5) 获取/设置记录锁( cmd = F_GETLK 或 F_SETLK )


FAQ

Q: 不带缓冲是什么意思?
A: 是指 read 和 write 都调用内核中的一个系统调用。

Q: 不同进程中的文件描述符表可不可能有相同的文件描述符?
A: 可能。
每个进程都维护自己的一个文件描述符表。幻数0, 1 和 2分别用作标准输入,标准输出和标准错误,已经被作为标准。当进程打开一个新文件时,文件描述符通常就为 3 。文件描述符实际上就是一个非负整数,并无特别之处。它与文件的关系就是:一个文件描述符只能对应一个打开文件,多个文件描述符可以对应同一个打开文件。但是不同的进程也有可能打开相同的文件,有可能会遇到相同的文件描述符指向同一个打开的文件。每个进程都维护自己的一个文件描述符表, 进程之间文件描述符是彼此独立的,没有必然的关系。

Q: 执行时关闭标志位有什么作用?
A: 关闭无用的文件描述符。
执行时关闭标志位,表示在执行 exec 时会关闭打开的文件。在父进程中fork多个子进程情况下,子进程将获得父进程的数据空间、堆和栈的副本。如果子进程不需要用到父进程中的某个打开文件,这个时候可以在父进程中设置 close-on-exec 标志位。当fork出子进程,然后执行exec 时就会自动关闭不需要的文件。

Q: open创建文件时,设置权限没有生效?
A: 使用 umask 查看是否设置了掩码。
如果创建文件时设置权限 0777,但是设置了掩码 0002,那么最终的权限为 0775。
计算方法: 0777 & ~0002 = 0775。
如果不熟悉,建议使用 man umask 查看帮忙文档。


参考

[1] Advanced Programming in the UNIX Environment - Third Edition
[2] 漫谈linux文件IO:
http://elf8848.iteye.com/blog/1944226


你可能感兴趣的:(linux)