shell編程入門,Linux應用程序編程

 2023-12-06 阅读 27 评论 0

摘要:系統編程概念 庫函數:C語言標準庫中ANSI C、ISO C、GNU C、POSIX ANSI C標準同時出現的就是ISO組織,將ANSI C加入了ISO的大家庭,定義了ISO C。除了在格式和排版等方面存在一些差別外,其他都與ANSI C相同。即ANSI C與ISO C 對于我們開發者來說完全

系統編程概念

庫函數:C語言標準庫中ANSI C、ISO C、GNU C、POSIX

ANSI C標準同時出現的就是ISO組織,將ANSI C加入了ISO的大家庭,定義了ISO C。除了在格式和排版等方面存在一些差別外,其他都與ANSI C相同。即ANSI C與ISO C 對于我們開發者來說完全相同

GNU 是為了實現自由開源目的一個基金會,它提供了很多基于POSIX標準的軟件和庫,比如glibc、gcc、emacs等等。
GNU C又叫做glibc,是Linux上的一個基礎庫,glibc C實現了POSIX C標準的庫函數功能,有些POSIX標準是單獨的庫函數存在的。glibc是linux上最常用的實現。

POSIX標準的誕生是為了統一個操作系統的接口,方便開發者開發程序,寫出可移植的代碼程序。基于POSIX標準的庫函數都是可以在持之此標準的操作系統平臺上移植。

系統調用流程

C語言標準庫有諸多庫函數組成
庫函數fopen利用系統調用open來執行打開文件的實際操作,設計庫函數是為了提供比底層系統調用更為方便的調用接口。
例如:printf函數可提供格式化輸出和數據緩存功能,而write()系統調用只能輸出字節塊。同理,malloc和free與底層的brk系統調動相比,對內存的釋放和分配頁容易許多

系統調用和庫函數的錯誤

每個系統調用通過手冊頁都有調用返回的可能值,并指出那些值表示錯誤。

man手冊中標號:(查看庫函數的參數使用man 3)
1.一般命令 2.系統調用 3.庫函數,涵蓋C標準函數庫 4.特殊文件(通常是/dev中的設備)和驅動程序 5.文件格式和約定 6.游戲和屏保 7.雜項 8.系統管理命令和守護進程。

打印錯誤值使用perror和strerror。通常系統調用錯誤時,返回值表示為-1表示出錯,并且設置全局整形變量errno設置為一個正值。

少數系統調用如getpriority()調用成功后,也會返回-1.判斷此類是否成功,通常在調用前將errno設置為0,調用函數后再檢查errno。

#include <stdio.h>
void perror(const char *msg)

使用它的簡單的例子:函數strerror會針對errnum參數中給定的錯誤號,返回相應的錯誤字符串。

fd = open(pathname. flags, mode);
if(fd == -1)
{perror("open");//char* str = strerror(errno);//printf("%s", str);exit(EXIT_FAILURE);
}

一、POSIX文件I/O編程

1.1 文件描述符

POSIX文件操作同樣也是以文件描述符來標識一個文件,與ANSI C文件描述符不同的是,POSIX文件描述符是int類型的一個整數值
POSIX文件描述符僅是一個索引值, 代表內核打開文件記錄表的記錄索引。在一個系統中,文件打開關閉比較頻繁,因此同一個POSIX 文件描述符的值在不同時間可能代表不同的文件。

Linux系統下默認一個進程最多可以打開1024個文件,用戶可以通過ulimit -n查看系統允許打開文件的數量。
stdin、stdout、stderr 文件描述符分比為0,1,2。
使用fileno()函數可以返回一個流對應的文件描述符,使用read、write之類的I/O系統調用處理。
使用fdopen可以將一個文件描述符轉換成文件流,就可以使用stdio庫函數中fread、fwrite之類的處理。

int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);

1.2 創建/打開/關閉文件

open()函數打開一個文件,在指定一定參數(O_CREAT - 創建文件)的情況下,會隱含調用creat()函數創建文件。open和creat函數定義如下:
在這里插入圖片描述
其中,sys/types.h包含基本系統數據類型;sys/stat.h包含文件狀態;fcntl.h包含文件控制定義。

(1) open函數

