最全的李慧芹APUE-文件IO筆記

来源:https://www.cnblogs.com/lietoast/archive/2023/09/23/17725483.html
-Advertisement-
Play Games

1. 定義通用返回結果類 ​ 定義ResultVO類,作返回給前端的對象結構,主要有4個欄位 code : 錯誤碼 data : 內容 message : 消息 description : 具體描述 import lombok.Data; import java.io.Serializable; / ...


文件 IO / 系統調用 IO

: 李慧芹老師的視頻課程請點這裡, 本篇為系統IO一章的筆記, 課上提到過的內容基本都會包含, 上一章為標準IO

文件描述符(fd)是在文件IO中貫穿始終的類型

本節內容

  1. 文件IO操作: open, close, read, write, lseek

  2. 文件IO與標準IO的區別

  3. IO的效率問題

  4. 文件共用問題

  5. 原子操作

  6. 程式中的重定向: dup, dup2

  7. 同步: sync, fsync, fdatasync

  8. 管家: fcntl(), ioctl()

FILE 與 fd

stdio中, 可以調用fopen()(依賴於sysio的open())獲得FILE結構體(結構如下表)指針:

欄位 說明
pos 文件位置
fd 文件描述符
... ...

磁碟上的每個文件有唯一的標識inode, 而每次調用open()時, 都會產生一個結構體, 該結構體包含了要打開的文件的所有信息(包括inode)

進程維護了一個數組(大小為1024), 存儲所有通過open()產生的結構體的首地址

文件描述符fd表示了某一結構體的首地址在上述數組中的下標位置, 因此, fd實際上就是int類型變數!

image

fd優先使用當前可用範圍內下標值最小的數組位置

設進程維護的數組為A, close()函數就相當於:

free(A[fd]);
A[fd] = NULL;

當發生如下圖所示情況(數組中的兩個指針同時指向同一個結構體)時:

image

close(4)並不會導致A[6]變為野指針, 這是由於結構體中包含引用計數器(counter)欄位, 只有當該欄位變為0時, 該結構體占用的空間才會被釋放

打開與關閉操作

  • 打開
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

int creat(const char *pathname, mode_t mode);

參數flags是一個點陣圖, 必須包含一個狀態選項:

模式 許可權
O_RDONLY 只讀
O_WRONLY 只寫
O_RDWR 讀寫

可以包含零或多個創建選項:

模式 說明
O_CREAT 有則情況, 無則創建
O_EXCL 必須打開一個新文件
O_APPEND 追加
O_TRUNC 截斷
O_ASYNC 信號驅動IO
O_DIRECT 最小化cache作用
O_DIRECTORY 必須打開目錄
O_LARGEFILE 打開的是大文件(該方法不如設置_FILE_OFFSET_BITS為64)
O_NOATIME 不需要更新文件最後讀的時間(節省文件更新時間)
O_NOFOLLOW 如果文件是符號鏈接, 那麼不打開它
O_NONBLOCK 非阻塞
O_SYNC 同步

cache vs buffer:

cache代表"讀的緩衝區"

buffer代表"寫的緩衝區"

open()creat()執行成功時返迴文件描述符, 失敗則返回-1

下標為fopen()的參數modeopen()的參數flags的比對:

mode flags
r O_RDONLY
r+ O_RDWR
w O_WRONLY|O_CREAT|O_TRUNC
w+ O_RDWR|O_TRUNC|O_CREAT

flags & O_CREAT != 0時, 則open()必須傳入mode, 創建的文件的許可權服從:

mode & ~umask
  • 關閉
#include <unistd.h>

int close(int fd);

成功返回0, 失敗返回-1; 一般認為close()不會失敗, 因此極少校驗返回值

讀寫與定位操作

#include <unistd.h>

// 嘗試從fd中讀取count個位元組到buf中
// 如果成功, 返回讀到的位元組數, 讀到文件尾, 返回0, 失敗返回-1
ssize_t read(int fd, void *buf, size_t count);
// 如果成功, 返回寫入的位元組數(返回0表示未寫入任何內容), 失敗返回-1
// 且會設置errno
ssize_t write(int fd, const void *fd, size_t count);
  • 定位
#include <sys/types.h>
#include <unistd.h>

// 從whence位置偏移offset個位元組
// whence選項: SEEK_SET(文件首), SEEK_CUR(當前位置), SEEK_END(文件尾)
off_t lseek(int fd, off_t offset, int whence);

重寫 mycpy

mycpy.c:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFSIZE 1024

int main(int argc, char **argv)
{
        int sfd, dfd;
        char buf[BUFSIZE];
        ssize_t rs, ws, pos;
        int flag = 1;

        if (argc < 3)
        {
                fprintf(stderr, "Usage: %s <src_file> <dst_file>\n", argv[0]);
                exit(1);
        }

        sfd = open(argv[1], O_RDONLY);
        if (sfd < 0)
        {
                perror("open()");
                exit(1);
        }
        dfd = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0600);
        if (dfd < 0)
        {
                close(sfd);
                perror("open()");
                exit(1);
        }

        while (1)
        {
                rs = read(sfd, buf, BUFSIZE);
                if (rs < 0)
                {
                        perror("read()");
                        break;
                }
                if (rs == 0)
                        break;

                pos = 0;
                while (rs > 0)
                {
                        ws = write(dfd, buf+pos, rs);
                        if (ws < 0)
                        {
                                perror("write()");
                                flag = 0;
                                break;
                        }
                        rs -= ws;
                        pos += ws;
                }
                if (!flag)
                        break;
        }

        close(dfd);
        close(sfd);

        exit(0);
}

Makefile:

CFLAGS+=-D_FILE_OFFSET_BITS=64 -Wall

執行以下命令:

make mycpy
./mycpy /etc/services ./out
diff /etc/services ./out

如果什麼也沒輸出, 則說明mycpy已正確執行

系統 IO 與標準 IO 比較

區別:

系統調用IO: 每調用一次, 會從user態切換到kernel態執行一次(實時性好)

標準IO: 數據先寫入緩衝區, 在某一事件(如: 強制刷新/緩衝區滿/換行, 詳見上一章對行緩衝/全緩衝/無緩衝的描述)發生時才會將緩衝區內數據寫入文件/設備(吞吐量大)

提醒:

fileno()可以拿出FILE *fd欄位

fdopen()可以將fd封裝到FILE *

但是, 絕不能將標準IO與系統調用IO混用!

絕大多數情況下, FILE結構體中的pos欄位與存儲文件所有信息的結構體的pos欄位值不相等! 如:

FILE *fp;

fputc(fp) // pos ++
fputc(fp) // pos ++

只代表FILE中的pos加二, 文件結構體的pos沒有增加, 該pos只會在各種事件後發生改變; 因此, 標準IO與系統調用IO混用基本就會導致錯誤, 如ab.c:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
    putchar('a');
    write(1, "b", 1);

    putchar('a');
    write(1, "b", 1);

    putchar('a');
    write(1, "b", 1);

    exit(0);
}

該程式會列印"bbbaaa", 可以用strace命令跟蹤系統調用IO的發生:

strace ./ab

該命令輸出的最後幾行表示系統調用IO發生的過程:

write(1, "b", 1b)                        = 1
write(1, "b", 1b)                        = 1
write(1, "b", 1b)                        = 1
write(1, "aaa", 3aaa)                      = 3
exit_group(0)                           = ?
+++ exited with 0 +++

IO 效率問題

在重寫mycpy的案例中, BUFSIZE為$2^n$, 問n為多少時, 效率最高

程式:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <math.h>

long long BUFSIZE;

int main(int argc, char **argv)
{
        int sfd, dfd;
        char *buf;
        ssize_t rs, ws, pos;
        int flag = 1, n = 0;

        if (argc < 4)
        {
                fprintf(stderr, "Usage: %s <src_file> <dst_file> <n>\n", argv[0]);
                exit(1);
        }

        n = atoi(argv[3]);
        if (n <= 0)
                exit(1);
        BUFSIZE = 1LL << (n-1);

        buf = malloc(BUFSIZE * sizeof(char));
        if (buf == NULL)
        {
                perror("malloc()");
                exit(1);
        }

        sfd = open(argv[1], O_RDONLY);
        if (sfd < 0)
        {
                perror("open()");
                exit(1);
        }

        while (1)
        {
                rs = read(sfd, buf, BUFSIZE);
                if (rs < 0)
                {
                        perror("read()");
                        break;
                }
                if (rs == 0)
                        break;

                pos = 0;
                while (rs > 0)
                {
                        ws = write(dfd, buf+pos, rs);
                        if (ws < 0)
                        {
                                perror("write()");
                                flag = 0;
                                break;
                        }
                        rs -= ws;
                        pos += ws;
                }
                if (!flag)
                        break;
        }

        close(dfd);
        close(sfd);

        exit(0);
}

