文件IO 第二天 (文件IO)

姓名:謝煥彬 學號:19020100303
一、Linux文件I/O概述
1、POSIX規(guī)范
POSIX(Portable Operating System Interface,可移植操作系統(tǒng)接口規(guī)范)標準最初由IEEE(Institute of Electrical and Electronics Engineers,電氣和電子工程師協(xié)會,是目前最大的全球性非營利性專業(yè)技術學會)制定,目的是提高UNIX環(huán)境下程序的可移植性。通俗來講,為一個兼容POSIX標準的操作系統(tǒng)編寫的應用程序,可以在任何其他兼容POSIX標準的操作系統(tǒng)上編譯執(zhí)行而無需修改代碼。常見的Linux與UNIX系統(tǒng)都支持POSIX標準。

2、虛擬文件系統(tǒng)VFS
Linux系統(tǒng)的一個成功的關鍵因素是它具有與其他操作系統(tǒng)共存的能力。Linux的文件系統(tǒng)由兩層結構搭建:上面的虛擬文件系統(tǒng)VFS(Virtual File System),和下面的各種不同的具體文件系統(tǒng)(例如Ext、FAT32、NFS等)。

VFS將各種具體的文件系統(tǒng)的公共部分抽取出來形成一個抽象層,位于用戶的程序與具體需要使用的系統(tǒng)中間,并提供系統(tǒng)調用接口。這樣我們只需針對VFS提供的系統(tǒng)調用進行文件操作而無需具體考慮底層細節(jié)。VFS屏蔽了用戶對底層細節(jié)的描述使得編程簡化。

可以使用指令:

cat /proc/filesystems

查看當前操作系統(tǒng)支持哪些具體文件系統(tǒng)。

3、文件與文件描述符

Linux操作系統(tǒng)是基于文件概念搭建起來的操作系統(tǒng)(“萬物皆文件”),基于這一點,所有的I/O設備都可以直接當做文件來處理。因此操作普通文件的操作函數(shù)與操作設備文件的操作函數(shù)是相同的,這樣大大簡化了系統(tǒng)對不同設備、不同文件的處理,提高了效率。

那么對于內核而言,內核是如何區(qū)分不同的文件呢?內核使用文件描述符來索引打開的文件。文件描述符是一個非負整數(shù),每當打開一個存在的文件或創(chuàng)建一個新文件的時候,內核會向進程返回一個文件描述符,當對文件進行相應操作的時候,使用文件描述符作為參數(shù)傳遞給相應的函數(shù)。

通常一個進程啟動時,都會打開三個流:標準輸入、標準輸出、標準錯誤輸出,這三個流的文件描述符分別是0、1、2,對應的宏定義是STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO??梢圆榭搭^文件unistd.h查看相關定義。

流的名稱 文件描述符 宏定義

標準輸入 0 STDIN_FILENO

標準輸出 1 STDOUT_FILENO

標準錯誤輸出 2 STDERR_FILENO

基于文件描述符的I/O操作雖然不能直接移植到諸如Windows系統(tǒng)等之外的操作系統(tǒng)上,但對于某些底層的I/O操作(例如驅動程序、網(wǎng)絡連接等)是唯一的操作途徑。

4、標準I/O與文件I/O的區(qū)別:
1.文件I/O又稱為低級磁盤I/O,遵循POSIX標準。任何兼容POSIX標準的操作系統(tǒng)都支持文件I/O。標準I/O又稱為高級磁盤I/O,遵循ANSI C相關標準。只要開發(fā)環(huán)境有標準C庫,標準I/O就可以使用。

在Linux系統(tǒng)中使用GLIBC標準,它是標準C庫的超集,既支持ANSI C中定義的函數(shù)又支持POSIX中定義的函數(shù)。因此Linux下既可以使用標準I/O,也可以使用文件I/O。

2.通過文件I/O讀寫文件時,每次操作都會執(zhí)行相關系統(tǒng)調用。這樣的好處是直接讀寫實際文件,壞處是頻繁的系統(tǒng)調用會增加系統(tǒng)開銷。標準I/O在文件I/O的基礎上封裝了緩沖機制,每次先操作緩沖區(qū),必要時再訪問文件,從而減少了系統(tǒng)調用的次數(shù)。

