信息安全系统实现与设计第四周——《Unix/Linux系统编程》第七、八章自学

第七、八章 文件操作与使用系统调用进行文件操作

第七章

这一章节主要介绍操作系统的操作级别,系统性地概述了各种操作,描述了低级别的文件操作以及对Linux系统中的EXT2文件系统做了相关介绍。

7.1文件操作级别

文件操作分为五个级别,按照从低到高的顺序排列如下。

(1)硬件级别∶硬件级别的文件操作包括∶

● fdisk∶将硬盘、U盘或SDC盘分区。

● mkfs∶格式化磁盘分区,为系统做好准备。

● fsck∶检查和维修系统。

● 碎片整理;压缩文件系统中的文件。

其中大多数是针对系统的实用程序。普通用户可能永远都不需要它们,但是它们是创建和维护系统不可缺少的工具。

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

每个操作系统内核均可为基本文件操作提供支持,下文列出了类unix系统内核中的一些函数,其中前缀k表示内核函数。

 kmount () . kumount ( )			(mount/umount file systems)
 kmkdir(), krmdir()				    (make/ remove directory)
 kchdir(), 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()					(lseak/close file descriptors)
 ksymlink(), kreadlink ()			(create/read symbolic link files)
 kstat(), kfstat(),klstat()			(get file status/information)
 kopendir(), kreaddiz()				(open/read airectories)

(3)系统调用

用户模式程序使用系统调用来访问内核函数

open(),read(),lseek()和close()都是c语言库函数。每个库函数都会发出一个系统调用,使进程进入内核模式来执行相应的内核函数

(4)I/O库函数

系统调用可以让用户读/写多个数据块,这些数据块只是一系列字节。

由于用户并不关系这些数据的意义,因此,C语言提供了一套标准的I/O函数,同时也提高了运行效率。

这些函数主要包括:

 FILE mode I/O: fopen(),fread();fwrite(),fseek(),fclose(),fflush()
 char mode I/O: getc(), getchar(); ugetc(); putc(),putchar()
 line mode I/O: gets() , fgets();puts( ) , fputs()
 formatted I/O: scanf(),fscanf().sscanf(); printf(),fprintf() , sprintf()

(5)用户命令

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

示例如下:

mkdir,rmdir,cd,pwd,ls,link,unlink,rm,cat,cp,mv,chmod,etc.

每个用户命令实际上是一个可执行程序,通常为库I/O函数,

其顺序如下:

Command => Library I/O function=> System call => Kernel Function

(6)sh脚本

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

7.2文件I/O操作

文件操作包括两个空间内的操作,一部分为内核空间,一部分为用户空间,因此操作模式也分为两部分,下面将分为两个部分分别介绍用户模式与内核模式下的操作

其中(1)-(4b)为用户模式,(5)-(10)为内核模式

用户模式