flags參數指定打開文件的方式,參數如下表所示:
在這里插入圖片描述
前三個為文件訪問模式標志,三個flags參數只能使用其中一個,不可同時使用。

O_TRUNC -文件存在且允許寫,則清空文件

其中open函數給出兩種定義,第二種多了mode參數,在flag參數指定為O_CREAT參數時,mode參數用于設置文件的權限,如下表所示:
在這里插入圖片描述
在創建新文件時,參數mode指定了文件的權限,但是通常會被umask修改,實際創建時的權限為mode&(~umask)注意,mode僅在創建新文件時有效

open打開失敗時,會返回-1,并且設置預定的全局變量errno,下表為出錯代碼及意義。

在這里插入圖片描述

創建新文件后,文件的atime(上次訪問時間)、ctime(創建時間)、mtime(修改時間)都被修改為當前時間,文件的上層目錄的atime和ctime也被修改。另外,如果打開時使用了O_TRUNC參數,則ctime和mtime被設置為當前時間。

(2)creat函數

在這里插入圖片描述

(3)close函數

在這里插入圖片描述
與其他系統調用一樣,應對close調用做錯誤檢查。
企圖關閉一個未打開的文件描述符或兩次關閉同一個文件描述符,會導致錯誤。
使用NFS網絡文件系統時保存文件,建議關閉文件時檢查返回值,防止文件寫入錯誤。如果NFS出現提交失敗,意味著數據沒有抵達遠程磁盤,close會調用失敗,出錯代碼如下:
在這里插入圖片描述

1.3 讀寫文件內容

(1)write函數

在這里插入圖片描述
write函數調用從buffer中讀取count字節的數據寫入由fd指代的已打開文件中。參數buf是寫入數據的緩沖開始地址,參數count表示要寫入多少字節的數據。
write調用的返回值為實際寫入文件中的字節數有可能小于count,失敗返回-1且設置errno代碼,錯誤代碼如下:
在這里插入圖片描述
在這里插入圖片描述
write調用成功并不能保證數據已經寫入磁盤,內核會緩存磁盤的I/O操作。

(2)read函數

在這里插入圖片描述
返回值:成功返回讀取的字節數,出錯返回-1并設置errno,如果在調read之前已到達文件末尾,則這次read返回0
參數count是請求讀取的字節數,讀上來的數據保存在緩沖區buf中,同時文件的當前讀寫位置向后移。注意這個讀寫位置和使用ANSI C標準I/O庫時的讀寫位置有可能不同,這個讀寫位置是記在內核中的,而使用C標準I/O庫時的讀寫位置是用戶空間I/O緩沖區中的位置。比如用fgetc讀一個字節,fgetc有可能從內核中預讀1024個字節到I/O緩沖區中,再返回第一個字節,這時該文件在內核中記錄的讀寫位置是1024,而在FILE結構體中記錄的讀寫位置是1。注意返回值類型是ssize_t,表示有符號的size_t,這樣既可以返回正的字節數、0(表示到達文件末尾)也可以返回負值-1(表示出錯)。
read函數返回時,返回值說明了buf中前多少個字節是剛讀上來的。有些情況下,實際讀到的字節數(返回值)會小于請求讀的字節數count,例如:讀常規文件時,在讀到count個字節之前已到達文件末尾。例如,距文件末尾還有30個字節而請求讀100個字節,則read返回30,下次read將返回0。
在這里插入圖片描述

1.4 文件內容定位

通過lseek函數設置文件偏移量
在這里插入圖片描述
使用參數whence指定的方式,按照whence指定的偏移位置開始+偏移量offset。
whence的三種設置方式:
在這里插入圖片描述
SEEK_END為文件最后一個字節的下一個字節。
whence為SEEK_SET時,offset為非負數。
lseek()調用成功后返回新的文件偏移量。下面的調用只是獲取文件偏移量的當前位置,沒有修改它。

curr = lseek(fd, 0, SEEK_CUR);

對于一個管道、FIFO、socket或者終端 ,使用lseek函數會返回-1,errono設置為EPIPE.

1.5 文件空洞

如果文件偏移量已經跨越了文件結尾,如果執行read則返回0,調用write可以在文件結尾后的任意位置寫入數據。
從文件結尾后到新寫入數據間的這段空間稱為文件空洞。文件空洞中是存在字節的,讀取空洞將返回以0(字節)填充的緩沖區。
而文件空洞不占用磁盤空間,直道文件空洞寫入數據才會為之分配磁盤塊。