3.文件I/O使用文件描述符打開操作一個文件,可以訪問不同類型的文件(例如普通文件、設備文件和管道文件等)。而標準I/O使用FILE指針來表示一個打開的文件,通常只能訪問普通文件。

二、文件I/O編程
1、打開文件
函數(shù)open()

需要頭文件:#include<sys/stat.h>

                     #include<fcntl.h>

函數(shù)原型:int open(const char *pathname,int flags,int perms);

函數(shù)參數(shù):pathname:打開文件名(可以包含具體路徑名)

                          flags:打開文件的方式,具體見下

                        perms:新建文件的權限,可以使用宏定義或者八進制文件權限碼,具體見下

函數(shù)返回值:成功:文件描述符

                    失?。?1

參數(shù)2flags具體可用參數(shù)(若使用多個flags參數(shù)可以使用|組合):

O_RDONLY:以只讀方式打開文件

O_WRONLY:以只寫方式打開文件

O_RDWR:以可讀可寫方式打開文件

O_CREAT:如果文件不存在,就創(chuàng)建這個文件,并使用參數(shù)3為其設置權限

O_EXCL:如果使用O_CREAT創(chuàng)建文件時文件已存在則返回錯誤信息。使用這個參數(shù)可以測試文件是否已存在

O_NOCTTY:若打開的是一個終端文件,則該終端不會成為當前進程的控制終端

O_TRUNC:若文件存在,則刪除文件中全部原有數(shù)據(jù)并設置文件大小為0

O_APPEND:以添加形式打開文件,在對文件進行寫數(shù)據(jù)操作時數(shù)據(jù)添加到文件末尾

注意:O_RDONLY與O_WRONLY與O_RDWR三個參數(shù)互斥,不可同時使用

若在參數(shù)2的位置有多個參數(shù)進行組合,注意使用按位或(|)運算符。

/** 可查看/usr/include/i386-linux-gnu/bits/fcntl.h文件看到具體的宏定義 **/

參數(shù)3perms表示新建文件的權限,可以使用宏定義或八進制文件權限碼。其中宏定義的格式是:S_I(R/W/X)(USR/GRP/OTH),其中R/W/X代表可讀/可寫/可執(zhí)行,USR/GRP/OTH代表文件所有者/文件組/其他用戶。例如:

S_IRUSR|S_IWUSR表示設置文件所有者具有可讀可寫權限,即0600。(一般情況下該參數(shù)都直接使用八進制文件權限碼因為使用宏定義的形式太復雜)。

2、關閉文件
函數(shù)close()

需要頭文件:#include<unistd.h>

函數(shù)原型:int close(int fd);

函數(shù)參數(shù):fd:文件描述符

函數(shù)返回值:成功:0

            失敗:-1

示例:使用open()與close()打開文件和關閉文件

include<stdio.h>

include<stdlib.h>

include<unistd.h>

include<sys/stat.h>

include<fcntl.h>

int main()

{

int fd;

if((fd=open("hello.txt",O_RDWR|O_CREAT|O_TRUNC,0666))<0)

{

    perror("fail to open file");

    exit(0);

}

close(fd);

return 0;

}
練習:說明以下在標準I/O中打開文件的模式所對應的在文件I/O中的模式(即flags的參數(shù)組合),其中文件名使用命令行傳參的形式

例:w+ ----> open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)

r

r+

w

w+

a

a+

答案:

r -----> open(argv[1],O_RDONLY)

r+ ----> open(argv[1],O_RDWR)

w -----> open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666)

w+ ----> open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)

a -----> open(argv[1],O_WRONLY|O_CREAT|O_APPEND,0666)

a+ ----> open(argv[1],O_RDWR|O_CREAT|O_APPEND,0666)

3、文件讀寫
函數(shù)read()

需要頭文件:#include<unistd.h>

函數(shù)原型:int read(int fd,void *buf,size_t count);

函數(shù)參數(shù):fd:文件描述符

               buf:讀取出的數(shù)據(jù)存放的緩沖區(qū)(內存地址)

            count:指定讀取的字節(jié)數(shù)

