1.簡(jiǎn)介
傳統(tǒng)的文件傳輸模式中(read/write和send/recv),需要在文件file,系統(tǒng)buffer和用戶buffer中反復(fù)I/O,造成內(nèi)存的浪費(fèi)與資源占用,大致流程如下.
- 1.調(diào)用
read(file, tmp_buf, len);
,切換user mode至kernel mode,將文件從磁盤讀取到kernel buffer中掛起;
關(guān)于read():
ssize_t read (int fd, void *buf, size_t count);
成功返回讀取的字節(jié)數(shù),出錯(cuò)返回-1并設(shè)置errno啤它,如果在調(diào)read之前已到達(dá)文件末尾,則這次read返回0衰粹。
read()會(huì)把參數(shù)fd所指的文件傳送nbyte個(gè)字節(jié)到buf指針所指的內(nèi)存中铝耻。若參數(shù)nbyte為0瓢捉,則read()不會(huì)有作用并返回0。返回值為實(shí)際讀取到的字節(jié)數(shù)某弦,如果返回0而克,表示已到達(dá)文件尾或無可讀取的數(shù)據(jù)。錯(cuò)誤返回-1,并將根據(jù)不同的錯(cuò)誤原因適當(dāng)?shù)脑O(shè)置錯(cuò)誤碼碎绎。
2.
read()
返回,切換kernel mode至user mode,把kernel buffer中緩存的數(shù)據(jù)復(fù)制到user buffer中;3.調(diào)用
write(socket, tmp_buf, len);
,切換user mode至kernel mode,把復(fù)制到user buffer中的數(shù)據(jù)再次復(fù)制到另一個(gè)與socket關(guān)聯(lián)的kernel buffer;
關(guān)于write():
ssize_t write(int fd, const void *buf, size_t nbyte);
write函數(shù)把buf中nbyte寫入文件描述符handle所指的文檔筋帖,成功時(shí)返回寫的字節(jié)數(shù)幕随,錯(cuò)誤時(shí)返回-1.
- 4.
write()
返回,切換kernel mode至user mode,將上一步緩存到kernel buffer中的數(shù)據(jù)復(fù)制到服務(wù)器協(xié)議棧中;
關(guān)于Linux User Mode和Kernel Mode
簡(jiǎn)單圖示:
這樣的傳輸方式固然簡(jiǎn)單可靠,但是由于一共進(jìn)行了四次跨space的I/O和四次mode切換,所以在傳輸size過大或數(shù)量過多的文件時(shí)效率堪憂.
在Linux 2.0+以后提供了一個(gè)sendfile()
的文件傳送方式,
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
文檔
sendfile()是作用于數(shù)據(jù)拷貝在兩個(gè)文件描述符之間的操作函數(shù).這個(gè)拷貝操作是內(nèi)核中操作的,所以稱為"零拷貝".sendfile函數(shù)比起read和write函數(shù)高效得多,因?yàn)閞ead和write是要把數(shù)據(jù)拷貝到用戶應(yīng)用層操作.
其大致流程如下:
- 1.調(diào)用
sendfile()
把磁盤中的數(shù)據(jù)復(fù)制到kernel buffer; - 2.把復(fù)制到kernel buffer中的數(shù)據(jù)復(fù)制到另一個(gè)socket關(guān)聯(lián)的kernel buffer中;
- 3.將上一步緩存到socket kernel buffer中的數(shù)據(jù)復(fù)制到服務(wù)器協(xié)議棧中;
以上流程中并沒有出現(xiàn)mode的切換,并且省略了涉及user buffer的兩次I/O,所以性能會(huì)比傳統(tǒng)方式優(yōu)異許多.
簡(jiǎn)單圖示:
2.實(shí)現(xiàn)
強(qiáng)烈建議先閱讀官方文檔:
XSendfile-Nginx官方文檔
- 1.首先需要確保Nginx支持sendfile:
$ sudo vi /etc/nginx/nginx.conf
>>
sendfile on;
- 2.既然涉及PHP的文件傳輸,header不能少:
header('Content-type: application/octet-stream');
// 這里的$s_fileName指的是被下載的文件名
header('Content-Disposition: attachment; filename="' . $s_fileName . '"');
// nginx sendfile
// 這里的$p_file指的是在nginx中約定的訪問路徑
header('X-Accel-Redirect: '.$p_file);
- 3.上一步的
$p_file
并不是指文件的實(shí)際路徑,而是nginx中約定的路由,所以需要配置nginx:
// 假設(shè) $p_file = "/demo/download/" . $s_fileName;
// 假設(shè)該文件的實(shí)際路徑為 /var/www/demo/_api.git/var/tmp/
location /demo/download {
internal;
alias /var/www/demo/_api.git/var/tmp/;
}
- 4.重啟一下相關(guān)服務(wù)以加載最新配置:
$ sudo service nginx reload
需要注意的是:
- 1.聲明Xsendfile的
header
必須包含約定的URI; - 2.在配置文件中約定的解析路徑必須被聲明為內(nèi)部調(diào)用(internal),這么做是為了防止外部URI的直接訪問;
- 3.根據(jù)實(shí)際需求選擇解析目錄使用
root
(實(shí)際目錄)還是alias
(虛擬目錄)關(guān)鍵字; - 4.另外Nginx提供了幾個(gè)header控制sendfile的配置:
X-Accel-Limit-Rate: 1024
X-Accel-Buffering: yes|no
X-Accel-Charset: utf-8