第7章 文件操作

文件操作级别

(1) 硬件级别

  • fdisk:将硬盘、u盘h或SDC盘分区
  • mkfs:格式化磁盘分区,为系统做好准备
  • fsck:检查和维修系统
  • 碎片整理:压缩文件系统中的文件

(2)操作系统内核中的文件系统函数

(3)系统调用

用户模式程序使用系统调用来访问内核函数。就是调用上述函数

(4)I/O库函数

用户通常需要读/写单独的字符、行或数据结构记录等,系统调用的是数据块,无意义

(5)用户命令

用户命令:用户可以使用Unix/Linux命令来执行文件操作,而不是编写程序。

(6) sh脚本

比系统调用方便得多,但是必须要手动输入命令,如果使用的是GUI, 必须要拖放文件图标和点击指向设备来输入,操作烦琐而且耗时。

文件I/O操作

(1 )用户模式下的程序执行操作

FILE *fp = fopen("file", "r") ; or FILE fp = fopen(''file^, ”w");
可以打开一个读/写文件流。
(2) fopen()在用户(heap)空间中创建一个FILE结构体,包含一个文件描述符fd、一 个fbuf [BLKSIZE]和一些控制变最。它会向内核中的kopen()发出一个fd = open("file", flags=READ or WRITE)系统调用,构建一个。penTable来表示打开文件示例。OpenTable 的mptr指向内存中的文件INODEo对于非特殊文件,INODE的i_block数组指向存储设 备上的数据块。成功后. fp会指向FILE结构体,其中fd是open。系统调用返回的文件描 述符。
(3 ) freadfubuf, size, nitem, fp):将 nitem 个 size 字节读取到 ubuf 上,通过:
•将数据从FILE结构体的ftuf±复制到ubuf上,若数据足够,则返回。
・如果fbuf没有更多数据,则执行(4a)。
( 4a)发岀read(fd, fbuf, BLKSIZE)系统调用,将文件数据块从内核读取到fbuf上,然 后将数据复制到ubuf上,直到数据足够或者文件无更多数据可复制。
(4b) fwrite(ubuf, size, nitem, fp):将数据从 ubuf 复制到 fbufe
•若(fbuf有空间):将数据复制到fbuf上,并返回。
•若(fbuf已满):发出write(fd, ftuf, BLKSIZE)系统调用,将数据块写入内核,然后 再次写入fbuf。
这样,fread()/fwrite()会向内核发岀read()/write。系统调用,但仅在必要时发出,而且 它们会以块集大小来传输数据,提高效率。同样,其他库I/O函数.如fgetc/fputc、fgets/ Iputs, fscanf/fprintf等也可以在用户空间内的FILE结构体中对命uf进行操作“
(5)内核中的文件系统函数:
假设非特殊文件的read(fd, fbuf[ ], BLKSIZE)系统调用”
(6)在read。系统调用中,fd是一个打开的文件描述符,它是运行进程的fd数组中的 一个索引,指向一个表示打开文件的OpenTable0
(7)OpenTable包含文件的打开模式、一个指向内存中文件INODE的指针和读/写文 件的当前字节偏移量。从0penTable的偏移量,
•计算逻辑块编号lbk0
•通过INODE.i_block[]数组将逻辑块编号转换为物理块编号blk0
(8 ) Minode包含文件的内存INODE。EMODE.i_block[]数组包含指向物理磁盘块的指 针。文件系统可使用物理块编号从磁盘块直接读取数据或将数据直接写入磁盘块,但将会导 致过多的物理磁盘I/O。
(9)为提高磁盘I/O效率,操作系统内核通常会使用一组1/0缓冲区作为高速缓存,以 减少物理I/O的数量。磁盘I/O缓冲区管理将在第12章中讨论。
(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)编号,然后査 询I/O缓冲区高速缓存,以执行以下操作:
.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()和下半部分的磁盘中断处理程序组成°
Upper-half of disk driver start_io(bp): Z/bp=a locked buffer in dev_list, opcode=R|W(ASYNC) (
enter bp into devzs I/O_queue;
if (bp is FIRST in I/O_queue) issue I/O command to device;
)
Lower-half o£ disk driver
Device_Interrupt_Handl«r:
(
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 buf£er in dev.I/0_queue;

低级别文件操作

分区

定义:一个块存储设备,如硬盘、u盘、SD卡等,可以分为几个逻辑单元,称为分区

各分区均可以格式化为特定的文件系统,也可以安装在不同的操作系统上。

分区表位于第一个扇 区的字节偏移446 (OxlBE)处,该扇区称为设备的主引导记录(MB

R)。表有4个条目,每个条目由一个16字节的分区结构体定义,即:

fdisk mydisk 在磁盘映像文件上运行fdisk

fdisk是一个交互程序。它有一个显示所有命令的帮助菜单。它收集用户的输入,在内存中 创

建一个分区表,该分区表仅在用户输入w命令时才被写入磁盘映像的MBR。

在内存 中,它允许用户创建、检査和修改分区。将分区写入磁盘后,通过q命令退出fdisk

通过t命令将它们更改为不同的文件系统类型。

格式化分区

fdisk只是将一个存储设备划分为多个分区。每个分区都有特定的文件系统类型.但是 分区还不

能使用。为了存储文件,必须先为特定的文件系统准备好分区

Linux支持多 种不同类型的文件系统,每个文件系统都期望存储设备上有特定的格式。在Linu

x中,命令

mkfs -t TYPE [-bsize] device nblocks

在一个nblocks设备上创建一个TYPE文件系统,每表块都是bsize字节,如果bsize未指定就默

认为1KB

格式化后的磁盘应是只包含根目 录的空文件系统。但是,Linux的mkfs始终会在根目录下创建

一个默认的lost+found目录 完成mkfs之后,设备就可以使用了。

由于虚拟文件系统不是真正的设备,它们必须作为循环设备挂载

挂载分区

man 8 losetup:显示用于系统管理的losetup实用工具命令:
(1 )用dd命令创建一个虚拟磁盘映像:

dd if=/dev/zero of=vdisk bs=1024 count=32768    #32K (1KB) blocks

(2 )在vdisk上运行fdisk来创建一个分区Pl:

fdisk vdisk

(3)使用以下扇区数在vdisk的分区1上创建一个循环设备:

losetup -o $(expr 2048 \* 512) --sizelimit $(expr 65535 \* 512) /dev/loopl vdisk

losetup需要分区的开始字节(start_sector512)和结束字节(end_sector512 )。循 环设备创建完成后,读进程可以使用命令

losetup -a

EXT2文件系统简介

EXT2文件系统数据结构

EXT2文件系统有1440个块,每个块大小为1KB。我们之所以选择1440块, 是因为它是(旧)软盘的块数,得到的磁盘映像可以直接作为虚拟(软)磁盘’在模拟基于 Intel x86的大多数PC虚拟机上使用

Block#0:引导块B0是引导块,文件系统不会使用它。它用于容纳从磁盘引导操作系 统的引导程序。

超级块

Block#1 :超级块(在硬盘分区中字节偏移量为1024 ) B1是超级块,用于容纳关于整 个文件系统的信息。

s_first_data_block : 0表示4KB块大小,1表示1KB块大小。它用于确定块组描述符的 起始块,即 s_first_data_block + lo
s_log_block_size 确定文件块大小,为 lKB*(2**s_log_block_size),例如 0 表示 1KB 块 大小,1表示2KB块大小,2表示4KB块大小,等等。最常用的块大小是用于小文件系统 的1KB和用于大文件系统的4KBO
s_mnt_count :已挂载文件系统的次数。当挂载计数达到max_mount_count时,fsck会 话将被迫检查文件系统的一致性。
s_magic是标识文件系统类型的幻数。EXT2/3/4文件系统的幻数是0xEF53。

块组描述符

Block#2 :块组描述符块(硬盘上的s_first_data_blocks-l ) EXT2将磁盘块分成几个 组。每个组有8192个块(硬盘上的大小为32K)。每组用一个块组描述符结构体描述。

位图

Block#8 :块位图(Bmap) (bg_block_bitmap)位图是用来表示某种项的位序列,例如 磁盘块或索引节点。位图用于分配和回收项。在位图中,0位表示对应项处于FREE状态, 1位表示对应项处于IN_USE状态。一个软盘有1440个块,但是Block#。未被文件系统使 用'所以,位图只有1439个有效位。无效位视作IN.USE处理,设置为1。
Block#9 :索引节点位图(Imap) ( bg_inode_bilmap) 一个索引节点就是用来代表一个 文件的数据结构。EXT2文件系统是使用有限数量的索引节点创建的。各索引节点的状态用 B9中Imap中的一个位表示。在EXT2 FS中,前10个索引节点是预留的。所以,空EXT2 FS的Imap以10个1开头,然后是0。无效位再次设置为1。

索引节点

Block#10 :索引(开始)节点块(bg_inode_table)每个文件都用一个128字节(EXT4 中的是256字节)的独特索引节点结构体表示。

i_block[15]数组包含指向文件磁盘块的指针,这些磁盘块有:

  • 直接块:i_block[0]至i-block[ll],指向直接磁盘块。

  • 间接块:i-block[12]指向一个包含256个块编号(对于1 KB BLKSIZE)的磁盘块, 每个

    块编号指向一个磁盘块。

  • 双重间接块:i_block[13]指向一个指向256个块的块,每个块指向256个磁盘块。

  • 三重间接块:i_block[14]是三重间接块。对于“小型” EXT2文件系统,我们可以忽 略它。

目录条目

dir_entry是一种可扩充结构。名称字段包含1到255个字符,不含终止NULL字节。所以 dir_entry的rec_len也各不相同。

显示超级块

基本方法是将超级块(Block#】

或1KB的1024偏移量位置)读入char buff 1024]中。让ext2_super_block *p结构体指向buf[]o 然后,利用p->field访问超级块结构体的各个字段。该方法类似于访问MBR中的分区表

