自学教材第7,8章,提交学习笔记(10分) 知识点归纳以及自己最有收获的内容 (3分) 问题与解决思路(2分) 实践内容与截图,代码链接(3分) ...(知识的结构化,知识的完整性等,提交markdown文档,使用openeuler系统等)(2分)
学习笔记
学习思维导图
知识点总结
文件操作级别
文件操作级别 文件操作分为五个级别,按照从低到高的顺序排列如下。
- (1)硬件级别∶硬件级别的文件操作包括∶
- fdisk∶将硬盘、U盘或SDC盘分区。
- mkfs∶格式化磁盘分区,为系统做好准备。
- fsck∶检查和维修系统。
- 碎片整理∶压缩文件系统中的文件。
其中大多数是针对系统的实用程序。普通用户可能永远都不需要它们,但是它们是创建和维护系统不可缺少的工具。
- (2)操作系统内核中的文件系统函数
每个操作系统内核均可为基本文件操作提供支持。
下面列出了类 Unix 系统内核中的一些函数,其中前缀k表示内核函数。
kumount(),kumount() (mount/umount file systems) kmkdir(),krmdir() (make/remove directory) kchair(),kgetCwd() (change directory,get CWD pathname) klink(),kunlink() (hard link/unlink files) kchmod(),kchown(),kutime() (change r|w|x permissions,owner,time) kcreat(),kopen() (create/open file for R,W,RW,APPEND) kread(),kwrite() (read/write opened files) klseek(),kclose() (Lseek/close file descriptors) keymlink(),kreadlink () (create/read symbolic 1ink files) kstat(),kfstat(),klatat() (get file status/information) kopendir(),kreaddir() (open/read directories)
- (3)系统调用∶用户模式程序使用系统调用来访问内核函数。例如,下面的程序可读取文件的第二个1024字节
#include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd, n; char buf[1024]; if (fd = open(argv[1], O_RDONLY) <0) exit(1); int k = lseek(fd, 1024, SEEK_SET); n = read(fd, buf, 1024); close(fd); printf("%d ", n); return 0; }
open:文件描述符的操作(如:open)返回的是一个文件描述符(int fd),内核会在每个进程空间中维护一个文件描述符表,所有打开的文件都将通过,此表中的文件描述符来引用。
fopen:流(如:fopen)返回的是一个文件指针(即指向FILE结构体的指针),FILE结构是包含有文件描述符的,fopen可以看做是open(fd直接操作的系统调用)的封装,它的优点是带有I/O缓存。
C语言的文件指针与文件描述符的相互转换可通过fdopen和fileno两个函数实现。它们都包含在头文件stdio.h中。
open()、read()、lseek()和 close()函数都是C语言库函数。每个库函数都会发出一个系统调用,使进程进入内核模式来执行相应的内核函数,例如open可进入kopen(),read可进入kread()函数,等等。
当进程结束执行内核函数时,会返回到用户模式,并得到所需的结果。在用户模式和内核模式之间切换需要大量的操作(和时间)。因此,内核和用户空间之间的数据传输成本昂贵。对于读/写文件,最好的方法是匹配内核的功能。内核会按数据块大小(从1KB到8KB)来读取/写入文件。(在Linux 中,硬盘的默认数据块大小是4KB,软盘的是1KB)
- (4)I/O库函数
用户通常需要读/写单独的字符、行或数据结构记录等。如果只有系统调用,用户模式程序则必须自己从缓冲区执行这些操作。C语言库提供了一系列标准的I/O函数,同时也提高了运行效率。I/O库函数包括:
FILE mode I/O: fopen(), fread(), fwrite(), fseek(), fclose(),fflush() char mode I/o: gete(), getchar(), ugetc(), putc(), putchar() line mode I/O: gets(), fgets(), putc(), puts(), fputs() formatted I/O: scanf(), fscanf(), sscanf(), printf(), fprintf(), sprintf()
- (5)用户命令
Linux中常用用户命令在之前的博客中有详细介绍 - (6)sh脚本
需要手动输入命令,(如果是GUI操作更为繁琐费时)。用sh语言编写,包含所有有效的Unix/Linux命令。
文件I/O操作
分为用户模式和内核模式操作
- 用户模式
(1)用户模式下的程序执行操作
FILE *p = fopen("file", "r"); or FILE *p = fopen( "file", "w");
可以打开一个读/写文件流。
(2) fopen()在用户(heap)空间中创建一个FILE结构体,包含一个文件描述符fd、一个fbuf[BLKSIZE]和一些控制变量。它会向内核中的kopen()发出一个fd = open("file",flags=READ or WRITE)系统调用,构建一个OpenTable来表示打开文件示例。OpenTable的mptr指向内存中的文件INODE。对于非特殊文件,INODE 的i_block数组指向存储设备上的数据块。成功后,fp会指向FILE结构体,其中fd是open()系统调用返回的文件描述符。
(3) fread(ubuf, size,nitem, fp):将nitem个size字节读取到ubuf上,通过:
·将数据从FILE结构体的fbuf上复制到ubuf上,若数据足够、则返回。
·如果fbuf没有更多数据,则执行(4a)。
(4a)发出read(fd, fbuf, BLKSIZE)系统调用,将文件数据块从内核读取到fbuf上,然后将数据复制到ubuf上,直到数据足够或者文件无更多数据可复制。
(4b)fwrite(ubuf, size, nitem, fp):将数据从ubuf复制到 fbuf。
·若(fbuf有空间):将数据复制到fbuf上,并返回。
·若(fbuf已满):发出 write(fd, fbuf, BLKSIZE)系统调用,将数据块写入内核,然后再次写入fbuf。
这样,fread()/fwrite()会向内核发出read(/write)系统调用,但仅在必要时发出,而且它们会以块集大小来传输数据,提高效率。同样,其他库I/O函数,如 fgetc/fputc、fgets/fputs、fscanf/fprintf等也可以在用户空间内的FILE结构体中对fbuf进行操作。 - 内核模式
(5)内核中的文件系统函数:
假设非特殊文件的read(fd, fbuf[], BLKSIZE)系统调用。
(6)在read()系统调用中,fd是一个打开的文件描述符,它是运行进程的fd数组中的一个索引,指向一个表示打开文件的 OpenTable。
(7)OpenTable包含文件的打开模式、一个指向内存中文件 INODE的指针和读/写文件的当前字节偏移量。从OpenTable的偏移量,
·计算逻辑块编号lbk。
·通过 INODE.i_block[]数组将逻辑块编号转换为物理块编号blk 。
(8)Minode包含文件的内存INODE。EMODE.i_block[]数组包含指向物理磁盘块的指针。文件系统可使用物理块编号从磁盘块直接读取数据或将数据直接写入磁盘块,但将会导致过多的物理磁盘I/O。
(9)为提高磁盘VO效率,操作系统内核通常会使用一组I/O缓冲区作为高速缓存,以减少物理I/O的数量。
(9a)对于read(fd, buf, BLKSIZE)系统调用,要确定所需的(dev, blk)编号,然后查询I/O缓冲区高速缓存,以执行以下操作:
.get a buffer = (dev, blk); .if (buffer's data are invalid){ start_io on buffer; wait for I/O completion; } .copy data from buffer to fbuf; .release buffer to buffer cache;
博客园上一张图能很好的表现这种关系:
(9b)对于write(fd, fbuf, BLKSIZE)系统调用,要确定需要的(dev, blk)编号,然后查询IO缓冲区高速缓存,以执行以下操作:
.get a buffer = (dev, blk); .if (buffer's data are invalid){ start_io on buffer; wait for I/O completion; } .copy data from buffer to fbuf; .release buffer to buffer cache;
(10)设备I/O:I/O缓冲区上的物理I/O最终会仔细检查设备驱动程序,设备驱动程序
由上半部分的start_io()和下半部分的磁盘中断处理程序组成。
Upper-half of disk driver
start_io(bp): //bp=a locked buffer in dev_list,opcode=R|W(ASYNC) { enter bp into dev's I/O_aueue; if (bp is FIRST in I/O_queue) issue I/O command to device; }
Lower-half of disk driver
Device_Interrupt_Handler: { bp = dequeue(first buffer from dev.I/O_queue); if(bp was READ){ mark bp data VALID; wakeup/unblock waiting process on bp; } else // bp was for delay write release bp into buffer cache; if(dev.I/O_queue NOT empty) issue I/O command for first buffer in dev.I/O_queue; }
低级别文件操作
- 分区
- 主引导记录(MBR)
一个块存储设备,如硬盘、U盘、SD卡等,可以分为几个逻辑单元,称为分区。各分区均可以格式化为特定的文件系统,也可以安装在不同的操作系统上。大多数引导程序,如GRUB、LILO等,都可以配置为从不同的分区引导不同的操作系统。分区表位于第一个扇区的字节偏移446 (0x1BE)处,该扇区称为设备的主引导记录(MBR)。表有4个条目,每个条目由一个16字节的分区结构体定义,即:
- 主引导记录(MBR)
stuct partition { u8 drive; //0x80 - active u8 head; //starting head u8 sector; //starting sector u8 cylinder: //starting cylinder u8 sys_type; //partion type u8 end_head; //end head u8 end_sector; //end sector u8 end_cylinder; //end cylinder u32 start_sector; //starting sector counting from 0 u32 nr_sectors; //number of sectors in partition };
每个扩展分区的第一个扇区是一个本地MBR。每个本地MBR在字节偏移量0x1BE处也有一个分区表,只包含两个条目。第一个条目定义了扩展分区的起始扇区和大小。第二个条目指向下一个本地MBR。所有本地MBR的扇区编号都与P4的起始扇区有关。
分区表中,CHS值仅对小于8GB 的磁盘有效。对大于8GB但小于4G扇区的磁盘,只有最后两个条目start_sector 和nr_sector有意义。
ET2文件系统简介
- Block#0:
引导块,文件系统不会使用它。它用于容纳从磁盘引导操作系统的引导程序。 - Block#1:
超级块(在硬盘分区中字节偏移量为1024)。用于容纳关于整个文件系统的信息。
超级块中一些重要字段
struct et2_super block { u32 s_inodes_count; /* Inodes count */ u32 s_blocks_count; /* Blocks count */ u32 s_r_blocks_count; /* Reserved blocks count */ u32 s_free blocks_count; /* Free blocks count */ u32 s_free_inodes_count; /* Free inodes count */ u32 s_first_data_block; /* First Data Block */ u32 s_log block_size; /* Block size */ u32 s_log_cluster_size; /* Al1ocation cluster size */ u32 s_blocks per_group; /* # Blocks per group * / u32 s_clusters per_group; /* # Fragments per group */ u32 s_inodes_per_group; /* # Inodes per group * / u32s_mtime; /* Mount time * / u32s_wtime; /* write time */ u16s_mnt_count; /* Mount coune* / s16 s_max_ntcount; /* Maximal mount count */ u16 B_magic; /* Magic signature */ //more non-essential fields u16 s_inode_size; /* size of inode structure*/ }
s_first_data_block:0表示4KB块大小,1表示1KB块大小。它用于确定块组描述符的起始块,即s_first_data_block +1。
s_log_block_size确定文件块大小,为1KB*(2**s_log_block_size),例如0表示 1KB块大小,1表示2KB块大小,2表示4KB块大小,等等。最常用的块大小是用于小文件系统的1KB和用于大文件系统的4KB。
s_mnt_count:已挂载文件系统的次数。当挂载计数达到max_mount_count时,fsck会话将被迫检查文件系统的一致性。
s_magic是标识文件系统类型的幻数。EXT2/3/4文件系统的幻数是OxEF53。
- Block#2
块组描述符(硬盘上的s_first_data_blocks-1)
EXT2将磁盘块分成几个组,每个组有8192个块(硬盘上的大小为32K)
struct ext2_group_dese { u32 bg_b1ock_bitmap; //Bmap bloak number u32 bg_inode_bitmap; //Imap block number u32 bg_inode_table; //Inodes begin block number u16 bg_free_blocks_count; //THESE are OBVIOUS u16 bg_free_inodes_count; u16 bg_used_dirs_count; u16 bg_pad; // ignore these u32 bg_reserved[3]; };
由于一个软盘只有1440个块,B2只包含一个块组描述符。其余的都是0。在有大量块组的硬盘上,块组描述符可以跨越多个块。块组描述符中最重要的字段是bg_block_bitmap.bg_inode_bitmap和 bg_inode_table,它们分别指向块组的块位图、索引节点位图和索引节点起始块。对于Linux格式的EXT2文件系统,保留了块3到块7。所以,bmap=8,imap=9,inode_table= 10。
-
Block #8 块位图(Bmap)
用来表示某种项的位序列。0表示对应项处于FREE状态,1表示处于IN_USE状态。1个软盘有1440个块,但Block#0未被文件系统使用,所以对应位图只有1439个有效位,无效位视作IN_USE处理,设置为1. -
Block #9 索引节点位图(Imap)
一个索引节点就是用来代表一个文件的数据结构。EXT2文件系统是使用有限数量的索引节点创建的。各索引节点的状态用B9 中 Imap中的一个位表示。在EXT2 FS 中,前10个索引节点是预留的。所以,空EXT2FS的Imap 以10个1开头,然后是0。无效位再次设置为1。 -
Block #10 索引(开始)节点块(bg_inode_table)
每个文件都用一个128字节(EXT4中的是256字节)的独特索引节点结构体表示。
系统调用
使用 man 2 NAME
查看对应手册
系统调用必须由程序发出,其最终用法像普通函数一样。每个系统调用都是一个库函数,它汇集系统调用参数,并最终向操作系统内核发出一个系统调用
int syscall(int a, int b, int c, int d);
其中,第一个参数a是系统调用编号,b、c、d是对应内核函数的参数。在基于Intel x86的Linux中,系统调用是由INT Ox80汇编指令实现的,可将CPU 从用户模式切换到内核模式。内核的系统调用处理程序根据系统调用编号将调用路由到一个相应的内核函数。当进程结束执行内核函数时,会返回到用户模式,并得到所需的结果。返回值≥0表示成功,-1表示失败。如果失败,errno变量(在errno.h中)会记录错误编号,它们会被映射到描述错误原因的字符串。
- 常用系统调用
stat 获取文件状态信息 open 打开一个文件进行读、写、追加 close 关闭打开的文件描述符 read 读取打开的文件描述符 write 写入打开的文件描述符 lseek 重新定位文件描述符的读/写偏移量 dup 将文件描述符复制到可用的最小描述编号中 dip2 将oldfd复制到newfd中,如果newfd一打开,先将其关闭 link 将新文件硬链接到旧文件 unlink 减少文件的链接数;如果链接数达到零,则删除文件 symlink 为文件创建一个符号链接 readlink 读取符号链接文件的内容 umask 设置文件创建掩码;文件权限为 (mask&~umasl)
实践操作
①创建一个虚拟磁盘映像
②查一下现有分区③运行fdisk
遇到的问题和解决:
问题:块组描述符是什么?在EXT2磁盘的作用是什么?
解决:每个块组都有一个相应的组描述符来描述它,所有的组描述符形成一个组描述符表,组描述符表可能占多个数据块。组描述符就相当于每个块组的超级块,一旦某个组描述符遭到破坏,整个块组将无法使用,所以组描述符表也像超级块那样,在每个块组中进行备份,以防遭到破坏。组描述符表所占的块和普通的数据块一样,在使用时被调入块高速缓存。32GB的Ext2分区,换算成KB就是33554432,假定块大小为4KB。在这种情况下,每个4KB的块位图描述32KB个数据块,即128MB。因此,最多需要33554432 / 4096 * 32 = 256个块组。显然,块越大,块组数越小。
问题:在使用系统调用进行文件操作的时候,链接文件有哪些分类
解决:链接文件时Linux文件系统的一个优势,如需要在系统上维护同一文件的两份或多分副本,除了保存多分单独的物理文件副本之外,还可以采用保存一份物理文件副本和多个虚拟副本的方法,这种虚拟的副本就称为链接,其是目录中指向文件真是位置的占位符。有两种不同类型的文件链接:符号链接、硬链接。符号链接是一个实实在在的文件。硬链接会创建独立的虚拟文件,其中包含了原始文件的信息及地址。但是从根本上而言他们是同一个文件。
总结
这一周的知识明显比前几周要难上许多,更多是靠自己在课下自学。知识不光停留在理论部分,实践操作部分也很重要。在后面的学习中,我会更多复习总结前面已经学习过的知识点。争取吃透后面章节的知识。