Try   HackMD

N07: ktcp

主講人: jserv / 課程討論區: 2025 年系統軟體課程

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 →
返回「Linux 核心設計」課程進度表

khttpd 程式碼導讀

掛載 khttpd 核心模組

掛載 khttpd 時,會執行函式 khttpd_init ,程式碼如下所示:

static int __init khttpd_init(void)
{
    int err = open_listen_socket(port, backlog, &listen_socket);
    if (err < 0) {
        pr_err("can't open listen socket\n");
        return err;
    }
    param.listen_socket = listen_socket;
    http_server = kthread_run(http_server_daemon, &param, KBUILD_MODNAME);
    if (IS_ERR(http_server)) {
        pr_err("can't start http server daemon\n");
        close_listen_socket(listen_socket);
        return PTR_ERR(http_server);
    }
    return 0;
}

khttpd 模組初始化的設定和 kecho 模組相似,但仍然可發現二者不同之處,最明顯在於 khttpd 不使用函式 alloc_workqueue,而用系統預設的 workqueue ,因此之後可討論二者之間的效能差異,以下主要將 khttpd 分成兩個部份

  • open_listen: 建立伺服器並等待連線
  • kthread_run: 用於建立一個立刻執行的執行緒

首先函式 open_listen 的部份,建立 socket 連線的步驟都相同,而這邊有個特別的函式 setsockopt ,以下節錄部份 open_listen 程式碼及 setsockopt 程式碼

static int open_listen_socket(ushort port, ushort backlog, struct socket **res)
{
    ...
    err = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
    if (err < 0)
        goto bail_setsockopt;

    err = setsockopt(sock, SOL_TCP, TCP_NODELAY, 1);
    if (err < 0)
        goto bail_setsockopt;

    err = setsockopt(sock, SOL_TCP, TCP_CORK, 0);
    if (err < 0)
        goto bail_setsockopt;

    err = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, 1024 * 1024);
    if (err < 0)
        goto bail_setsockopt;

    err = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, 1024 * 1024);
    if (err < 0)
        goto bail_setsockopt;
    ...
}

static inline int setsockopt(struct socket *sock,
                             int level,
                             int optname,
                             int optval)
{
    int opt = optval;
    return kernel_setsockopt(sock, level, optname, (char *) &opt, sizeof(opt));
}

這邊要留意判斷 Linux 核心版本,參考 Support Linux v5.8+ (#5)net: remove kernel_setsockopt 發現函式 kernel_setsockopt 在 Linux v5.8 之後已被移除,因此在 khttpd 模組裡有對應不同 Linux 核心版本的實作

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0)

接著研究像是 SOL_SOCKETSOL_TCP 這類設定的意義,分別參考 socket(7) - Linux man pagetcp(7) — Linux manual page ,以下整理 khttpd 所使用到的設定,關於其中 SO_REUSEADDR,可對照 What is the meaning of SO_REUSEADDR (setsockopt option) - Linux?

  • SOL_SOCKET
Setting Description
SO_REUSEADDR 在原本的連線結束後,有使用相同 IP 及 Port 的連線要求出現,讓 socket 可直接重新建立連線
SO_RCVBUF 設定 socket receive buffer 可接收的最大數量
SO_SNDBUF 設定 socket send buffer 可送出的最大數量
  • SOL_TCP
Setting Description
TCP_NODELAY 關閉 Nagle's algorithm — 參考 Best Practices for TCP Optimization in 2019
TCP_CORK 常搭配 TCP_NODELAY 使用,為了避免不斷送出資料量不多 (小於 MSS) 的封包,使用 TCP_CORK 可將資料匯聚並且一次發送資料量較大的封包 — 參考 Is there any significant difference between TCP_CORK and TCP_NODELAY in this use-case?

建立 socket 後,使用函式 kthread_run 建立執行緒並執行函式 http_server_daemon

int http_server_daemon(void *arg)
{
    struct socket *socket;
    struct task_struct *worker;
    struct http_server_param *param = (struct http_server_param *) arg;

    // 登記要接收的 signal
    allow_signal(SIGKILL);
    allow_signal(SIGTERM);

    // 判斷執行緒是否該被中止
    while (!kthread_should_stop()) {
        int err = kernel_accept(param->listen_socket, &socket, 0);
        if (err < 0) {
            // 檢查目前執行緒是否有 signal 發生
            if (signal_pending(current))
                break;
            pr_err("kernel_accept() error: %d\n", err);
            continue;
        }
        worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME);
        if (IS_ERR(worker)) {
            pr_err("can't create more worker process\n");
            continue;
        }
    }
    return 0;
}