显示位图

程序以十六进制数字形式打印每个字节的索引节点位图。在imap中,位是从低位地址到高位地址线性存储的。前16位(从低到高)是b' 11111111 11100000',但是被以十六进制形式打印成ff07,这并不能说明什么信息,因为位是按相反 顺序打印的,即从高位地址到低位地址。

第8章 使用系统调用进行文件操作

系统调用

在操作系统中,进程有两种模式:

  • 内核 (Kmode)
  • 用户 (Umode)

系统调用(简称syscall)是一种允许进程进入Kmode以 执行Umode不允许操作的机制

复刻子进程、修改执行映像.甚至是终止等操作都必须在内核中执行。

系统调用(简称syscall)是一种允许进程进入Kmode以 执行Umode不允许操作的机制。

系统调用手册页

在线手册页保存在/usr/man/目录中(Goldt等 1995 ;

Kcrrisk 2010, 2017 )o 而在 Ubuntu Linux 中,则保存在 /usr/share/man 目录中。

man2 子目录中列出了所有系统调用手册页。

使用系统调用进行文件操作

系统调用必须由程序发出,它们的用法就像普通函数调用一样,每个系统调用都是一个库函数,它汇集系统调用参数,并最终向操作系统内核发出一个系统调用

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);

常用的系统调用

