剖析Linux上的kHttpd網頁伺服

剖析Linux上的kHttpd網頁伺服

一,           前言

Linux的各式伺服器應用中,網頁伺服器是許多人都會採用的方案,在過去的應用中,網頁伺服器與作業系統核心是分離的個體,也就是說Linux的核心負責執行程式的排成以及提供網路層的運作機制。而在上層屬於使用者模式的網頁伺服器,主要的功能就是呼叫由核心所提供的系統呼叫來取得連結網路的功能,在這樣的機制下,核心模式與使用者模式的結合應用,建構出了目前各式的網頁伺服器。

        原本把核心與網頁伺服器分離的架構,也是現在幾乎所有作業系統所採取的解決方案,例如Windows 2000+IISFreeBSD  Linux 搭配 Apache或是其他種類的網頁伺服器。不過現在Linux的核心,為了可以加速網頁擷取的效率,對於這樣的架構提出了不同的應用與解決方案。首先把網頁伺服器的功能,建構到了核心的程式碼中,透過這樣的方式無形中也加快了當我們取的網頁時的效率,除此之外,還保留了賦予動態網頁能力的機制在。




二,實際應用

        筆者所使用的Linux核心為2.4.8,需要進行以下的設定

#
# Using defaults found in .config
#
*
* Code maturity level options
*
Prompt for development and/or incomplete code/drivers (CONFIG_EXPERIMENTAL) [Y/n/?]
….….
……………..
*
* Networking options
*
……………..
Kernel httpd acceleration (EXPERIMENTAL) (CONFIG_KHTTPD) [M/n/y/?]


        接著,編譯所設定好的Linux核心,並且載入系統即可。

        由於目前kHTTPd只提供靜態網頁的功能,也就是說如果讀者所使用的網頁伺服器需要用到PHP或是CGI的話,kHTTPd本身將無法提供這樣的機制。所以筆者在同一部伺服器上面也安裝了Apache 1.3.20php-4.0.6,並且把Apache Web ServerPort 號碼設為8080,而把kHTTPd所使用的Port號碼設為80。﹝關於Apache的資訊請參閱www.apache.orgphp的資訊請參閱www.php.org


搭配ApachePHP伺服器

Step1
首先我們先載入khttpd模組,指令如下:
[root@itri /]#    insmod /lib/modules/2.4.8/kernel/net/khttpd/khttpd.o

Step2
設定kHttpd的一些參數,我們現在希望靜態(Static)網頁給kHttpd處理,而動態的網頁,例如php,則由kHttpd丟給Apache去處理,輸入指令如下相關參數請參閱/usr/src/linux/net/khttpd/README )

echo 8080 > /proc/sys/net/khttpd/clientport
echo 80 > /proc/sys/net/khttpd/serverport
echo /var/www > /proc/sys/net/khttpd/documentroot
echo php > /proc/sys/net/khttpd/dynamic
echo shtml > /proc/sys/net/khttpd/dynamic
echo 1 > /proc/sys/net/khttpd/start

Step3
現在我們寫一個靜態的網頁demo.html放在網頁目錄下,網頁內容如下:
<center>
<h1>
Hello ! kHttpd ~~

編輯完之後馬上來看看我們的第一個網頁(靜態)
真的成功了耶!!!真是令人興奮,接著來試試看動態的網頁是否也能夠成功呢?
首先別忘了要把Apachehttpd.conf檔案裡面的Port 設定為8080,之後在啟動Apache

現在我們寫兩個簡單的PHP程式
demo1.php內容如下:
<center>
<form action=demo2.php method=post>
你的大名<input type=text name=uid>
<input type=submit name=send value=OK!>
</form>

demo2.php內容如下:
<center>
<h1>
<? echo “Hello ! “,$uid ; ?>
先看看demo1.php,執行結果如下:
在我們輸入 ”Linuxer man” 後,執行結果如下:

Step4
如果要關閉kHttpd請輸入以下的指令喔!
echo 1 > /proc/sys/net/khttpd/stop
echo 1 > /proc/sys/net/khttpd/unload
rmmod khttpd