整體程式邏輯類似 kecho 核心模組,首先登記 SIGKILLSIGTERM ,接著使用函式 kthread_should_stop 判斷負責執行函式 http_server_daemon 的執行緒是否應該中止,使用函式 kernel_accept 接受 client 連線要求,成功建立後使用函式 kthread_run 建立新的執行緒並且執行函式 http_server_worker

執行 http_server_worker

每條連線都由一個子執行緒負責,該執行緒進入 http_server_worker 後依序完成下列工作:

  1. 設定 HTTP parser 的回呼 (callback) 函式,用於將回應資料傳送給 client
  2. 進入事件驅動的主迴圈,藉由 kthread_should_stop() 檢查是否需要終止
  3. 從 socket 讀取資料
  4. http_parser_execute() 解析請求內容
  5. 連線結束時關閉 socket 並釋放配置的緩衝區等資源
static int http_server_worker(void *arg)
{
    char *buf;
    struct http_parser parser;
    // 設定 callback function
    struct http_parser_settings setting = {
        .on_message_begin = http_parser_callback_message_begin,
        .on_url = http_parser_callback_request_url,
        .on_header_field = http_parser_callback_header_field,
        .on_header_value = http_parser_callback_header_value,
        .on_headers_complete = http_parser_callback_headers_complete,
        .on_body = http_parser_callback_body,
        .on_message_complete = http_parser_callback_message_complete};
    struct http_request request;
    struct socket *socket = (struct socket *) arg;

    allow_signal(SIGKILL);
    allow_signal(SIGTERM);

    buf = kmalloc(RECV_BUFFER_SIZE, GFP_KERNEL);
    if (!buf) {
        pr_err("can't allocate memory!\n");
        return -1;
    }

    request.socket = socket;
    // 設定 parser 初始參數
    http_parser_init(&parser, HTTP_REQUEST);
    parser.data = &request;
    
    // 判斷執行緒是否該被中止
    while (!kthread_should_stop()) {
        // 接收資料
        int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1);
        if (ret <= 0) {
            if (ret)
                pr_err("recv error: %d\n", ret);
            break;
        }
        // 解析收到的資料
        http_parser_execute(&parser, &setting, buf, ret);
        if (request.complete && !http_should_keep_alive(&parser))
            break;
    }
    kernel_sock_shutdown(socket, SHUT_RDWR);
    sock_release(socket);
    kfree(buf);
    return 0;
}

設定回呼函式的部份,主要是用來送出回應 client 的資料,以下為相關函式

static int http_parser_callback_message_complete(http_parser *parser)
{
    struct http_request *request = parser->data;
    http_server_response(request, http_should_keep_alive(parser));
    request->complete = 1;
    return 0;
}

static int http_server_response(struct http_request *request, int keep_alive)
{
    char *response;

    pr_info("requested_url = %s\n", request->request_url);
    if (request->method != HTTP_GET)
        response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501;
    else
        response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY
                              : HTTP_RESPONSE_200_DUMMY;
    http_server_send(request->socket, response, strlen(response));
    return 0;
}

而呼叫以下函式的時機在於解析整個資料後,可在函式 http_parser_execute 裡找到相關實作。

接著探討整個 khttpd 關鍵的函式 http_parser_execute ,其功能就是將收到的資料進行解讀,並傳送給 client

