一、梗概
第七章讨论了多种文件系统;解释了操作系统中的各种操作级别,包括为文件存储准备存储设备、内核中的文件系统支持函数、系统调用、文件流上的 1/O库函数、用户命令和各种操作的sh脚本;系统性概述了各种操作,包括从用户空间的文件流读/写到内核空间的系统调用,直到底层的设备I/O驱动程序级别;描述了低级别的文件操作,包括磁盘分区、显示区表的示例程序、文件系统的格式化分区以及挂载磁盘分区;介绍了 Linux 系统的EXT文件系统,包括 EXT2文件系统的系统数据结构、显示超级块、组描述符、块和索引节点位图以及目录内容的示例程序。
第八章述了如何使用系统调用进行文件操作;解释了系统调用的作用和 Linux 的在线手册页;展示了如何使用系统调用进行文件操作;列举并解释了文件操作中最常用的系统调用;阐明了硬链接和符号链接文件;具体解释了stat 系统调用;基于stat信息,开发了一个类似于ls 的程序来显示目录内容和文件信息;接着,讲解了open-close-lseek 系统调用和文件描述符;然后,展示了如何使用读写系统调用来读写文件内容;在此基础上,说明了如何使用系统调用来显示和复制文件;还演示了如何开发选择性文件复制程序,其行为类似于一个简化的Linux dd实用程序。
二、知识点总结
1、文件操作级别
1)硬件级别
2)操作系统内核中的文件系统函数
3)系统调用:用户模式使用系统调用来访问内核函数。
4)I/O库函数
5)用户命令
6)Sh脚本
2、文件I/O操作
函数操作:
1)open函数
头文件:#include<fcntl.h>
函数原型:
int open(const char *path, int oflag,........)
参数说明:
path:一般指需要打开或者创建的文件名字
oflag:常用的标志(O_RDONLY、O_WRONLY、O_RDWR、O_CRAET、O_TRUNC)
2)close函数
头文件:#include<unistd.h>
函数原型:int close(int fd)
参数说明:fd:需要关闭的文件描述符
3)lseek函数
头文件:#include<unistd.h>
函数原型:
off_t lseek(int fd, off_t offset, int whence)
参数说明:
fd: 需要操作文件的文件描述符
offset:与whence有关具体如下:
whence = SEEK_SET 文件的偏移量设置位距文件的开始处的offset个字节
whence = SEEK_CUR 文件的偏移量设置为当前值加上offset个字节
whence = SEEK_END 将文件的偏移量设置成文件长度加上offset个字节
返回值:返回新的文件偏移量
4)read函数
头文件:#include<unistd.h>
函数原型:
ssize_t read(int fd, void *buf, size_t nbytes )
参数说明:
fd: 需要读取文件的文件描述符
buf:读取的数据存入buf中
nbytes:需要读取的字节数
5)write函数
头文件:#include<unsitd.h>
函数原型:
ssize_t write(int fd, void *buf, size_t nbytes)
参数说明:
fd: 需要写入文件的文件描述符
buf:存储需要写数据的buf
nbytes:需要写入文件的字节数
3、使用系统调用进行文件操作
我们可以通过两种方式访问文件,一种是linux的操作系统接口,即系统调用,另一种是GNU在对系统调用进行封装优化处理后的标准输入输出函数<stdio.h>。
针对输入输出操作直接使用底层系统调用时的效率很低,因为在执行系统调用函数时,Linux必须实现用户态和内核态之间的切换,这种开销是非常大的,库函数通过设置缓冲区每次读写大量数据而减少系统调用次数。
系统调用(系统调用是操作系统提供给用户程序的一组“特殊”函数接口,用户通过这组接口获得操作系统提供的服务)中操作I/O的函数,都是针对文件描述符的。
通过文件描述符可以直接对相应文件进行操作,如:open、close、write、read、ioctl
//#define STDIN_FILENO 0 //标准输入的文件描述符
//#define STDOUT_FIFENO 1 //标准输出的文件描述符
//#define STDERR_FILENO 2 //标准错误的文件描述符
程序运行起来后这三个文件描述符是默认打开的
4、链接文件
在linux系统中有种文件是链接文件,可以为解决文件的共享使用。链接的方式可以分为两种,一种是硬链接(Hard Link),另一种是软链接或者也称为符号链接(Symbolic Link)。可使用ll命令查看哪些是链接文件。
硬链接是指通过索引节点来进行链接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都会给它分配一个编号,这个编号被称为索引节点编号(Inode Index)或者Inode,它是文件或者目录在一个文件系统中的唯一标识,文件的实际数据放置在数据区域(data block),它存储着文件重要参数信息,也就是元数据 (metadata),比如创建时间、修改时间、文件大小、属主、归属的用户组、读写权限、数据所在block号等。如图所示:
在Linux系统中,多个文件名指向同一索引节点(Inode)是正常且允许的。一般这种链接就称为硬链接。硬链接的作用之一是允许一个文件拥有多个有效路径名,这样用户就可以建立硬链接到重要的文件,以防止“误删”源数据(很多硬件,如netapp存储中的快照功能就应用了这个原理,增加一个快照就多了一个硬链接)。
软链接(也叫符号链接),类似于windows系统中的快捷方式,与硬链接不同,软链接就是一个普通文件,只是数据块内容有点特殊,文件用户数据块中存放的内容是另一文件的路径名的指向,通过这个方式可以快速定位到软连接所指向的源文件实体。软链接可对文件或目录创建。
软链接作用:
便于文件的管理,比如把一个复杂路径下的文件链接到一个简单路径下方便用户访问。
节省空间解决空间不足问题,某个文件系统空间已经用完了,但是现在必须在该文件系统下创建一个新的目录并存储大量的文件,那么可以把另一个剩余空间较多的文件系统中的目录链接到该文件系统中。
删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接就变成了死链接。
5、系统调用函数
write系统调用
write,就是把缓冲区的数据写入文件中。注意,这里的文件时广泛意义的文件,比如写入磁盘、写入打印机等等。
Linux 中write()的函数原型:
size_t write(int fildes, const void *buf, size_t nbytes);
参数说明:
fildes:文件描述符,标识了要写入的目标文件。例如:fildes的值为1,就像标准输出写数据,也就是在显示屏上显示数据;如果为 2 ,则想标注错误写数据。
*buf:待写入的文件,是一个字符串指针。
nbytes:要写入的字符数。
函数返回值:size_t 返回成功写入文件的字符数。需要指出的是,write可能会报告说他写入的字节比你所要求的少。这并不一定是个错误。在程序中,你需要检查
error已发现错误,然后再次调用write写入剩余的数据。
read系统调用
系统调用read是从文件中读出数据。要读取的文件用文件描述符标识,数据读入一个事先定义好的缓冲区。他返回实际读入的字节数。
Linux中read的函数原型:
size_t read(int fildes, void *buf, size_t nbytes);
参数说明:
fildes:文件描述符,标识要读取的文件。如果为0,则从标准输入读数据。类似于scanf()的功能。
*buf:缓冲区,用来存储读入的数据。
nbytes:要读取的字符数。
返回值:size_t返回成功读取的字符数,它可能会小于请求的字节数。
系统调用open的作用是打开一个文件,并返回这个文件的描述符。
简单地说,open建立了一条到文件或设备的访问路径。如果操作成功,它将返回一个文件描述符,read和write等系统调用使用该文件描述符对文件或
设备进行操作。这个文件描述符是唯一的,他不会和任何其他运行中的进程共享。如果两个程序同时打开一个文件,会得到两个不同的问价描述符。
Linux中open的函数原型有两个:
int open(const char *path, int oflags);
int open(const char *path, int oflags, mode_t mode );
参数说明:
path:准备打开的文件或设备名字。
oflags:指出要打开文件的访问模式。open调用必须指定如下所示的文件访问模式之一:
open调用可以在oflags参数中包括下列可选模式的组合(用”按位或“操作):
O_APPEDN: 把写入数据追加在文件的末尾。
O_TRUNC: 把文件长度设为零,丢弃以后的内容。
O_CREAT: 如果需要,就按参数mode中给出的访问模式创建文件。
O_EXCL: 与O_CREAT一起调用,确保调用者创建出文件。使用这个模式可防止两个程序同时创建一个文件,如果文件已经存在,open调用将失败。
三、最有收获的内容
这次学习,我最有收获的内容是加深了对系统调用的理解。简单来说,系统调用就是用户程序和硬件设备之间的桥梁。用户程序在需要的时候,通过系统调用来使用硬件设备。
1)用户程序通过系统调用来使用硬件,而不用关心具体的硬件设备,这样大大简化了用户程序的开发。
比如:用户程序通过write()系统调用就可以将数据写入文件,而不必关心文件是在磁盘上还是软盘上,或者其他存储上。
2)系统调用使得用户程序有更好的可移植性。
只要操作系统提供的系统调用接口相同,用户程序就可在不用修改的情况下,从一个系统迁移到另一个操作系统。
3)系统调用使得内核能更好的管理用户程序,增强了系统的稳定性。
因为系统调用是内核实现的,内核通过系统调用来控制开放什么功能及什么权限给用户程序。
这样可以避免用户程序不正确的使用硬件设备,从而破坏了其他程序。
4)系统调用有效的分离了用户程序和内核的开发。
用户程序只需关心系统调用API,通过这些API来开发自己的应用,不用关心API的具体实现。
内核则只要关心系统调用API的实现,而不必管它们是被如何调用的。
四、问题与解决思路
1、给定文件路径“/home/hello”,操作系统时如何找到该文件的位置?
答:1)查找根目录的目录项。Linux有规定,根目录的目录项必须存放在2号inode中。
2)根目录的目录项中存着根目录下的子目录目录项和文件的数据块信息。通过根目录的目录项可以找到home对应的inode。
3)根据home对应的inode找到home的目录项。
4)在home目录项中找到hello文件的inode。
5)根据hello文件的inode中的数据块指针找到存储有hello文件内容的数据块。
2、系统调用与普通过程调用的区别是什么?
相同点:
改变指令流程
重复执行和公用
改变指令流程后需要返回原处
不同点:
系统调用是动态调用,而CALL调用方式是静态调用;
执行状态不同
进入方式不同
与进程调度的关系不同:
嵌套或递归调用
1) 系统调用是动态调用,而CALL调用方式是静态调用;
系统调用是动态调用,程序中不包含被调用代码
好处:
(1)用户程序长度缩短
(2)当OS升级时,调用方不必改变
系统调用方式的调用地址和返回地址都是不固定的:系统调用指令中不包含调用地址,只包含功能号;系统调用返回指令中也不包含返回地址,通过栈保存和弹出返回地址。
CALL调用方式是静态调用:被调用代码与调用代码在同一程序之内。CALL调用方式,其调用地址是固定的,包含在调用语句中
五、实践内容
1、显示文件内容
该程序功能相当于linux cat命令