linux系统编程——文件IO——fd,open, read,write,close
1.前言
1.1 为什么要浪费时间在简单的文件上
是的,文件的各种操作很具体简单,但是这是针对普通文件,由于linux设计思想是一切皆文件,所以文件IO被实现为支持很多对象的操作,总之重要的不是文件,而是文件IO。
1.2 概述
在用户层,一切文件IO都是基于fd,而fd是 task_struct : file_struct : fd_array[] 的索引,所以 操作fd就是操作 fd_array[fd],而fd_array[fd] 是 struct file * 指针,所以真正操作对象是 struct file,到这里便是面向对象的思想,不同类型文件的 f_ops,f_inode 等是不同的,如 普通文件的 f_inode 是 指真在磁盘上的文件,而 管道的 f_inode 是 页缓存。总之下面情况很复杂,而C语言不支持面向对象实现,所以一个结构体内嵌入多种不同类的属性,或一个成员被多种类属性复用,总之,这导致用户层的接口也是高度复用的,也就是说一些参数在某些类型文件时有用,有时又没有意义,而返回值根据不同文件类型,有不同可能。简单说文件IO被设计的简洁,但充满细节,必须明确操作的文件究竟是什么类型。
2.文件描述符
应用层我们使用 正整数 操作文件,称为 fd,这个fd实际索引一个 struct file 结构。
struct file 是进程和文件的会话,struct file * 就是会话的cookie,kernel会将所有会话建立成链表。
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct inode *f_inode;
const struct file_operations *f_op;
spinlock_t f_lock;
atomic_long_t f_count;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
struct address_space *f_mapping;
...
}
f_inode 是用于引用存放文件实际数据的空间,所以不同类型文件的 f_inode是不同的
f_op,文件操作方法,同样这也不同
f_lock,说明当通过同一个会话操作文件时,操作是原子的,当然不同会话操作同个文件时,kernel无法保证原子性
f_count,本会话的引用数,当没有进程应用该会话时,释放会话
f_pos,操作 f_inode 对应数据空间的 当前位置,有些类型的文件 无用
f_mapping, 将文件和页缓存关联
由于task_struct 记录 指向 struct file 的指针,所以fork() 后,子进程直接拷贝父进程的task_struct 导致,子进程和 父进程指向相同的 struct file,也就说使用相同会话。
3. api
3.1 open
int open(const char *name, int flags);
int open(const char *name, int flags, mode_t mode);
3.1.1 flags
- flags : 分为必须 标志,和可选标志。
必须标志: O_RDONLY, O_WRONLY, O_RDWR
可选标志:
O_APPEBD : 写入前修改 struct file : f_pos到文件结尾。
O_ASYNC : 如果文件变成可读或可写,则产生一个信号,默认SIGIO,用于 终端机或 socket,但不能用于常规文件
O_CREAT : 若文件不存在,则创建。若已存在,除非同时指定O_EXCL,否则忽略
O_DIRECT : 进行直接IO
O_DIRECTORY : 若不是目录,则失败
O_EXCL : 与 O_CREAT 连用
O_NOFOLLOW : 指定此标记时,打开符号连接文件,open失败,没指定本标记,打开 符号连接文件,符号连接被解析,而打开目标文件
O_NONBLOCK : 若可能,以非阻塞方式打开,让进程避免因资源不可的导致进行休眠。此行为用于定义FIFO
O_SYNC : 进行同步IO,直到数据被实际写入磁盘前都不会完成写入操作。一般读操作已经是同步方式了,所以此标志不会影响读操作
O_TRUNC : 若文件存在,并是常规文件,则将文件截短为0长度。不要对其它类型文件使用此标志。
3.1.2 mode
当使用 O_CREAT 标志时,mode被使用,用于设置新建文件的权限。
为了可移植性,建议使用 S_Ixxx 宏
文件实际的权限是由于 mode 和 umask 共同计算的,如 mode为 0666,umask为 022,则权限为 0644 ( 0666 & ~022)。
作为编程人员不需要考虑umask的存在,umask是为程序用户管理文件权限设置的。
3.1.3 常用代码片段
创建或打开并截断文件
fd = open(file, O_WRONLY | O_CREAT | O_TRUNC,
S_IWUSR | S_IRUSR | S_IWGRP | S_IRGRP | S_ROTH);
if (fd == -1)
/* error */
上面代码很常用,所以有个 creat(),通常creat是这样的
int creat(const char *name, int mode)
{
return open(name, O_WRONLY | O_CREAT | O_TRUNC, mode);
}
3.2 read
ssize_t read(int fd, void *buf, size_t len);
若f_pos有用,则从f_pos开始读,最多读len个字节,并移动f_pos,若f_pos没用,则从当前位置读。
read最复制的部分是返回值
3.2.1 复杂的返回值
0 < ret < len : 不是错误,可能是被信号中断,或管道坏了等。
ret == 0 : 当前f_pos到了文件末尾(end-of-file,EOF)
-1 : 若使用了 O_NONBLOCK,则可能 当前没有数据,errno为 EAGAIN。若没有用 O_NONBLOCK,进程进入睡眠,但被信号打断,errno为 EINTR,为可恢复错误
3.2.2 代码片段
最简单的写法:
char buf[32];
nr = read(fd, buf, sizeof(buf));
if (nr == -1)
这个实现有很多问题,首先read在读取足够数据前返回,发生可恢复错误
考虑如下改进,
对于没有使用 O_NONBLOCK
ssize_t ret;
while (len != 0 && (ret = read(fd, buf, len) != 0)) {
if (ret == -1) {
if (errno == EINTR)
continue;
perror("read");
break;
}
len -= ret;
buf += ret;
}
使用 O_NONBLOCK
ssize_t nr;
start:
nr = read(fd, buf, BUFSIZ);
if (nr == -1) {
if (errno == EINTR)
goto start;
if (errno == EAGAIN)
/* 做其他事,稍后重新read */
else
/* 错误 */
}
3.2.3 size_t 和 ssize_t
ssize_t read(int fd, void *buf, size_t len);
size_t 通常是 unsigned int, ssize_t 是 signed int,所以 size_t 的最大值大于 ssize_t。
所以 若需要进行大量数据读取,通常这么做
if (len > SSIZE_MAX)
len = SSIZE_MAX;
3.3 write
ssize_t write(int fd, const void *buf, size_t count);
write和read类似,最大的不同是write可以返回0,表示什么都没写入,下次可以继续写(read则应该结束读)。
3.3.1 代码片段
write同样需要检测是否写完,特别是对于socket
ssize_t ret;
while (len != 0 && (ret = write(fd, buf, len) != 0)) {
if (ret == -1) {
if (errno == EINTR)
continue;
perror("read");
break;
}
len -= ret;
buf += ret;
}
对于普通文件则不需要循环
nr = write(fd, &buf, count);
if (nr == -1)
/* 一定错误 */
else if (nr != count)
/* 可能错误 */
3.3.2 特别的O_APPEND
考虑多个进程写 同个文件尾部,若进程打开文件后,用lseek移动f_pos到文件尾部,开始写,第一个进程写后,第二个进程的f_pos并没有改变,所以会发生冲突。
解决方法是用 O_APPEND,如此在每次写前,都会保证f_pos为当前文件尾部。
3.3.3 写回操作
实际write只是把应用缓存的数据拷贝到内核缓存,并将对应缓存标记为脏页,数据并没有写入磁盘,之后内核将所有脏页,按照他认为的最佳顺序依次写入磁盘。内核的此操作称为写回。
这有两个问题:无法保证写回时机,无法保证写回顺序。
应用程序可以使用 fsync和 fdatasync控制写回,fsync会写回 文件数据和 文件元数据,fdatasync只会写回文件数据,所以fdatasync效率更高。
此外还有sync(),将所有文件的所有缓存数据写回磁盘,问题是耗时更久
O_SYNC 让所有write隐身执行 fsync
O_DIRECT 让应用程序绕过内核缓存,用户空间缓存与设备之间IO,所有IO将同步,直到操作完成后才返回
3.4 close
可能有多个 fd_array[fd](不同进程) 指向同一个 struct file,没进行一次close,struct file的被引用数就减一,减到0时struct file被释放,
而 struct file : f_inode 指向 struct inode,当struct file被释放,inode的被引用数减一,当inode的被引用数为0,kernel会释放该 inode在内存的副本。
所以 open会导致 kernel构建一个inode副本,close会导致副本的释放。
所以当 open了一个文件,然后删除该文件(减少inode的硬链接为0),实际inode不会被设为空闲,直到close 导致inode副本被释放,inode才会被空闲
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?