size_t http_parser_execute (http_parser *parser, const http_parser_settings *settings, const char *data, size_t len) { ... for (p=data; p != data + len; p++) { ch = *p; if (PARSING_HEADER(CURRENT_STATE())) COUNT_HEADER_SIZE(1); reexecute: switch (CURRENT_STATE()) { ... case s_start_req: { if (ch == CR || ch == LF) break; parser->flags = 0; parser->uses_transfer_encoding = 0; parser->content_length = ULLONG_MAX; if (UNLIKELY(!IS_ALPHA(ch))) { SET_ERRNO(HPE_INVALID_METHOD); goto error; } parser->method = (enum http_method) 0; parser->index = 1; switch (ch) { case 'A': parser->method = HTTP_ACL; break; case 'B': parser->method = HTTP_BIND; break; case 'C': parser->method = HTTP_CONNECT; /* or COPY, CHECKOUT */ break; case 'D': parser->method = HTTP_DELETE; break; case 'G': parser->method = HTTP_GET; break; case 'H': parser->method = HTTP_HEAD; break; case 'L': parser->method = HTTP_LOCK; /* or LINK */ break; case 'M': parser->method = HTTP_MKCOL; /* or MOVE, MKACTIVITY, MERGE, M-SEARCH, MKCALENDAR */ break; case 'N': parser->method = HTTP_NOTIFY; break; case 'O': parser->method = HTTP_OPTIONS; break; case 'P': parser->method = HTTP_POST; /* or PROPFIND|PROPPATCH|PUT|PATCH|PURGE */ break; case 'R': parser->method = HTTP_REPORT; /* or REBIND */ break; case 'S': parser->method = HTTP_SUBSCRIBE; /* or SEARCH, SOURCE */ break; case 'T': parser->method = HTTP_TRACE; break; case 'U': parser->method = HTTP_UNLOCK; /* or UNSUBSCRIBE, UNBIND, UNLINK */ break; default: SET_ERRNO(HPE_INVALID_METHOD); goto error; } UPDATE_STATE(s_req_method); CALLBACK_NOTIFY(message_begin); break; } ... case s_message_done: UPDATE_STATE(NEW_MESSAGE()); CALLBACK_NOTIFY(message_complete); if (parser->upgrade) { /* Exit, the rest of the message is in a different protocol. */ RETURN((p - data) + 1); } break; ... } ... } ... }

函式 http_parser_execute 主要是一個很大的迴圈,將讀取到的資料的每個字元進行解讀,這邊特別提到兩種情況,分別是 s_start_reqs_message_done

在第 7 行可看到整個函式的使用,第 15 行可看到 s_start_req 的情況,其功能是當一開始進行解析時,會使用第一個字元判斷該要求是屬於那一種的類型,可在第 31 ~ 48 行找到各種的對應

第 57 行可看到 s_message_done 的實作,其功能是解析資料完畢後,要給 client 對應的回應,主要是使用以下的巨集進行上面提過的回呼函式呼叫 (位於第 59 行)

CALLBACK_NOTIFY(message_complete);

比較 khttpd 和 CS:APP 給定的網站伺服器

理解 khttpd 的整體流程後,可將其與 CS:APP 中介紹的 TINY Web 伺服器進行比較。

TINY server flow
上圖為 CS:APP 教材提供的伺服器流程。可觀察到:

  1. 兩者建立 socket 的步驟皆為
    socketbindlistenaccept → 資料傳輸
    差異在呼叫介面:khttpd 直接使用 Linux 核心 API,而 TINY Web 在使用者空間透過標準系統呼叫
  2. I/O 部分,khttpd 採用核心函式 kernel_recvmsgkernel_sendmsg;TINY Web 則以自寫的 RIO (可靠 I/O) 套件包裝 readwrite,簡化阻塞處理與緩衝管理

其他主要差別:

  • 執行層級:khttpd 位於 kernel space,TINY Web 屬 user space
  • 並行策略:khttpd 以多執行緒並行服務多連線,TINY Web 單執行緒依序處理,每次僅服務一位 client

khttpd 實作的缺失

在函式 http_server_worker 執行迴圈的部份,如下所示

while (!kthread_should_stop()) {
	// 接收資料
	int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1);
	if (ret <= 0) {
		if (ret)
			pr_err("recv error: %d\n", ret);
		break;
	}
	// 解析收到的資料
	http_parser_execute(&parser, &setting, buf, ret);
	if (request.complete && !http_should_keep_alive(&parser))
		break;
}

觀察程式碼後發現,用於接收資料的緩衝區 buf 在每次迴圈結束並未清空,殘留內容可能影響下一次解析而產生非預期結果。

為驗證此現象,進行一項簡單測試:先執行 telnet localhost 8081 連入伺服器,依序送出 GET /12345 HTTP/1.1GET / HTTP/1.1

GET /12345 HTTP/1.1

HTTP/1.1 200 OK
Server: khttpd
Content-Type: text/plain
Content-Length: 12
Connection: Keep-Alive

Hello World!
GET / HTTP/1.1

