• 《Unix/Linux系统编程》第七、八章学习笔记 20201209戴骏


    一、知识点归纳

    第七章 文件操作

    1.文件操作级别

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

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

      • fdisk:将硬盘、U盘或SDC盘分区。
      • fmkfs:格式化磁盘分区,为系统做好准备。
      • 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
      kcreat(); kopen() (create/open file for R,W,RW,APPEND)
      kread(), kwrite() (read/write opened files)
      klseek(), kclose() (Iseek/close file descriptors)
      ksymlink(), kreadlink() (create/read symbolic link files)
      kstat(), kfstat(), klstat() (get file status/infoirmation)
      kopendir(), kreaddir() (open/read directories)
    • (3)系统调用:用户模式程序使用系统调用来访问内核函数。
      open()、read()、lseek()和close()函数都是C语言库函数。每个库函数都会发岀一个系统调 用,使进程进入内核模式来执行相应的内核函数,例如open可进入kopen(), read可进入kread()函数,等等。当进程结束执行内核函数时,会返回到用户模式,并得到所需的结果。 在用户模式和内核模式之间切换需要大量的操作(和时间)。因此,内核和用户空间之间的 数据传输成本昂贵。虽然可以发出read(fd,buf,1)系统调用来只读取一个字节的数据,但是 这种做法是不明智的,因为一个字节也会带来可怕的高成本。我们在每次必须进入内核时, 都要尽可能不虚此行。对于读/写文件,最好的方法是匹配内核的功能。内核会按数据块大小(从1KB到8KB)来读取/写入文件。例如,在Linux中,硬盘的默认数据块大小是 4KB,软盘的是1KB。因此,每个读/写系统调用还要尝试一次传输一个数据块。
      下面的程序可读取文件的第二个1024节。

      #include <fcntl.h>
      int main(int argc< char *argv[ )) // run as a.out filename
      {
        int fd, n;
        char buf[1024];
        if ((fd = open(argv[1]> O_RDONLY)) < 0)   // if open() fails 
          exit(l);
        lseek(fd, 1024, SEEK_SET);	// Iseek to byte 1024
        n = read(fd, bufr 1024);	// try to read 1024 bytes
        close(fd);
      }
      
    • (4)1/O库函数:系统调用可让用户读/写多个数据块,这些数据块只是一系列字节。它们不知道,也不关心数据的意义。用户通常需要读/写单独的字符、行或数据结构记录等。如果只有系统调用,用户模式程序则必须自己从缓冲区执行这些操作。大多数用户会认为这非常不方便。为此,C语言库提供了一系列标准的I/O函数,同时也提高了运行效率。I/O库函数包括:

      • FILE mode I/O:fopen(),fread(); fwrite(),tfseek(),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();print£(),fprintf(),sprintf()
        除了读/写内存位置的sscanf()/sprintf()函数之外,所有其他I/O库函数都建立在系统调用之上,也就是说,它们最终会通过系统内核发岀实际数据传输的系统调用。
    • (5)用户命令:用户可以使用Unix/Linux命令来执行文件操作,而不是编写程序。用户命令的示例如下:

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

      每个用户命令实际上是一个可执行程序(cd除外),通常会调用库I/O函数,而库I/O函数再 发出系统调用来调用相应的内核函数。用户命令的处理顺序为:
      Command => Library I/O function => System call => Kernel Function
      或者
      Command ======================== > System call => Kernel Function

    • (6)sh脚本:虽然比系统调用方便得多,但是必须要手动输入命令,如果使用的是GUI,
      必须要拖放文件图标和点击指向设备来输入,操作烦琐而且耗时。sh脚本是用sh编程语言编写的程序,可通过命令解释程序sh来执行。sh语言包含所有的有效Unix/Linux命令。它 还支持变量和控制语句,如if、do、for、while, case等。实际上,sh脚本广泛用于Unix/Linux系统编程。除sh之外,Perl和Tel等其他许多脚本语言也使用广泛。

    2.文件I/O操作

    文件I/O操作示意图:

    在图中,双线上方的上半部分表示内核空间,下半部分表示进程的用户空间。该图显示了进程读/写文件流时的操作序列。控制流用标签(1)到(10)标识,说明如下。
    下面的步骤(1)〜(46)是用户模式下的操作,步骤(5)〜(10)是内核模式下的操作。

    • (1) 用户模式下的程序执行操作
      FILE *fp = fopen("file", "r"); 
      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) fread(ubuf, 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/lputs、 fscanf/fprintf等也可以在用户空间内的FILE结构体中对命uf进行操作“
    • (5)内核中的文件系统函数:
      假设非特殊文件的read(fd, fbuf[], BLKSIZE)系统调用。
    • (6)在read()系统调用中,fd是一个打开的文件描述符,它是运行进程的fd数组中的一个索引,指向一个表示打开文件的OpenTable。
    • (7)OpenTable包含文件的打开模式、一个指向内存中文件INODE的指针和读/写文件的当前字节偏移量。从0penTable的偏移量,
      •计算逻辑块编号lbk。
      •通过INODE.i_block[]数组将逻辑块编号转换为物理块编号blk。
    • (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): //bp=a locked buffer in dev_list, opcode=R|W(ASYNC) 
      {
         enter bp into dev's I/O_queue;
         if (bp is FIRST in I/O_queue) 
            issue I/O command to device;
      }
      
      -------------------Lower-half o£ 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 buf£er in dev.I/0_queue;
      }
      

    3.低级别文件操作

    • 分区
      一个块存储设备,如硬盘、u盘、SD卡等,可以分为几个逻辑单元,称为分区,各分区均可以格式化为特定的文件系统,也可以安装在不同的操作系统上。大多数引导程序,如 GRUB、LILO等,都可以配置为从不同的分区引导不同的操作系统。分区表位于第一个扇 区的字节偏移446(OxlBE)处,该扇区称为设备的主引导记录(MBR)。表有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在字节偏移量OxlBE处也有一个分区表,只包含两个条目。第一个条目定义了扩展分区的起始房区和大小。第二个条目指向下一个本地MBR。所有本地MBR的扇区编号都与P4的起始扇区有关。照例,链表以最后一个本地MBR中的0结尾。在分区表中,CHS值仅对小于8GB的磁盘有效:对大于8GB但小于4G扇区的磁盘,只有最后两个条目start_sector和nr_sector有意义。接下来,我们将通过示例来演示fdisk和分区。由于使用计算机的真实磁盘进行操作会非常危险,所以我们要使用一个虚拟磁盘映像.它只是一个普通的文件,但是看起来像一个真实磁盘。

      • (1)在Linux下,例如Ubuntu,创建一个名为mydisk的虚拟磁盘映像文件。
        dd if=/dev/zero of=mydisk bs=1024 count=1440
        dd是一个将1440(1KB)个0字节块写入目标文件mydisk的程序。我们选择count=1440, 因为它是旧软盘的1KB字节块的数量。必要时,读者可指定更大的库编号。

      • (2)在磁盘映像文件上运行fdisk:
        fdisk mydisk
        fdisk是一个交互程序。它有一个显示所有命令的帮助菜单。它收集用户的输入,在内存中创建一个分区表,该分区表仅在用户输入w命令时才被写入磁盘映像的MBR。但是在内存中,它允许用户创建、检査和修改分区。将分区写入磁盘后,通过q命令退出fdisk。下表显示了一个fdisk会话的结果,该会话将磁盘映像划分为3个主分区(p1至p3)和一个扩展分区P4。扩展分区P4被进一步划分为更多的分区(P5至P7 ),所有这些分区都位于扩展分区的区域内。在开始创建时,分区类型都默认为Linux。可以通过t命令将它们更改为不同的文件系统类型。每个文件系统类型都有一个用十六进制唯一值,例如0x07表示HPFS/NTFS、0x83表示Linux等,这些值可通过t命令显示出来(见下表)。

        Device Boot Start End Sectors Size Id Type
        mydtskl 1 719 719 359.5K 7 HPFS/NTFS/exFAT
        mydtsk2 720 1439 720 360K 83 Linux
        mydtsk3 1440 1799 360 180K 81 Minix / old Linux
        mydtsk4 I860 2879 1080 540K 5 Extended
        mydtsk5 1801 2159 359 179.5K 6 FAT16
        mydtska 2161 2519 359 179.5K 83 Linux
        mydtsk7 2521 2879 359 179.5K 82 Linux swap / Solaris
        Command (m for help): q
    • 格式化分区
      fdisk只是将一个存储设备划分为多个分区。每个分区都有特定的文件系统类型.但是分区还不能使用。为了存储文件,必须先为特定的文件系统准备好分区匸该操作习惯匕称为 格式化磁盘或磁盘分区。在Linux中,它被称为mkfs,表示Make文件系统匸Linux支持多种不同类型的文件系统,每个文件系统都期望存储设备上有特定的格式。在Linux中,命令

      mkfb -t TYPE [-b bsize] device nblocko
      

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

      mkfs -t ext2 vdiek 1440 或 mke2fs vdisk 1440
      

      使用1440 ( 1KB)个块将vdisk格式化为EXT2文件系统。格式化后的磁盘应是只包含根目录的空文件系统。但是,Linux的mkfs始终会在根目录下创建一个默认的lost+found目录 完成mkfs之后,设备就可以使用了。在Linux中,还不能访问新的文件系统°它必须挂载 到根文件系统中的现有目录中。/mnt目录通常用于挂载其他文件系统。由于虚拟文件系统不 是真正的设备,它们必须作为循环设备挂载,如

      sudo mount -o loop vdisk /mnt 
      

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

      sudo amount /mnt 或 sudo umount vdiak
      

      以卸载设备,将其与根文件系统分离。设备上保存的文件应保留在该设备中。
      要指出的是,目前大多数Linux系统,例如Slackware 14.1和Ubuntu 15.10,都可以检测到包含文件系统的便携设备,并直接挂载它们。例如,当Ubuntu用户将U盘插入USB 端口时,Ubuntu可以检测设备,自动挂载设备,并在弹出窗口中显示文件,允许用户直接 访问文件。用户可以输入mount命令来査看设备(通常是/dev/sdbl)的挂载位置,以及挂载的文件系统类型。如果用户拔出U盘,Ubuntu也会检测到,并自动卸载设备。但是,随意拔出便携设备可能会损坏设备上的数据。因为Linux内核通常使用延迟数据写入来写入设备。为了确保数据的一致性,用户应该先卸载设备,然后再断开连接。
      Linux mount命令可以挂载实际设备的分区或整个虚拟磁盘,但不能挂载虚拟磁盘的分区。如果某个虚拟磁盘包含多个分区,那么必须先将这些分区与循环设备关联起来。

    • 挂载分区
      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)命令,使用默认的起始和最后扇区编号来创建一个分区P1。然后,输入w命令将分区表写入vdisk并退出fdisk。vdisk应包含一个分区P1[start=2048, end=65535].该分区的大小是63488个扇区。
      • (3)使用以下扇区数在vdisk的分区1上创建一个循环设备:
        losetup -o $(expr 2048 \* 512) --sizelimit $(expr 65535 \* 512) /dev/loop1 vdisk
        
        losetup需要分区的开始字节(start_sector512)和结束字节(end_sector512 )。读者可手动计算这些数值,并在losetup命令中使用它们。可用类似方法设置其他分区的循环设备。循环设备创建完成后,读进程可以使用命令
        losetup -a
        
        将所有循环设备显示为/dev/loopN。
      • (4)格式化/dev/loop1,它是一个EXT2文件系统:
        mke2fs -b 4096 /dev/loopl 7936  # mke2fs with 7936 4KB blocks
        
        该分区的大小是63488个扇区。4KB块的扇区大小是63488 / 8=7936。
      • (5)挂载循环设备:
        mount /dev/loop1 /mnt	# mount as loop device
        
      • (6)访问作为文件系统一部分的挂载设备:
        (cd /mnt; mkdir bin boot dev etc user)  # populate with DIRs
        
      • (7)设备使用完毕后,将其卸载。
        umount /mnt
        
      • (8)循环设备使用完毕后,通过以下命令将其断开:
        losetup -d /dev/loop1	# detach a loop device.
        

    4. EXT2文件系统简介

    多年来,Linux一直使用EXT2 ( Card等1995; EXT2 2001 )作为默认文件系统。EXT3 (EXT3 2015 )是EXT2的扩展,,EXT3中增加的主要内容是一个日志文件,它将文件系统的更改记录在日志中。日志可在文件系统崩溃时更快从错误中恢复。没有错误的EXT3文件系统与EXT2文件系统相同。EXT3的最新扩展是EXT4 ( Cao等2007)。EXT4的主要变化是磁盘块的分配。在EXT4中,块编号是48位。EXT4不是分配不连续的磁盘块,而是分配连续的磁盘块区,称为区段。

    • EXT2文件系统数据结构
      在Linux下,我们可以创建一个包含简单EXT2文件系统的虚拟磁盘,如下文所示:
      (1). dd if=/dev/zero of=mydisk bs=1024 count=1440
      (2). mke2£s -b 1024 mydisk 1440
      得到的EXT2文件系统有1440个块,每个块大小为1KB。我们之所以选择1440块, 是因为它是(旧)软盘的块数,得到的磁盘映像可以直接作为虚拟(软)磁盘在模拟基于Intel x86的大多数PC虚拟机上使用,例如QUEM、VirtualBox和VMware等,这种EXT2文件系统的布局如图所示。

      为了方便讨论,我们先假设使用这个基本文件系统布局。在适当的时候.我们会指出一 些变化,包括硬盘上大型EXT2/3 FS中的变化。下面来简要解释一下磁盘块的内容。
      Block#0:引导块 B0是引导块,文件系统不会使用它。它用于容纳从磁盘引导操作系统的引导程序。

    • 超级块
      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 */
         ul6	s_nmt_count;	        /*	Mount count */
         S16	s_max_mnt_count ;	/*	Maximal mount count */
         U16	s_magic;	        /*	Magic signature */
         // more non-essential fields		
         ul6	s_inode_size;	        /*	size of inode structure */
      }
      

      大多数超级块字段的含义都非常明显。只有少数几个字段需要详细解释。
      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和用于大文件系统的4KB。
      s_mnt_count :已挂载文件系统的次数。当挂载计数达到max_mount_count时,fsck会话将被迫检查文件系统的一致性。
      s_magic是标识文件系统类型的幻数。EXT2/3/4文件系统的幻数是0xEF53。

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

      struct ext2_group_desc (
         u32	bg_block_bitmap;	//	Bmap block number
         u32	bg_inode_bitmap;	//	Imap block number
         u32	bg_inode_table;	        //	Inodes begin block number
         U16	bg_free_blocks_count;	//	THESE are OBVIOUS
         ul6	bg_free_inodes_count;		
         ul6	bg_used_dirs_count;		
         ul6	bg_pad;	                //	ignore these
         u32	bg_reserved[3];	
      

      由于一个软盘只有1440个块,B2只包含一个块组描述符。其余的都是在有大量块组的硬盘上。块组描述符可以跨越多个块。块组描述符中最重要的字段是bg_block_bitmap,bg_inode_bitmap和bgjnode table,它们分别指向块组的块位图、索引节点位图和索引节点起始块。对于Linux格式的EXT2文件系统,保留了块3到块7,所以,bmap=8,imap=9,inode_table= 10。

    • 位图
      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字节)的独特索引节点结构体表示。下面列出了主要索引节点字段。

      struct ext2_inode {
         ul6	i_mode;	//	16 bits = |tttt|ugs|rwx|rwx|rwx|
         ul6	i_uid;	        //	owner 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	i_dtime;		
         ul6	i_gid;	        //	group ID
         ul6	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	i_pad[7];	//	for inode size = 128 bytes
      }
      

      在索引节点结构体中,i_mode为U16或2字节无符号整数。

      在i_mode字段中,前四位表示文件类型。例如,tttt=1000表示REG文件,0100表示DIR文件等。接下来的3位ugs表示文件的特殊用法。最后9位是文件保护的rwx权限位。
      i_size字段表示文件大小(以字节为单位)。各时间字段表示自1970年1月1日0时0分0秒以来经过的秒数。所以,每个时间字段都是一个非常大的无符号整数,可借助以下库函数将它们转换为日历形式:

      char *ctime(&time_field)
      

      将指针指向时间字段,然后返回一个日历形式的字符串。例如:

      printf ( "%s" , ctime (&inode.i_atime);	// note: pass & of time field
      

      以日历形式打印i_atime。
      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文件系统,我们可以忽略它。
      索引节点大小(128或256)用于平均分割块大小(1KB或4KB),所以,每个索引 节点块都包含整数个索引节点。在简单的EXT2文件系统中,索引节点的数量是184个(Linux默认值)。索引节点块数等于184/8=23个。因此,索引节点块为B10至B32-每个索 引节点都有一个独特的索引节点编号,即索引节点在索引节点块上的位置+1。注意,索引节点位置从0开始计数,而索引节点编号从1开始计数。0索引节点编号表示没有索引节点。根目录的索引节点编号为2。同样,磁盘块编号也从1开始计数,因为文件系统从未使用块0。块编号0表示没有磁盘块。
      数据块:紧跟在索引节点块后面的是文件存储块。假设有184个索引节点,第一个实际数据块是B33,它就是根目录/的i_block[0]。

    • 目录条目
      EXT2目录条目:目录包含dir_entry结构,即:

      struct ext2_dir_entry_2{
         u32 inode;                 //inode number; count from 1, NOT 0
         u16 rec_len;               //this entry's length in bytes
         u8 file_type;              //name length in bytes
         u8 file_type;              //not used
         char name[EXT2_NAME_LEN];  //name: 1-255 chars, no ending NULL
      };
      

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

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

    1. 系统调用

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

    2.系统调用手册页

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

    man 2 stat : display man pages of atat(), fstat() and Istat() syacalls
    man 2 open: display man pages of open() syscall
    man 2 read: display man pages of read() syscall, etc.
    

    许多系统调用需要特别包含头文件,手册页的SYNOPSIS(概要)部分列出了这些文件 如果没有合适的头文件,C编译器可能会因为syscall函数名称类型不匹配而发出许多警告 一些系统调用可能还需要特定的数据结构作为参数,必须在手册页中描述这些参数

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

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

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

    其中,第一个参数a是系统调用编号,b、c、d是对应内核函数的参数,在基于Intel x86的 Linux中,系统调用是由INT 0x80汇编指令实现的,可将CPU从用户模式切换到内核模式。内核的系统调用处理程序根据系统调用编号将调用路由到一个相应的内核函数。当进程结束执行内核函数时,会返回到用户模式,并得到所需的结果。返回值N0表示成功,-1表示失败。如果失败,errno变量(在errno.h中)会记录错误编号,它们会被映射到描述错误原因的字符串。
    该程序发出一个mkdir()系统调用来创建新目录。mkdir()系统调用需要一个路径名和一个权限(八进制的0766)。如果没有新目录,则系统调用成功,返回值为0。如果不止一次运行该程序,由于目录已经存在,则在第二次或后续任何运行时会失败,返回值为-1。在这种情况下,该程序会打印消息:

    errno=17: File exists
    

    除了 mkdir()之外,该程序还演示了 chdir()和getcwd()系统调用的用法。
    简单的系统调用:下面列出了一些简单的文件操作系统调用。鼓励读者编写C程序来使用、测试它们。

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

    4.常用的系统调用

    本节,我们将讨论一些最常见的文件操作的系统调用。其中包括:

    • stat:获取文件状态信息
      int stat (char *filename, struct 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)
    • lseek:重新定位文件描述符的读/写偏移量
      int lseek(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);

    5. 链接文件

    在Unix/Linux中,每个文件都有一个路径名。但是,Unix/Linux允许使用不同的路径名来表示同一个文件。这些文件叫作LINK (链接)文件。有两种类型的链接,即硬链接和软链接或符号链接。

    • 硬链接文件
      硬链接:命令

      ln oldpath newpath
      

      创建从newpath到oldpath的硬链接。对应的系统调用为:

       link(char *oldpath, char *newpath)
      

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

      unlink(char *pathname)
      

      会减少文件的链接数。如果链接数变为0,文件会被完全删除。这就是rm(file)命令的作用。 如果某个文件包含非常重要的信息,就最好创建多个链接到文件的硬链接,以防被意外删除。

    • 符号链接文件
      软链接:命令

      ln -s oldpath newpath # ln command with the -s flag
      

      创建从newpath到oIdpath的软链接或符号链接。对应的系统调用是:

      symlink(char *oldpath, char *newpath)
      

      newpath是LNK类型的普通文件,包含oldpath字符串。它可作为一个绕行标志,使访问指向链接好的目标文件。与硬链接不同,软链接适用于任何文件,包括目录。软链接在以下情况下非常有用.
      (1)通过一个较短的名称来访问一个经常使用的较长路径名称,例如:
      x -> aVeryLongPathnameFile
      (2)将标准动态库名称链接到实际版本的动态库,例如:
      libc.so.6 -> libc.2.7.so
      当将实际动态库更改为不同版本时,库安装程序只需更改(软)链接以指向新安装的库。
      软链接的一个缺点是目标文件可能不复存在了。如果是这样,绕行标志可能引导可怜的 司机摔下悬崖。在Linux中,会通过Is命令以适当的深色RED显示此类危险,提醒用户链 接已断开。此外,如果foo -> /a/b/c是软链接,open("foo", 0)系统调用将打开被链接的文件 /a/b/c,而不是链接文件自身。所以。pen()/read()系统调用不能读取软链接文件,反而必须 要用readlink系统调用来读取软链接文件的内容。

    6. stat系统调用

    stat/lstat/fstat系统调用可将一个文件的信息返回。命令man 2 stat会显示stat系统调用的手册页,如下文所述。

    • stat文件状态
      下面是关于stat系统调用的Linux开发者手册的内容。

      • 名称
        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信息。
        lstat与stat相同,除非是符号链接,统计链接本身,而不是链接所引用文件。所以,stat和lstat的区别是:stat遵循链接,但lstat不是。
        fstat与stat相同,也只在文件名处说明filedes (由open ( 2 )返回)所指向的打开文件。
    • stat结构体
      所有的stat系统调用都以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
          time_t	st_atime;	/* time of last access */
          time_t   st_mtime;       /* time of last modification */
          time_t   st_ctime;       /* time of last change */
      

      size是用字节表示的文件大小。符号链接的大小是指它所包含的路径名称长度,末尾没有NULL。
      st_blocks值是用512字节块表示的文件大小。(可能小于st_size/512,例如当文件有漏 洞时°)st_blksize值表示有效文件系统I/O的“首选”块大小。(以较小的块写入文件可能导 致低效的读取-修改-重写。)
      并非所有的Linux文件系统都能实现所有的时间字段。一些文件系统类型允以这样一 种方式挂载,即文件访问不会导致st_atime字段的更新"见mount ( 8 )中的“noatime”。)
      通过文件访问更改所含的 st_atime,例如 exec (2 )、rnknod ( 2 )、pipe ( 2 )、utime ( 2 ) 和read (2)(大于零字节)。其他例程,如mmap ( 2 ),可能会,也可能不会更新st_atime。
      通过文件修改,如 mknod (2 )、truncate ( 2 )、utime ( 2 )和 write ( 2 )(大于零字节),
      更改所包含的st_mtimeo此外,还可以通过创建或删除目录中的文件来更改目录的st_mtime0 所包含的st_mtime不会因为所有者、组、硬链接数或模式的变化而变化。
      通过写入或设置索引节点信息(即所有者、组、链接数、模式等)更改所包含的st_ctime。

    • stat与文件索引节点
      stat与文件索引节点:首先,我们来阐明stat如何工作。每个文件都有一个独有的索引节点数据结构,包含文件的所有信息。下文给出了Linux中EXT2文件系统的索引节点结构体。

      struct ext2_inode(
         u16 i_mode;
         u16 i_uid;
         u32 i_size;
         u32 i_atime;
         u32 i_ctime;
         u32 i_mtime;
         u32 i_dtime;
         u16 i_gid;
         u16 i_links_count;
         u32 i_blocks;
         u32 i_flags;
         u32 i_reservedl;
         u32 i_block[15];
         u32 pad[7];
      }; // inode=128 bytes in ext2/3 FS; 256 bytes in ext4
      

      每个索引节点在存储设备上都有唯一的索引节点编号(ino)。每个设备都由一对(主、次)设备号标识,例如0x0302表示/dev/hda2, 0x0803表示/dev/sda3等。stat系统调用只是 查找文件的索引节点并将信息从索引节点复制到stat结构体中,但是st_dev和st_ino除外,它们分别是设备号和索引节点编号。在Unix/Linux中,所有时间字段都是自1970年1月1日0时0分0秒以来经过的秒数。它们可通过库函数ctime(&time)转换为日历形式。

    • opendir-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 []
      }
      

    在dirent结构体中,POSIX只要求必须保留d_name字段。其他字段取决于具体的系统。 opendir()返回一个DIR指针dirp。每个readdir(dirp)调用返回一个dirent指针,指向目录中 下一个条目的dirent结构体。当目录中没有更多条目时,则返回一个NULL指针。

    • readlink 函数
      Linux的open。系统调用遵循符号链接。因此,无法打开符号链接文件并读取其内容。
      要想读取符号链接文件的内容,我们必须使用readlink系统调用,即:

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

      它将符号链接文件的内容复制到bufsize的but[]中,并将实际复制的字节数返回。

    • Is 程序
      下面给出了一个简单的Is程序,它的行为类似于Linux的Is-1命令。这里的目的并非 要另外编写一个Is程序来重复操作。而是要说明如何使用各种系统调用来显示目录下的文 件信息。通过学习示例程序代码,读者还应该能够理解如何实现Linux的Is命令。

    /*******'	
    #include	<stdio.h>
    #include	<stdlib.h>
    #include	<string.h>
    #include	<sys/stat.h>
    #include	<time.h>
    #include	<sys/types.h>
    ttinclude	<dirent.h>
    
    struct stat mystat, *sp;
    char *tl = "xwrxwrxwr	";
    char *t2 =		
    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);
    )
    

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

    8. read()系统调用

    #include <unistd.h>
    int read(int fd, void *buf, int nbytes);
    

    read()将n个字节从打开的文件描述符读入用户空间中的buf[]0返回值是实际读取的 字节数,如果read()失败,会返回-1,例如当fd无效时.注意,buf{]区必须有足够的空间 来接收。个字节,并且返回值可能小于n个字节,例如文件小于n个字节,或者文件无更多需要读取的数据。还要注意,返回值是一个整数,而不是文件结束(EOF)符,因为文件中没有文件结束符文件结束符是I/O库函数在文件流无更多数据时返回的一个特殊整数值 (-1) 。

    8.9 write()系统调用

    #include <unistd.h>
    int write(int fd, void *buf, int nbytes);
    

    write()将n个字节从用户空间中的buf[]写入文件描述符,必须打开该文件描述符进行写、读写或追加。返回值是实际写入的字节数,通常等于n个字节,如果write。失败,则为-1,例如由于出现无效的fd或打开fd用于只读等。

    二、问题与解答

    问题:代码无法在Ubuntu终端中运行
    解答:在书中和网上寻找解答方案

    三、实验与截图

    复制文件





    显示目录


  • 相关阅读:
    rename 批量重命名
    shell脚本实现轮询查看进程是否结束
    mysql 修改max_connections
    window10下的solr6.1.0入门笔记之---安装部署
    php下载大文件
    【转】Pyhton 单行、多行注释符号使用方法及规范
    window10系统下使用python版本实现mysql查询
    Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A
    【Visual Studio】 使用EF、 Linq2Sql快速创建数据交互层(一)
    【OPCAutomation】 使用OPCAutomation实现对OPC数据的访问
  • 原文地址:https://www.cnblogs.com/daijun123/p/16717957.html
Copyright © 2020-2023  润新知