UNIX环境高级编程-File I/O

Chapter 3. File I/O

文件描述符

在Linux系统中,打开的文件是用一个整数来表示的,表示打开文件的整数,称之为文件描述符。当需要往写数据/读数据时,读写函数都需要文件描述符作为参数,以便系统知道用户操作的时哪个文件。

基本操作

open/creat

mode选项 解释
O_RDONLY 读方式打开
O_WRONLY 写方式打开
O_RDWR 读写方式打开
O_CREAT 创建文件,如果文件存在,会被截断
O_TRUNC 截断
O_APPEND 追加
O_EXCL 和O_CREAT一起用,如果文件存在则失败

close

关闭文件

read/write

读写文件,会导致文件指针移动。

文件指针和lseek

文件指针是一个整数,描述当前读写位置,可以使用lseek移动文件指针。

文件共享

两个进程可以打开同一个文件进行操作,实现数据的共享。但是当两个文件打开同一个文件进行写操作时,会相互覆盖。当文件被打开两次时,两个文件描述符有各自的文件指针。

内核保存一个全局的文件描述结构体,而一个文件打开两次之后,两个结构体各自有各自的文件指针。

dup/dup2/dup3

dup函数可以复制文件描述符,让两个文件描述符指向同一个文件结构,通过dup复制文件描述符和两次打开文件描述符不同,所以两个文件描述符共享一个文件指针。

当一个文件描述符被关闭时,关闭的是内核的文件描述结构,但是如果文件描述结构体中,引用计数器不为1,那么close函数就只是减少了引用计数器而已。

dup2是将旧文件描述符复制到新的文件描述符(即建立连接).
dup3在dup2的基础上添加了更多操作选项,

文件原子操作

原子操作是指一个操作一旦启动,则无法被能破坏它的其它操作打断。

写文件

无论是两次打开还是dup,同时操作一个文件都可能引起混乱,解决这个问题的方法是,可以通过O_APPEND解决这个问题。O_APPEND选项可以使得当一个写操作正在进行时,另外一个对该文件的写操作会阻塞等待。

这意味着有O_APPEND选项的文件描述符,写操作无法被打断。

创建文件

除了写操作有原子性问题,创建文件也有,如果两个进程同时调用creat或者带O_CREAT的open,创建同一个文件时,可能会出现这种情况,第一个操作创建成功之后,写入数据,而第二个操作的O_CREAT把数据抹去了。

但是如果在O_CREAT之后,加上O_EXCL,那么可以避免这种情况。

fcntl和ioctl

fcntl可以用来设置文件描述符属性、文件状态属性、文件描述符复制、设置文件锁、设置文件通知等功能,这里只表示学习通过fcntl修改文件描述符属性。

如果一个文件描述符没有O_APPEND属性,但是后来又需要这个属性,那么可以通过fcntl来设置。

ioctl是一个杂项函数,一般用于文件底层属性设置

文件映射

文件映射能将硬盘映射到进程的地址,这样可以像操作内存一样操作文件,而且效率很高,但是有一定限制:

  • 文件长度必须大于等于映射长度
  • 映射的offset必须是页的整数倍

页的尺寸获取方式
– 命令行getconf -a | grep PAGE_SIZE
– 函数sysconf(_SC_PAGE_SIZE)

临时文件

可以通过mktemp(3)来获取一个临时文件路径,但是该文件不一定在/tmp目录下,在哪个目录下需要程序员指定。

缓存

为了提高IO效率,系统为应用程序提供了缓存服务。当应用程序写数据到硬盘时,内核只是将数据写入内核缓存,然后返回成功。

缓存的存在隐藏风险,如果缓存数据未写入硬盘时,发生断电故障,那么会导致数据的不完整性。

关键数据的不完整,可能会导致系统崩溃。

使用O_SYNC选项打开文件时,那么写入操作将保证数据写入到硬盘再返回,当然这个选项导致IO效率降低。

也可以使用syncfsyncfsyncdata之类的函数,将数据写入硬盘。

文件属性

使用man 2 stat 查看详细内容:

struct stat {
    dev_t     st_dev;         /* ID of device containing file */
    ino_t     st_ino;         /* Inode number */
    mode_t    st_mode;        /* File type and mode */
    nlink_t   st_nlink;       /* Number of hard links */
    uid_t     st_uid;         /* User ID of owner */
    gid_t     st_gid;         /* Group ID of owner */
    dev_t     st_rdev;        /* Device ID (if special file) */
    off_t     st_size;        /* Total size, in bytes */
    blksize_t st_blksize;     /* Block size for filesystem I/O */
    blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */

    /* Since Linux 2.6, the kernel supports nanosecond
       precision for the following timestamp fields.
       For the details before Linux 2.6, see NOTES. */