stat:获取文件状态信息
int stat (char ♦filename, stxruct stat *buf) int fstat(int filedes, struct stat *buf) int Istat(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)
Iseek:重新定位文件描述符的读/写偏移量
int Iseek(int fd, int offset, int whence)
dup:将文件描述符复制到可用的最小描述符编号中
int dup(int oldfd);
dup2:将oldfd复制到newfd中,如果newfd已打开,先将其关闭
int dup2(Int oldfd, int newfd)
link:将新文件硬链接到旧文件
int link(char *oldPath, char *newPath)
unlink:取消某个文件的链接;如果文件链接数为0,则删除文件
int uniink(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);

链接文件

硬链接文件

硬链接:命令:In oldpath newpath
创建从newpath到oldpath的硬链接。对应的系统调用为:
link(char *oldpath, char *newpath)
系统调用:
unlink(char *pathname)

硬链接文件会共享文件系统中相同的文件表示数据结构(索引节点)。

文件链接数会记录链 接到同一索引节点的硬链接数量。

硬链接仅适用于非目录文件。否则,它可能会在文件系统 名称空间中创建循环,这是不允

许的。

符号链接文件

软链接:命令
In -s oldpath newpath # In command with the -s flag
创建从newpath到oIdpath的软链接或符号链接。对应的系统调用是:
symlink(char *oldpath, char *newpath)

软链接在以下情况下非常有用:

  1. 通过一个较短的名称来访问一个经常使用的较长路径名称
  2. 将标准动态库名称链接到实际版本的动态库

软链接的一个缺点是目标文件可能不复存在了。

stat系统调用

stat/lstat/fstat系统调用可将一个文件的信息返回

stat文件状态

名称
stat, fstat, Istat - get file status
概要
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *file_name, struct stat *buf);
int fstat(int filedes, struct stat *buf);
int Istat(const char *file_name, struct stat *buf);

注意:
这些函数会返回指定文件的信息。不需要拥有文件的访问权限即可获取该信息,但是需 要指向文件的路径中所有指定目

录的捜索权限。

stat按文件名统计指向文件,并在缓冲区中填写stat信息。

Istat与stat相同,除非是符号链接,统计链接本身,而不是链接所引用文件。所以, stat和Istat的区别是:stat遵循链接,

但Istat不是。

fstat与stat相同,也只在文件名处说明filedes (由open ( 2 )返回)所指向的打开文件。

stat结构体

