新聞中心
splice 原理重溫
我們先來回顧一下 splice 的原理:

創(chuàng)新互聯(lián)專注于尚志網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗。 熱誠為您提供尚志營銷型網(wǎng)站建設(shè),尚志網(wǎng)站制作、尚志網(wǎng)頁設(shè)計、尚志網(wǎng)站官網(wǎng)定制、重慶小程序開發(fā)服務(wù),打造尚志網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供尚志網(wǎng)站排名全網(wǎng)營銷落地服務(wù)。
如上圖所示,使用 splice? 拷貝數(shù)據(jù)時,需要通過管道作為中轉(zhuǎn)。splice? 首先將 頁緩存? 綁定到 管道? 的寫端,然后通過 管道? 的讀端讀取到 頁緩存? 的數(shù)據(jù),并且拷貝到 socket 緩沖區(qū)中。
管道有個 環(huán)形緩沖區(qū)?,這個 環(huán)形緩沖區(qū)? 需要綁定真實的物理內(nèi)存頁。而 splice 就是將管道的 環(huán)形緩沖區(qū)? 綁定到文件的 頁緩存,如下圖所示:
通過將文件頁緩存綁定到管道的環(huán)形緩沖區(qū)后,就可以通過管道的讀端讀取文件頁緩存的數(shù)據(jù)。
splice 代碼實現(xiàn)
splice 的使用過程,要將文件內(nèi)容發(fā)送到客戶端連接的步驟如下:
首先,使用splice() 系統(tǒng)調(diào)用將文件的內(nèi)容與管道綁定。
然后,使用splice() 系統(tǒng)調(diào)用將管道的數(shù)據(jù)拷貝到客戶端連接 socket。
我們先來看看 splice() 系統(tǒng)調(diào)用的實現(xiàn),代碼如下:
asmlinkage
( fd_in, *off_in,
fd_out, *off_out,
len, flags)
{
error;
*, *;
fput_in, fput_out;
...
error = -EBADF;
in = fget_light(fd_in, &fput_in); // 1. 獲取數(shù)據(jù)輸入方文件對象
(in) {
(in->f_mode & FMODE_READ) {
out = fget_light(fd_out, &fput_out); // 2. 獲取數(shù)據(jù)輸出方文件對象
(out) {
(out->f_mode & FMODE_WRITE)
// 3. 調(diào)用 do_splice() 函數(shù)進(jìn)行下一步操作
error = do_splice(in, off_in, out, off_out, len, flags);
fput_light(out, fput_out);
}
}
fput_light(in, fput_in);
}
error;
}
splice()? 系統(tǒng)調(diào)用主要調(diào)用 do_splice()? 函數(shù)進(jìn)行下一步處理,我們來分析一下 do_splice()? 函數(shù)的實現(xiàn)。do_splice() 函數(shù)主要分兩種情況進(jìn)行處理,代碼如下:
(struct file *in, *off_in,
struct file *out, *off_out,
len, flags)
{
*;
offset, *off;
ret;
// 情況1: 如果輸入端是一個管道?
pipe = pipe_info(in->f_path.dentry->d_inode);
(pipe) {
...
// 調(diào)用 do_splice_from() 函數(shù)管道數(shù)據(jù)拷貝到目標(biāo)文件句柄
ret = do_splice_from(pipe, out, off, len, flags);
...
ret;
}
// 情況2: 如果輸出端是一個管道?
pipe = pipe_info(out->f_path.dentry->d_inode);
(pipe) {
...
// 調(diào)用 do_splice_to() 函數(shù)將文件內(nèi)容與管道綁定
ret = do_splice_to(in, off, pipe, len, flags);
...
ret;
}
-EINVAL;
}
如上面代碼所示,do_splice() 函數(shù)分兩種情況處理,如下:
如果輸入端是一個管道,則調(diào)用do_splice_from() 函數(shù)進(jìn)行處理。
如果輸出端是一個管道,則調(diào)用do_splice_to() 函數(shù)進(jìn)行處理。
下面我們分別來說明這兩種情況的處理過程。
1. 輸入端是一個管道
如果輸入端是一個管道(也就是說從管道拷貝數(shù)據(jù)到輸出端句柄),那么將會調(diào)用 do_splice_from()? 函數(shù)進(jìn)行處理,do_splice_from() 函數(shù)的實現(xiàn)如下:
(struct pipe_inode_info *pipe, struct file *out,
*ppos, len, flags)
{
...
out->f_op->splice_write(pipe, out, ppos, len, flags);
}
如果輸出端是一個普通文件,那么 out->f_op->splice_write()? 將會指向 generic_file_splice_write()? 函數(shù)。如果輸出端是一個 socket,那么 out->f_op->splice_write()? 將會指向 generic_splice_sendpage() 函數(shù)。
下面將以 generic_file_splice_write()? 函數(shù)作為分析對象,generic_file_splice_write()? 函數(shù)會調(diào)用 __splice_from_pipe() 進(jìn)行下一步處理,如下所示:
(struct pipe_inode_info *pipe, struct file *out,
*ppos, len, flags)
{
...
ret = __splice_from_pipe(pipe, &sd, pipe_to_file);
...
ret;
}
我們接著來分析 __splice_from_pipe() 函數(shù)的實現(xiàn):
__splice_from_pipe(struct pipe_inode_info *pipe, struct splice_desc *sd,
splice_actor *actor)
{
...
(;;) {
(pipe->nrbufs) {
// 1. 獲取管道環(huán)形緩沖區(qū)
* = -> + ->;
* = ->;
...
// 2. 把管道環(huán)形緩沖區(qū)的數(shù)據(jù)拷貝到輸出端文件。
// 其中 actor 指針指向 pipe_to_file() 函數(shù),由 generic_file_splice_write() 函數(shù)傳入
err = actor(pipe, buf, sd);
(err <= 0) {
(!ret && err != -ENODATA)
ret = err;
;
}
...
}
...
}
...
ret;
}
對 __splice_from_pipe() 函數(shù)進(jìn)行簡化后,邏輯就很簡單。主要過程如下:
獲取管道環(huán)形緩沖區(qū)(管道的實現(xiàn)可以參考《圖解 | Linux進(jìn)程通信 - 管道實現(xiàn)》一文)。
調(diào)用pipe_to_file() 函數(shù)把管道環(huán)形緩沖區(qū)的數(shù)據(jù)拷貝到輸出端的文件中。
所以,輸入端是一個管道的調(diào)用鏈如下:
sys_splice()
└→ do_splice()
└→ do_splice_from()
└→ generic_file_splice_write()
└→ __splice_from_pipe()
└→ pipe_to_file()
2. 輸出端是一個管道
如果輸出端是一個管道(也就是說將輸入端與管道綁定),那么將會調(diào)用 do_splice_to()? 函數(shù)進(jìn)行處理,do_splice_to() 函數(shù)的實現(xiàn)如下:
static long
do_splice_to(struct file *in, loff_t *ppos, struct pipe_inode_info *pipe,
size_t len, unsigned int flags)
{
...
return in->f_op->splice_read(in, ppos, pipe, len, flags);
}
如果輸入端是一個普通文件,那么 in->f_op->splice_read()? 將會指向 generic_file_splice_read()? 函數(shù)。如果輸出端是一個 socket,那么 in->f_op->splice_read()? 將會指向 sock_splice_read() 函數(shù)。
下面將以 generic_file_splice_read()? 函數(shù)作為分析對象,generic_file_splice_read()? 函數(shù)會調(diào)用 __generic_file_splice_read() 進(jìn)行下一步處理,如下所示:
static int
__generic_file_splice_read(struct file *in, loff_t *ppos,
struct pipe_inode_info *pipe,
size_t len, unsigned int flags)
{
...
struct page *pages[PIPE_BUFFERS];
struct splice_pipe_desc spd = {
.pages = pages,
...
};
...
// 1. 查找已經(jīng)存在頁緩存的頁面
spd.nr_pages = find_get_pages_contig(mapping, index, nr_pages, pages);
index += spd.nr_pages;
...
// 2. 如果有些頁緩存還不存在,那么申請新的頁緩存
while (spd.nr_pages < nr_pages) {
page = find_get_page(mapping, index);
...
pages[spd.nr_pages++] = page;
index++;
}
// 3. 如果頁緩存與硬盤的數(shù)據(jù)不一致,那么先從硬盤同步到頁緩存
for (page_nr = 0; page_nr < nr_pages; page_nr++) {
...
page = pages[page_nr];
...
if (!PageUptodate(page)) {
...
error = mapping->a_ops->readpage(in, page); // 從硬盤讀取數(shù)據(jù)
...
}
...
spd.nr_pages++;
index++;
}
...
// 4. 將頁緩存與管道綁定
if (spd.nr_pages)
return splice_to_pipe(pipe, &spd);
return error;
}
__generic_file_splice_read()? 函數(shù)的代碼比較長,為了更易于分析,所以對其進(jìn)行了精簡。從精簡后的代碼可以看出,__generic_file_splice_read() 函數(shù)主要完成 4 個步驟:
查找要綁定的頁緩存是否已經(jīng)存在(已經(jīng)從硬盤同步到頁緩存)。
如果還有沒有同步到內(nèi)核的頁緩存,那么申請新的頁緩存。
如果頁緩存與硬盤的數(shù)據(jù)不一致,那么先從硬盤同步到頁緩存。
調(diào)用splice_to_pipe() 函數(shù)將頁緩存與管道綁定。
所以最終會調(diào)用 splice_to_pipe()? 函數(shù)將頁緩存與管道綁定,我們來看看 splice_to_pipe() 函數(shù)的實現(xiàn):
ssize_t
splice_to_pipe(struct pipe_inode_info *pipe, struct splice_pipe_desc *spd)
{
unsigned int spd_pages = spd->nr_pages;
int ret, do_wakeup, page_nr;
...
for (;;) {
...
if (pipe->nrbufs < PIPE_BUFFERS) {
// 指向管道環(huán)形緩沖區(qū)當(dāng)前位置
int newbuf = (pipe->curbuf + pipe->nrbufs) & (PIPE_BUFFERS - 1);
struct pipe_buffer *buf = pipe->bufs + newbuf;
// 將環(huán)形緩沖區(qū)與頁緩存綁定
buf->page = spd->pages[page_nr];
buf->offset = spd->partial[page_nr].offset;
buf->len = spd->partial[page_nr].len;
buf->private = spd->partial[page_nr].private;
buf->ops = spd->ops;
if (spd->flags & SPLICE_F_GIFT)
buf->flags |= PIPE_BUF_FLAG_GIFT;
pipe->nrbufs++;
page_nr++;
ret += buf->len;
...
if (pipe->nrbufs < PIPE_BUFFERS)
continue;
break;
}
...
}
...
return ret;
}
splice_to_pipe() 函數(shù)代碼雖然比較長,但是邏輯很簡單,就是將管道的環(huán)形緩沖區(qū)與文件的頁緩存進(jìn)行綁定,這樣就能過通過管道的讀端來讀取頁緩存的數(shù)據(jù)。
所以,輸出端是一個管道的調(diào)用鏈如下:
sys_splice()
└→ do_splice()
└→ do_splice_to()
└→ generic_file_splice_read()
└→ __generic_file_splice_read()
└→ splice_to_pipe()
總結(jié)
本文主要介紹了 splice? 的原理與實現(xiàn),splice? 是 零拷貝技術(shù)? 的一種實現(xiàn)。希望通過本文,能夠讓讀者對 零拷貝技術(shù) 有更深入的理解。
名稱欄目:一文讀懂零拷貝技術(shù)
本文網(wǎng)址:http://m.fisionsoft.com.cn/article/dpdeeci.html


咨詢
建站咨詢