HTTP/1.1 200 OK
Server: khttpd
Content-Type: text/plain
Content-Length: 12
Connection: Keep-Alive

Hello World!

雖然可見伺服器正常回應,但是查看核心模組相關的訊息

[186673.227429] khttpd: buf = GET /12345 HTTP/1.1
[186673.338073] khttpd: buf = 
                T /12345 HTTP/1.1
[186673.338083] khttpd: requested_url = /12345
[186733.791423] khttpd: buf = GET / HTTP/1.1
                1.1
[186733.918134] khttpd: buf = 
                T / HTTP/1.1
                1.1
[186733.918155] khttpd: requested_url = /

實測顯示 buf 內容確實會受到前一次輸入影響;雖然此範例未出現錯誤,但無法保證其他情境亦安全。

每送出一個請求時會看到 2 次 buf =,原因在於 HTTP 以 2 個 \r\n 判斷結束,使用者需按 2 次 Enter 按鍵才形成完整請求。

可在迴圈結束前呼叫 memset 清空 buf,避免殘留資料干擾下一次解析。

while (!kthread_should_stop()) {
+	int ret;
+	memset(buf, 0, RECV_BUFFER_SIZE);
+	ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1);
	if (ret <= 0) {
		if (ret)
			pr_err("recv error: %d\n", ret);
		break;
	}
	pr_info("buf = %s", buf);
	// 解析收到的資料
	http_parser_execute(&parser, &setting, buf, ret);
	if (request.complete && !http_should_keep_alive(&parser))
		break;
}

接著可再次嘗試上面的實驗,以下為模組輸出的結果

[187284.736753] khttpd: buf = GET /12345 HTTP/1.1
[187284.849034] khttpd: buf = 
[187284.849045] khttpd: requested_url = /12345
[187300.646245] khttpd: buf = GET / HTTP/1.1
[187300.784082] khttpd: buf = 
[187300.784103] khttpd: requested_url = /

可很明顯看到參數 buf 已經不會被之前的輸入給影響

減少 printk 的使用

在實作之前,先使用 htstress.c 測試原本 server 的效能,這裡使用命令 ./htstress http://localhost:8081 -t 3 -c 20 -n 200000 進行測試

requests:      200000
good requests: 200000 [100%]
bad requests:  0 [0%]
socker errors: 0 [0%]
seconds:       8.246
requests/sec:  24252.937

http_server.h 新增以下結構

enum {
    TRACE_accept_err = 1,  // accept 失敗總數
    TRACE_cthread_err,     // create thread 失敗總數
    TRACE_kmalloc_err,     // kmalloc 失敗總數
    TRACE_recvmsg,     	   // recvmsg 總數
    TRACE_sendmsg,         // sendmsg 總數
    TRACE_send_err,        // send request 失敗總數
    TRACE_recv_err,        // recv request 失敗總數
};

struct runtime_state {
    atomic_t accept_err, cthread_err;
    atomic_t kmalloc_err, recvmsg;
    atomic_t sendmsg, send_err;
    atomic_t recv_err;
};
extern struct runtime_state states;

而在 khttpd 裡,最常呼叫的 pr_info 位於函式 http_server_response ,以下為修改過程

static int http_server_response(struct http_request *request, int keep_alive)
{
    char *response;
+   int ret;

-   pr_info("requested_url = %s\n", request->request_url);
    if (request->method != HTTP_GET)
        response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501;
    else
        response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY
                              : HTTP_RESPONSE_200_DUMMY;
    ret = http_server_send(request->socket, response, strlen(response));
+   if (ret > 0)
+       TRACE(sendmsg);
+   return 0;
}

這裡將 pr_info 移除,改成使用計算送出次數的方式,可避免每次送出資料前,都要先印出的多餘動作,而其他的部份也是做相同的事

最後輸入命令 ./htstress http://localhost:8081 -t 3 -c 20 -n 200000 並測試:

requests:      200000
good requests: 200000 [100%]
bad requests:  0 [0%]
socker errors: 0 [0%]
seconds:       6.606
requests/sec:  30274.801

可見伺服器處理效率有明顯上升,再使用命令 dmesg 查看實際運作狀況,如下所示

[164105.005808] khttpd: recvmsg : 200046
[164105.005815] khttpd: sendmsg : 200046
[164105.005817] khttpd: kmalloc_err : 0
[164105.005819] khttpd: cthread_err : 0
[164105.005821] khttpd: send_err : 0
[164105.005823] khttpd: recv_err : 0
[164105.005824] khttpd: accept_err : 0

