文件的维护方式
默认情况下,文件是在磁盘中以inode形式维护的。
对于打开文件,内核维护了3个数据结构:
- 进程级文件描述符表,用来映射文件描述符和文件句柄(文件对象);
- 系统级打开文件表,用来映射文件句柄和inode;
- 文件系统的inode表,用来标记文件数据位置。
上述三者的关系:
- 在进程 A 中,文件描述符 1 和 20 都指向同一个打开的文件句柄(文件对象),这可能是通过调用 dup()、dup2()或 fcntl()而形成的;
- 进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向同一个打开的文件句柄。这可能是 fork() 后出现的,或socket传递了一个打开的文件描述符给另一个进程;
- 进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件句柄,但这些句柄均指向相同的 i-node ,换言之,指向同一文件。是因为每次 open() 时返回的文件描述符通常是不一样的。
打开文件 open
函数原型:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
//成功返回文件描述符,失败返回-1
open的操作过程: (1)判断文件路径是否合法,是否创建文件;(2)从磁盘加载文件数据到内存,创建file对象;(3)返回文件描述符。
若打开文件时发生错误,open()将返回−1,错误号 errno 标识错误原因,也可以使用perror
显示错误原因。
open中的flag参数和open返回的错误码
标志 | 用途 | 错误码 | 含义 |
---|---|---|---|
O_RDONLY | 以只读方式打开 | EACCES | 权限拒绝 |
O_WRONLY | 以只写方式打开 | EISDIR | 是目录而非文件 |
O_RDWR | 以读写方式打开 | EMFILE | 进程级文件描述符到达上限 |
O_CLOEXEC | fork时,子进程不继承该文件描述符 | ENFILE | 系统级文件描述符达到上限 |
O_CREAT | 若文件不存在,则创建文件 | ENOENT | 没有这样的文件或目录 |
O_DIRECT | 直接 I/O 模式,即数据不缓存到系统内存中,直接读写磁盘 | EROFS | 只读文件 |
O_EXCL | 若文件存在,则open返回失败 | ETXTBSY | 是可执行文件 |
O_LARGEFILE | 在 32 位系统中打开大于2G的文件 | ||
O_NOCTTY | 不要让 pathname(所指向的终端设备)成为控制终端 | ||
O_NOFOLLOW | 对符号链接不予解引用 | ||
O_TRUNC | 覆写文件 | ||
O_APPEND | 追加文件 | ||
O_ASYNC | 异步IO | ||
O_NONBLOCK | 以非阻塞方式打开 | ||
O_SYNC | 以同步方式写入文件 |
读取文件 read
函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
//成功返回读取字节数,失败返回-1
:::tip{title=“read的作用”}
- 按字符流(每次读取一个字符,先入先出顺序)读取count个字符;
- 读到
\n
时结束读取; - 读到
EOF
时结束读取; - 如果想把缓冲区内容交给printf输出,那么在read读取后,手动向缓冲区添加
\0
来结束字符串; - 因为缓冲区可能比要读取的数据大,因此返回值可以比缓冲区小。
:::
写入文件 write
函数原型:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
//成功返回写入字节数,失败返回-1
关闭文件 close
函数原型:
int close(int fd);
//成功返回0,失败返回-1
:::tip{title=“close”}
- close并不会直接关闭文件,而是先减少引用计数;
- 当引用计数为0时,则关闭文件;
:::
改变文件偏移量 lseek
函数原型:
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//成功返回新的偏移量,失败返回-1
:::tip{title=“where选项”}
SEEK_SET,相对文件头
SEEK_CUR,相对当前偏移量
SEEK_END,相对文件尾
:::
通用IO操作 ioctl
函数原型:
int ioctl(int fd, unsigned long request, ...);
//成功返回值取决于定义,失败返回-1
ioctl就是自定义操作,可以自己制定请求编号代表的含义,实现非标IO。
改变文件属性 fcntl
函数原型:
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
//成功返回值取决于cmd,失败返回-1
例如,改变函数的标志位:
int flags;
flags = fcntl(fd , F_GETFL);
//判断文件标志位
if(flags & O_SYNC){
printf("设置了同步标志位");
}
//判断访问权限
int accessMode;
accessMode=flags & O_ACCMODE;
if(accessMode == O_RDONLY){
printf("设置了只读权限");
}
//更改文件标志位
flags |= O_APPEND;
fcntl(fd,F_SETFL,flags);
:::tip{title=“逻辑操作的作用”}
&
,判断标志位,如if (info_nd->wxflag & WX_OPEN)
;
|
,设置标志位,info_nd->wxflag |= WX_OPEN
;
& ~
,清除标志位,int wxflag = info_od->wxflag & ~WX_DONTINHERIT;
;
:::
复制文件描述符 dup
函数原型:
#include <unistd.h>
int dup(int oldfd);
//成功返回最小文件描述符,失败返回-1
int dup2(int oldfd, int newfd);
//成功返回新描述符,失败返回-1
dup2是dup和close组合的化简方式:
close(2);
newfd = dup(1);
:::tip{title=“提示”}
新旧文件描述符共享同一打开文件句柄所含的文件偏移量和状态标志。然而,新文件描述符有其自己的一套文件描述符标志,且其 close-on-exec 标志(FD_CLOEXEC)总是处于关闭状态。
dup2除了FD_CLOEXEC标志位不一样,还有什么不一样?
除了 FD_CLOEXEC 标志位不同之外,dup2 复制的新文件描述符和旧文件描述符在其他方面是完全相同的,比如指向同一个打开的文件,共享相同的文件位置偏移量,以及相同的文件状态标志等。
:::
参考链接
《Linux-Unix系统编程手册–第5章深入深究IO文件–5.5复制文件描述符》