(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()和下半部分的磁盘中断处理程序组成。

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

7.3低级别文件操作

(1)分区

一个块存储设备可以分为几个逻辑单元称为分区。各分区均可格式化为特定的文件系统,也可以安装在不同的操作系统上。分区表位于第一个扇区的字节偏移446(ox1BE)处,该扇区称为设备的主引导记录。表有4个条目,每个条目由一个16字节的分区结构体定义,即:

stuct partition {

u8 drive;			// 0x80 - active

u8 head;			// starting head

u8 sector;			// starting sector

u8 cylinder;		// starting cylinder

u8 sys_type;		// partition 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 )

如果某分区是扩展类型(类型编号=5),那么它可以划分为更多分区。假设分区P4是扩展类型,它被划分为扩展分区P5、P6、P7。扩展分区在扩展分区区域内形成一个链表。

每个扩展分区的第一个扇区是一个本地MBR。每个本地MBR在字节偏移量0xIBE处也有一个分区表,只包含两个条目。第一个条目定义了扩展分区的起始扇区和大小。第二个条目指向下一个本地MBR。所有本地MBR的扇区编号都与P4的起始扇区有关。照例,链表以最后一个本地MBR中的0结尾。在分区表中,CHS值仅对小于8GB的磁盘有效。对大于8GB但小于4G扇区的磁盘,只有最后两个条目start _sector 和nr sector有意义。

(2)格式化分区

fdisk只是将一个存储设备划分为多个分区。每个分区都有特定的文件系统类型,但是分区还不能使用。为了存储文件,必须先为特定的文件系统准备好分区。该操作习惯上称为格式化磁盘或磁盘分区。在Linux中,命令

mkfs -t TYPE [-b bsize] device nblocks

在一个nblocks设备上创建一个TYPE文件系统,每个块都是bsize字节。如果bsize未指定,则默认大小为1KB。例如假设是EXT2/3文件系统,它作为Linux的默认文化系统,因此,

mkfs -t ext2 vdisk 1440

使用1440个块将vdisk格式化为EXT2文件系统。

在Linux中,还不能访问新的文件系统。它必须挂在到跟文件系统中的现有目录。由于细腻文件系统不是真正的设备i,他们必须作为循环设备挂载,如:

sudo mount -o loop vdisk /mnt

将 vdisk挂载到/mnt 目录中。不带任何参数的 mount命令会显示Linux系统的所有挂载设备。挂载完成后,挂载点/mnt 改变,与挂载设备的根目录相同。用户可以将目录(cd)更改为/mnt,像往常一样对设备进行平铺操作。挂载后的设备使用完成后,将cd从/mnt中取出,然后输入

sudo umount /mnt 

以卸载设备,将其与根文件系统分离。设备上保存的文件应保留在该设备中。

(3)挂载分区

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

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

2.在vdisk. 上运行fdisk来创建一一个分区P1:

 fdisk vdisk

输人n(new)命令,使用默认的起始和最后扇区编号来创建一个分区Pl。然后,输人w命令将分区表写人vdisk并退出fdisko vdisk 应包含-个分区P1 [start=2048, end=65535]。该分区的大小是63488个扇区。

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

losetup -o $(expr 2048 * 512) --sizelimit $(expr 65535 * 512) /dev/1oop1vdisk

losetup需要分区的开始字节(start_ sector512) 和结束字节(end_ sector512)。 读者可手动计算这些数值,并在losetup命令中使用它们。可用类似方法设置其他分区的循环设备。循环设备创建完成后,读进程可以使用命令

losetup - a

将所有循环设备显示为/dev/loopN。

4./dev/loop1,它是一个EXT2文件系统:

mke2fs -b 4096 /dev/loop1 7936 # mke2fs with 7936 4KB blocks

该分区的大小是63488个扇区。4KB块的扇区大小是63488 /8=7936

5.挂载循环设备:

mount /dev/ 1oop1 /mnt # mount as loop device

6.访问作为文件系统一部分的挂载设备:

(cd /mnt; mkdir bin boot dev etc user) # populate with DIRs

7.设备使用完毕后,将其卸载。

umount /mnt

8.循环设备使用完毕后,通过以下命令将其断开:

losetup -a /dev/loop1 # detach a loop device.

7.4 EXT2文件系统简介

EXT2第二代扩展文件系统(英语:second extended filesystem,缩写为 ext2),是LINUX内核所用的文件系统。它开始由Rémy Card设计,用以代替ext,于1993年1月加入linux核心支持之中。

而EXT3为其拓展,增加了一个日志文件,用来记录文件系统的更改记录。

EXT4又作为EXT3的最新扩展,改为分配连续的磁盘块区,称为区段。

(1)文件系统数据结构

在Linux中可以根据(旧)软盘的块数创建1440个块,每个大小为1KB.下面简要介绍磁盘块的内容

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

超级块

(2)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。

(3)块组描述符

lock#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。

(4)位图

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。

(5)索引节点

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

struct ext2_inode {

u16i_mode;		// 16 bits =|ttttlugs|rwx|rwx|rwxl

u16 i_uid;		// owmer uid 

u32 i_size;		// file size in bytes 

u32 i_atime;	//time fields in seconds

u32 i_ctime;	// since 00:00:00,1-1-1970

u32 i_mtime;

u32 _dtime;

u16 i_gid;		// group ID 

u16 i_links_count;// hard-link count

u32 i_blocks;	// number of 512-byte sectors 

u32 i_flags;	//IGNORE

u32 i_reservedl;// IGNORE

u32 i_block[15];// See details below 

u32 _pad[7];	// for inode size = 128 bytes

i block【15】数组包含指向文件磁盘块的指针,这些磁盘块有∶

● 直接块∶ i block【0】至 i-block【11】,指向直接磁盘块。

●间接块∶i-block【12】指向一个包含 256个块编号(对于1KB BLKSIZE)的磁盘块,
每个块编号指向一个磁盘块。
·

●双重间接块∶i block【13】指向一个指向 256个块的块,每个块指向 256个磁盘块。

●三重间接块∶i block【14】是三重间接块。对于"小型"EXT2文件系统,我们可以忽略它。

第八章

这一章节主要介绍如何使用系统调用进行文件操作,结识了系统调用作用,列举了常用系统调用,具体介绍了stat系统调用等内容。

8.1系统调用及Linux在线手册

(1)在操作系统中,进程以两种不同的模式运行,即内核模式和用户模式,简称Kmode和Umode。在Umode中,进程的权限非常有限。它不能执行任何需要特殊权限的操作。特殊权限的操作必须在Kmode下执行。系统调用(简称syscall)是一种允许进程进入Kmode以执行Umode不允许操作的机制。复刻子进程、修改执行映像,甚至是终止等操作都必须在内核中执行。

(2)在Unix以及大多版本的Linux中,在线手册页保存在/usr/man/目录中(Goldt等1995;Kerrisk 2010,2017)。而在Ubuntu Linux中,则保存在/usr/share/man目录中。man2子目录中列出了所有系统调用手册页。sh命令man 2 NAME显示了系统调用名称的手册页。

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

int syscall(int a,int b,int c, int d);

其中,第一个参数 a是系统调用编号,b、c、d是对应内核函数的参数。在基于Intel x86的Linux中,系统调用是由INT0x80汇编指令实现的,可将CPU从用户模式切换到内核模式。 内核的系统调用处理程序根据系统调用编号将调用路由到个相应的内核函数。 当进程结束执行内核函数时,会返回到用户模式,并得到所需的结果。返回值≥0表示成功,-1表示失败。如果失败,errno 变量(在errno.h中)会记录错误编号,它们会被映射到描述错误原因的字符串。

简单的系统调用:

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

    8.3链接文件

    (1)硬链接文件

    ln oldpath  newpath
    

    创建从newpath到oldpath的硬链接。

    对应系统调用为:

    link(char *oldpath, char *newpath)
    

    硬链接文件会共享文件系统中相同的文件表示数据结构(索引节点)。文件链接数会记录链接到同一索引节点的硬链接数量。硬链接仅适用于非目录文件。否则,它可能会在文件系统名称空间中创建循环。

    (2)符号链接文件(软链接)

    ln -s oldpath newpath 
    

    创建从newpath到oldpath的软链接或符号链接。

    对应的系统调用为:

    link(char *oldpath, char *newpath)
    

    newpath是LNK类型的普通文件,包含oldpath字符串。与硬链接不同的是软链接适用于任何文件,包括目录。软链接在以下情况下非常有用。
    1.通过一个较短的名称来访问一个经常使用的较长路径名称。
    2.将标准动态库名称链接到实际版本的动态库。

    8.4stat系统调用

    stat函数可用于返回指定文件的信息而不需要拥有相关文件的访问权限,但是需要指向文件的路径中所有指定目录的搜索权限。

    可由下图概括:

  • 实践部分:

    通过以下程序读取文件的第二个1024字节

在实践过程中出现无法编译的错误,经过发现主要存在几个问题
1.缺少文件操作相关的头文件

2.代码中存在无用的空格与中文符号(由于过于隐蔽,很难发现)

最后通过查询添加上书中代码本来没有的头文件stlib.h与unistd.h,并每行检验空格与中文字符的存在,最终解决问题

posted @ 2021-10-10 22:18  20191303姜淳译  阅读(86)  评论(0编辑  收藏  举报