函數(shù)返回值:成功:讀到的字節(jié)數(shù) 或 0(表示文件已結尾)

                    失?。?1

函數(shù)write()

需要頭文件:#include<unistd.h>

函數(shù)原型:ssize_t write(int fd,void *buf,size_t count);

函數(shù)參數(shù):fd:文件描述符

               buf:待寫入的數(shù)據(jù)存放的緩沖區(qū)

           count:指定寫入的字節(jié)數(shù)

函數(shù)返回值:成功:已寫的字節(jié)數(shù)

                    失敗:-1

示例:使用read()和write()函數(shù),先向文件中寫入一些數(shù)據(jù),之后讀取出來

include<stdio.h>

include<stdlib.h>

include<strings.h>

include<unistd.h>

include<sys/stat.h>

include<fcntl.h>

define MAX 128

int main(int argc,char *argv[])

{

int fdread,fdwrite;

char readbuffer[MAX]={0},writebuffer[MAX];    //相當于自己設置了兩塊緩沖區(qū)

if(argc<2)

{

    perror("arguments are too few");

    exit(0);

}

//先打開文件寫入數(shù)據(jù)

if((fdwrite=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666))<0)

{

    perror("fail to open file");

    exit(0);

}

printf("請輸入寫入的內容:");

scanf("%[^\n]",writebuffer);

write(fdwrite,writebuffer,MAX);

close(fdwrite);

//再打開文件讀取剛寫入的內容

int n=0,sum=0;

if((fdread=open(argv[1],O_RDONLY))<0)

{

    perror("fail to open file");

    exit(0);

}

while((n=read(fdread,readbuffer,MAX))>0)

{

    sum += n;

    printf("%s",readbuffer);

    bzero(readbuffer,MAX);

}

printf("共讀取到%d個字節(jié)\n",sum);

close(fdread);

return 0;

}
執(zhí)行示例程序后查看文件可以發(fā)現(xiàn)文件除了寫入的字符外,還有部分亂碼字符。這是因為writebuffer[]數(shù)組沒有初始化,存儲了部分隨機數(shù),未被數(shù)據(jù)覆蓋掉的部分也同時被寫入了文件中。

思考:如何解決這個問題?

練習:使用文件I/O的read()/write()函數(shù)實現(xiàn)文件的復制

include<stdio.h>

include<stdlib.h>

include<unistd.h>

include<sys/stat.h>

include<fcntl.h>

define MAX 128

int main(int argc,char *argv[])

{

int fdread,fdwrite;

char buffer[MAX]={0};

int n=0,sum=0;

if(argc<3)

{

    printf("arguments are too few, Usage:%s <src_file> <dst_file>\n",argv[0]);

    exit(0);

}

if((fdread=open(argv[1],O_RDONLY))<0)

{

    perror("fail to open file");

    exit(0);

}

if((fdwrite=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0666))<0)

{

    perror("fail to open file");

    exit(0);

}

while((n=read(fdread,buffer,MAX))>0)

{

    sum += n;

    write(fdwrite,buffer,n);

}

printf("復制文件成功,共操作%d字節(jié)\n",sum);

close(fdread);

close(fdwrite);

return 0;

}

4、文件定位
函數(shù)lseek()

需要頭文件:#include<unistd.h>

                     #include<sys/types.h>

函數(shù)原型:off_t lseek(int fd,off_t offset,int whence);

函數(shù)參數(shù):fd:文件描述符

           offset:相對于基準點whence的偏移量,正數(shù)表示向前移動,負數(shù)表示向后移動,0表示不移動

        whence:基準點(取值同標準I/O內fseek()函數(shù)第三個參數(shù))

函數(shù)返回值:成功:當前讀寫位置

                    失?。?1

其中第三個參數(shù)whence的取值如下:

    SEEK_SET:代表文件起始位置,數(shù)字表示為0

    SEEK_CUR:代表文件當前的讀寫位置,數(shù)字表示為1

    SEEK_END:代表文件結束位置,數(shù)字表示為2

lseek()僅將文件的偏移量記錄在內核內而不進行任何I/O操作。