struct stat{		
dev_t	st_dev;	/* device */
ino_t	st_ino;	/* inode */
mode_t	st_mode;	/* protection */
nlink_t	st_nlink;	/* number of hard links */
uid_t	st_uid;	/* user ID of owner */
gid_t	st_gid;	/* group ID of owner */
dev_t	st_rdev;	/* device type (if inode device) */
off_t	st_size;	/* total size, in bytes */
u32	st_blk5ize;	/* blocksize for filesystem I/O */
u32	st_blocks;	/* number of blocks allocated 7
	st_atime;	/* time of last access */
		/* time of last modification */
time_t	st_ctime;	/* time of last change */

stat与文件索引节点

s_	_IFMT	0170000	bitmask for the file type bitfields
s	IFSOCK	0140000	socket
s	IFLNK	0120000	symbolic link
s	IFREG	0100000	regular file
s	IFBLK	0060000	block device
s	IFDIR	0040000	directory
s	IFCHR	0020000	character device
s	IFIFO	0010000	fifo
s	ISUID	0004000	set UID bit
s	ISGID	0002000	set GID bit (see below)
s	ISVTX	0001000	sticky bit (see below)
s	IRWXU	00700	mask for file owner permissions
s	IRUSR	00400	owner has read permission
s	IWUSR	00200	owner has write permission
s	IXUSR	00100	owner has execute permission
s	IRWXG	00070	mask for group permissions
s	IRGRP	00040	group has read permission
s	IWGRP	00020	group has write permission
s	IXGRP	00010	group has execute permission
s	IRWXO	00007	mask for permissions for others (not in group)
s	IROTH	00004	others have read pezmission
s	IWOTH	00002	others have write permission
s	IXOTH	00001	others have execute permission

8.6.5opendir-readdir

函数目录也是一个文件。我们应该能像其他任何普通文件一样,打开一个READ目录,然 后读取和显示它的内容。

因 此,用户可能无法正确读取和解释目录的内容。鉴于此,POSIX为目录文件指定了以下接口函数。

#include <dirent.h>
DIR *open(dirPath); // open a directory named dirPath for READ
struct dirent *readdir(DIR *dp); // return a dirent pointer

Linux中的dirent结构体是:

struct dirent(
u32 d_ino; // inode number
ul6 d_reclen;
char d_naxne []
}

要想读取符号链接文件的内容,我们必须使用readlink系统调用

int readlink(char *pathname, char buf[ ]f int bufsize)

ls程序

int ls_file(char *fname)
{
struct stat fstat, *sp;
int r, i;
char ftime[64];
sp = &fstat;
if ( (r = Istat(fname, &fstat)) < 0)( printf("can* t stat %s\n", fname); exit(l);
}
if ((sp->st_mode & printf("%c",z-z)	OxFOOO)==	0x8000) // if (S_ISREG())
if ((sp->st_mode &	OxFOOO)==	0x4000) // if (S_ISDIR())
printf("%c",)	•	
if ((sp->st_mode &	OxFOOO)==	OxAOOO) // if (S_ISLNK())
printf (吳	)	i	
for (i=8; i >= 0; i	—)(	
if (sp->st_mode &	;(1 « i))	// print r|w|x
printf	tl[i]);	
else		
printf	t2[i]);	// or print -
)
printf("%4d ",sp->st_nlink);	// link count
printf("%4d ",sp->st_gid);	// gid
printf("%4d ",sp->st_uid);	// uid
printf("%8d ",sp->st_size);	// file size
// print time		
strcpy(ftime, ctime(&sp->st_ctime)); // print time in calendar form
ftime[strlen(ftime)	-1] = o;	// kill \n at end
printf("%s	",ftime);
// print name
printf("%s", basename(fname)); // print file basename
// print -> linkname if symbolic file
if ((sp->st_mode & OxFOOO)== OxAOOO){
// use readlink() to read linkname
printf(" -> %s", linkname); // print linked name
}
printf("\n");
}
int ls_dir(char *dname)
{
// use opendir(), readdir(); then call ls_file(name)
}
int main(int argc, char *argv[])
(
struct stat mystat, *sp = &mystat;
int r ;
char *filename, path[1024], cwd[256];
filename	// default to CWD
if (argc > 1)
filename = argv[l];	// if specified a filename
if (r = Istat(filename, sp) < 0)(
printf("no such file %s\n", filename);
exit(l);
}
strcpy(path, filename);
if (path[0] != '/'}{ // filename is relative : get CWD path
getcwd(cwd, 256);
strcpy(path, cwd); strcat(path, " /") ; strcat(path,filename);
}
if (S_ISDIR(sp->st_mode))
ls_dir(path);
else
ls_file(path);
)

open-close-lseek 系统调用

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 fdr char buf[ ], int count);
Iseek:将文件描述符的字节偏移量重新定位为偏移量
int Iseek(int fd, int offset, int whence)/
umask:设置文件创建掩码;文件权限为(mask &〜umask)

实验代码(示例8.1)

#include <stdio.h>
#include <errno.h>

int main(int argc, char * argv[]){
        int r;
        for(int i=1;i<argc;i++){
                r = mkdir(argv[i],0766);
                if(r<0){
                        printf("error occured\n");
                        return -1;
                }
        }
        return 0;
}

实验截图