    struct timespec st_atim;  /* Time of last access */
    struct timespec st_mtim;  /* Time of last modification */
    struct timespec st_ctim;  /* Time of last status change */

#define st_atime st_atim.tv_sec      /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
属性 释义
dev_t st_dev 设备号
ino_t st_ino inode编号
mode_t st_mode 访问权限相关
nlink_t st_nlink 硬链接数量
uid_t st_uid 拥有该文件的用户
gid_t st_gid 拥有该文件的组
dev_t st_rdev 设备号
off_t st_size 文件尺寸
blksize_t st_blksize 文件系统的IO尺寸
blkcnt_t st_blocks 占用的block数量,一个block为512字节
time_t st_atime 最后访问时间
time_t st_mtime 最后修改时间
time_t st_ctime 最后文件状态修改时间

文件类型

文件类型包括七种,在stat结构中,保存了文件的文件类型属性,它的文件类型属性保存在st_mode中。但是七种类型,只需要3位即可,而st_mode是一个整数,因此它还保存其它内容,如果需要判断一个文件属于何种类型,需要一些宏的帮助。

文件类型属性是只读的属性,无法修改。\

使用man 2 inode 查看详细内容:

S_ISREG(m)  is it a regular file?
S_ISDIR(m)  directory?
S_ISCHR(m)  character device?
S_ISBLK(m)  block device?
S_ISFIFO(m) FIFO (named pipe)?
S_ISLNK(m)  symbolic link?  (Not in POSIX.1-1996.)
S_ISSOCK(m) socket?  (Not in POSIX.1-1996.)

文件的用户和组

Linux是一个多用户操作系统,因此每个文件都有属性,记录着这个文件属于特定的用户/组。

用户/组信息可以被修改,可以通过命令chown来修改文件所属的用户和组信息。

  • 修改文件所属用户和组,需要特定权限。
  • 新文件所属用户和组,是创建该文件的进程所属用户和组。

关于实际账户与有效账户:

账户 释义
实际账户 登陆系统时的账户
有效账户 决定进程的访问资源的账户

文件访问权限

文件使用了9个位来表示访问权限,和文件类型一起,保存在st_mode中。此9位分成3组,每组3个位,分别表示//执行权限,而三个组分别表示拥有该文件的账户,拥有该文件的组,其它用户组的权限。如果对应位是1,表示有权限,如果是0表示没有权限。

1 1 1 1 0 1 1 0 1

文件访问权限经常用8进制来表示,比如上表的权限位可以表示为0755,意思是拥有它的账户对这个文件有读/写/执行权限,而拥有它的组有读/执行权限,其它账户对它有读/执行权限。

Linux提供一些宏,来测试文件的权限位,使用man 2 inode 查看详细内容:

S_ISUID     04000   set-user-ID bit
S_ISGID     02000   set-group-ID bit (see below)
S_ISVTX     01000   sticky bit (see below)

S_IRWXU     00700   owner has read, write, and execute permission
S_IRUSR     00400   owner has read permission
S_IWUSR     00200   owner has write permission
S_IXUSR     00100   owner has execute permission

S_IRWXG     00070   group has read, write, and execute permission
S_IRGRP     00040   group has read permission
S_IWGRP     00020   group has write permission
S_IXGRP     00010   group has execute permission

S_IRWXO     00007   others (not in group) have read,  write,  and execute permission
S_IROTH     00004   others have read permission
S_IWOTH     00002   others have write permission
S_IXOTH     00001   others have execute permission

文件长度

st_size保存文件的长度,write函数会修改该属性,也可以通过truncate修改文件大小,truncate可以扩大文件或者缩小文件,缩小文件时,文件内容会被删减。

文件大小可以通过命令lswc -cstat命令获取。
也可以通过fseekftell函数配合获取,或者直接通过stat函数获取文件长度。

文件时间

对文件的访问,会导致文件时间发生变化。系统会自动记录用户对文件的操作的时间戳,以便将来可以查询文件修改时间。

如果需要故意修改,那么可以通过utime函数,修改文件的访问时间和修改时间。

touch命令也可以将文件的时间修改为当前时间。touch命令的副作用是,如果参数所指文件不存在,那么创建一个空文件。

当用户进行大规模拷贝时,cp操作会修改文件的访问时间,如果想提高效率,可以使用-p选项,避免文件属性的修改,同时加快速度。

inode

一个硬盘分区,被格式化之后,可以认为硬盘被划分成两部分:管理数据和数据。管理数据部分保存着这个分区的分区信息,以及inode表。

inode保存文件的属性信息,stat命令能看到的信息,大部分都是保存在inode里的,一个inode占用128或者256字节,这个依赖具体的文件系统,每当在硬盘上创建一个文件/目录时,系统为这个文件/目录分配一个inode。值得注意的是,文件名,不存在inode中,而是存在文件所在目录的文件内容部分。

数据块

数据部分被简单的、按照等大尺寸的划分成n块,一般每块数据块的尺寸为1024-4096,由具体文件系统决定。

当创建一个文件时,系统为该文件分配一个inode。如果往该文件写数据,那么系统为该文件分配数据块,inode会记录这个数据块位置,当一个数据块不够用时,系统会继续为它分配数据块。

当创建一个目录时,系统为该目录分配一个inode,同时分配一个数据块,并且在该数据块中,记录文件.和..对应的inode。

如果在该目录下创建文件newfile,那么参考上一节内容,会为该文件创建inode,最后将newfile文件名和它的inode,作为一条记录,保存在目录的数据块中。

硬链接和软链接

硬链接不占用inode,只占用目录项。
软链接占用inode。

虚拟文件系统VFS

内存无法加载硬盘所有内容,因为一般内存比硬盘小,但是在Linux内核中,维护了一个虚拟文件系统,将硬盘的目录结构映射到内存中。这个映射一般只包含已经被打开的文件。