HTTP keep-alive 模式

下圖將 HTTP 傳輸方式分為 2 種:multiple connections 與 persistent connection。
multiple connections 在伺服器回應後關閉連線;persistent connection 則維持同一 TCP 連線,可於其內處理多個請求。

根據 HTTP 規範 可得:

  1. HTTP/1.0 預設採 multiple connections;若需維持連線,標頭須加入
    Connection: keep-alive
  2. HTTP/1.1 預設採 persistent connection,可在單一連線內連續處理多項請求;若想結束連線,需送出 Connection: close

khttpd 測試步驟:透過 telnet localhost 8081 連線,輸入以下命令並觀察伺服器回應。

  • GET / HTTP/1.0
    ​​HTTP/1.1 200 OK
    ​​Server: khttpd
    ​​Content-Type: text/plain
    ​​Content-Length: 12
    ​​Connection: Close
    
  • GET / HTTP/1.1
    ​​HTTP/1.1 200 OK
    ​​Server: khttpd
    ​​Content-Type: text/plain
    ​​Content-Length: 12
    ​​Connection: Keep-Alive
    

從回應中的 Connection: 欄位可見,khttpd 依 HTTP 版本自動切換連線模式,證實已內建 keep‑alive 支援。

Linux 核心如何處理傳遞到核心模組的參數

參考 Passing Command Line Arguments to a Module ,發現 kernel 是使用巨集 module_param 傳遞參數,接著可在檔案 main.c 發現該巨集的使用,可得知 khttpd 可讓使用者自己設定 portbacklog

static ushort port = DEFAULT_PORT;
module_param(port, ushort, S_IRUGO);
static ushort backlog = DEFAULT_BACKLOG;
module_param(backlog, ushort, S_IRUGO);

接著研究 module_param 的實作,可在 linux/include/linux/moduleparam.h 找到數個定義,將相關定義表示在下方

#define module_param(name, type, perm)				\
	module_param_named(name, name, type, perm)

#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_##type, module_param_cb__MODULE_PARM_TYPE 做討論

param_check_##type

由於 khttpd 的變數是使用 ushort 的型態,因此巨集會被展開成 param_check_ushort ,以下為相關巨集

#define param_check_ushort(name, p) __param_check(name, p, unsigned short)

/* All the helper functions */
/* The macros to do compile-time type checking stolen from Jakub
   Jelinek, who IIRC came up with this idea for the 2.4 module init code. */
#define __param_check(name, p, type) \
	static inline type __always_unused *__check_##name(void) { return(p); }

從註解很明顯可知道 param_check_##type 的目的是要在編譯時期就判斷變數 p 是否真的是 type 型態,方法是藉由回傳 p 判斷函式是否回傳相同型態

module_param_cb

/**
 * module_param_cb - general callback for a module/cmdline parameter
 * @name: a valid C identifier which is the parameter name.
 * @ops: the set & get operations for this parameter.
 * @arg: args for @ops
 * @perm: visibility in sysfs.
 *
 * The ops can have NULL set or get functions.
 */
#define module_param_cb(name, ops, arg, perm)				      \
	__module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0)

