作者:qwer | 来源:互联网 | 2023-09-23 16:54
第三章 文件IO
一:主要内容:
本章主要讲述UNIX系统中的文件系统,包括文件信息的记录方式;文件的函数;文件的共享等。
二:文件描述符
对于内核来说,所有打开的文件都是用文件描述符来标识的,文件描述符是一个非负整数。每当一个文件打开或者创建时,内核都会想进程返回一个文件描述符来标识该文件,这个描述符可以用来进行其他操作,说白了就是内核区别多个打开文件的一个flag。
值得一提的是,UNIX已经提前将三个文件描述符与标准输入输出和错误相关联,这三个文件描述符就是0、1、2.其中0对应标准输入,1对应标准输出,2对应标准错误。
文件描述符存在一个变化范围:0-OPEN_MAX-1。早期的系统上限值为19,现在很多系统都将其增加值63.
三:函数open和openat
这两个函数可以打开或者创建一个文件,函数声明如下:
int open(const char *path, int oflag, ...);
int openat(int fd, const char *path, int oflag, ...);
path代表文件路径,oflag代表了文件的一些说明,如是否为只读打开,是否为追加模式,是否对path为非目录是返回错误等等。
值得一提的是,这两个函数所返回的文件描述符一定是未使用的文件描述符值最小的那个。
open函数和openat函数区别不大,当path为绝对路径的是否这两个函数功能完全一样;当path为相对路径的时候且fd为AT_FDCWD时,openat中的path是以当前目录为基准;当path为相对路径且fd不是AT_FDCWD,openat中的path以fd为基准。
在这里我们还需要考虑一下文件名称和路径名称截断的问题。有的系统会将超过文件名称的部分阶段,有的会报错。一般来说现在Linux系统文件名称的最大长度为255,在windows下面,单个文件名的长度限制是255,完整的路径长度(如E:\test\aaa.txt这样限制是260)。
四:函数create
函数create的声明如下:
int create(const char *path, mode_t mode);
文件创建成功返回对应fd,失败返回-1.
五:函数close
函数close的声明如下:
int close(int fd);
六:函数lseek
函数lseek声明如下:
off_t lseek(int fd, off_t offset, int whence);
其中fd为文件描述符,offset为偏移量,whence为偏移量的设置方式。
当whence为SEEK_SET,文件的偏移量设置为文件开始处offset个字节;当whence为SEEK_CUR,文件的偏移量设置为当前值加offset,offset可正可负;当whence为SEEK_END,文件的偏移量设置为文件长度加offset,offset可正可负。
需要注意的是,如果文件的偏移量设置的有问题,可能会导致文件中间有一部分为空(比如当前偏移量为10,强行将偏移量改为20,那么中间又10个字节的空洞)。
七:函数read
函数read声明如下:
ssize_t read(int fd, void *buf, size_t nbytes);
如果read执行成功,返回读取的字节数;如果已经读到了文件末尾,返回0.
八:函数write
write函数声明如下:
ssize_t write(int fd, const void *buf, size_t nbytes);
一般来说返回值与写入的值nbytes相同,否则表示出错。一般出错的原因是磁盘已经写满了。
九:IO的效率
对于同一个文件,只是用read和write进行复制,但是指定的nbytes不同,发现当nbytes的值超过32字节的时候效率比较高,并且在超过32字节之后所消耗的差不多,这是由于大多数的文件系统为了改善性能都会有一个预读的技术。
十:文件共享
UNIX系统支持在不同进程间共享打开的文件。内核使用三种数据结构表示打开的文件:
1.进程表记录项中的包含有一张打开文件描述符表,每个文件描述符表包括:
a)文件描述符标志(close_on_exec)
文件描述符标志是一个进程所有文件描述符的位图标志,每个比特位代表一个打开的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄。
b)指向一个文件表项的指针
2.内核为每个打开的文件维持一张文件表,每个文件表项包含:
a)文件状态标志(读、写、添写、同步和非阻塞等)
b)当前文件偏移量
c)指向该文件v节点表项的指针
3.每个打开的文件都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针。对于绝大多数文件,v节点还包含了该文件的i节点。这些信息是在打开文件时从磁盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。例如:i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件实际数据块在磁盘上所在位置的指针等等。
如果两个独立的进程各自打开同一个文件,假设第一个进程在文件描述符3上打开该文件,另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都得到一个文件表项,因为每个进程都有它自己的对该文件的当前偏移量。而对于一个给定的文件,只有一个v节点表项。
十一:原子操作
所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束。
在多线程的环境中,如果两个线程同时对一个文件进行设置偏移量,写文件的操作可能会导致一些问题。但是如果可以将定位偏移量和写操作封装为一个原子操作就不会出现问题,UNIX提供了函数pread和函数pwrite来实现原子操作。
十二:dup和dup2函数
dup和dup2也是两个非常有用的调用,它们的作用都是用来复制一个文件的描述符。它们经常用来重定向进程的stdin、stdout和stderr。这两个函数的原形如下:
int dup( int oldfd );
int dup2( int oldfd, int targetfd );
利用函数dup,我们可以复制一个描述符。传给该函数一个既有的描述符,它就会返回一个新的描述符,这个新的描述符是传给它的描述符的拷贝。这意味着,这两个描述符共享同一个数据结构。例如,如果我们对一个文件描述符执行lseek操作,得到的第一个文件的位置和第二个是一样的。
十三:函数sync、fsync和fdatasync
一般来说磁盘IO写数据都是通过缓冲区来写,而不是立即去写。sync函数会将修改过的你缓冲区写入到磁盘,但是并不等待写磁盘操作结束就返回;fsync支队给定的fd起作用并且会等到写入磁盘操作返回;fdatasync函数类似于fsync,但是它仅影响文件的数据部分,对数据之外的文件属性并不起作用。
十四:函数fcntl和ioctl
fcntl是用来修改已经打开文件的属性的函数,包含5个功能:
复制一个已有文件描述符,功能和dup和dup2相同,对应的cmd:F_DUPFD、F_DUPFD_CLOEXEC。
当使用这两个cmd时,需要传入第三个参数,fcntl返回复制后的文件描述符,此返回值是之前未被占用的描述符,并且必须一个大于等于第三个参数值。F_DUPFD命令要求返回的文件描述符会清除对应的FD_CLOEXEC标志;F_DUPFD_CLOEXEC要求设置新描述符的FD_CLOEXEC标志。
获取、设置文件描述符标志,对应的cmd:F_GETFD、F_SETFD。
用于设置FD_CLOEXEC标志,此标志的含义是:当进程执行exec系统调用后此文件描述符会被自动关闭。
获取、设置文件访问状态标志,对应的cmd:F_GETFL、F_SETFL。
获取当前打开文件的访问标志,设置对应的访问标志,一般常用来设置做非阻塞读写操作。
获取、设置记录锁功能,对应的cmd:F_GETLK、F_SETLK、F_SETLKW。
作为记录锁功能使用。
获取、设置异步I/O所有权,对应的cmd:F_GETOWN、F_SETOWN。
获取和设置用来接收SIGIO/SIGURG信号的进程id或者进程组id。返回对应的进程id或者进程组id取负值。
ioctl函数放到第十八章和第十九章介绍
十五:/dev/fd
对于/dev/fd/n操作,等同于复制文件描述符n的操作。