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效率降低。
也可以使用sync
,fsync
,fsyncdata
之类的函数,将数据写入硬盘。
文件属性
使用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
可以扩大文件或者缩小文件,缩小文件时,文件内容会被删减。
文件大小可以通过命令ls
,wc -c
,stat
命令获取。
也可以通过fseek
和ftell
函数配合获取,或者直接通过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内核中,维护了一个虚拟文件系统,将硬盘的目录结构映射到内存中。这个映射一般只包含已经被打开的文件。