系统级I/O学习笔记——文件的打开、关闭以及共享

简单了解Unix I/O下文件的打开与关闭以及共享文件

在了解文件之前,我们显然需要对本文讨论的文件所属的范畴,即Unix I/O有一个大致的了解。

Unix I/O 是什么

人们描述Linux系统中时,往往伴随着一句话:一切皆文件
一个Linux 文件就是一个m个字节的序列:
B0,B1,…,Bk,…,Bm-1

所有的I/О设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输人和输出都被当作对相应文件的读和写来执行。这种方式允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行:

  • 打开文件。 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  • Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO,它们可用来代替显式的描述符值。请一定要记住这一条,在这之后会非常重要
  • 改变当前的文件位置。 对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为k。
  • 读写文件。 一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k≥m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。
    类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置é开始,然后更新k。
  • 关闭文件。 当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

让我们试着打开一个文件

进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的:

#include <sys/types.h>
#include <sys/stat.h>#include <fcntl.h>
int open(char *filename,int flags,mode_t mode);
//返回:若成功则为新文件描述符,若出错为-1.

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:

  • O_RDONLY:只读。
  • O_WRONLY:只写。
  • O_RDWR:可读可写。

例如,下面的代码说明如何以读的方式打开一个已存在的文件:
fd = Open("foo.txt",O_RDONLY, 0) ;

flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:

  • O_CREAT:如果文件不存在,就创建它的一个截断的(truncated)(空)文件。
  • O_TRUNC:如果文件已经存在,就截断它。
  • O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。

例如,下面的代码说明的是如何打开一个已存在文件,并在后面添加一些数据:
fd = Open("foo.txt",O_WRONLY|O_APPEND,0);

最后,进程通过调用close函数关闭一个打开的文件。

#include <unistd.h>
	int close(int fd) ;
	//返回:若成功则为0,若出错则为—1。

关闭一个已关闭的描述符会出错。

我们现在可以试试看这个了:

#include "csapp.h"
int main(){
	int fd1,fd2;
	fd1 = Open( "foo.txt",O_RDONLY,0);close(fd1);
	fd2 - Open( "baz.txt",O_RDONLY,0);printf("fd2 = %dkn" ,fd2);
	exit(0) ;
}

也许你会认为打开fd1,关闭fd1,再打开fd2,这样会显示fd2 = 0或者fd2 = 1。实际上不是这样的,还记得刚才我们提到的吗?

Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO,它们可用来代替显式的描述符值。请一定要记住这一条,在这之后会非常重要

所以实际上,标准输入、输出和错误已经将0、1、2号文件位置占据了,之后的打开文件的操作都是从3开始的,因此我们得到的程序运行结果其实是fd2 = 3

何为共享文件

内核用三个相关的数据结构来表示打开的文件,我们简单地概括为:

  • 描述符表(descriptor table)。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
  • 文件表(file table)。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成(针对我们的目的)包括当前的文件位置、引用计数(reference count)(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
  • v-node表(v-node table)。同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st_mode和 st_size成员。

我们通过一张图来形象地了解典型情况
典型的情况
不难想象,多个描述符也可以通过不同的文件表表项来引用同一个文件。例如,如果以同一个 filename调用open函数两次,就会发生这种情况。关键思想是每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。具体就会如下图所示:
情况2

让我们试试典型情况的分析

#include "csapp.h"
int main(){
	int fd1,fd2;
	char c;
	fd1 = Open("foobar.txt",O_RDONLY,0);
	fd2 = Open("foobar.txt",O_RDONLY,0);
	Read(fd1,&c,1);
	Read(fd2,&c, 1);
	printf("c = %c\n", c);
	exit(0);
}

很好理解,在这个例子中,fd1与fd2之间并没有什么关系,他们分别打开了foobar.txt(其中由6个ASCII码字符“foobar”组成)因此,程序输出结果为

c = f

那如果是加上fork呢?(foobar.txt文件不改变)

接下来谈论的题目牵扯到了 fork 和 wait,如果不记得的话,可以查看我之前那篇学习笔记,系统级I/O学习笔记——文件的打开、关闭以及共享,其中有说明。

#include "csapp.h"
int main(){
	int fd;
	char c;
	fd = Open("foobar.txt",O_RDONLY,0);
	if (Fork() == 0) {
		Read(fd,&c,1);
		exit(0);
	}
	wait(NULL);
	Read(fd,&c,1);
	printf("c = %c\n",c);
	exit(0);
}

父子进程中,实际上文件是共享的。子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置。如下图所示:
在这里插入图片描述
所以我们不难分析,在我们给出的例子中,父子进程共享一个打开文件夹,fork后,子进程读取一位,而父进程则wait等待至子进程结束,之后再读取一位,实际上总共读取了2位,程序输出:

c = o

总结

Linux内核使用三个相关的数据结构来表示打开的文件。描述符表中的表项指向打开文件表中的表项,而打开文件表中的表项又指向v-node表中的表项。每个进程都有它自己单独的描述符表,而所有的进程共享同一个打开文件表和v-node表。理解这些结构的一般组成,就能使我们清楚地理解文件共享和之后会学习到的I/O重定向。
欢迎大家留言一起学习,共同进步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值