在 in-kernel HTTP server 中,提到:
"From a technical standpoint,I believe the kernel will be"more of the same" and that all the _really_interesting stuff will be going out in user space."
khttpd 這樣的實作可讓 Linux 核心成為提供讀取靜態網站頁面的服務,減少核心和使用者層級之間的 context switch 和資料複製。
Linux 應用程式在讀取和傳送網路資料的流程,簡化為以下 (K 表示核心, U 表示使用者層級):
file->f_op->read()
- 透過資料所在的檔案系統,使用提供的低階 read 操作接著是將讀到的資料透過網路傳送出去:
Linux 核心提供 sendfile 系統呼叫 (註: BSD 家族也有作業系統提供 sendfile
,但是語意有出入),於是我們只要指定 socket file description 和將要送出去的檔案給 sendfile,後者即可在核心模式中,將檔案自儲存裝置讀取複製出來,不再經過任何多餘核心和使用者層級的資料複製,直接從指定的裝置輸出。
以下逐步探討個別系統呼叫的行為。舉例來說,伺服器讀取一個檔案,然後把檔案資料經由 socket 將資料傳送給客戶端。程式碼包含以下:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
流程示意:
1
到 2
: 當執行 read 函式 (由 glibc 或相似的 libc 提供) 後,read 系統呼叫被觸發,檔案資料會經由 DMA 傳遞於核心模式內的 buffer,然後再由 CPU 將檔案資料搬到使用者層級的 buffer (tmp_buf) 中。3
到 4
: 執行 write 函式後,write 系統呼叫被觸發,藉由 CPU 將使用者層級的 buffer 的資料搬到 socket buffer 裡頭,資料傳遞到 socket buffer 後,會再經由 DMA,將資料送出去給客戶端。討論:
是否可減少使用者層級 buffer 的操作呢?可以,例如透過 mmap 來取代 read:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
流程示意:
1
到 2
: mmap 執行後,一如上述 read 系統呼叫,將檔案資料經由 DMA 複製一份到核心模式的 buffer,但不同的是,read 需要將核心 buffer 複製到使用者層級的 buffer,mmap 卻不會,因為 mmap 的使用者 buffer 和核心模式的 buffer 共享同一塊硬體分頁 (page),因此 mmap 可減少一次 CPU copy。3
到 4
: write 執行後,把核心模式的 buffer 經由 CPU 複製到 socket buffer,再經由 DMA 傳輸到客戶端。討論:
sendfile 系統呼叫的提出,就是克服上述問題,取代 read/write 兩個操作,並減少 context switch 和資料複製的次數:
sendfile(socket, file, len);
流程示意:
1
: sendfile 執行後,檔案資料會經由 DMA 傳遞給核心 buffer,再由 CPU 複製到socket buffer2
: 將 socket buffer 的資料經由 DMA 傳遞到客戶端,所以執行 2 次 DMA Copy 及 1 次的 CPU Copy討論:
可用以下命令查詢網路卡是否支持 scatter-gather 特性:
$ ethtool -k enp5s0 | grep scatter-gather
參考輸出:
scatter-gather: on
tx-scatter-gather: on
tx-scatter-gather-fraglist: off [fixed]
Zero-Copy 流程示意:
1
: sendfile 執行後,檔案資料經由 DMA 傳給核心模式的 buffer,但已不會再把資料複製到 socket buffer,後者只需理會有哪些核心模式的 buffer 的地址及資料長度,因此用 apend 模式。2
: 資料傳給客戶端也用 DMA,但來源變成核心模式 buffer。因此,不需要 CPU 去搬動資料,而是純粹 DMA 搬資料來實現 Zero-Copy。
sendfile 只適用於將資料從檔案拷貝到 socket,同時需要硬體的支援,Linux 核心自 v2.6.17
引入 splice 系統呼叫,不僅不需要硬體支援,還提供兩個 file descriptor (fd) 之間的資料 Zero-Copy。在 Linux 核心 2.6.33 後的版本,sendfile 已不再受限於複製資料到 socket 的操作,可用於任意的檔案。
splice(fd_in, off_in, fd_out, off_out, len, flags);
使用者程式讀寫資料的流程如下:
cat
UNIX 工具的 cat 是 "concatenate" 的意思,該命令具體作用為
concatenate files and print on the standard output
透過上述 sendfile 和 splice 系統呼叫,我們可實作一個更快的 cat,專案: fastcat
用 bench 工具測量:
bench './simple 100-0.txt'
benchmarking ./simple 100-0.txt
time 176.2 ms (169.3 ms .. 189.5 ms)
0.995 R² (0.985 R² .. 1.000 R²)
mean 175.6 ms (173.4 ms .. 179.7 ms)
std dev 4.361 ms (1.680 ms .. 6.660 ms)
variance introduced by outliers: 12% (moderately inflated)
cat
bench 'cat 100-0.txt'
benchmarking cat 100-0.txt
time 2.691 ms (2.658 ms .. 2.719 ms)
0.999 R² (0.999 R² .. 1.000 R²)
mean 2.662 ms (2.650 ms .. 2.675 ms)
std dev 41.02 μs (32.51 μs .. 50.45 μs)
bench './fastcat 100-0.txt'
benchmarking ./fastcat 100-0.txt
time 1.520 ms (1.479 ms .. 1.566 ms)
0.995 R² (0.991 R² .. 0.999 R²)
mean 1.514 ms (1.497 ms .. 1.532 ms)
std dev 63.00 μs (46.37 μs .. 84.38 μs)
variance introduced by outliers: 29% (moderately inflated)
shell 特殊變數:
$?
: 上個執行過的命令所返回的狀態值$$
: 目前 shell 的 process ID$ ls -l /proc/$$/fd
參考輸出:
lrwx------ 1 0 -> /dev/pts/0
lrwx------ 1 1 -> /dev/pts/0
lrwx------ 1 2 -> /dev/pts/0
lrwx------ 1 255 -> /dev/pts/0
l-wx------ 1 3 -> /dev/null