/* 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 __section("__param")					\
	__aligned(__alignof__(struct kernel_param))			\
	= { __param_str_##name, THIS_MODULE, ops,			\
	    VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }

__module_param_call 建立一個型態為 kernel_param 且名稱為 __param_##name 的結構,並告訴編譯器以下資訊

  • 將資料放在 __param
  • 對齊 __alignof__(struct kernel_param) 的大小

接著查看結構 kernel_param 的宣告

struct kernel_param {
	const char *name;
	struct module *mod;
	const struct kernel_param_ops *ops;
	const u16 perm;
	s8 level;
	u8 flags;
	union {
		void *arg;
		const struct kparam_string *str;
		const struct kparam_array *arr;
	};
};

由以上資訊我們可得到最後 __module_param_call 建立的結構,以變數 port 作為範例,如以下所示

struct kernel_param _param_name {
	const char *name = "port";
	struct module *mod = THIS_MODULE;
	const struct kernel_param_ops *ops = &param_ops_ushort;
	const u16 perm = VERIFY_OCTAL_PERMISSIONS(S_IRUGG);
	s8 level = -1;
	u8 flags = 0;
	void *arg = &port;
};

最後使用命令 readelf -r khttpd.ko 查看 __param 的區域,的確有 portbacklog 的資料

Relocation section '.rela__param' at offset 0xc1a48 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000000  000700000001 R_X86_64_64       0000000000000000 .rodata + 1248
000000000008  005600000001 R_X86_64_64       0000000000000000 __this_module + 0
000000000010  005400000001 R_X86_64_64       0000000000000000 param_ops_ushort + 0
000000000020  000e00000001 R_X86_64_64       0000000000000000 .data + 4
000000000028  000700000001 R_X86_64_64       0000000000000000 .rodata + 1250
000000000030  005600000001 R_X86_64_64       0000000000000000 __this_module + 0
000000000038  005400000001 R_X86_64_64       0000000000000000 param_ops_ushort + 0
000000000048  000e00000001 R_X86_64_64       0000000000000000 .data + 6

__MODULE_PARM_TYPE

#define __MODULE_PARM_TYPE(name, _type)					  \
	__MODULE_INFO(parmtype, name##type, #name ":" _type)

#define __MODULE_INFO(tag, name, info)					  \
	static const char __UNIQUE_ID(name)[]				  \
		__used __section(".modinfo") __aligned(1)		  \
		= __MODULE_INFO_PREFIX __stringify(tag) "=" info

#define MODULE_PARAM_PREFIX KBUILD_MODNAME "."

#define KBUILD_MODNAME /* empty */

參考〈Linux 核心模組掛載機制〉可知 __UNIQUE_ID 的功能

  • __UNIQUE_ID 會根據參數產生一個不重複的名字,其中使用到的技術是利用巨集中的 ## 來將兩個參數合併成一個新的字串
  • 透過 __attribute__ 關鍵字告訴編譯器,這段訊息
    • 要被放在 .modinfo 區 (__section(".modinfo"))
    • 不會被程式使用到,所以不要產生警告訊息 (__used)
    • 最小的對齊格式是 1 bit (__aligned(1))
  • 巨集 __stringify 的目的是為了把參數轉換成字串形式
  • 巨集 MODULE_PARAM_PREFIX 由巨集 KBUILD_MODNAME"." 組合而成,簡單來說就只是個字串

最後以變數 port 為例,會產生以下巨集

#define __MODULE_INFO(tag, name, info)					  \
	static const char __UNIQUE_ID(name)[]				  \
		__used __section(".modinfo") __aligned(1)		  \
		= ".parmtype=port:ushort."

接著使用命令 objdump -s khttpd.ko 查看 .modinfo 的區域

...
Contents of section .modinfo:
  ...
  0070 00706172 6d747970 653d6261 636b6c6f  .parmtype=backlo
+ 0080 673a7573 686f7274 00706172 6d747970  g:ushort.parmtyp
+ 0090 653d706f 72743a75 73686f72 74007372  e=port:ushort.sr
...

繼續根據〈Linux 核心模組掛載機制〉,使用 strace 追蹤 insmod khttpd.ko

$ sudo strace insmod khttpd.ko port=1999 execve("/usr/sbin/insmod", ["insmod", "khttpd.ko", "port=1999"], 0x7fff08f9ff70 /* 25 vars */) = 0 brk(NULL) = 0x5607976a6000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 ... mmap(NULL, 1366608, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa7c761b000 finit_module(3, "port=1999", 0) = -1 EEXIST (File exists) write(2, "insmod: ERROR: could not insert "..., 62insmod: ERROR: could not insert module khttpd.ko: File exists ) = 62 munmap(0x7fa7c761b000, 1366608) = 0 close(3) = 0 exit_group(1) = ? +++ exited with 1 +++

查看位於第 8 行 finit_module 的實作,參考 kernel/module.cfinit_module(2) - Linux man page

int finit_module(int fd, const char *param_values, int flags);

對應 strace 的結果

  • fd = 3
  • param_values = "port=1999"
  • flag = 0
SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags)
{
	...
	return load_module(&info, uargs, flags);
}

函式 finit_module 呼叫函式 load_module ,接著繼續分析

static int load_module(struct load_info *info, const char __user *uargs,
		       int flags)
{
	/* Now copy in args */
	mod->args = strndup_user(uargs, ~0UL >> 1);
	if (IS_ERR(mod->args)) {
		err = PTR_ERR(mod->args);
		goto free_arch_cleanup;
	}
}