效能評估:
        到底kHttpd比一般的User Mode WebServer好在哪裡呢?我們現在來看看一個官方統計資料,其中藍色的是kHttpd,而紅色的為Apache

X軸代表目前的請求數目,Y軸代表每秒完成的請求數目。

從上圖不難發現kHttpd在處理靜態網頁的效率比一般WebServer好太多了,若是我們能利用kHttpd與其他可以處理動態網頁的WebServer結合,那麼將會大大的節省系統的資源喔 !!接下來我們就來看看為什麼kHttpd為什麼執行效率會如此的好呢?




三,系統架構
        如下圖所示,為在Linux環境中透過使用者架設網頁伺服器存取網頁的架構示意圖

使用者端的網頁伺服器,透過80h號中斷呼叫System Call “sys_socketcall”,這個System Call的原始碼位於 “net/socket.c”,這段程式主要的工作是根據系統呼叫所要呼叫的功能,轉由其它的核心函式來執行,主要提供的功能如下












參數
說明
所呼叫的核心函式
SYS_SOCKET
產生新的Socket
sys_socket
SYS_CONNECT
建立TCP/IP連線
sys_connect
SYS_LISTEN
等待連線要求
sys_listen
SYS_ACCEPT
接收連線要求
sys_accept
SYS_SEND
送出TCP/IP封包
sys_send
SYS_SENDTO
送出UDP/IP封包
sys_sendto
SYS_RECV
接收TCP/IP封包
sys_recv
SYS_RECVFROM
接收UDP/IP封包
sys_recvfrom





        因為本篇文章,主要是以Linux 核心所提供的網頁伺服器作為討論的主題,所以筆者在此針對User Mode的網頁伺服器送出與接收封包的核心程式碼部分,做一個介紹。

其實sys_sendsys_recv都是各自透過sys_sendtosys_recvfrom來完成工作的,如下

asmlinkage long sys_send(int fd, void * buff, size_t len, unsigned flags)
{
        return sys_sendto(fd, buff, len, flags, NULL, 0);
}
asmlinkage long sys_recv(int fd, void * ubuf, size_t size, unsigned flags)
{
        return sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);
}

        sys_sendto來說,它會呼叫move_addr_to_kernel來把User Mode的資料拷貝到Kernel Mode,最後再透過呼叫核心函式sock_sendmsg來把封包繼續處理下去。

        同理,sys_recvfrom會呼叫sock_recvmsg來把讀取封包資料,再呼叫move_addr_to_user把核心所讀取到的封包拷貝到User Mode的記憶體中。

        會多花這一層功夫的原因,是因為網頁伺服器本身位於User Mode,若要經由Kernel ModeTCP/IP層來讀取資料,就要透過Socket的介面,來存取由核心所存放的資料。不過因為兩者位於不同的CPU特權等級,例如:User Mode的程式位於Ring 3Kernel Mode的程式位於Ring 0,所以說Ring 3的程式並不能隨意的讀寫位於Ring 0程式所掌握的記憶體,而位於Ring 0的程式﹝Kernel Mode﹞,卻可以讀寫Ring 3User Mode﹞的記憶體。因為這樣的特性,如果說我們的網頁伺服器是屬於User Mode的應用程式,每一次的收送封包資料,都要進行一次User ModeKernel Mode資料的拷貝與交換,對於系統效能上也有一定程度的影響。


        所以說,一旦我們把網頁伺服器置入了核心的程式碼中,讓它在位於Kernel Mode的記憶體中運作,將可以省去透過觸發80號中斷與拷貝User ModeKernel Mode過程的資源耗費。如下圖所示


當然囉,我們也透過檢示kHTTPd的原始碼來驗證我們的觀點。首先在原始碼檔案 “net/khttpd/main.c” 中,我們可以看到khttpd的初始化函式khttpd_init()

int __init khttpd_init(void)
{
……..….
(void)kernel_thread(ManagementDaemon,NULL, CLONE_FS | CLONE_FILES | CLONE_SIGHAND);
..…..
}

會產生函式ManagementDaemonKernel Mode Thread,在函式ManagementDaemon中會呼叫函式StartListening,進行以下的動作

./usr/src/linux/net/khttpd/sockets.c

