20201322陈俊池 学习笔记4
一、知识点归纳
第七章 文件操作
7.1 文件操作级别
文件操作分为五个级别: (1)硬件级别:
-
fdisk:将硬件、U盘或SDC盘分区。
-
mkfs:格式化磁盘分区,为系统做好准备。
-
fsck:检查和维修系统。
-
碎片整理:压缩文件系统中的文件。
(2)操作系统中的文件系统函数
每个操作系统内核均可为基本文件操作提供支持
(3)系统调用:用户模式程序使用系统调用来访问内核函数
open(),read(),lseek()和close()都是c语言库函数。每个库函数都会发出一个系统调用,使进程进入内核模式来执行相应的内核函数,当进程结束执行内核函数使,会返回到用户模式,并得到所需的结果。
(4)I/O库函数
系统调用可以让用户读/写多个数据块,这些数据块只是一系列字节。他们不知道,也不关心数据的意义。用户通常需要读/写单独的字符、行或数据结构记录等。
(5)用户命令
用户可以使用Unix/Linux命令来执行文件操作,而不是编写程序。
(6)sh脚本
虽然比系统调用方便的多,但是必须要手动呼入命令,如果使用的是GUI,必须要拖放文件图标和点击指向设备来输入,操作繁琐而且耗时。
7.2 文件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): .write data to the I/O buffer; .mark buffer as dataValid and DIRTY (for delay-write to disk); .release the buffer to buffer cache;
(10)设备I/O:I/O缓冲区上的物理I/O最终会仔细检查设备驱动程序,设备驱动程序 由上半部分的start_io()和下半部分的磁盘中断处理程序组成。
7.3 低级别文件操作
7.3.1 分区
一个块存储设备,如硬盘、U盘、SD卡等,可以分为几个逻辑单元,称为分区。各分区均可以格式化为特定的文件系统,也可以安装在不同的操作系统上。大多数引导程序,如GRUB、LILO等,都可以配置为从不同的分区引导不同的操作系统。分区表位于第一个扇区的字节偏移446(0x1BE)处,该扇区称为设备的主引导记录(MBR)。
如果某分区是扩展类型(类型编号=5),那么它可以划分为更多分区。假设分区P4是扩展类型,它被划分为扩展分区P5、P6、P7。扩展分区在扩展分区区域内形成一个链表。
每个扩展分区的第一个扇区是一个本地MBR。每个本地MBR在字节偏移量0xIBE处也有一个分区表,只包含两个条目。第一个条目定义了扩展分区的起始扇区和大小。第二个条目指向下一个本地MBR。所有本地MBR的扇区编号都与P4的起始扇区有关。照例,链表以最后一个本地MBR中的0结尾。在分区表中,CHS值仅对小于8GB的磁盘有效。对大于8GB但小于4G扇区的磁盘,只有最后两个条目start _sector 和nr sector有意义。
7.3.2 格式化分区
fdisk只是将一个存储设备划分为多个分区。每个分区都有特定的文件系统类型,但是分区还不能使用。为了存储文件,必须先为特定的文件系统准备好分区。该操作习惯上称为格式化磁盘或磁盘分区。
7.4.2 超级块
Block#1:超级块(在硬盘分区中字节偏移量为1024)B1是超级块,用于容纳关于整个文件系统的信息。下文说明了超级块结构中的一些重要字段。
struct ext2_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; // Allocation 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
u32 s_mtime; // Mount time
u32 s_wtime; // Write time
u32 s_mnt_count; // Mount count
u16 s_max_mnt_count; // Maximal mount count
u16 s_magic; // Magic signature
// more non-essential fields
u16 s_inode_size; // size of inode structure
};
第八章 使用系统调用进行文件操作
8.1 系统调用
在操作系统中,进程以两种不同的模式运行,即内核模式和用户模式,简称Kmode和Umode。在Umode中,进程的权限非常有限。它不能执行任何需要特殊权限的操作。特殊权限的操作必须在Kmode下执行。系统调用(简称syscall)是一种允许进程进入Kmode以执行Umode不允许操作的机制。复刻子进程、修改执行映像,甚至是终止等操作都必须在内核中执行。
8.2 系统调用手册
在Unix以及大多版本的Linux中,在线手册页保存在/usr/man/目录中(Goldt等1995;Kerrisk 2010,2017)。而在Ubuntu Linux中,则保存在/usr/share/man目录中。man2子目录中列出了所有系统调用手册页。sh命令man 2 NAME显示了系统调用名称的手册页。 例如:
man 2 stat man 2 open man 2 read
许多系统调用需要特别包含头文件,手册页的SYNOPSIS(概要)部分列出来这些文件。如果没有合适的头文件,会有警告。一些系统调用还需要特定的数据结构作为参数,必须在手册页中描述这些参数。
8.3 使用系统调用进行文件操作
系统调用必须由程序发出。它们的用法就像普通函数调用一样。每个系统调用都是一个库函数,它汇集系统调用参数,并最终向操作系统内核发出一个系统调用。
简单的系统调用:
-
access:检査对某个文件的权限
int access(char •pathname, int mode);
-
chdir:更改目录
int chdir(const char *path);
-
chmod:更改某个文件的权限
int chmod(char *path, mode_t mode);
-
chown:更改文件所有人
int chown(char *name, int uid, int gid);
-
chroot:将(逻辑)根目录更改为路径名
int chroot (char *patiiname);
-
getcwd:获取CWD的绝对路径名
char *getcwd(char *buf, int size);
-
mkdir:创建目录
int mkdir(char *pathname, mode_t mode);
-
rmdir:移除目录(必须为空)
int rmdir (char *pathname);
-
link:将新文件名硬链接到旧文件名
int link(char *oldpath, char *newpath);
-
unlink:减少文件的链接数;如果链接数达到0,则删除文件
int uniink(char *pathname);
-
symlink:为文件创建一个符号链接
int symliak(char *oldpath, char *newpath);
-
rename:更改文件名称
int rename(char *oldpath, char *newpath);
-
utime:更改文件的访问和修改时间
int utime(char *pathname, struct utimebuf *time)
-
以下系统调用需要超级用户权限:
-
mount:将文件系统添加到挂载点目录上
int mount(char *specialfile, char *mountDir);
-
umount:分离挂载的文件系统
int umount(char *dir);
-
mknod:创建特殊文件
int mknod(char *path, int mode, int device);
8.4 常用的系统调用
-
stat:获取文件状态信息
int stat(char *filename, struct stat *buf);
int fstat(int filedes, struct stat *buf);
int lstat(char *filename, struct stat *buf);
-
open:打开一个文件进行读、写、追加
int open(char *file, int flags, int mode);
-
close:关闭打开的文件描述符
int close(int fd);
-
read:读取打开的文件描述符
int read(int fd, char buf[], int count);
-
write:写入打开的文件描述符
int write(int fd, char buf[], int count);
-
lseek:重新定位文件描述符的读/写偏移量
int lseek(int fd, int offset, int whence);
-
dup:将文件描述符复制到可用的最小描述符编号中
int dup(int oldfd);
-
dup2:将oldfd复制到newfd中,如果文件链接数为0,则删除文件
int dup2(int oldfd, int newfd);
-
link:将新文件硬链接到旧文件
int link(char *oldPath, char *newPath);
-
unlink:取消某个文件的链接;如果文件链接数为0,则删除文件
int unlink(char *pathname);
-
symlink:创建一个符号链接
int symlink(char *target, char *newpath);
-
readlink:读取符号链接文件的内容
int readlink(char *path, char *buf, int bufsize);
-
umask:设置文件创建掩码;文件权限为(mask & ~umask)
int umask(int umask);
二、问题与解决思路
对文件绝对路径到索引节点之间的转换不太理解
解决:
①目录的存储格式:目录以前规定不超过14个字符,而目录类型的inode都是前面预留的,所以目录的inode编号一般也比较小,基本上就是2个字节存储,所以一个目录表项的结构大小是16字节(linux很多都是宁可浪费一点点的资源,比如空间资源,也将结构体设计得比较简单一些的思路,比如这里的目录表项结构,其实可以设计成可变长度,但是那样代码维护起来困难,而且读写性能也一般,再想想,平时在做性能优化的时候多数都是通过空间来换取时间,为何不在一开始的时候,就设计一些简单的结构体换取相对良好的性能呢?),这里面的每个表内容很简单,前两个字节是inode编号,后面14个字节是对应的目录或者文件的名字,类似于key,value的一张表。 ②路径转换成inode:首先将路径根据/符号进行分割,一开始是/,/表示根目录,在文件系统创建的时候,mkfs的时候,就会创建这么一个根节点目录,并存储在全局变量中,这个就是根节点,能否改变?其实可以的,在某个进程中可以用chroot系统调用,改变该进程的根节点inode编号(这个就是changeroot的实现原理,其实就是做一个系统调用,将这个全局变量的值替换掉,这样就可以从一个指定的目录开始作为根目录了),②有了根目录的inode之后,读取该inode通常在高速缓冲中,然后根据其磁盘块指针(应该是很小的,直接指针,找到对应的磁盘块,理论上应该也在高速缓冲中),从这里面就可以获取到目录表项,然后查询两个/之间的那个文件的名称,如果有,再看下当前进程的用户是否有权限搜索、读取目录等,类似于权限控制,如果有的话,就把这个节点的inode号得到,然后再重复前面的过程,根据inode号得到磁盘块指针,再获取目录表项,这样递归的寻找下去,最好找到所需的文件或者目录。目录虽然存储在磁盘上,但是多数都是在高速缓冲中,所以实际操作起来并不慢的。这样最后就可以实现将一个绝对路径名称转换成一个inode节点编号的算法了。 参考:https://blog.csdn.net/XiaoH0_0/article/details/102760449
三、实践内容与截图
代码:
#include <stdio.h>
#include <errno.h>
int main()
{
char buf[256],*s;
int r;
r = mkdir("newdir", 0766);
if ( r < 0 )
printf("errno=%d : %s\n",errno,strerror(errno));
r = chdir("newdir");
s = getcwd(buf, 256);
printf("CWD = %s\n",s);
}
代码截图:
结果截图:
实现简单ls程序
代码:
#include<stdio.h> #include<sys/types.h>
#include<dirent.h>
#include<string.h>
#define MAX_LEN 150
int g_leavelen = MAX_LEN;
int g_maxlen;
void do_ls(char []);
int main(int argc,char *argv[])
{
if(argc==1)
{
do_ls("."); // .代表当前目录
}
else
{
while(--argc)
{
printf("%s:\n",*++argv);
do_ls(*argv);
}
}
return 0;
}
void do_ls(char dirname[])
{
DIR*dir_ptr;
struct dirent*direntp;
if((dir_ptr=opendir(dirname))==NULL) //打开失败
{
fprintf(stderr,"lsl:cannot open %s\n",dirname);
}
else //打开成功
{
while((direntp=readdir(dir_ptr))!=NULL) //依次读取目录,找到最大长度
{
if(direntp->d_name[0]!='.')
{
printf("%s ",direntp->d_name);
}
}
closedir(dir_ptr);
}
}
截图: