欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Linux文件重定向&&文件緩沖區(qū)解讀

 更新時間:2025年02月06日 14:25:45   作者:阿赭ochre  
文章介紹了C語言中的文件輸入輸出操作,包括標準庫函數(shù)、系統(tǒng)調(diào)用接口、文件描述符、文件重定向、文件緩沖區(qū)等概念,并通過代碼示例進行了詳細說明

一、C文件接口

stdin & stdout & stderr

C默認會打開三個輸入輸出流,分別是stdin, stdout, stderr

仔細觀察發(fā)現(xiàn),這三個流的類型都是FILE*, fopen返回值類型,文件指針

  • fwrite向指定文件寫入內(nèi)容
  • fread從指定文件讀取內(nèi)容

fprintf根據(jù)指定的format(格式)發(fā)送信息(參數(shù))到由stream(流)指定的文件,fprintf可以使得信息寫入到指定的文件

調(diào)用C文件接口,以w的形式打開,若文件不存在,會在當前目錄下新建文件,當前路徑就是進程的當前路徑cwd,如果改變了進程的cwd就可以在其他目錄下新建文件

w寫入前都會對文件進行清空,a在文件結(jié)尾追加寫,兩者都是寫入

C默認打開的三個輸入輸出流不是C語言的特性,而是操作系統(tǒng)的特性,進程會默認打開鍵盤,顯示器,顯示器

二、系統(tǒng)文件I/O

2.1認識系統(tǒng)文件I/O

  • 文件其實是在磁盤上的,磁盤是外設(shè),對文件進行訪問,就是對硬件進行訪問
  • 任何用戶都不能直接訪問硬件的數(shù)據(jù) ,而必須通過系統(tǒng)調(diào)用
  • 幾乎所有的庫只要是訪問硬件設(shè)備,必須封裝系統(tǒng)調(diào)用
  • C文件接口就是一種庫函數(shù),是對系統(tǒng)調(diào)用的封裝

2.2系統(tǒng)文件I/O

open( )

#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);
  • pathname: 要打開或創(chuàng)建的目標文件
  • flags: 打開文件時,可以傳入多個參數(shù)選項,用下面的一個或者多個常量進行 “ 或 ” 運算,構(gòu)成 flags

參數(shù) :

  • O_RDONLY: 只讀打開
  • O_WRONLY: 只寫打開
  • O_RDWR : 讀寫打開
  • O_CREAT : 若文件不存在,則創(chuàng)建它,需要使用 mode(例0666) 選項,來指明新文件的訪問權(quán)限
  • O_APPEND: 追加寫
  • O_TRUNC: 每一次寫入都清空文件

返回值:

  • 成功:新打開的文件描述符
  • 失?。?1

代碼示例:

umask( )可以用來設(shè)置掩碼的值

比特方位式的標志位傳遞方式通過位運算來實現(xiàn)

2.3系統(tǒng)調(diào)用和庫函數(shù)

上面的 fopen fclose fread fwrite 都是C標準庫當中的函數(shù),我們稱之為庫函數(shù)(libc)

open close read write lseek 都屬于系統(tǒng)提供的接口,稱之為系統(tǒng)調(diào)用接口

可以認為,f#系列的函數(shù),都是對系統(tǒng)調(diào)用的封裝,方便二次開發(fā)。

2.4open( )的返回值--文件描述符

Linux進程默認情況下會有3個缺省打開的文件描述符,分別是標準輸入0, 標準輸出1, 標準錯誤2

0,1,2對應(yīng)的物理設(shè)備一般是:鍵盤,顯示器,顯示器

linux下文件描述符的分配規(guī)則:從0下標開始,尋找最小沒有被使用過的數(shù)組位置,它的下標就是新文件的文件描述符--結(jié)合訪問文件的本質(zhì)來說明

代碼示例:

  • 因為C庫函數(shù)是對系統(tǒng)接口的封裝,系統(tǒng)接口下只認識文件描述符,所以C庫自己提供的FILE結(jié)構(gòu)體中必定也包含著文件描述符,用_fileno記錄

如果關(guān)閉了1號文件,printf就無法向1號文件(顯示器)寫入了 ,但可以向3號文件寫入,所以我們打印就只能看到n的值

2.5訪問文件的本質(zhì)

任何一個被打開的文件在內(nèi)存中都要被管理起來,操作系統(tǒng)如果管理被打開的文件?----先描述再組織

當我們打開文件時,操作系統(tǒng)在內(nèi)存中要創(chuàng)建相應(yīng)的數(shù)據(jù)結(jié)構(gòu)來描述目標文件--file結(jié)構(gòu)體(直接或間接包含如下屬性:文件的基本屬性,文件的內(nèi)核緩沖區(qū)信息,引用計數(shù),struct file*next,在磁盤的什么位置),表示一個已經(jīng)打開的文件對象而進程執(zhí)行open系統(tǒng)調(diào)用,所以必須讓進程和文件關(guān)聯(lián)起來,每個進程都有一個指針*files, 指向一張表files_struct,該表最重要的部分就是包涵一個指針數(shù)組,每個元素都是一個指向打開文件的指針!

所以,本質(zhì)上,文件描述符就是該數(shù)組的下標,只要拿著文件描述符,就可以找到對應(yīng)的文件

  • 當一個進程open()一個文件時,操作系統(tǒng)會在struct_file的指針數(shù)組中從下標為0的地方在開始尋找一個沒有被使用過的數(shù)組位置,填入要打開文件的struct file*,再將數(shù)組下標返回給open( )調(diào)用,作為該文件的文件描述符fd
  • 當一個進程要向某個文件寫入的時候,操作系統(tǒng)只認識文件描述符,根據(jù)文件描述符找到對應(yīng)的數(shù)組下標,根據(jù)數(shù)組下標位置里的內(nèi)容找到所對應(yīng)的文件再寫入
  • close關(guān)閉文件本質(zhì)上是清空對應(yīng)fd數(shù)組下標位置的內(nèi)容,再將該fd內(nèi)容指向的文件的引用計數(shù)--,引用計數(shù)為0才釋放銷毀相應(yīng)的struct_ file

三、文件重定向

3.1認識文件重定向

關(guān)閉1號文件再打開新文件 ,向1號文件寫入內(nèi)容

可以看到,原來要向1號文件(顯示屏)打印的信息,被寫入到了新打開的文件,其中,fd=1。這種現(xiàn)象叫做輸出重定向

常見的重定向有:>輸出重定向, >>追加重定向, <輸入重定向

追加重定向

輸入重定向

3.2文件重定向的本質(zhì)

  • 文件重定向的本質(zhì):將1號文件描述符在指針數(shù)組中對應(yīng)位置的內(nèi)容,用log.txt文件描述符在指針數(shù)組中對應(yīng)位置的內(nèi)容進行覆蓋,原本數(shù)組內(nèi)的指向1號文件的文件指針就被替換成log.txt的文件指針,當我們再向1號文件描述符寫入內(nèi)容的時候,就是向文件指針指向的log.txt內(nèi)寫入而不再寫到標準輸出
  • dup2系統(tǒng)調(diào)用
  • 原本向顯示屏打印的內(nèi)容被寫入到log.txt文件中

3.3在shell中添加重定向功能

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<ctype.h>
#include<fcntl.h>

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGV_SIZE 32

#define NONE -1
#define IN_RDIR     0
#define OUT_RDIR    1
#define APPEND_RDIR 2

extern char** environ;
char commandline[LINE_SIZE];
char* argv[ARGV_SIZE];
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];

int lastcode=0;
int quit=0;

char *rdirfilename = NULL;
int rdir = NONE;

const char* getuser()
{
    return getenv("USER");
}

const char* gethostname()
{
    return getenv("HOSTNAME");
}

void getpwd()
{
    getcwd(pwd,sizeof(pwd));
}

void check_redir(char *cmd)
{

    // ls -al -n
    // ls -al -n >/</>> filename.txt
    char *pos = cmd;
    while(*pos)
    {
        if(*pos == '>')
        {
            if(*(pos+1) == '>'){
                *pos++ = '\0';
                *pos++ = '\0';
                while(isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir=APPEND_RDIR;
                break;
            }
            else{
                *pos = '\0';
                pos++;
                while(isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir=OUT_RDIR;
                break;
            }
        }
        else if(*pos == '<')
        {
            *pos = '\0'; // ls -a -l -n < filename.txt
            pos++;
            while(isspace(*pos)) pos++;
            rdirfilename = pos;
            rdir=IN_RDIR;
            break;
        }
        else{
            //do nothing
        }
        pos++;
    }
}

void interact(char* cline,int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getuser(),gethostname(),pwd);
    char* s=fgets(cline,size,stdin);
    assert(s);
    (void)s;
    cline[strlen(cline)-1]='\0';

    //printf("echo : %s",cline);
    //ls -a -l > myfile.txt
    check_redir(cline);
}

int splitstring(char cline[],char* _argv[])
{
    int i=0;
    _argv[i++]=strtok(cline,DELIM);
    while(_argv[i++]=strtok(NULL,DELIM));

    return i-1;
}

void normalexcute(char* _argv[])
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        //continue;
        return ;
    }
    else if(id==0)
    {

        int fd = 0;

        // 后面我們做了重定向的工作,后面我們在進行程序替換的時候,難道不影響嗎???
        if(rdir == IN_RDIR)
        {
            fd = open(rdirfilename, O_RDONLY);
            dup2(fd, 0);
        }
        else if(rdir == OUT_RDIR)
        {
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if(rdir == APPEND_RDIR)
        {
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND, 0666);
            dup2(fd, 1);
        }
        //子進程執(zhí)行指令
        //execvpe(argv[0],argv,environ);
        execvp(argv[0],argv);
    }
    else{
        int status=0;
        pid_t rid=waitpid(id,&status,0);
        if(rid==id)
        {
            lastcode=WEXITSTATUS(status);
        }
    }
}

int buildcommand(char* _argv[],int _argc)
{
    if(_argc==2&&strcmp(_argv[0],"cd")==0)
    {
        chdir(_argv[1]);
        getpwd();
        sprintf(getenv("PWD"),"%s",pwd);
        return 1;
    }
    else if(_argc==2&&strcmp(_argv[0],"export")==0)
    {
        strcpy(myenv,_argv[1]);
        putenv(myenv);
        return 1;
    }
    else if(_argc==2&&strcmp(_argv[0],"echo")==0)
    {
        if(strcmp(_argv[1],"$?")==0)
        {
            printf("%d\n",lastcode);
            lastcode=0;
        }
        else if(*_argv[1]=='$')
        {
            char* s=getenv(_argv[1]+1);
            if(s) printf("%s\n",s);
        }
        else{
            printf("%s\n",_argv[1]);
        }

        return 1;

    }

    //特殊處理ls
    if(_argc==2&&strcmp(_argv[0],"ls")==0)
    {
        _argv[_argc++]="--color";
        _argv[_argc]=NULL;
    }

    return 0;

}

int main()
{
    while(!quit)
    {
        //交互問題,獲得命令行參數(shù)
        interact(commandline,sizeof commandline);

        //字符串分割,解析命令行參數(shù)
        int argc = splitstring(commandline,argv);
        if(argc==0) continue;

        //指令的判斷
        int n=buildcommand(argv,argc);        

        //普通指令的執(zhí)行
        if(!n)normalexcute(argv);
    }
        return 0;
}

  • 進程歷史打開的文件以及文件的重定向關(guān)系,并不會被程序替換所影響??!進程程序替換之后影響頁表右邊的物理地址所指向的內(nèi)容,虛擬地址并左邊的部分并不會受到影響
  • 程序替換并不會影響文件訪問

3.4stdout和stderr

  • stdout和stderr對應(yīng)的硬件設(shè)備都是顯示屏,訪問的都是同一個文件(引用計數(shù))
  • 在重定向的時候,默認只對stdout的fd進行重定向

代碼示例:

如果對1號和2號文件都要進行重定向呢?

示例:./mytest 1> log.txt 2>err.txt

示例:./mytest > log.txt 2>&1

3.5如何理解“linux下一切皆文件” --以對外設(shè)的IO操作為例

  • 不同的外設(shè)在進行IO操作時都有自己對應(yīng)的讀寫方法,放在struct device里
  • 這些讀寫方法如何被找到?--由struct operation_func來對讀寫方法進行管理,該結(jié)構(gòu)體里存在指向?qū)?yīng)讀寫法的函數(shù)指針
  • 如何找到struct operation_func?--由struct file來對struct operation_func進行管理,file結(jié)構(gòu)體存在指向struct operation_func的指針,基于struct file之上的被稱為虛擬文件系統(tǒng)(VFS)--一切皆文件
  • 當我們打開一個文件的時候,通過進程的pcb數(shù)據(jù)結(jié)構(gòu)找到struct struct_file,操作系統(tǒng)根據(jù)文件描述符的分配規(guī)則,在struct struct_file的指針數(shù)組中為該文件分配一個fd;當我們要訪問一個外設(shè)的時候,根據(jù)該外設(shè)文件fd對應(yīng)的數(shù)組下標內(nèi)容找到該外設(shè)文件的struct file,根據(jù)file結(jié)構(gòu)體找到對應(yīng)的struct operation_func,由于訪問的外設(shè)的不同,在struct operation_func中根據(jù)函數(shù)指針找到對應(yīng)的讀寫方法,就可以對外設(shè)進行訪問了

四、文件緩沖區(qū)

4.1認識FILE

因為IO相關(guān)函數(shù)與系統(tǒng)調(diào)用接口對應(yīng),并且?guī)旌瘮?shù)封裝系統(tǒng)調(diào)用,所以本質(zhì)上,訪問文件都是通過fd訪問的

所以C庫當中的FILE結(jié)構(gòu)體內(nèi)部,必定封裝了fd

4.2文件緩沖區(qū)引入

  • 對比有無fork( )的代碼

我們發(fā)現(xiàn) printf 和 fwrite (庫函數(shù))都輸出了 2 次,而 write 只輸出了一次(系統(tǒng)調(diào)用),為什么呢?肯定和 fork有關(guān)!

再來驗證一個現(xiàn)象:

不加'\n'并且在最后close(1)

代碼運行的結(jié)果是:只有系統(tǒng)調(diào)用接口寫入的內(nèi)容被打印出來了

加上'\n',結(jié)果又不一樣了

4.3文件緩沖區(qū)的原理

C語言會提供一個緩沖區(qū),我們調(diào)用C文件接口寫入的數(shù)據(jù)會被暫存在這個緩沖區(qū)內(nèi),緩沖區(qū)的刷新方式有三種:

  1. 無緩沖:直接刷新,一般我們使用的fflush( )就是無緩沖的刷新方式
  2. 行緩沖:遇到'\n'才刷新,一般對應(yīng)顯示器
  3. 全緩沖:緩沖區(qū)滿了才刷新,一般對應(yīng)普通文件的寫入
  4. 特殊說明:進程結(jié)束的時候會自動刷新緩沖區(qū)

在操作系統(tǒng)的內(nèi)核中也存在一個內(nèi)核級別的緩沖區(qū),目前認為,只要將數(shù)據(jù)刷新到了內(nèi)核,數(shù)據(jù)就可以到硬件了,內(nèi)核緩沖區(qū)也有自己的刷新方式

為什么要有C層面的緩沖區(qū)?

  1. 用戶不需要一步一步將數(shù)據(jù)寫入到硬件中,而是可以直接調(diào)用C庫為我們提供的讀寫方法,將數(shù)據(jù)交給庫函數(shù)來處理,解決用戶的效率問題
  2. 我們真正存到文件里的都是一個個的字符,調(diào)用C庫的讀寫方法,可以在放入緩沖區(qū)之前將我們的數(shù)據(jù)格式化成字符串,再刷新到內(nèi)核中進而寫入文件,C層面的緩沖區(qū)可以配合格式化的工作

C為我們提供的緩沖區(qū)在FILE結(jié)構(gòu)體里,F(xiàn)ILE里面有相關(guān)緩沖區(qū)的字段和維護信息,F(xiàn)ILE屬于用戶層面,而不屬于操作系統(tǒng)

文件寫入的過程:

  1. 首先,在文件寫入之前,進程會打開一個文件,通過對各種內(nèi)核數(shù)據(jù)結(jié)構(gòu)的訪問和操作,獲得該文件的文件描述符
  2. 如果使用系統(tǒng)調(diào)用接口來對文件進行寫入,數(shù)據(jù)直接通過write和fd寫入對應(yīng)的內(nèi)核級別緩沖區(qū),默認最后都會刷新到硬件中
  3. 如果使用fwrite等庫函數(shù)來對文件進行寫入,首先,在語言層面會malloc出一個FILE結(jié)構(gòu)體,F(xiàn)ILE里面有對應(yīng)的緩沖區(qū)信息以及文件的fd,然后內(nèi)容會先被暫存在C層面的緩沖區(qū),如果是無緩沖,數(shù)據(jù)直接被刷新到內(nèi)核中,如果是行緩沖,遇到'\n'就會被刷新到內(nèi)核中,如果是全緩沖,等緩沖區(qū)滿了就被刷新到內(nèi)核中
  4. 由于庫函數(shù)是對系統(tǒng)調(diào)用接口的封裝,用戶通過write和fd將數(shù)據(jù)刷新到對應(yīng)的文件的內(nèi)核緩沖區(qū)內(nèi),再由該內(nèi)核緩沖區(qū)刷新到外設(shè)

4.4解釋現(xiàn)象

為什么不加'\n'并且close(1)的時候,使用庫函數(shù)寫入的內(nèi)容不會被顯示?

不加'\n',調(diào)用庫函數(shù)寫入的數(shù)據(jù)都會被暫存在C層面的緩沖區(qū)

close(1)后,即使進程退出后緩沖區(qū)會自動刷新,但是此時已經(jīng)找不到1號文件的fd了,緩沖區(qū)內(nèi)的數(shù)據(jù)也無法被寫入到內(nèi)核中,最后也不會顯示到顯示器上

加了'\n'即使最后close(1),遇到'\n'緩沖區(qū)就會立馬將數(shù)據(jù)刷新到內(nèi)核中,就會顯示到顯示器上

為什么fork()之后重定向C接口會被調(diào)用兩次?

  1. 重定向后,緩沖區(qū)的刷新方式會從行緩沖變成全緩沖,也就說,數(shù)據(jù)要么等到緩沖區(qū)滿了再被刷新,要么等待進程結(jié)束后再刷新,所以我們放在緩沖區(qū)中的數(shù)據(jù),就不會被立即刷新,甚至fork之后
  2. fork( )之后,創(chuàng)建子進程,子進程會繼承父進程的內(nèi)核數(shù)據(jù)結(jié)構(gòu)對象的內(nèi)容,父子進程在一開始的時候數(shù)據(jù)和代碼是共享的,緩沖區(qū)也屬于數(shù)據(jù)
  3. 進程退出后,要對緩沖區(qū)的數(shù)據(jù)進行統(tǒng)一刷新,刷新就是對數(shù)據(jù)進行訪問寫入,此時父子數(shù)據(jù)會發(fā)生寫時拷貝,所以當父進程準備刷新的時候,子進程也就有了同樣的一份數(shù)據(jù),隨即產(chǎn)生兩份數(shù)據(jù)
  4. 由于write沒有所謂的緩沖區(qū),write()寫入的數(shù)據(jù)直接在內(nèi)核中,所以write( )的數(shù)據(jù)只有一份

總結(jié)

printf fwrite 庫函數(shù)會自帶緩沖區(qū),而 write 系統(tǒng)調(diào)用沒有帶緩沖區(qū)。這里所說的緩沖區(qū), 都是用戶級緩沖區(qū)。其實為了提升整機性能,OS也會提供相關(guān)內(nèi)核級緩沖區(qū)

那這個用戶級緩沖區(qū)誰提供呢? printf fwrite 是庫函數(shù), write 是系統(tǒng)調(diào)用,庫函數(shù)在系統(tǒng)調(diào)用的“上層”, 是對系統(tǒng) 調(diào)用的“封裝”,但是 write 沒有緩沖區(qū),而 printf fwrite 有,說明該緩沖區(qū)是二次加上的,由C標準庫提供

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • linux服務(wù)器上安裝jdk的兩種方法(yum+下載包)

    linux服務(wù)器上安裝jdk的兩種方法(yum+下載包)

    這篇文章主要給大家介紹了關(guān)于在linux服務(wù)器上安裝jdk的兩種方法,分別是利用yum安裝和從官網(wǎng)下載包安裝,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧
    2018-05-05
  • SSH 登錄工具常用命令

    SSH 登錄工具常用命令

    既然申請了國外的主機,那么SSH登錄工具肯定是必不可少的,這里羅列一些常用的SSH命令,以備查用。
    2009-02-02
  • 詳解Linux CPU負載和CPU使用率

    詳解Linux CPU負載和CPU使用率

    在本篇文章里小編給各位分享了關(guān)于Linux CPU負載和CPU使用率的相關(guān)知識點內(nèi)容,有需要的朋友們參考下。
    2019-07-07
  • Linux定時自動刪除舊垃圾文件的Autotrash工具

    Linux定時自動刪除舊垃圾文件的Autotrash工具

    今天小編就為大家分享一篇關(guān)于Linux定時自動刪除舊垃圾文件的工具,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2018-09-09
  • linux下如何把進程/線程綁定到特定cpu核上運行

    linux下如何把進程/線程綁定到特定cpu核上運行

    這篇文章主要介紹了linux下如何把進程/線程綁定到特定cpu核上運行問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-08-08
  • Linux Swap空間利用率過高問題

    Linux Swap空間利用率過高問題

    這篇文章主要介紹了Linux Swap空間利用率過高問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • 干貨 | Linux新手入門好書推薦

    干貨 | Linux新手入門好書推薦

    今天在知乎上看到了這樣一個問答:學(xué)習(xí)操作系統(tǒng)的知識,看哪本書好?讀完之后,我決定理一下操作系統(tǒng)方面的好書推薦給需要學(xué)習(xí)這個方向知識的人。下面這篇文章主要給Linux新手們推薦了一些入門的好書,需要的朋友可以參考下。
    2017-10-10
  • Linux字符終端如何用鼠標移動一個紅色矩形詳解

    Linux字符終端如何用鼠標移動一個紅色矩形詳解

    這篇文章主要給大家介紹了關(guān)于Linux字符終端如何用鼠標移動一個紅色矩形的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家學(xué)習(xí)或者使用Linux具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • Linux中如何查看usb設(shè)備信息

    Linux中如何查看usb設(shè)備信息

    這篇文章主要介紹了Linux中如何查看usb設(shè)備信息問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • Linux如何配置本地yum源(光盤鏡像掛載)

    Linux如何配置本地yum源(光盤鏡像掛載)

    這篇文章主要介紹了Linux如何配置本地yum源(光盤鏡像掛載),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-05-05

最新評論