1. 程式人生 > 其它 >2021-2022-1-diocs-Linux系統程式設計第四周學習筆記

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", 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)