測試該程式的腳本:

#!/bin/bash

for((i=1;i<=25;i++))
do
        echo $i;
        time ./mycpy ~/dance.mp4 ./dance.mp4 $i;
        diff ~/dance.mp4 ./dance.mp4;
        rm -f ./dance.mp4;
done

運行結果:

image

經過測試(測試環境: 操作系統: Ubuntu22 CPU: 64位ARM架構 記憶體: 2G), BUFSIZE在64~256k大小時, 效率達到最高, 預設情況下, 16M的BUFFSIZE不會引發段錯誤

文件截斷

#include <unistd.h>
#include <sys/types.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

將一個文件截斷到length長度

作業

不打開臨時文件的情況下, 刪除文件的某一行:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>

#include "mygetline.h"

int main(int argc, char **argv)
{
        char *linebuf = NULL;
        size_t bufsize = 0;
        FILE *wfp, *rfp;

        int curline = 1, l;

        if (argc < 3)
        {
                fprintf(stderr, "Usage: %s <file name> <line number>\n", argv[0]);
                exit(1);
        }

        rfp = fopen(argv[1], "r");
        if (rfp == NULL)
        {
                perror("open file");
                exit(1);
        }
        wfp = fopen(argv[1], "r+");
        if (wfp == NULL)
        {
                perror("open file");
                fclose(rfp);
                exit(1);
        }

        l = atoi(argv[2]);
        if (l <= 0)
        {
                fprintf(stderr, "illegal line number %s: %s", argv[2], strerror(errno));
                fclose(wfp);
                fclose(rfp);
                exit(1);
        }

        while (mygetline(&linebuf, &bufsize, rfp) >= 0)
        {
                if (curline != l)
                {
                        fputs(linebuf, wfp);
                        fputc((int)'\n', wfp);
                }
                curline ++;
        }

        truncate(argv[1], ftell(wfp));

        mygetline_free(&linebuf);

        fclose(wfp);
        fclose(rfp);

        exit(0);
}

要瞭解mygetline()mygetline_free(), 請查看上一節內容

原子操作

原子操作: 不可分割的操作

原子操作的作用: 解決競爭和衝突

dup

舉例說明: 下麵有代碼dup.c, 要在// 代碼:一行後, 多行註釋前編寫一些代碼, 使得hello!不被列印到終端上, 而是列印到/tmp/out文件中:

#include <stdlib.h>
#include <stdio.h>

#define FNAME "/tmp/out"

int main()
{
        // 代碼:

        /***********************/
        puts("hello!");

        exit(0);
}

可以做如下修改:

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FNAME "/tmp/out"

int main()
{
        // 代碼:
        int fd;

        close(1); // 關閉 stdout

        // 打開/tmp/out, 使其占用進程維護的stream數組的下標1的位置
        // 該位置原先由stdout占用
        fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600);
        if (fd < 0)
        {
                perror("open()");
                exit(1);
        }

        /***********************/
        puts("hello!");

        exit(0);
}

執行以下命令:

make dup
./dup
cat /tmp/out

引入dup():

#include <unistd.h>

// 將oldfd複製到stream數組下標最小的可用位置上
int dup(int oldfd);

有了dup()後, 可以把上述代碼修改為:

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FNAME "/tmp/out"

int main()
{
        // 代碼:
        int fd;

        // 打開/tmp/out, 使其占用進程維護的stream數組的下標1的位置
        // 該位置原先由stdout占用
        fd = open(FNAME, O_WRONLY|O_CREAT|O_TRUNC, 0600);
        if (fd < 0)
        {
                perror("open()");
                exit(1);
        }

        close(1); // 關閉stdout
        dup(fd);  // 將fd複製到1號
        // 當前stream數組下標4,1位置的指針指向同一個文件結構體

        /***********************/
        puts("hello!");

        exit(0);
}