注意:lseek()函數(shù)僅能操作常規(guī)文件,一些特殊的文件(例如socket文件、管道文件等)無法使用lseek()函數(shù)。

示例:讀取文件的最后10個字節(jié)的數(shù)據(jù)

include<stdio.h>

include<stdlib.h>

include<unistd.h>

include<sys/stat.h>

include<fcntl.h>

define MAX 10

int main(int argc,char *argv[])

{

int fd;

char buffer[MAX]={0};

int n=0,sum=0;

if(argc<2)

{

    printf("arguments are too few\n",argv[0]);

    exit(0);

}

if((fd=open(argv[1],O_RDONLY))<0)

{

    perror("cannot open file");

    exit(0);3

}

lseek(fd,-10,SEEK_END);

if(read(fd,buffer,MAX)>0)

    printf("讀到的數(shù)據(jù):%s\n",buffer);

else

    printf("讀取出錯!\n");

close(fd);

return 0;

}
思考:若將基準點設置為SEEK_END但是偏移量是正數(shù)(即從文件末尾再向后偏移),會產生什么情況?

/*******************文件空洞******************/

若將lseek()函數(shù)的基準點設置為SEEK_END但是偏移量是正數(shù)(即從文件末尾再向后偏移),則會產生“文件空洞”的情況。#但是不能從文件頭向前偏移#

文件的偏移量是從文件開始位置開始計算的,若文件的偏移量大于了文件的實際數(shù)據(jù)長度,則會延長該文件,形成空洞。

示例:創(chuàng)建一個有空洞的文件。故意在文件結尾偏移好多個字節(jié),然后再寫入數(shù)據(jù)

include<stdio.h>

include<stdlib.h>

include<string.h>

include<unistd.h>

include<sys/stat.h>

include<fcntl.h>

int main(int argc,char *argv[])

{

int fd;

int n;

char buf1[]="LiLaoShiZhenShuai!";

char buf2[]="ABCDEFG";

if(argc<2)

{

    printf("arguments are too few\n");

    exit(0);

}

if((fd=open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666))<0)

{

    perror("cannot open file");

    exit(0);

}

write(fd,buf1,strlen(buf1));//首先寫入某些數(shù)據(jù)

n=lseek(fd,0,SEEK_END);//返回文件末尾位置求出文件大小

printf("原先的文件大小是%d\n",n);

n=lseek(fd,987654321,SEEK_END);//在文件末尾向后偏移很多字節(jié)

printf("此時偏移量是%d\n",n);

write(fd,buf2,strlen(buf2));//寫入buf內數(shù)據(jù)

n=lseek(fd,0,SEEK_END);

printf("空洞后文件大小是%d\n",n);

close(fd);

return 0;

}
程序執(zhí)行后,使用vim查看該文件,會發(fā)現(xiàn)在兩段數(shù)據(jù)之間有一段亂碼數(shù)據(jù),并且使用ls -l指令查看,文件的大小也變大了。

在UNIX/Linux文件操作中,文件位移量可以大于文件的當前長度,在這種情況下,對該文件的下一次寫將延長該文件,并在文件中構成一個空洞,這一點是允許的。位于文件中但沒有寫過的字節(jié)都被設為0,用read讀取空洞部分讀出的數(shù)據(jù)是0。

空洞文件作用很大,例如迅雷下載文件,在未下載完成時就已經(jīng)占據(jù)了全部文件大小的空間,這時候就是空洞文件。下載時如果沒有空洞文件,多線程下載時文件就都只能從一個地方寫入,這就不是多線程了。如果有了空洞文件,可以從不同的地址寫入,就完成了多線程的優(yōu)勢任務。

/*******************文件空洞end***************/

5、文件鎖(選學)
通過之前的open()/close()/read()/write()/lseek()函數(shù)已經(jīng)可以實現(xiàn)文件的打開、關閉、讀寫等基本操作,但是這些基本操作是不夠的。對于文件的操作而言,“鎖定”操作是對文件(尤其是對共享文件)的一種高級的文件操作。當某進程在更新文件內數(shù)據(jù)時,期望某種機制能防止多個進程同時更新文件從而導致數(shù)據(jù)丟失,或者防止文件內容在未更新完畢時被讀取并引發(fā)后續(xù)問題,這種機制就是“文件鎖”。

