文章目录
UNIX环境高级编程(APUE)
第3章 文件I/O(系统调用I/O)
文件I/O常用的函数
open()、read()、write()、lseek()、close()、dup()、fcntl()、sync()、fsync()、ioctl()
3.1 文件描述符
所有打开文件都由文件描述符引用,文件描述符是一个非负整数。
其中0 -> stdin,1 -> stdout,2 -> stderr
3.2 函数open和openat
#include <fcnt1.h>
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_EXEC 只执行打开
O_SEARCH 只搜索打开
以上五个常量必须指定并且只能指定一个
O_APPEND 每次写都追加到文件末尾
O_CLOEXEC 把FD_CLOEXEC常量设置为文件描述符标志
O_CREAT 如果此文件不存在则创建。使用此选项需指定mode参数
O_DIRECTORY 如果path不是目录,则出错
O_EXCL 如果同时指定O_CREAT,并且文件已存在,则出错
O_NOCTTY 如果path引用为终端设备,则不分配该设备作为此进程的控制终端
O_NOFOLLOW 如果path引用一个符号链接,则报错
O_NONBLOCK 如果path引用的是一个FIFO、一块特殊文件或一个特殊字符文件,则I/O采用非阻塞模式
O_SYNC 使每次write等待物理I/O操作完成
O_TRUNC 如果此文件存在,并且作为只写或者读写模式打开,将其长度截断为0
O_DSYNC 使每次write等待物理I/O操作完成,但如果该写操作不影响读取刚写入的数据,则不需要等待文件属性更新
O_RSYNC 使每一个文件描述符作为参数进行读操作等待,直至所有对文件同一部分挂起的写操作都完成
open和openat函数返回的文件描述符为最小的未使用描述符数值
open和openat的区别
- path参数是绝对路径名,二者无区别
- path参数是相对路径名,fd参数指出相对路径名在文件系统中的开始地址,fd参数是通过打开相对路径名所在目录进行获取
- path指定相对路径名时,fd参数具有特殊值AT_FDCWD表示路径名在当前工作目录中获取
为什么引入openat?
引入openat主要有两个目的
- 让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录
- 避免TOCTTOU
3.3 函数creat
#include <fcnt1.h>
int creat(const char* path, mode_t mode);
//返回值:成功,返回文件描述符;失败返回-1
creat只能以只写的形式打开文件。
3.4 函数close
#include <unistd.h>
int close(int fd);
//返回值:成功,返回0;失败,返回-1
关闭一个文件时还会释放该进程加在文件上的所有记录锁。
进程终止时,内核自动关闭所有打开文件
3.5 函数lseek
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//返回值:成功,返回新的文件偏移量;失败,返回-1
whence参数解释offset
SEEK_SET offset从文件开始处记录
SEEK_CUR offset从文件当前值记录,其值可正可负
SEEK_END 偏移量为文件长度加offset,其值可正可负
如果文件描述符指向一个管道、FIFO或者网络套接字,则lseek返回-1。
lseek仅记录文件偏移量,不引起I/O操作;允许文件偏移量大于文件当前长度,该情况下会在文件中构成空洞,空洞在磁盘上不占用存储区。
3.6 函数read
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t nbytes);
//返回值:读到的字节数,若已到文件尾,返回0;出错,返回-1
- 读普通文件,在读到要求字节数之前就达到文件尾,则下一次调用时返回0
- 从终端设备读时,通常一次最多读一行
- 从网络读时,由于网络缓冲机制可能会造成返回值小于要求读的字节数
- 从管道或者FIFO读时,如果管道包含的字节少于所需数量,只返回实际字节数
读操作从文件当前偏移量处开始,在成功返回之前,修改偏移量到实际读到的字节处。
3.7 函数write
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t nbytes);
//返回值:成功,返回已写字节数;失败,返回-1
写操作返回值通常与参数nbytes相同,否则出错。
一般出错原因有两个:
- 磁盘以写满
- 超过一个给定进程的文件长度限制
写成功返回前,偏移量也指向写的位置处
3.8 文件共享
在研究文件共享之前,首先要搞清楚内核是怎么表示一个打开文件的
内核一般使用3种数据结构来表示一个打开文件
每个进程在进程表中都会有一个记录项,其包含一张打开文件描述符表,这张表为一个矢量,描述符表示其下标。与每个文件描述符相关联的是文件描述符标志和一个指向文件表项的指针
那么文件指针所指向的文件表又包含什么?简单来说其内容包括:文件状态位标志、当前文件偏移量和指向该文件v节点表项的指针
那么v节点表里又有什么呢?简单来说其内容包括:文件类型和对文件进行各种操作函数的指针。
此外对大多数文件来说,v节点还包含该文件的i节点。i节点包括:文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等内容
如果两个独立进程各自打开同一文件,则会存在以下对应关系
每个进程都将获得属于自己的文件表项,这是因为每个进程都可以拥有他自己的文件表项以及对该文件的偏移量。
- 在write后,文件表中偏移量这个元素将会增加所写入的字节数,如果偏移量超出当前文件长度,那么将会改写i节点中的文件长度将其设置为当前偏移量
- 如果用O_APPEND标志打开文件,文件表中文件偏移量这项便会根据i节点中当前文件长度值进行变化,使得新写入的数据追加到文件尾
- 如果lseek定位到文件当前尾端,那么文件表项中的文件偏移量将会调整到i节点中的当前文件长度处,注意:lseek只修改文件表项中的当前文件偏移量,而不进行I/O操作。
此外关于文件标志和文件状态标志之间的区别,可以理解为:
文件标志只用于一个进程,即一个进程对应一个文件标志
而文件状态标志则是与文件表项所绑定的,即允许多个进程访问一个文件表,因此进程与文件状态标志之间的关系可以是多对一的。
3.9 原子操作
原子操作的概念是为何提出的?先来看一个问题:
假设A、B两个独立进程对同一文件进行追加写操作,此时每个进程有一个自己的文件表项,但是共享一个v节点表。A首先调用lseek,将文件当前偏移量设置问1500字节;然后B进程运行,也将B进程的当前文件偏移量改为1500,然后调用write写入100字节。由于二者共享一个v节点,那么当前文件长度因为B进程的写入操作从1500扩展为1600字节,此时A进程再调用write操作,但是A进程的当前文件偏移量为1500,这也就导致A进程的write操作将B进程刚写入的100字节覆盖。
为什么会出现以上问题?
原因是在进行追加数据到文件尾操作过程中,以上步骤实际上可以拆分为两步,分别是:定位到文件尾和写。由于分两步进行,那么在其中一步结束后如果内核临时挂起进程,就可能会导致以上情况的出现。
那么解决以上问题的方法为:使这两个操作在其他进程眼里是一个操作,也就是原子操作。我们只需要在打开文件时设置O_APPEND标志,这样就使得内核在每次写操作之前,都将当前偏移量设置为该文件尾端,于是我们也就不必调用lseek。
关于原子操作的例子:
#include <unistd.h>
ssize_t pread(int fd, void* buf, size_t nbytes, off_t offset);
//返回值:读到的字节数,如果已到文件尾,返回0;出错,返回-1
ssize_t pwrite(int fd, const void* buf, size_t nbytes, off_t offset);
//返回值:成功,返回写入字节数;出错,返回-1
3.10 函数dup和dup2
这两个函数的主要功能为复制文件描述符
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
//返回值:成功,返回新的文件描述符;失败,返回-1
由dup返回的新文件描述符一定是当前可用文件描述符最小值
对于dup2来说,我们可以用fd2来指定新的文件描述符。**如果fd2打开,则先将其关闭;如果fd=fd2,则直接返回fd2。
dup和dup2返回的新文件描述符与参数fd共享一个文件表项。
3.11 函数sync、fsync和fdatasync
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
//返回值:成功,返回0;失败,返回-1
sync的作用为:将修改过的块缓冲排入写队列,然后返回,整个过程不等待实际的写磁盘操作结束。也就是说sync主要起刷新缓冲区的作用。
fsync只对文件描述符fd指定的文件起作用,并且要保证写磁盘操作结束后再返回。fsync会更改文件内的数据以及文件属性。
fdatasync与fsync作用相同,但是其只改变文件数据部分。
3.12 函数fcntl
fcntl函数主要作用为改变打开文件的属性
#include <fcntl.h>
int fcntl(int fd, int cmd, .../*int arg*/);
//返回值:成功,依赖于cmd命令;失败,返回-1
fcntl的5种功能
- 复制一个已有描述符(cmd = F_DUPFD或F_DUPFD_CLOSEXEC)
- 获取/设置文件描述符标志(cmd = F_GETFD或F_SETFD)
- 获取/设置文件状态标志(cmd = F_GETFL或F_SETFL)
- 获取/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN)
- 获取/设置记录锁(cmd = F_GETLK、F_SETLK或F_SETLKW)
- F_DUPFD 复制文件描述符。新的文件描述符作为函数值返回。新的文件描述符的值为未打开的描述符中大于等于第3个参数值中的最小值
- F_GETFD 返回fd对应的文件描述符标志
- F_SETFD 设置fd对应的文件描述符标志
- F_GETFL 返回fd对应的文件状态标志
- F_SETFL 设置fd对应的文件状态标志
- F_GETOWN 获取当前接收SIGIO和SIGURG信号的进程ID或者进程组ID
- F_SETOWN 设置接收SIGIO和SIGURG信号的进程ID或进程组ID