Try   HackMD

2020q1 Homework4 (khttpd)

contributed by < haogroot >

tags: linux2020

測試環境

Kernel version: 5.3.0-40-generic
OS: Ubuntu 19.10
CPU model: Intel® Core™ i7-8565U CPU @ 1.80GHz

自我檢查

  • 參照 fibdrv 作業說明 裡頭的「Linux 核心模組掛載機制」一節,解釋 $ sudo insmod khttpd.ko port=1999 這命令是如何讓 port=1999 傳遞到核心,作為核心模組初始化的參數呢?

在 khttpd_main.c 中,用到巨集 module_param

module_param(port, ushort, S_IRUGO);

在 linux 裏面的原始定義在 include/linux/moduleparam.h

#define module_param(name, type, perm)				\
	module_param_named(name, name, type, perm)
  • name 為 kernel module parameter name 。
  • type 為 parameter 的 data type 。
  • perm 則代表在 sysfs 中的 visibility (0444 代表 world-readable, 0644 代表 root-writable)。

因此可以理解為建立了一個 ushort type 的變數 port 做為 kernel module parameter。

若將上述巨集展開可得 module_param_named(port, port, ushort, S_IRUGO),以下摘自include/linux/moduleparam.h

#define module_param_named(name, value, type, perm)			   \
	param_check_##type(name, &(value));				   \
	module_param_cb(name, &param_ops_##type, &value, perm);		   \
	__MODULE_PARM_TYPE(name, #type)

再將上述展開可得

param_check_ushort(port, &(port));
module_param_cb(port, &param_ops_ushort, &port, perm);
__MODULE_PARM_TYPE(port, #ushort)

param_check_ushort 是在 compile-time 時候做 type-checking,
module_param_cb 是一個 callback function,繼續將其展開可得 __module_param_call(MODULE_PARAM_PREFIX, port, &param_ops_ushort, &port, perm),發現他用於註冊 module parameter。

以下摘自 include/linux/moduleparam.h:

/* This is the fundamental function for registering boot/module
   parameters. */
#define __module_param_call(prefix, name, ops, arg, perm, level, flags)	\
	/* Default value instead of permissions? */			\
	static const char __param_str_##name[] = prefix #name;		\
	static struct kernel_param __moduleparam_const __param_##name	\
	__used								\
    __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \
	= { __param_str_##name, THIS_MODULE, ops,			\
	    VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }

參照 fibdrv 作業說明 裡頭的「Linux 核心模組掛載機制」一節,MODULE_XXX 系列的巨集在最後都會被轉變成

static const char 獨一無二的變數[] = "操作 = 參數"

並且透過 __attribute__ 告訴編譯器,將這個變數放在 __param 段中,這部份我們可以透過 objdump -s khttpd.ko -j __param 來發現確實有名為 __param 的 section。

透過 strace 來追蹤 insert khttpd module 的過程:

$ sudo strace insmod khttpd.ko port=1999

可以看到以下 log:

finit_module(3, "port=1999", 0)         = 0

可以參照 fibdrv 作業說明 裡頭的「Linux 核心模組掛載機制」一節,有描述如何載入 module 的過程。

一開始我們將 perm 設為 S_IRUGO,即為 444,代表此 parameter 為 world-readable ,在 insert kernel module 過後,我們可以從 sysfs 內得知 module parameter port 的值,透過以下命令驗證確實可以得到 port 的值:

$ cat /sys/module/khttpd/parameters/port
  • 參照 CS:APP 第 11 章,給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Reference: CS:APP ch11 slides

CS:APP 第 11 章中範例中 open_listenfd() 與 kHTTPd 中 open_listen_socket() 都是處理一樣的任務,最終都會產生一個可以監聽是否有新連線要求的 file descriptor。

但在 CS:APP 第 11 章中提供的 server 範例中, server 每次只能處理一個 client ,書中稱之為 Iterative Server,而 kHTTPd 則能夠同時處理多個 client 連線,這樣的 server 稱之為 Concurrent Server.

kHTTPd 透過 kernel thread 來負責監聽是否有來自 client 的連接需求,使得 kHTTPd 能夠同時處理多個 client。

    http_server = kthread_run(http_server_daemon, &param, KBUILD_MODNAME);

每當有新連線建立,再透過新的 kernel thread 負責處理與每個 client 間資料的交換。

int http_server_daemon(void *arg)
{
    ...

    while (!kthread_should_stop()) {
        int err = kernel_accept(param->listen_socket, &socket, 0);
        ...
        worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME);
        ...
    }
    return 0;
}
  • htstress.c 用到 epoll 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何?

Benchmarking 命令如下,執行總共 10,000 個 request,同時只有 1 個連線,開啟 4 個 thread (根據你的 cpu cores 開啟幾個 thread ),針對 http://localhost:8081/ 進行測試。

# run HTTP benchmarking
$ ./htstress -n 100000 -c 1 -t 4 http://localhost:8081/

先觀察 htstress.c 的行為,在 htstress.c 中開啟 threads,每個 thread 均執行 worker

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
為什麼要執行 worker(0)

若是直接透過 pthread_create 新建所有需要的 thread 時,會發生 main thread 繼續往下執行,並沒有等待所有的 thread 執行結束,這樣的狀況我們也無法統計結果。 若是透過 pthread_exit()pthread_join() 來等待所有 thread 執行完畢,這樣狀況下 main thread 也會被 block 住造成資源的浪費,因此在這裡新建了 ( num_threads-1 ) 個 thread,同時也讓 main thread 執行 worker(0) ,使得所有 thread 都充份用來做 benchmarking 。

    /* run test */
    for (int n = 0; n < num_threads - 1; ++n)
        pthread_create(&useless_thread, 0, &worker, 0);

    worker(0);

接著我們來看 static void *worker(void *arg)
epoll_create : 產生 epoll 專用的 file descriptor,裏面所帶參數須大於 0,自從 Linux 2.6.8 後,裏面參數已不代表任何意義。
epoll_wait : wait for an I/O event on an epoll file descriptor.

static void *worker(void *arg)
{
    epoll_create();
    init_conn();
    ...
}

init_conn() :負責建立連線並新增 EPOLLOUT 到 epoll 的 interest list , EPOLLOUT 代表 file is available for write operations.

static void init_conn(int efd, struct econn *ec)
{
    struct epoll_event evt = {
        .events = EPOLLOUT,
        .data.ptr = ec,
    };

    if (epoll_ctl(efd, EPOLL_CTL_ADD, ec->fd, &evt)) {
        ...
    }
}

接著 epoll_wait 會等待發生在 epoll file descriptor 的 I/O event ,前面在 init_conn 我們已經註冊 EPOLLOUT ,所以只要 file descriptor 準備可以去寫入了,epoll_wait 就會將準備好的 file descriptors 返回。

返回後開始將資料寫入 socket ,並且將關注的 event type 修改為 EPOLLIN

static void *worker(void *arg)
{
    for (;;) {
        do {
            nevts = epoll_wait(efd, evts, sizeof(evts) / sizeof(evts[0]), -1);
        } while (!exit_i && nevts < 0 && errno == EINTR);

        for (int n = 0; n < nevts; ++n) {
            if (evts[n].events & EPOLLOUT) {
                ret = send(ec->fd, outbuf + ec->offs, outbufsize - ec->offs, 0);
                ...                    
                if (ec->offs == outbufsize) {
                    evts[n].events = EPOLLIN;
                    evts[n].data.ptr = ec;

                    if (epoll_ctl(efd, EPOLL_CTL_MOD, ec->fd, evts + n)) {
                        ...
                    }
                }
            }
            ...
        }
    }
}

接著當 socket 有資料可以讀時, epoll_wait 返回 file descriptor ,接著即可從中讀取資料,透過判斷式 if (c == '4' || c == '5') 來確認是否是 bad request ,在 HTTP response code 定義中, 400 - 499 代表 client errors , 500 - 599 代表 server errors 。

recv 返回 0 時,代表 socket 被關閉或是沒有資料可讀時,會關閉 file descriptor,接著透過 atomic_fetch_subatomic_fetch_add 來操作 multi-thread 共用的變數 num_requests , good_requestsbad_requests

如果沒有達到 max_request , 就會再次使用 init_conn() 來建立新連線,藉此不斷測試。

            else if (evts[n].events & EPOLLIN) {
                for (;;) {
                    ret = recv(ec->fd, inbuf, sizeof(inbuf), 0);

                    if (ret <= 0)
                        break;

                    if (ec->offs <= 9 && ec->offs + ret > 10) {
                        char c = inbuf[9 - ec->offs];
                        if (c == '4' || c == '5')
                            ec->flags |= BAD_REQUEST;
                    }
                        ...
                }
                if (!ret)
                {
                    int m = atomic_fetch_add(&num_requests, 1);

                    if (max_requests && (m + 1 > (int) max_requests))
                        atomic_fetch_sub(&num_requests, 1);
                    else if (ec->flags & BAD_REQUEST)
                        atomic_fetch_add(&bad_requests, 1);
                    else
                        atomic_fetch_add(&good_requests, 1);

                    if (max_requests && (m + 1 >= (int) max_requests)) {
                        end_time();
                        return NULL;
                    }

                    if (ticks && m % ticks == 0)
                        printf("%d requests\n", m);

                    init_conn(efd, ec);
                }
            }
  • epoll 作用為何

在 Linux 設計哲學中,所有的東西都是 file ,所以 socket 也是 file ,如果我們對 socket 進行讀寫,就相當於操作 I/O 。

今天如果要進行 http benchmarking ,在 user space 的 process 是無法直接操作 I/O ,必須透過 system call 要求 kernel 協助 I/O 操作 (在 worker() 中即使用 send()recv() 來對 socket 讀寫)。

而 I/O 操作又分為以下兩種,我們以讀取資料為例:

  1. blocking I/O : 如果資料還沒準備好,則 process 會被 block 住而進入睡眠,直到資料準備好後才會喚醒 process 並將資料從 kernel space 複製到 user space 。
  2. non-blocking I/O : 當資料還沒準備好, 會馬上返回錯誤而 process 不會被 block 住。若是資料準備好了,則馬上將資料複製到 user space 。

如果今天是 non-blocking I/O ,一但沒有資料,變成需要不斷去操作 I/O 直到資料準備好,相當浪費 cpu 時間,這作法顯然不實際;
而若是使用 blocking I/O ,雖然我們可以透過 multi-thread 方式來應付每個 I/O ,但這樣會造成資源的浪費,與更多 context-switching 等影響。
因此 linux kernel 提供了 I/O multiplexing 機制來解決這種問題,提供方法讓單一 process 可以同時監控多個 file descriptor ,一旦某個 file descriptor 準備好,就通知 process 來進行讀寫操作, Linux 提供了 3 種這樣的機制,分別為 poll, selectepoll

epoll v.s. select / poll

  • epoll 在 epoll_wait() 時候可以隨時新增、修改或刪除 file descriptors
  • epoll 只會返回準備好的 file descriptor , select / poll 必須要逐一確認 file descriptor
  • epoll 的可移植性較差,並不是所有系統都支援。

Reference

  1. 淺談I/O Model
  2. I/O Multiplexing: The select and poll Functions
  3. LINUX – IO MULTIPLEXING – SELECT VS POLL VS EPOLL

kHTTPd 缺失

kthread_stop 檢查

kernel/thread.c 中,提到兩種狀況下 kernel thread 會終止

  1. the function in thread to run 自己呼叫 do_exit()
  2. return when kthread_should_stop() return true (若 kthread_stop() 被呼叫,kthread_should_stop() 就會回傳 true)

觀察 kHTTPd server 的程式碼,透過 khttpd_init() 指定建立一個 kernel thread 執行 http_server_daemon()
http_server_daemon() 負責接收來自連線要求,並且為新連線建立 kernel thread 執行 http_server_worker()khttp_server_daemon() 會不斷確認 kthread_should_stop() 是否有 return true ,如果有的話這個 thread 也會終止並返回 0。

int http_server_daemon(void *arg)
{
    while (!kthread_should_stop()) {
        int err = kernel_accept(param->listen_socket, &socket, 0);
        ...
        worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME);
        ...
    }
    return 0;
}

而在 khttpd_exit() 時就會呼叫 kthread_stop() ,因此我們可以確認在 module 被移除時, 在 khttpd_init() 所建立的 kernel thread 會被正確停止 ,但這邊有一個缺失需要修正,在 kernel/kthread.c 原始碼 裡頭 kthread_stop() 的註解提到:

If threadfn() may call do_exit() itself, the caller must ensure task_struct can't go away.

如果要 stop thread 之前,必須確認該 thread 還存在,因此應該在移除前做檢查,修正為以下程式碼:

static void __exit khttpd_exit(void)
{
    if (http_server)
        kthread_stop(http_server);
}

接著探討在 http_server_daemon 中為每個新連線建立新的 kernel thread 並執行 http_server_worker ,在後者內同樣利用 kthread_should_stop 來檢查,但是在程式碼卻沒有地方呼叫 kthread_stop() 來終止,導致為每個新連線建立的 kernel thread 並沒有被正確釋放。

釋放 kernel thread

如果 kHTTPd module 都已經被移除,那他所建立的 thread 也應該被移除,在 http_server_daemon() 回傳之前應該先將他所建立的 kernel threads 都釋放。

對於正確釋放 kernel thread 有兩個想法可以觀察:

  1. http_server_daemon() 所建立的 thread 可以主動確認 http_server_daemon() 是否已經結束來決定是否該終止自己。
  2. 如果 http_server_daemon() 準備回傳,可以傳送訊號給所有由他所建立的 child thread ,通知他們需要結束自己。

從下方 struct task_struct原始碼定義中看到有相關成員可能是我們可以利用的,如果 parent thread 和 child thread 之間是已經有所連結,那我們前面的兩個方向就有機會達成。

struct task_struct {
    
    ...
    /* Recipient of SIGCHLD, wait4() reports: */
    struct task_struct __rcu	*parent;

    /*
     * Children/sibling form the list of natural children:
     */
    struct list_head		children;
    
    /* The signal sent when the parent dies: */
    int	    pdeath_signal;
    ...
    
    /* PID/PID hash table linkage. */
    struct pid			*thread_pid;
    struct hlist_node		pid_links[PIDTYPE_MAX];
    struct list_head		thread_group;
    
    ...
}

在 trace 過程中看到 group_send_sig_info() 或是 exit_notify_info() 等可以對 process group 或是 relatives 傳送 singal 的 function,或許可以用來處理移除 orphan threads.

  • group_send_sig_info() : send signal info to all the members of a group
  • exit_notify() : Send signals to all our closest relatives so that they know to properly mourn us..

在觀察上面兩個函式時,有提到 thread group 這個概念,開始研究相關資料。

PID type

include/linux/pid.h 中有以下 enum pid_type 定義 :

enum pid_type
{
	PIDTYPE_PID,
	PIDTYPE_TGID,
	PIDTYPE_PGID,
	PIDTYPE_SID,
	PIDTYPE_MAX,
};
  • 每個 process 被建立時,都會有自己的 PID ( process identifier ) 。
  • 一個 process 下可有多個 thread ,因此有 thread group 這個概念, thread group 中所有 thread 的 TGID 都等於 thread group 的主 thread 的 PID ,這樣就可以把 signal 發給指定 PID 內所有的 thread ,而每個 thread 則擁有自己專屬的 PID ,這樣 scheduler 才能夠將各個 thread 獨立排程 。
  • PGID 代表 process group 的 id ,每個 process 都屬於一個 process group ,PGID 等同 process group leader 的 PID ,通常 process group 的第一個成員就是 process group leader 。
  • SID 是 session id 。

以下這張圖可以幫我們更好理解 PIDTGID 的關係。

                      USER VIEW
     <-- PID 43 --> <----------------- PID 42 ----------------->
                         +---------+
                         | process |
                        _| pid=42  |_
                      _/ | tgid=42 | \_ (new thread) _
           _ (fork) _/   +---------+                  \
          /                                        +---------+
    +---------+                                    | process |
    | process |                                    | pid=44  |
    | pid=43  |                                    | tgid=42 |
    | tgid=43 |                                    +---------+
    +---------+
     <-- PID 43 --> <--------- PID 42 --------> <--- PID 44 --->
                     KERNEL VIEW

Reference to https://stackoverflow.com/a/9306150/4545634

根據以上,我們朝 TGID 來著手,在 http_server_daemon 要回傳之前,對整個 thread group 發送訊號來告知他們該結束。
我們先透過這些 thread 的 pidtgid 來確認他們是否屬於同一 thread group,透過 printk, task_pid_nr()task_tgid_nr() 來印出 thread 的 pidtgid

結果如下,可以得知他們不屬於同一個 thread group。

[220765.004939] http_server_worker gets the signal PID: 15635, TGID: 15635 
[220765.004949] khttpd: requested_url = /
[220765.039951] khttpd: requested_url = /favicon.ico
[220788.393210] http_server_daemon PID: 15431, TGID: 15431 

進一步透過 ps 來查看更多詳細資訊:

$ ps -ejf | { head -1; grep khttpd;}
UID        PID  PPID  PGID   SID  C STIME TTY          TIME CMD
root     28369     2     0     0  0 08:20 ?        00:00:00 [khttpd]
root     29477     2     0     0  0 08:28 ?        00:00:00 [khttpd]

這裡看到 PPIDPPID 代表 parent process ID , 是由 parent process 啟動該 process 的。

去查 ppid = 2 是誰,發現他是 kthreaadd

$ ps 2
  PID TTY      STAT   TIME COMMAND
    2 ?        S      0:02 [kthreadd]

再透過 $ ps axjf 可以發現原來許多 process 都是由 kthreadd 所建立的。前面原本預期在 http_server_daemon() 內執行 kthread_run 來建立 kernel thread 並執行 http_server_worker() ,所以他們之間應該會是 parent 與 child process 的關係,但最後發現他們的 parent process 都是 kthreadd

根據 linux kernel development 3rd edition 中 kernel Threads 章節,

Kernel threads are created on system boot by other kernel threads. Indeed, a kernel thread can be created only by another kernel thread.The kernel handles this automatically by forking all new kernel threads off of the kthreadd kernel process.

在這個章節我們使用 kthread_run() 最終會使用 __kthread_create_on_node() ,在後者中就會喚醒 kthreadd 來幫忙建立 kernel thread ,這也是為什麼我們會看到所有 kernel thread 的 parent 都是 ppid 為 2 的 kthreadd

Reference

  1. kthread_run() source code link
  2. kthread_create_on_node() source code link
  3. https://www.win.tue.nl/~aeb/linux/lk/lk-10.html
  4. What are PID and PPID?

allow_signal(int sig) : 讓 kernel thread 知道這個 signal 會被處理,不要 drop 這個 signal.

send_sig 是 Linux v0.11 就存在的 kernel API,在本核心模組用來做「善後」操作,不過這樣的使用有機會縮減,請思考如何進行。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

原本覺得在 khttp_exit() 可以拿掉 send_sig() ,因為透過使用 kthread_stop 可以使得 while (!kthread_should_stop()) 離開,並讓 http_server_daemon 回傳,但沒想到拿掉 send_sig 後, http_server_daemon 卻無法結束。

這個觀察很重要。翻閱 kthread_stopped 原始程式碼,你會注意到 dequeue_signal 的使用,而在 kthreadd 也涉及 signal 的初始化,那為何核心內部的執行緒和 signal 有緊密關聯呢?請繼續思考

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

[實驗一]
設計一個簡單實驗 (實驗程式碼連結),不使用 send_sig() ,可以發現我同樣在 init 時期透過 kthread_run() 來建立 kthread ,並在模組被移除時,僅透過 kthread_stop() 來終止 kthread ,卻沒有碰到如同 kHTTPd 那樣卡住的情況。有兩個差別

  1. http_server_daemon 中有使用 allow_signal()signal_pending()
  • 在實驗中加入 allow_signalsignal_pending 也不會導致 thread 卡住
  1. http_server_daemon 中又建立新的 kthread 。

[實驗二]
另外一個實驗 (實驗程式碼連結)我在 kernel thread 中再去建立新的 kthread ,同樣沒有碰到如同 kHTTPd 一樣卡住無法結束情況,但是碰到了 BUG: unable to handle page fault for address 。接下來翻閱程式碼來尋找方向,這應該是跟 thread 沒有被正確釋放有關。

閱讀 kthread_stop()kthread_create()

kthread_stop

翻閱 kthread_stop 原始程式碼

/**
 * kthread_stop - stop a thread created by kthread_create().
 * @k: thread created by kthread_create().
 *
 * Sets kthread_should_stop() for @k to return true, wakes it, and
 * waits for it to exit. This can also be called after kthread_create()
 * instead of calling wake_up_process(): the thread will exit without
 * calling threadfn().
 *
 * If threadfn() may call do_exit() itself, the caller must ensure
 * task_struct can't go away.
 *
 * Returns the result of threadfn(), or %-EINTR if wake_up_process()
 * was never called.
 */
int kthread_stop(struct task_struct *k)
{
    ...
    struct kthread *kthread;

    // 與 put_task_struct() 相對應,對 atomic variable 進行操作
    get_task_struct(k);    
    
    /*  從 task_struct 來拿到 kthread  */ 
    /* 可以對照於 set_kthread_struct() 來看 */
    kthread = to_kthread(k);  
    /* set tkthreas_should_stop  */
    set_bit(KTHREAD_SHOULD_STOP, &kthread->flags);
    /*  wake up thread whose state is TASK_PARKED and wait it for return */
    kthread_unpark(k);
    /* Wake up a specific process and move it to the set of runnable process */
    wake_up_process(k);
    /* 等待 kthread 結束 */
    wait_for_completion(&kthread->exited);
    ret = k->exit_code;
    /* 最終使用 free_task 來正確釋放 kthread */
    put_task_struct(k);
    ...
    return ret;
}

kthread_create()

kthread_create() 最終會使用 __kthread_create_on_node() ,在後者,會將需要新建的 kthread 內容放進 struct kthtrad_create_info 內,並喚醒 kthreadd_task ,接著會等到 kthreadd_task 將 kthread 建立完成後才返回。

struct task_struct *__kthread_create_on_node( ... )
{
    struct kthread_create_info *create = ...
    wake_up_process(kthreadd_task);
    if (unlikely(wait_for_completion_killable(&done))) {
        ...
        wait_for_completion(&done);
    }
}

kthreadd_task 是一個執行 kthreadd 函式的 kernel thread , kthreadd 專門負責為整個 linux 建立 kernel thread 。

值得注意的是,在 create_kthread() 中,當創立新 kthread 時,預設是不會關注任何 signals ,這也是為什麼在 kHTTPd 中會使用 allow_signal() 來關注特定訊號的原因。

static void create_kthread(struct kthread_create_info *create)
{
    /* We want our own signal handler (we take no signals by default). */
    pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
    ...
}

回到前面的問題,為什麼拿掉 send_sig 會導致 http_server_daemon() 卡住呢?
再回頭觀察 kHTTPd 的程式碼,透過 pr_info() 來觀察,可以發現當 http_server_daemon 啟動後,他停在 kernel_accept()
kernel_accept 最終會呼叫 inet_csk_accept() ,在後者內由於 socket 並不是 non-blocking,因此會再使用 inet_csk_wait_for_connect() 來進入睡眠來等待新連線,觀察下方程式碼,可以發現有四種狀況能夠讓此函數返回,也就代表 kernel_accept() 不會是卡住的狀況。其中一種狀況,就是透過 signal_pending() 來確認是否有收到任何訊號。

/*
 * Wait for an incoming connection, avoid race conditions. This must be called
 * with the socket locked.
 */
static int inet_csk_wait_for_connect(struct sock *sk, long timeo)
{
    ...
    for (;;) {
        prepare_to_wait_exclusive(sk_sleep(sk), &wait,
                                TASK_INTERRUPTIBLE);
        release_sock(sk);
        if (reqsk_queue_empty(&icsk->icsk_accept_queue))
            timeo = schedule_timeout(timeo);
        sched_annotate_sleep();
        lock_sock(sk);
        err = 0;
        if (!reqsk_queue_empty(&icsk->icsk_accept_queue))
            break;
        err = -EINVAL;
        if (sk->sk_state != TCP_LISTEN)
            break;
        err = sock_intr_errno(timeo);
        if (signal_pending(current))
            break;
        err = -EAGAIN;
        if (!timeo)
            break;
        }

    return err;
}

到這邊也能夠釐清我前面的問題:

原本覺得在 khttp_exit() 可以拿掉 send_sig() ,因為透過使用 kthread_stop 可以使得 while (!kthread_should_stop()) 離開,並讓 http_server_daemon 回傳,但沒想到拿掉 send_sig 後, http_server_daemon 卻無法結束。

透過 send_sig() 送出的訊號讓 http_server_daemon() 中的 kernel_accept() 能夠順利回傳。 另外有一點值得注意的是, kthreadd 透過 create_kthread() 建立的 kthread ,預設狀況下是會忽略所有的訊號的,所以若在 http_server_daemon 中沒有透過 allow_signal() 來告知 kthread 要去處理特定訊號的話, send_sig() 同樣不會成功讓 kernel_aceept() 回傳的。

以 cmwq 改寫

程式碼 https://github.com/haogroot/khttpd/commit/18d649dcb94c8a47865065e56ad76c322e90ff8b

benchmarking command:

./htstress -n 100000 -c 1 -t 8 http://localhost:8081/

原始作法量測結果:

requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socker errors: 0 [0%]
seconds: 2.249
requests/sec: 44473.461

以 workqueue 改寫:

requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socker errors: 0 [0%]
seconds: 1.031
requests/sec: 97031.891