2011年3月18日 星期五

Linux Kernel Sendfile() 的提升 Server 效能之路

Standard
Apache 和 Samba 這類伺服器,主要以傳送檔案資料的工作為主,他們最常做的工作不外乎是開啟檔案(Open)、讀取(Read)、寫入網路連線(Write to Socket)。但是以 Kernel 的角度來說,這樣一個讀取檔案資料和傳送出去的流程相當繁複,並擁有最少兩次的 Kernel/User Space 資料搬移, 導致同一筆資料需要經過兩次多餘的複製。舉例來說,若一個檔案有 1MB,則 Linux Kernel 需要多做 2MB 的記憶體複製,使得效能經常消耗在這種地方,尤以 CPU 不夠快的平台上狀況特別明顯。

而過去曾有 khttpd 這樣的實作,讓 Linux Kernel 自成一個小型的 Web Server,提供一個極有效率方式的讀取靜態網站頁面和 Server 服務,其加速的方法,便是於 Kernel Space 讀取檔案並直接從網路連線送出資料,目的也在於減少 Kernel/User space 之間不必要的 context switch。

想瞭解 User Space 和 Kernel 的資料搬移狀況,我們可以來研究應用程式在讀取和傳送網路資料的流程,經簡化後大致上是(以下簡稱 User Space 為 US,Kernel Space 為 KS):
  1. [US] open()
  2. [KS] do_sys_open()
  3. [KS] do_filp_open() - 找到檔案,並從所在的檔案系統取得 struct file
  4. [KS] Return File Object
  5. [US] malloc() - 準備一塊記憶體當 buffer
  6. [US] read(file, buffer)
  7. [KS] vfs_read(file) - 標準 VFS 的檔案讀取 API
  8. [KS] file->f_op->read() - 使用資料所在的檔案系統(filesystem),其提供的低階 read 操作
  9. [KS] copy_to_user(buffer) - 將檔案資料從硬碟讀出來後,複製一份到 user space 的 buffer

接著是將讀到的資料透過網路傳送出去:
  1. [US] write(Socket, buffer) - 將 buffer 內資料傳送出去
  2. [KS] copy_from_user(buffer) - 將 user space 的 buffer 複製回 kernel space
  3. [kS] Send data - 送出資資料

通常,我們不可能學 khttpd,將 Server 都寫進 Kernel,但如果只是單純讀取並將資料透過網路傳送出去,Kernel 提供了 sendfile() system call,這是一種可行的手段以減少資料搬移,除 Linux 之外,其他作業系統也都有提供此 API。我們只要指定 socket file description 和將要送出去的檔案給 sendfile(),它便會幫我們在 Kernel Space 將檔案從硬碟讀取複製出來,不再經過任何多餘 Kernel/User Space copy,直接從 Socket 送出。

sendfile() 在 Linux Kernel 內的運作流程大致上為:
  1. sendfile()
  2. do_sendfile()
  3. do_splice_direct()
  4. file->f_open->file_splice_read()


有撰寫過 Filesystem Driver 應該都知道,file_splice_read() 是為了提供管線(Pipe)而存在的機制,使資料可以在管線中傳送(這部份因超過本文範疇,在此暫不多做討論)。而 sendfile() 的實作便利用了 pipe 機制,使檔案的資料能流入 Socket 的檔案中,不用做多餘的複製。

後記

這次為了某個案子,寫了一支 Filesystem Driver,而該 Filesystem 會被 Samba 存取。實際測試過程中,發現相較於其他常見的 Filesystem(如:FAT),我們的 Filesystem 讀取速度慢了約 27% 左右,後來才發現是 Samba 採用 sendfile(),但我們的 filesystem並未實作 file_splice_read() ,所以都使用一般的方式讀取檔案。補上相關實作後,速度就提升到與其他 filesystem 一樣了。