UNIX环境高级编程——文件I/O
3.1 文件描述符
作用:唯一表示一个文件(unix中设备也被看作文件)
当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。
文件描述符的范围:0~OPEN_MAX-1
标准shell建立的文件描述符关联:
STDIN_FILENO (文件描述符:0):标准输入
STDOUT_FILENO (文件描述符:1):标准输出
STDERR_FILENO (文件描述符:2):标准错误
3.2 函数open和openat
利用open或openat函数可以打开或创建一个文件
#include<fcntl.h>
int open(const char *path, int oflag, ....);
int openat(int fd, const char *path, int flog, ....);
参数
path:
打开或创建的文件的名字
oflag:
oflag参数 | 功能 |
---|---|
O_RDONLY | 只读打开 |
O_WEONLY | 只写打开 |
O_RDWR | 读、写打开 |
O_EXEC | 只执行打开 |
O_SEARCH | 只搜索打开 |
O_APPEND | 每次写时都追加到文件末尾 |
O_CLOEXEC | 把FD_CLOEXEC设置为文件描述符标志 |
O_CREAT | 若文件不存在则创建它。open函数需要指定第3个参数mode(openat指定第四个参数mode),用来指定文件的权限位 |
O_DIRECTORY | 如果path引用的不是目录,则出错 |
O_EXCL | 如果同时指定了O_CREAT,而文件已经存在,则出错。 |
O_NOCTTY | 如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端 |
O_NOFOLLOW | 如果path是一个符号链接,则出错 |
O_NONBLOCK | 如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式 |
O_SYNC | 使每次weite等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需要的I/O |
O_TRUNC | 如果文件存在,并且为只写或读写成功打开,则将其长度截断为0 |
O_TTY_INIT | 如果打开一个还未打开的终端设备,设置非标准termios结构,使其符合Single UNIXSpecification |
O_DSYNC | 使每次write操作要等待物理I/O完成,但是如果该操作并不影响刚写入的数据,则不需要等待文件更新 |
O_RSYNC | 使每一个以文件描述符作为参数进行的read等待,直至所有对文件同一部分挂起的写操作完成 |
fd:
fd参数把open和openat区分开,共有三种可能性
1.path参数指定绝对路径名。这种情况下fd参数被忽略。
2.path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址(通过打开相对路径名所在的目录来获取)。
3.path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。这种情况下,路径名在当前工作目录中获取
文件名和路径名截断
如果文件名过长,常量(_POSIX_NO_TRUNC)决定要截断过长的文件名或路径名,还是返回出错
3.3 函数creat
调用creat创建一个新文件。
#include <fcntl.h>
int create(const char *path, mode_t mode);
//返回值:若成功,返回为只写打开的文件描述符;若出错,返回-1
此函数等效于:
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
creat不足之处:以只写的方式打开创建的文件
3.4 函数close
调用close函数关闭一个打开的文件。
#include <unistd.h>
int close(int fd)
//返回值:若成功,返回0;若出错,返回-1
关闭一个进程时还会释放该进程加在该文件生的所有记录锁
3.5 函数lseek
每个打开文件都有一个与其相关联的“当前文件偏移量(通常为非负整数)”用以度量从文件开始处计算的字节数。
通常,读、写操作都是从当前文件偏移量开始的,并使偏移量增加所读写的字节数。系统默认情况下,若非指定O_APPEND选项,该偏移量设置为0。
调用lseek可以显式地为一个打开文件设置偏移量。
#include <unistd.h>
off_t lessk(int fd, off_t offset, int whence);
//返回值:若成功,返回新的文件偏移量,若出错,返回-1
通常,文件的当前偏移量是一个非负整数,但是,某些设备可能允许负的偏移量。对于普通文件来说,偏移量必须为非负值。因此在比较lseek的返回值时应当测试它是否等于-1,而不是判断是否小于0。
3.6 函数read
调用read函数打开文件中读数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);//void *表示通用指针
//返回值:读到的字节数,若已到文件尾,返回0;若出错返回-1
- 当读取普通文件时,如果读到要求字数之前就已经到达尾端,则read返回读到的字节数。
- 当从终端设备读取时,通常一次最多读一行。
- 当从网络读时,网络中的缓冲机制可能造成返回之小于所要求读的字节数。
- 当从管道或FIFO读时,如若管道包含的字节少于所需数量,那么read之返回实际可用的字符数。
- 当从面向记录的设备(如磁带)读取时,一次最多返回一个记录。
3.7 函数write
调用write函数向打开文件写数据。
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t nbytes);
//返回值:若成功,返回已写的字节数;若出错,返回-1
如果返回值通常与参数nbytes值相同,否则表示出错,writee出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。
3.8 I/O的效率
当缓冲区大小与磁盘block大小一致时系统cpu时间最少(效率最高)当继续增大缓冲区大小时并不会提高效率
3.9 文件共享
UNIX系统支持在不同进程间共享打开文件。
内核使用三种结构打开文件:
(1) 进程表项:每个进程中都有一个记录项,记录项中包含一张打开文件描述符表。
文件描述符标志
指向一个文件表项的指针
(2) 文件表项:内核为所有打开文件维持一张文件表,每个表项包含:
文件状态标志
当前文件偏移量
指向该文件v节点表项的指针
(3) v节点表项:每个打开文件都有一个v节点表项
文件类型
进行各种操作函数的指针
大多数文件还包含i节点(索引节点)
若两个独立进程各自打开了同一个文件,则有下图所示关系
之所以每个进程可以获得自己的文件表项,是因为这可以使每个进程都有自己对该文件的当前偏移量
3.10 原子操作
多进程同时对同一个文件进行写操作,会产生混乱,原因是两个进程使用了两个分开的函数调用。
解决办法为使这两个操作对于其他进程而言组成一个原子操作。
可以理解为多步不同的操作组成一个操作,该操作原子的执行,要么执行完所有步骤,要么一步也不执行,不可能只执行一部分。
函数pread和pwrite
pread相当于调用lseek后调用read,但有些许区别:
- 调用pread时,无法中断其定位和读操作
- 不更新当前文件的偏移量
pwrite相当于调用lseek后调用write,并且也具有类似区别。
3.11 函数dup和dup2
两个函数都可以用来复制一个现有的文件描述符。
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
//两函数的返回值:若成功,返回新的文件描述符;若出错,返回-1
- 用dup返回的文件描述符一定是当前可用文件描述符中的最小值。
- 对于dup2,可以用fd2参数指定新描述符的值,如果fd2已经打开了,则先将其关闭。若fd == fd2,返回fd2,而不关闭它,否则fd2的FD_CLOEXEC文件描述符标志就会被清空,这样fd2在调用exec时总是打开的状态。
这些函数返回的新文件描述符与参数fd共享一个文件表项,如下图所示:
3.12 函数sync、fsync和fdatasync
当我们向文件写入数据时,内核通常将文件复制到缓冲区,然后排入队列,晚些时候再写入磁盘,这种方式称为延迟写
通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX提供了sync、fsync和fdatasync三个函数。
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
//返回值:若成功,返回0;若出错,返回-1
void sync(void);
- sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
- fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束后才返回。fsync可用于数据库这样的应用程序
- fdatasync类似于fsync,但它只会影响文件的数据部分。除数据外,fsync还会同步更新文件的属性。
3.13 函数fcntl
fcntl函数可以改变已经打开的文件属性
#include <fcntl.h>
int fcntl(int fd, in tcmd, .../*int arg*/);
//返回值:若成功,则依赖于cmd;若出错,返回-1
fcntl有以下5个功能:
- 复制一个已经存在的描述符(cmd = F_DUPFD 或 F_DUPFD_CLOEXEC)
- 获取/设置文件描述符标志(cmd = F_GETFD 或 F_SETFD)
- 获取/设置文件标志状态(cmd = F_GETFL 或 F_SETFL)
- 获取/设置异步I/O所有权(cmd = F_GETOWN 或 F_SETOWN)
- 获取设置记录锁(cmd = F_GETLK 、F_SETLK 或 F_SETLKW)
cmd中的前8种:
参数cmd | 功能 |
---|---|
F_DUPFD | 复制文件描述符fd |
F_DUPFD_CLOEXEC | 复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新的文件描述符。 |
F_GETFD | 返回对应于fd的文件标志符 |
F_SETFD | 设置文件描述符标志,新标志按照第3个参数设置 |
F_GETFL | 返回文件状态 |
F_SETFL | 设置文件状态标志,新标志按照第3个参数设置 |
F_GETOWN | 获取当前接收SIGIO和SIGURS信号的进程ID或进程祖ID |
F_SETOWN | 设置接收SIGIO和SIGURS信号的进程ID或进程祖ID |
当支持同步写时,系统时间和时钟时间将显著增加。
fcntl的必要性:我们的程序在一个描述符上进行操作,根本不知道shell打开的相应文件的文件名。因为是shell打开的,因此不能按照我们的要求设置O_SYNC标志,使用fcntl,我们只需要知道文件描述符就可以修改文件的属性了。
3.14 函数ioctl
ioctl一直是I/O操作的杂货箱,不能用以上其他函数表示的I/O操作基本上都可以用ioctl表示。
终端I/O是使用ioctl最多的地方
#include <unistd.h>
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...)
除POSIX.1所说明的基本操作以外,终端I/O的ioctl命令都需要头文件<termios.h>
3.15 /dev/fd
用文件来表示文件描述符
来源:CSDN
作者:dojofj
链接:https://blog.csdn.net/weixin_45218800/article/details/103943270