1.6 修改已打開文件的屬性

fcntl函數獲取或改變已打開文件的性質,cmd支持的操作范圍很廣,后續內容再補充。
在這里插入圖片描述

其中cmd為F_GETFL可以獲取文件狀態表示。
判定文件的訪問模式需要與O_ACCMODE與fcntl范圍的值相與進行判斷。

flag = fcntl(fd, F_GETFL);
accessMode = flag & O_ACCMODE;
if( accessMode  == O_WRONLY(O_RDONLY/O_RDWR) ) 
{
}

F_SETFL可以設置文件的狀態標識。允許更改的標志有O_APPEND,O_NONBLOCK,O_NOATIME,O_ASYNC,O_DIRECT.

1.7 獨占方式創建一個文件

情況1:

同時制定O_EXCL與O_CREAT作為open()的標志位時,如果打開的文件已經存在,則open將返回一個錯誤。這個機制,可以保證進程是打開文件的創建者,防止其他進程競爭。

情況2:

向文件尾部追加數據

if( lseek(fd, 0, SEEK_END) == -1 )
{
}
if(write() != len )
{
}

如果第一個進程運行到lseek和write之間時,被相同代碼的第二個進程打斷,則兩個進程在寫入數據前,將文件偏移量設置為相同位置,第一個進程再調度時,會覆蓋第二個進程已寫入的數據。此時,出現競爭狀態,為避免在打開文件時需要加入O_APPEND標志。

而NFS文件系統不支持O_APPEND,從而可能導致上述臟寫入問題。

1.8 文件描述符和打開文件之間的關系

多個文件描述符指向同一打開文件。這些文件描述符可在相同或不同的進程中打開。

內核維護的3個數據結構

  • 進程級的文件描述符表
  • 系統級的打開文件表
  • 文件系統的i-node表

如下圖所示:

在這里插入圖片描述

針對每個進程,內核為其維護打開文件的文件描述符表。該表的每一條目都記錄了單個文件描述符的相關信息,如下所示:

  • 控制文件描述符操作的一組標志。
  • 對打開文件句柄的引用。

內核對所有打開的文件維護有一個系統級的描述表格。也稱之為打開文件表,并將表中各條目稱為打開文件句柄。一個打開文件句柄存儲了與一個打開文件相關的全部信息,如下所示:

  • 當前文件偏移量(調用read()和write()時更新,或使用lseek()直接修改)。
  • 打開文件時所使用的狀態標志(open()的flag參數)。
  • 文件訪問模式(調用open()時所設置的只讀模式,只寫模式或讀寫模式)。
  • 與信號驅動I/O相關的設置。
  • 對該文件的i-node對象的引用。

每個文件系統都會為駐留其上的所有文件建立一個i-node表。i-node信息,具體如下:

  • 文件類型(例如,常規文件,套接字或FIFO)和訪問權限。
  • 一個指針,指向該文件所持有的鎖的列表。
  • 文件的各種屬性,包括文件大小以及與不同類型操作相關的時間戳。

圖中,(1) 在進程A中,文件描述符1和20都指向同一個打開的文件句柄(標號為23).這可能是通過調用dup(),dup2()或fcntl()形成的。

(2) 進程A的描述符2和進程B的文件描述符2都指向同一個打開的文件句柄(標號為73)。這種情形可能是在調用fork后出現(即,進程A和進程B之間是父子關系),或者當某進程通過UNIX域套接字將一個打開的文件描述符傳遞給另一進程時,也會發生。

(3) 此外,進程A的描述符0和進程B的描述符3分別指向不同的打開文件句柄,但這些句柄均指向i-node表中的相同條目(1976),換言之,指向同一文件。發生這種情況是因為每個進程各自對同一文件發起了open()調用。同一個進程兩次打開同一文件,也會發生類似情況。