從上述程式碼可看到從命令列的輸入參數已經被複製到 mod->arg ,且 mod 的型態為 struct mod ,參考 include/linux/module.h

struct module {
	...
	/* The command line arguments (may be mangled).  People like
	   keeping pointers to this stuff */
	char *args;
	...
}

找到了 args 的宣告,從註解可知道 args 的目的就是儲存 command line 的設定參數

回到 load_module ,發現了函式 parse_args ,從註解可知道是要將 command line 的字串拆解

/* Module is ready to execute: parsing args may do that. */
after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,
			  -32768, 32767, mod,
			  unknown_module_param_cb);

進到函式 parse_args ,參考 kernel/params.c

/* Args looks like "foo=bar,bar2 baz=fuz wiz". */
char *parse_args(const char *doing,
		 char *args,
		 const struct kernel_param *params,
		 unsigned num,
		 s16 min_level,
		 s16 max_level,
		 void *arg,
		 int (*unknown)(char *param, char *val,
				const char *doing, void *arg))
{
	char *param, *val, *err = NULL;

	/* Chew leading spaces */
	args = skip_spaces(args);

	if (*args)
		pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);

	while (*args) {
		int ret;
		int irq_was_disabled;

		args = next_arg(args, &param, &val);
		/* Stop at -- */
		if (!val && strcmp(param, "--") == 0)
			return err ?: args;
		irq_was_disabled = irqs_disabled();
		ret = parse_one(param, val, doing, params, num,
				min_level, max_level, arg, unknown);
		if (irq_was_disabled && !irqs_disabled())
			pr_warn("%s: option '%s' enabled irq's!\n",
				doing, param);

		switch (ret) {
		case 0:
			continue;
		case -ENOENT:
			pr_err("%s: Unknown parameter `%s'\n", doing, param);
			break;
		case -ENOSPC:
			pr_err("%s: `%s' too large for parameter `%s'\n",
			       doing, val ?: "", param);
			break;
		default:
			pr_err("%s: `%s' invalid for parameter `%s'\n",
			       doing, val ?: "", param);
			break;
		}

		err = ERR_PTR(ret);
	}

	return err;
}

函式 parse_args 做了以下:

  1. 使用函式 skip_spaces 將字串的第一個字元如果為空白字元,將空白字元全部移除
  2. 使用函式 next_arg 找到下一個 argument ,參考 lib/cmdline.c
  3. 使用函式 parse_one 將試著將 argument 加進 module 裡,該函式位於 kernel/params.c

最後討論函式 parse_one

static int parse_one(char *param, char *val, const char *doing, const struct kernel_param *params, unsigned num_params, s16 min_level, s16 max_level, void *arg, int (*handle_unknown)(char *param, char *val, const char *doing, void *arg)) { unsigned int i; int err; /* Find parameter */ for (i = 0; i < num_params; i++) { if (parameq(param, params[i].name)) { if (params[i].level < min_level || params[i].level > max_level) return 0; /* No one handled NULL, so do it here. */ if (!val && !(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG)) return -EINVAL; pr_debug("handling %s with %p\n", param, params[i].ops->set); kernel_param_lock(params[i].mod); if (param_check_unsafe(&params[i])) err = params[i].ops->set(val, &params[i]); else err = -EPERM; kernel_param_unlock(params[i].mod); return err; } } if (handle_unknown) { pr_debug("doing %s: %s='%s'\n", doing, param, val); return handle_unknown(param, val, doing, arg); } pr_debug("Unknown argument '%s'\n", param); return -ENOENT; }

注意第 17 行的部份, linux 核心逐步尋找符合的參數,並在第 29 行呼叫函式指標 params[i].ops->set(val, &params[i]) ,將輸入的資料複製到模組的資料裡,以下為其結構宣告

struct kernel_param_ops {
	/* How the ops should behave */
	unsigned int flags;
	/* Returns 0, or -errno.  arg is in kp->arg. */
	int (*set)(const char *val, const struct kernel_param *kp);
	/* Returns length written or -errno.  Buffer is 4k (ie. be short!) */
	int (*get)(char *buffer, const struct kernel_param *kp);
	/* Optional function to free kp->arg when module unloaded. */
	void (*free)(void *arg);
};