int StartListening(const int Port)
{
..…….
error = sock_create(PF_INET,SOCK_STREAM,IPPROTO_TCP,&sock);
………
error = sock->ops->bind(sock,(struct sockaddr*)&sin,sizeof(sin)); 
…….
error=sock->ops->listen(sock,48);
………
}

如果比較,User Mode應用程式透過80h號中斷所觸發的核心函式sys_bindsys_listen,如下

asmlinkage long sys_bind(int fd, struct sockaddr *umyaddr, int addrlen)
{
  ..…..
   if((sock = sockfd_lookup(fd,&err))!=NULL)
      {
      if((err=move_addr_to_kernel(umyaddr,addrlen,address))>=0)
       err = sock->ops->bind(sock, (struct sockaddr *)address,addrlen);
     sockfd_put(sock);
     }
 return err;
}



asmlinkage long sys_listen(int fd, int backlog)
{
        struct socket *sock;
        int err;
                                                                               
        if ((sock = sockfd_lookup(fd, &err)) != NULL) {
                if ((unsigned) backlog > SOMAXCONN)
                        backlog = SOMAXCONN;
                err=sock->ops->listen(sock, backlog);
                sockfd_put(sock);
        }
        return err;
}


        我們不難發現,其實sys_bind sys_listem所做的工作都是直接透過sock->ops來執行bindlisten的函式。所以說位於Kernel modekhttpdUser ModeWeb Server比較的話,主要的不同在於khttpd的核心伺服器可以更為有效的減低User ModeKernel Mode彼此運作的資源耗費,而增加系統的運作效能。


















四,kHttpd程式架構
現在我們更進一步的來了解kHttpd的程式流程,首先我們先來看看kHttpd的架構:(khttpd架構圖.bmp)

由上圖我們可以發現在kHttpd的架構中,若請求為靜態(Static)網頁,則由kHttpd服務,相反的若請求為動態(Dynamic)網頁,則將請求丟給User Mode Web Server

現在我們來深入研究kHttpd的程式流程,我們大概可以分為五個步驟來討論:
1.載入模組(Module)
2.啟動kHttpd
3.執行期間
4.關閉kHttpd
5.移除模組(Module)

1.    載入模組
當我們key in完載入模組的指令之後,系統會呼叫位於main.c裡的module_init(khttpd_init) ,透過此系統提供的函式再呼叫位於main.c中的int __init khttpd_init(void){}函式來初始化kHttpd

int __khttpd_init(void)函式中載入khttpd支援的Mime-types,以及一些屬於Dynamic的種類,接著再執行 kernel_thread()系統呼叫產生一thread執行static int ManagementDaemon(void *unused)函式,我們稱此TreadManagement Daemon
如下圖(khttpd_init_call_managementdaemon.bmp)

int __init khttpd_init(void)程式碼解說:
第2行:增加Mime型態
第4行:增加Dynamic型態
第6行:產生一Tread執行int ManagementDaemon(void *unused)

接著由Management Daemon這支Tread執行static int ManagementDaemon(void *unused) ,如下圖:(managementdaemon.bmp)
static int ManagementDaemon(void *unused)程式碼解說:
7行:等待使用者啟動
10行:ManagementDaemon開始傾聽Port
13~15行:初始化三個queue
17行:clean all queue
21~26行:進入等待使用者輸入stop的迴圈,並且產生兩TreadMainDaemon0MainDaemon1
28行:當使用者輸入stop後,停止傾聽StopListening();
32行:當使用者移除模組時,結束Management Daemon

2.    啟動kHttpd

當我們啟動khttpd時,Management Daemon會產生兩Tread,我們稱MainDaemon0
MainDaemon1,用來處理使用者的請求。如下圖
( managementdaemon_call_maindaemon.bmp)

MainDaemon0MainDaemon1這兩個Tread執行的函式為static int MainDaemon (void *cpu_pointer),這個函式主要的功能為判斷四個Queue中是否有請求需要執行。

程式執行到目前為止,有三個Daemon在負責kHttpd的請求,架構圖如下:
(tree_tread.bmp)
3.    執行期間