對于共享文件而言,不同的進程對同一個文件進行同時讀寫操作將極有可能出現(xiàn)讀寫錯誤、數(shù)據(jù)亂碼等情況。在Linux系統(tǒng)中,通常采用“文件鎖”的方式,當某個進程獨占資源的時候,該資源被鎖定,其他進程無法訪問,這樣就解決了共享資源的競爭問題。

文件鎖包括建議性鎖(又名“協(xié)同鎖”)和強制性鎖兩種。建議性鎖要求每個相關進程訪問文件的時候檢查是否已經(jīng)有鎖存在并尊重當前的鎖(也可以不尊重)。一般情況下不建議使用建議性鎖,因為無法保證每個進程都能自動檢測是否有鎖,Linux內核與系統(tǒng)總體上都堅持不使用建議性鎖。而強制性鎖是由內核指定的鎖,當一個文件被加強制性鎖的過程中,直至該所被釋放之前,內核將阻止其他任何進程對該文件進行讀或寫操作,每次讀或寫操作都得檢測鎖是否存在。當然,采用強制性鎖對內核的性能影響較大,每次內核在操作文件的時候都需要檢查是否有強制性鎖。

在Linux內核提供的系統(tǒng)調用中,實現(xiàn)文件上鎖的函數(shù)有l(wèi)ockf()和fcntl(),其中l(wèi)ockf()用于對文件加建議性鎖,這里不再講解。fcntl()函數(shù)既可以加建議性鎖,也可以加強制性鎖。同時,fcntl()還能對文件某部分上記錄鎖。所謂記錄鎖,其實就是字節(jié)范圍鎖,它能鎖定文件內某個特定區(qū)域,當然也可鎖定整個文件。

記錄鎖又分為讀鎖和寫鎖兩種。其中讀鎖又稱為共享鎖,它用來防止進程讀取的文件記錄被更改。記錄內可設置多個讀鎖,但當有一個讀鎖存在的時候就不能在該記錄區(qū)域設置寫鎖。寫鎖又稱為排斥鎖,在任何時刻只能有一個程序對文件的記錄加寫鎖,它用來保證文件記錄被某一進程更新數(shù)據(jù)的時候不被其他進程干擾,確保文件數(shù)據(jù)的正確性,同時也避免其他進程“弄臟”數(shù)據(jù)。文件記錄一旦被設置寫鎖,就不能再設置任何鎖直至該寫鎖解鎖。

本節(jié)只簡單講述fcntl()對文件施加讀鎖和寫鎖并查看兩種鎖的效果,有關函數(shù)fcntl()的更詳細用法請查閱fcntl()手冊(man fcntl)。

函數(shù)fcntl()

需要頭文件:#include<unistd.h>

                    #include<sys/types.h>

                    #include<fcntl.h>

函數(shù)原型:int fcntl(int fd,int cmd,struct flock *lock_set);

函數(shù)參數(shù):fd:文件描述符

          cmd:檢測鎖或設置鎖

          lock_set:結構體類型指針,結構體struct flock需要事先設置,與第二個參數(shù)連用

函數(shù)返回值:成功:0

            失敗:-1

第二個參數(shù)cmd表示該操作對文件的命令,若該命令是對文件檢測鎖或施加鎖,則需要第三個參數(shù):

F_GETLK:檢測文件鎖狀態(tài),檢測結果存放在第三個參數(shù)的結構體的l_type內

F_SETLK:對文件進行鎖操作,鎖操作類型存放在第三個參數(shù)的結構體的l_type內

F_SETLKW:同F(xiàn)_SETLK,不過使用該參數(shù)時若不能對文件進行鎖操作則會阻塞直至可以進行鎖操作為止(W即wait,等待)

(更多參數(shù)請參閱fcntl()函數(shù)的使用手冊)

第三個參數(shù)是對文件施加鎖操作的相關參數(shù)設置的結構體

