最全的李慧芹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 MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...