kHttpd執行的期間可能碰到的情況有下列幾種:
a.       請求靜態網頁
(1)   請求的檔案存在於Clientcache中,並且Server端此檔案沒有更改過
(2)   請求新的檔案
b.      請求動態網頁
c.       請求網頁檔名找不到

我們一個一個的來看看程式的流程是如何?
我們可以從int MainDaemon(void *cpu_poing)函式中知道MainDaemon0MainDaemon1一直在測試四個Queue中是否有請求需要執行,一但有請求被Management Daemon接收,則MainDaemon會進入int WaitForHeaders(CPUNR)函式,如下圖:(waitqueue_decode.bmp)


int WaitForHeaders(CPUNR)函式主要功能為下:
1.      解析使用著請求的Http Request表頭資訊。
2.      判斷出請求為靜態或動態,若靜態則由kHttpd服務,若動態則由User Mode Web Server服務。

int WaitForHeaders(CPUNR)程式碼解說:
2行:呼叫DecodeHeader函式來解析表頭資訊。
4~8行:變數IsForUserSpace!=0表示此請求丟給UserspaceQueue
9~12行:變數IsForUserSpace==0表示此請求丟給DataSendingQueue

static int DecodeHeader(const int CPUNR, struct http_request *Request)程式碼解說:
2行:檢查http表頭資訊。
3行:檢查URL是否含有不合法的字元,請求的檔案是否存在。
4行:檢查請求的Mime型態。
5~10行:若請求的Mime型態未知,則此請求丟給User Mode Web Server,利用變數IsForUserSpace
11~15行:若URL函不合法字元,或請求檔案不存在,則此請求丟給User Mode Web Server,利用變數IsForUserSpace
16~28行:請求由kHttpd服務,kHttpd送出表頭。
18~22行:請求的檔案存在於使用者的cache中,並且檔案沒有更新過,因此送出快取區的副本。
23~27行:請求的檔案不存在於Client端的cache中,或存在於Client端的cache中,但是檔案已經更新過。

以上的流程是請求(Request)經由MainDaemon呼叫int WaitForHeaders(CPUNR)函式來處理,處理之後會有幾種情形。
1.     請求為靜態網頁且存在於Clientcache,而且檔案最近沒有更新過,則MainDaemon直接送出快取區的副本,如下圖(stack_send_httpdheader.bmp),由Send304()呼叫SendBuffer()

2.      若請求為請求新的檔案時,則MainDaemon呼叫SendHTTPHeader(Request)送出HTTP表頭,並且經由DataSending(CPUNR)函式呼叫到SendBuffer_async()函式來送出資料,如下圖(stack_send_httpheader.bmp)
3.      若請求為動態網頁或檔名找不到,則MainDaemon呼叫int Userspace(const int CPUNR)將請求丟給User Mode Web Server


int Userspace(const int CPUNR)程式碼解說:
2行:由AddSocketToAcceptQueue回傳值判斷是否有User Mode Web Server存在。
4行:請求已丟給User Mode Web Server,刪除此請求。
8~9行:不存在User Mode Web Server,送出Http錯誤訊息,並刪掉此請求。

static int AddSocketToAcceptQueue(struct socket *sock,const int Port)
程式碼解說:
此函式主要功能在於偵測是否有User Mode Web Server若有則將請求丟給User Mode Web Server
4.      關閉kHttpd

當使用者key in停止kHttpd的指令(stop)之後,如圖(MainDaemon.bmp)第二行的變數sysctl_khttpd_stop將被設為1,因此MainDaemon0MainDaemon1將跳出迴圈並且結束此函式,而此時Management Daemon並不會結束,直到使用者key in指令(unload)之後,如圖(managementdaemon.bmp)Management Daemon才會跳出迴圈結束此函式。
5.    移除模組

當使用者key in移除模組的指令後,系統會呼叫位於main.c裡面的module_exit(khttpd_cleanup)kHttpd模組從系統中移除。

因為kHttpd會先處理掉Static的網頁,而再將Dynamic的網頁交由User Mode Web Server處理,因此可以大大增加服務的效率!!

六,   參考資料
[2]Linux kernel 2.4.8kHttpd source code (/usr/src/linux/net/khttpd/)