然而, 在多線程場景中, 當前線程可能在執行close(1)後, CPU時間片結束, 其他線程打開的文件描述符會占據1下標位置(操作不原子)

dup2

為瞭解決dup()操作不原子的問題, 有了dup2():

#include <unistd.h>

int dup2(int oldfd, int newfd);

dup2()會將newfd複製到oldfd的位置上, 如果oldfd已被占用, 則首先關閉oldfd; 如果newfd == oldfd, 那麼dup2()什麼也不做, 直接返回newfd

因此, close(1); dup(fd);可被重寫為:

dup2(fd, 1);

if (fd != 1)
    close(fd);

sync

將buffer和cache同步到磁碟上:

#include <unistd.h>

void sync(void);

在解除設備掛載時, 將還沒寫入磁碟的數據儘快寫入磁碟

可以使用fsyncfdatasync指定寫入數據的位置:

#include <unistd.h>

int fsync(int fd);

int fdatasync(int fd);

fcntl

#include <unistd.h>
#include <fcntl.h>

// 對fd執行cmd命令
int fcntl(int fd, int cmd, .../* arg */);

具體有哪些命令詳見man fcntl, 文件描述符所變的魔術基本都來源於該函數(比如: dup()dup2()就是封裝好的fcntl)

ioctl

設備相關的內容

#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);

/dev/fd

虛目錄, 顯示的是當前進程(如同照鏡子, 誰去查看/dev/fd, 就會看到誰的文件描述符的信息)的文件描述符信息, 如:

ls -l /dev/fd

該命令會輸出ls命令實現所用到的文件描述符的信息:

lrwxrwxrwx 1 root root 13 Sep 14 08:27 /dev/fd -> /proc/self/fd

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 第一個vue-cli程式的準備工作 什麼是vue-cli? vue-cli是官方提供的一個腳手架工具,用於快速生成一個vue項目模板。 預先定義好的目錄結構和代碼,就好比咱們在創建maven項目時可以選擇創建一個骨架項目,這個骨架項目就是腳手架,有利於我們更加快速的開發。 環境準備(所涉及到的命令都 ...
  • 背景 在做管理台項目時,我們會經常使用到表單+表格+彈窗表單的組合,以完成對數據的增、刪、查、改。 在vue2+elementui項目中,使用彈窗dialog+表單form,實現對數據的添加和修改。 每次關閉彈窗時,使用resetFields方法對錶單進行重置。 下一次打開彈窗時, 如果是添加數據, ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 問題描述 最近遇到一個有意思的問題,項目中有一個地方,點擊需要跳轉到一個新的功能變數名稱地址 筆者使用a標簽做跳轉,跳是跳過去了,可是跳過去以後,反而打不開了,顯示403佛伯樂 蛤? 大致這樣的代碼: <a href="http://abcdef ...
  • 本文來盤點微軟開源的十大前端項目,這些項目在 Github 上獲得了超過 45 萬 Star! Visual Studio Code Visual Studio Code 是一款由微軟開發的開源的代碼編輯器。它支持多種編程語言,如C、C++、C#、Python、JavaScript 和 TypeSc ...
  • 1.原始值和引用值 ECMScript變數包含兩種不同類型是數據:原始值和引用值。 原始值:最簡單的數據。有6中原始值:Undefined、Null、Boolean、Number、String和Symbol。原始值是按值訪問。 引用值:由多個值構成的對象。三大引用類型:1.object 2.Arra ...
  • 1、案例:猜數字 設置一個1-10之間的隨機數,然後輸入進行猜數字,猜的大了怎麼樣、猜的小了怎麼樣、猜對了怎麼樣 知識點:設置隨機數 、if判斷 、while迴圈 寫題思路: 1.設置彈框提出問題 2.定義一個隨機數0-10的數組 3.if 判斷 取值的範圍,在其範圍內反饋的結果 4.while迴圈 ...
  • 搭建後臺管理系統模板 2.1項目初始化 今天來帶大家從0開始搭建一個vue3版本的後臺管理系統。一個項目要有統一的規範,需要使用eslint+stylelint+prettier來對我們的代碼質量做檢測和修複,需要使用husky來做commit攔截,需要使用commitlint來統一提交規範,需要使 ...
  • Uber公司技術棧介紹 Uber(Uber Technologies,Inc.)中文譯作“優步”,是一家美國矽谷的科技公司。Uber在2009年,由加利福尼亞大學洛杉磯分校輟學生特拉維斯·卡蘭尼克和好友加勒特·坎普(Garrett Camp)創立。因旗下同名打車APP而名聲大噪。Uber已經進入中國 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 微服務架構已經成為搭建高效、可擴展系統的關鍵技術之一,然而,現有許多微服務框架往往過於複雜,使得我們普通開發者難以快速上手並體驗到微服務帶了的便利。為瞭解決這一問題,於是作者精心打造了一款最接地氣的 .NET 微服務框架,幫助我們輕鬆構建和管理微服務應用。 本框架不僅支持 Consul 服務註 ...
  • 先看一下效果吧: 如果不會寫動畫或者懶得寫動畫,就直接交給Blend來做吧; 其實Blend操作起來很簡單,有點類似於在操作PS,我們只需要設置關鍵幀,滑鼠點來點去就可以了,Blend會自動幫我們生成我們想要的動畫效果. 第一步:要創建一個空的WPF項目 第二步:右鍵我們的項目,在最下方有一個,在B ...
  • Prism:框架介紹與安裝 什麼是Prism? Prism是一個用於在 WPF、Xamarin Form、Uno 平臺和 WinUI 中構建鬆散耦合、可維護和可測試的 XAML 應用程式框架 Github https://github.com/PrismLibrary/Prism NuGet htt ...
  • 在WPF中,屏幕上的所有內容,都是通過畫筆(Brush)畫上去的。如按鈕的背景色,邊框,文本框的前景和形狀填充。藉助畫筆,可以繪製頁面上的所有UI對象。不同畫筆具有不同類型的輸出( 如:某些畫筆使用純色繪製區域,其他畫筆使用漸變、圖案、圖像或繪圖)。 ...
  • 前言 嗨,大家好!推薦一個基於 .NET 8 的高併發微服務電商系統,涵蓋了商品、訂單、會員、服務、財務等50多種實用功能。 項目不僅使用了 .NET 8 的最新特性,還集成了AutoFac、DotLiquid、HangFire、Nlog、Jwt、LayUIAdmin、SqlSugar、MySQL、 ...
  • 本文主要介紹攝像頭(相機)如何採集數據,用於類似攝像頭本地顯示軟體,以及流媒體數據傳輸場景如傳屏、視訊會議等。 攝像頭採集有多種方案,如AForge.NET、WPFMediaKit、OpenCvSharp、EmguCv、DirectShow.NET、MediaCaptre(UWP),網上一些文章以及 ...
  • 前言 Seal-Report 是一款.NET 開源報表工具,擁有 1.4K Star。它提供了一個完整的框架,使用 C# 編寫,最新的版本採用的是 .NET 8.0 。 它能夠高效地從各種資料庫或 NoSQL 數據源生成日常報表,並支持執行複雜的報表任務。 其簡單易用的安裝過程和直觀的設計界面,我們 ...
  • 背景需求: 系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。 原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書 ...
  • 本文介紹下電腦設備關機的情況下如何通過網路喚醒設備,之前電源S狀態 電腦Power電源狀態- 唐宋元明清2188 - 博客園 (cnblogs.com) 有介紹過遠程喚醒設備,後面這倆天瞭解多了點所以單獨加個隨筆 設備關機的情況下,使用網路喚醒的前提條件: 1. 被喚醒設備需要支持這WakeOnL ...
  • 前言 大家好,推薦一個.NET 8.0 為核心,結合前端 Vue 框架,實現了前後端完全分離的設計理念。它不僅提供了強大的基礎功能支持,如許可權管理、代碼生成器等,還通過採用主流技術和最佳實踐,顯著降低了開發難度,加快了項目交付速度。 如果你需要一個高效的開發解決方案,本框架能幫助大家輕鬆應對挑戰,實 ...