• 2021-2022-1-diocs-Linux系统编程第四周学习笔记


    20191218 2021-2022-1-diocs-文件操作&使用系统调用进行文件操作(第四周学习笔记)

    思维导图

    • 第七章思维导图

    • 第八章思维导图

    知识点总结

    文件操作级别

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

    • (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;
    } 
    
    

    其中,fd描述符和FILE*是有区别的。通过查阅资料,我了解到

    图中文件描述符即为文件描述符数组的下标。习惯上,标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1,标准错误(standard error)是 2。当打开一个新的文件,文件描述符将会是3。
    文件描述符的分配规律:从当前未使用的最小的整数开始分配;
    文件描述符的缺点:不能移植到UNIX以外的系统上去,也不直观。
    简单归纳:fd只是一个整数,在open时产生,起到一个索引的作用,进程通过PCB中的文件描述符表找到该fd所指向的文件指针file。
    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):
    .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;
    }
    

    低级别文件操作

    • 分区
      • 主引导记录(MBR)
        一个块存储设备,如硬盘、U盘、SD卡等,可以分为几个逻辑单元,称为分区。各分区均可以格式化为特定的文件系统,也可以安装在不同的操作系统上。大多数引导程序,如GRUB、LILO等,都可以配置为从不同的分区引导不同的操作系统。分区表位于第一个扇区的字节偏移446 (0x1BE)处,该扇区称为设备的主引导记录(MBR)。表有4个条目,每个条目由一个16字节的分区结构体定义,即:
    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有意义。

    虚拟磁盘映像创建

    在磁盘映像文件上运行fdisk

    帮助文档说明

    进行分区操作并同步

    查看文件系统十六进制唯一值

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

    尝试在OpenEuler上创建文件系统

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

    格式化后的磁盘应是只包含根目录的空文件系统。但是,Linux的mkfs始终会在根目录下创建一个默认的lost+found目录。完成mkfs之后,设备就可以使用了。

    在Linux中,还不能访问新的文件系统。它必须挂载到根文件系统中的现有目录中。/mnt目录通常用于挂载其他文件系统。由于虚拟文件系统不是真正的设备,它们必须作为循环设备挂载。

    发现vdisk还不能访问

    • 挂载分区
      为解决之前vdisk不能访问的问题,现在进行挂载分区
      查看losetup的用法

      创建分区P1

      创建一个循环设备

    格式化/dev/loop1

    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)
    
  • 相关阅读:
    shellshock溢出攻击
    内核编译与系统调用
    模块与系统调用
    20199315《Linux内核原理与分析》第十二周作业
    20199315《Linux内核原理与分析》第十一周作业
    Linux下的静态链接库和动态链接库
    2019-2020-1 20199315《Linux内核原理与分析》第九周作业
    2019-2020-1 20199315 《Linux内核原理与分析》 第八周作业
    2019-2020-1 20199315《Linux内核原理与分析》第七周作业
    2019-2020-1 20199315《Linux内核原理与分析》第六周作业
  • 原文地址:https://www.cnblogs.com/20191218tangqiheng/p/15357806.html
Copyright © 2020-2023  润新知