上述討論揭示出如下要點:

  • 兩個不同的文件描述符。若指向同一打開文件句柄,將共享同一文件偏移量(上面的情況1、2)。因此,如果通過其中之一文件描述符來修改文件偏移量(由調用read(),write(),lseek()所致),那么從另一文件描述符也會觀察到這一變化。無論這兩個文件描述符分屬于不同進程,還是同屬于一個進程,情況都是如此。
  • 要獲取和修改打開的文件標志(例如,O_APPEND,O_NONBLOCK,O_ASYNC),可執行fcntl()的F_GETFL和F_SETFL操作,其對作用域的約束與上一條類似。
  • 文件描述符標志為進程和文件描述符所私有。對這一標志的修改不會影響到同一進程或不同進程中的其他文件描述符。

1.9 復制文件描述符

(1) dup

dup調用賦值一個打開的文件描述符oldfd,并返回一個新描述符,二者都指向同一個文件句柄。系統分配的是編號值最低的未用文件描述符。

#include <unistd.h>
int dup(int oldfd);

(2) dup2

dup2系統調用為oldfd參數所指向的文件描述符創建副本,其編號由newfd參數指定。

int dup2(int oldfd, int newfd);

如果由newfd參數所指定的文件描述符之前已經打開了,dup2會將其關閉。(dup2會忽略關閉newfd帶來的錯誤,所以安全的做法是在調用dup2之前,若newfd打開了,使用close進行關閉)

1、調用dup2成功,則返回副本的文件描述符編號(即newfd)。
2、如果oldfd為無效文件描述符,則調用失敗返回錯誤EBADF,且不關閉newfd。
3、如果oldfd有效,且與newfd相等,則什么都不做,不關閉newfd,并將其作為調用結果返回。

(3) fcntl()的F_DUPFD和F_DUPFD_CLOEXEC

fcntl()的F_DUPFD是復制文件的另一個接口,更為靈活性。
F_DUPFD命令要求返回的文件描述符會清除對應的FD_CLOEXEC標志;F_DUPFD_CLOEXEC要求設置新描述符的FD_CLOEXEC標志。

newfd = fcntl(oldfd, F_DUPFD, startfd);

調用oldfd創建一個副本,且使用大于等于startfd的最小未用值作為描述符編號

問題1:fwrite都是帶緩沖區的,wrtie不帶緩沖區,了解一下具體的區別?

(4) 執行時關閉標志FD_CLOEXEC

在執行exec()之前,程序有時需要確保關閉某些特定的文件描述符。從安全的角度出發,應當在加載新程序之前確保關閉哪些不必要的文件描述符。對所有此類描述符執行close()調用就可以實現。然而這一做法存在如下局限。

  • 某些描述符可能是由庫函數打開的。但庫函數無法使主程序在執行exec()之前關閉相應的文件描述符。作為基本原則,庫函數應總是為其打開的文件設置執行時關閉(close-on-exec)標志。
  • 如果exec()因某種原因而調用失敗,可能還需要使描述符保持打開狀態。如果這些描述符已經關閉,將他們重新打開并指向相同文件的難度很大,基本上不可能。

內核為每個文件描述符提供了執行時關閉標志

如果設置了這一標志,那么在成功執行exec()時,會自動關閉該文件描述符,如果調用exec()失敗,文件描述符會保持打開狀態。

但是,當使用dup(),dup2()或fcntl()為一文件描述符創建副本時,總是會清除副本描述符的執行時關閉標志。

調用open函數O_CLOEXEC模式打開的文件描述符,或是使用fcntl設置FD_CLOEXEC選項,這二者得到(處理)的描述符在通過fork調用產生的子進程中均不被關閉。淺析open函數O_CLOEXEC模式和fcntl函數FD_CLOEXEC選項

(5) dup3

因此dup3新增flag參數,且該參數目前只支持O_CLOEXEC,使得內核為新文件設置close-on-exe標志(FD_CLOEXEC)。

int dup3(int oldfd, int newfd, int flags);

1.10 在文件特定偏移量處的I/O:pread()和pwrite()

ssize_t pread( int fd, void* buf, size_t count, off_t offset);ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);	

在特定偏移量處進行讀寫,不會改變文件的偏移量。
pread()調用等于將如下調用納入同一原子操作:

off_t orig;
orig = lseek(fd, 0, SEEK_CUR);
s = read(fd, buf, len);
lseek(fd, orig, SEEK_SET);

