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才会被空闲

posted on   开心种树  阅读(610)  评论(0编辑  收藏  举报

编辑推荐:
· .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吗?
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示