注意:必須定義struct flock類型結構體并初始化結構體內的數(shù)據(jù),然后使用地址傳遞的方式傳遞參數(shù),不允許直接定義struct flock* 類型指針直接傳參

關于struct flock的成員如下:

struct flock

{

    short l_type;    //文件鎖類型:F_RDLCK, F_WRLCK, F_UNLCK

    short l_whence;    //起始位置:SEEK_SET, SEEK_CUR, SEEK_END

    off_t l_start;

    off_t l_len;

    pid_t l_pid;    //將文件鎖定的進程PID(僅在F_GETLK時有效)

}

結構體成員說明:

l_type:有三個參數(shù)

        F_RDLCK:讀鎖(共享鎖)

        F_WRLCK:寫鎖(排斥鎖)

        F_UNLCK:無鎖/解鎖

l_whence:相對于偏移量的起點,參數(shù)等同于fseek()與lseek()中的whence參數(shù)

        SEEK_SET:位置為文件開頭位置

        SEEK_CUR:位置為文件當前讀寫位置

        SEEK_END:位置為文件結尾位置

l_start:加鎖區(qū)域在文件中的相對位移量,與l_whence的值共同決定加鎖區(qū)域的起始位置

l_len:加鎖區(qū)域的長度,若為0則表示直至文件結尾EOF

l_pid:具有阻塞當前進程的鎖,其持有的進程號會存放在l_pid中,僅由F_GETLK返回

思考:如何設置該結構體內的成員使得加鎖的范圍為整個文件?

答案:設置l_whence為SEEK_SET,l_start為0,l_len為0即可。

示例:使用fcntl()函數(shù)對文件進行鎖操作。首先初始化結構體flock中的值,然后調用兩次fcntl()函數(shù)。第一次參數(shù)設定為F_GETLK判斷是否可以執(zhí)行flock內所描述的鎖操作;第二次參數(shù)設定為F_SETLK或F_SETLKW對該文件進行鎖操作。

注意:需要至少兩個終端運行該程序才能看到效果

include<stdio.h>

include<stdlib.h>

include<unistd.h>

include<fcntl.h>

include<sys/types.h>

int lock_set(int fd,int type)

{

struct flock lock;

lock.l_whence = SEEK_SET;

lock.l_start = 0;

lock.l_len = 0;//三個參數(shù)設置鎖的范圍是全文件

lock.l_type = type;//type的參數(shù)由主調函數(shù)傳參而來

lock.l_pid = -1;



//第一次操作,判斷該文件是否可以上鎖

fcntl(fd,F_GETLK,&lock);

if(lock.l_type!=F_UNLCK)//如果l_type得到的返回值不是F_UNLCK則證明不能加鎖,需判斷原因

{

    if(lock.l_type==F_RDLCK)

    {

        printf("This is a ReadLock set by %d\n",lock.l_pid);

    }

    else if(lock.l_type==F_WRLCK)

    {

        printf("This is a WriteLock set by %d\n",lock.l_pid);

    }

}



//第二次操作,對文件進行相應鎖操作

lock.l_type = type;

if((fcntl(fd,F_SETLKW,&lock))<0)

{

    printf("Lock Failed:type = %d\n",lock.l_type);

    return -1;

}

switch(lock.l_type)

{

    case F_RDLCK:

        printf("ReadLock set by %d\n",getpid());break;

    case F_WRLCK:

        printf("WriteLock set by %d\n",getpid());break;

    case F_UNLCK:

        printf("ReleaseLock by %d\n",getpid());

        return 1;

        break;

}

return 0;

}

int main(int argc, const char *argv[])

{

int fd;

if((fd=open("hello.txt",O_RDWR))<0)

{

    perror("fail to open hello.txt");

    exit(0);

}

printf("This pid_no is %d\n",getpid());

//給文件上鎖

lock_set(fd,F_WRLCK);

printf("Press ENTER to continue...\n");

getchar();

//給文件解鎖

lock_set(fd,F_UNLCK);

close(fd);

return 0;

}
這里標注比較隨意,可以看我的印象筆記:文件IO 第二天 (文件IO)
————————————————
版權聲明:本文為CSDN博主「nan_lei」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權協(xié)議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/nan_lei/article/details/81488349

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容