對于多線程編程,已打開的文件偏移量為所有線程所共享,當調用pread()或pwrite()時,多線程對同意文件描述符性質IO操作,且不會收到其他線程修改文件偏移量而受到影響

如果試圖用上面的代碼替代的話,會引發競爭狀態,而pread()和pwrite()都是原子操作。

1.11 分散輸入和集中輸出:readv()和writev()

函數原型

ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

其中,iov中每個成員都是如下形式的數據結構:

struct iovec{void *iov_base;		/*Start address of buffer*/size_t iov_len;		/*Number of bytes to transfer to/from buffer*/
};

系統實現可以通過<limits.h>文件中IOV_MAX來通告這一限額,也可以在系統運行時調用sysconf(_SC_IOV_MAX)來獲取這一限額。要求該限額不得少于16。Linux將IOV_MAX的值定義為1024。

作用

readv()

從iov[0]開始一次進行填滿緩沖區, 成功返回讀取字節數,文件結束則返回0,出錯返回-1

writev()

將iov所指定的所有緩沖區中數據拼接起來(從iov[0]依次開始),然后寫入文件。 成功返回寫入字節數,出錯返回-1

特點

1、會改變打開文件句柄1的當前文件偏移量
2、是原子操作
readv和writev作用是便捷,可以對多個輸出的內容,通過數組順序一次性發送,不需要逐個逐個發送緩沖區的內容,并且多次發起write調動則不具備原子性。

1.12 在指定的文件偏移量出執行分散輸入/集中輸出

ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);
ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset);

preadv()系統調用結合了readv()和pread(2)。 它執行與readv()相同的任務,但是添加了第四個參數offset,該參數指定要在其上執行輸入操作的文件偏移量

pwritev()系統調用結合了writev()和pwrite(2)。 它執行與writev()相同的任務,但是添加了第四個參數offset,該參數指定要在其上執行輸出操作的文件偏移量

與pread()和pwrite()一樣不會更改文件偏移量。

1.13 截斷文件:truncate()和ftruncate()

兩個函數目的都是將文件大小設置為length參數指定的值

int truncate(const char *pathname, off_t length) //pathname就是路徑
int ftruncate(int fd, off_t length); //該系統調用不會修改文件偏移量

文件當前長度大于參數length時,超出部分都會被丟棄;若小于則都會在文件尾部添加一系列空字節或是一個文件空洞。

truncate函數使用前不需要使用open函數打開文件,直接使用路徑名字符串。

ftruncate函數則需要在可寫狀態下打開文件獲取文件描述符

1.14 非阻塞I/O

在打開文件使用fcntl()指定O_NONBLOCK標志,目的有二:
(1):若open()調用未能立即打開文件,則返回錯誤,而非陷入阻塞。
(2):調用open()成功后,后續的I/O操作也是非阻塞的。
管道,FIFO,套接字,設備都支持非阻塞模式。

1.15 /dev/fd目錄

對于每一個進程,內核都提供有一個特殊的虛擬目錄/dev/fd。該目錄中包含/dev/fd/n形式的文件名,其中n是與進程中的打開文件描述符相對應的編號,打開/dev/fd目錄中的一個文件等同于復制相應的文件描述符,所以,如下兩行代碼是等價的

fd = open("/dev/fd/1", O_WRONLY);
fd = dup(1);

1.16 創建臨時文件

有些程序需要創建一些臨時文件,僅供其在運行期間使用,程序終止后立即刪除。

(1) mkstemp

#include <stdlib.h>
int mkstemp(char *template);
/*returns file descriptor if OK, -1 on error*/

mkstemp()函數生成一個唯一文件名并打開該文件,返回一個可用于I/O調用的文件描述符,該模板參數采用路徑名形式,其中最后6個字符必須為XXXXXX,這6個字符將被替換,以保證文件名的唯一性,且修改后的參數將通過template參數傳回去。

(2) tmpfile

#include <stdio.h>
FILE *tmpfile(void);

二、文件I/O緩沖

2.1 文件I/O的內核緩沖:緩沖區高速緩存

read() 和 write() 系統調用在操作磁盤文件時不會直接發起磁盤訪問,而是僅僅在用戶空間緩沖區與內核緩沖區高速緩存之間復制數據。

write(fd, "abc", 3);

write() 后立即返回。在后續某個時刻,內核會將其緩沖區中的數據寫入磁盤(因此可以說系統調用與磁盤操作并不同步)。如果在此期間,另一進程試圖讀取該文件的這幾個字節,那么內核將自動從緩沖區高速緩存中提供這些數據,而不是從文件中。
于此同理,對于輸入而言,內核從磁盤中讀取數據并儲存到內核緩沖區中。read()調用將從該緩沖區讀取數據,直至把緩沖區中的數據取完。這時,內核會將文件的下一段內容讀入緩沖區高速緩存(這里有所簡化。對于序列化的文件訪問,內核通常會嘗試執行預讀,以確保在需要之前就將文件的下一數據讀入緩沖區高速緩存)。

從內核2.4開始,不在維護單獨的緩沖區高速緩存,將文件I/O緩沖區置于頁面高速緩存中,其中還有諸如內存映射文件的頁面。

緩沖區大小對I/O調用性能的影響:

如果與文件發生大量的數據傳輸,通過采用大塊空間緩沖數據,以及執行更少的系統調用,可以極大的提高I/O性能。
若強制在數據傳輸到磁盤前阻塞輸出操作,則調用write()所需的時間會顯著上升。

2.2 stdio 庫的緩沖

使用stdio庫可以使編程者免于自行處理對數據的緩沖,如fprintf(),fscanf(),fgets(),fputs,fputc(),fget()。

(1) setvbuf

使用setvbuf函數,可以控制stdio庫使用緩沖的方式,setvbuf()調用將影響后續在指定流上進行的所有I/O操作

setvbuf(FILE *stream, char *buf, int mode, size_t size);

stream: 標識將要修改的文件流
buf 和 size 針對stream要使用的緩沖區。(需要動態或靜態指定堆上的空間,若buf為NULL,則stdio庫會自動分配一個)
mode:
1._IONBF 不緩沖,stderr默認屬于此類型,每個stdio庫函數立即調用write()或read(),并且忽略buf,size參數。
2._IOLBF 行緩沖,在輸入一個換行符之前緩沖數據
3._IOFBF 全緩沖,單次讀寫數據的大小與緩沖區相同,磁盤的流默認采用此模式。

(2) setbuf

setbuf(FILE *stream, char *buf)

setvbuf(fp,buf ,(buf != NULL)? _IOFBF:_IONBF, BUFSIZ );

(3) 刷新stdio緩沖區

這樣的一個代碼:

printf("hello");
write(STDOUT_FILENO,"nihao.\n", 7);

I/O系統調用會將數據傳遞到內核緩沖區高速緩存,而stdio庫會等到用戶空間的緩沖區填滿后,在使用write將其傳遞到內核緩沖區高速緩存中。

通常情況下,printf函數的輸出會在write函數的輸出之后出現。將IO系統調用和stdio函數混合使用時,使用fflush可以規避這一問題。或者,使用setvbuf或setbuf使用戶緩沖區失效

#include<stdio.h>
int fflush(FILE *stream)

若參數為NULL,則刷新所有。
刷新輸入緩沖區時,將丟棄已緩沖的輸入數據。
關閉相應流時,會自動刷新緩沖區。

1.應顯式調用fflush(stdout),避免stdin輸入導致的stdout緩沖區屬性。一個輸出操作不能緊跟一個輸入操作,需要在二者之間調用fflush或者使用文件定位函數fseek、fsetpos或rewind。
2.反之,一個輸入操作同樣不能緊跟一個輸出操作(C陷阱與缺陷中也提及)

2.3 控制文件I/O的內核緩沖

(1) 用于控制文件I/O內核緩沖的系統調用

fsync(int fd);

該系統調用將使緩沖數據和與打開文件描述符fd相關的所有元數據都刷新到磁盤上

(2) 使所有寫入同步:O_SYNC

在調用open()函數時如指定O_SYNC標志,則會使所有后續輸出同步。

fd = open(path, O_WRONLY | O_SYNC);

調用open后,每個write調用都會自動將文件數據和元數據刷新到磁盤中。謹慎使用,會影響性能。

(3) I/O緩沖小結

在這里插入圖片描述

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://808629.com/191324.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 86后生记录生活 Inc. 保留所有权利。

底部版权信息