Try   HackMD

2022q1 Homework1 (lab0)

contributed by < eric88525 >

實驗環境

$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ lscpu
Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   48 bits physical, 48 bits virtual
CPU(s):                          16
On-line CPU(s) list:             0-15
Thread(s) per core:              2
Core(s) per socket:              8
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       AuthenticAMD
CPU family:                      25
Model:                           33
Model name:                      AMD Ryzen 7 5800X 8-Core Processor
Stepping:                        0
Frequency boost:                 enabled
CPU MHz:                         2200.000
CPU max MHz:                     4850.1948
CPU min MHz:                     2200.0000
BogoMIPS:                        7585.34
Virtualization:                  AMD-V
L1d cache:                       256 KiB
L1i cache:                       256 KiB
L2 cache:                        4 MiB
L3 cache:                        32 MiB
NUMA node0 CPU(s):               0-15

作業要求

  • 在 GitHub 上 fork lab0-c
  • 依據上述指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改,要能滿足 $ make test 自動評分系統的所有項目。
  • qtest 提供新的命令 shuffle,允許藉由 Fisher–Yates shuffle 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作
  • qtest 提供新的命令 web,提供 web 伺服器功能,注意: web 伺服器運作過程中,qtest 仍可接受其他命令
  • 除了修改程式,也要編輯「作業區」,增添開發紀錄和 GitHub 連結,除了提及你如何逐步達到自動評分程式的要求外,共筆也要涵蓋以下:
    • 開啟 Address Sanitizer,修正 qtest 執行過程中的錯誤
      • 先執行 qtest 再於命令提示列輸入 help 命令,會使 開啟 Address Sanitizer 觸發錯誤,應予以排除
    • 運用 Valgrind 排除 qtest 實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗
    • 解釋 select 系統呼叫在本程式的使用方式,並分析 console.c 的實作,說明其中運用 CS:APP RIO 套件 的原理和考量點。可對照參考 CS:APP 第 10 章重點提示
    • 研讀論文〈Dude, is my code constant time?〉,解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 Student's t-distribution 及程式實作的原理。注意:現有實作存在若干致命缺陷,請討論並提出解決方案;
  • 指出現有程式的缺陷 (提示: 和 RIO 套件 有關) 或者可改進之處 (例如更全面地使用 Linux 核心風格的鏈結串列 API),嘗試實作並提交 pull request
  • 截止日期:
    • Mar 1, 2022 (含) 之前
    • 越早在 GitHub 上有動態、越早接受 code review 並持續改進者,評分越高

指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改,記得也該實作 q_sort 函式。

queue.c 僅提供介面 (interface) 但尚未有完整程式實作,需要由學員補完並逐步精進,包含以下:

  • q_new: 建立新的「空」佇列;
  • q_free: 釋放佇列所佔用的記憶體;
  • q_insert_head: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則);
  • q_insert_tail: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則);
  • q_ _head: 在佇列開頭 (head) 移去 (remove) 給定的節點;
  • q_release_element: 釋放特定節點的記憶體空間;
  • q_size: 計算目前佇列的節點總量;
  • q_delete_mid: 移走佇列中間的節點,詳見 LeetCode 2095. Delete the Middle Node of a Linked List
  • q_delete_dup: 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 LeetCode 82. Remove Duplicates from Sorted List II
  • q_swap: 交換佇列中鄰近的節點,詳見 LeetCode 24. Swap Nodes in Pairs
  • q_reverse: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點;
  • q_sort: 以遞增順序來排序鏈結串列的節點,可參閱 Linked List Sort 得知實作手法;

開發過程

q_new

建立空的list_head,並閱讀linux kernel api和list.h來簡化相關程式

struct list_head *q_new()
{
    struct list_head *p = malloc(sizeof(struct list_head));
    // allocate space fail, return null
    if (p == NULL)
        return NULL;
    // init pointer
    INIT_LIST_HEAD(p);
    return p;
}

q_free

  • 參考 kernel api,list_for_each_entry_safe 會用暫存n來記錄下一個位置,就可以安全刪除當前節點
  • link
void q_free(struct list_head *l)
{
    if (l == NULL)
        return;

    element_t *pos = NULL, *n = NULL;
    list_for_each_entry_safe (pos, n, l, list)
        q_release_element(pos);

    free(l);
}

q_insert_head

  • kernel api 已經提供相關實作,只需要處理記憶體管理和字串拷貝

list_add: Insert a new entry after the specified head. This is good for implementing stacks.

bool q_insert_head(struct list_head *head, char *s)
{
    if (!head)
        return false;
    // create new node and assign value to it
    element_t *new_node = malloc(sizeof(element_t));

    if (!new_node)
        return false;

    new_node->value = strdup(s);

    if (!new_node->value) {
        q_release_element(new_node);
        return false;
    }
    list_add(&new_node->list, head);
    return true;
}

q_insert_tail

  • kernel api 已經提供相關實作,只需要處理記憶體管理和字串拷貝

list_add_tail: Insert a new entry before the specified head. This is useful for implementing queues.

bool q_insert_tail(struct list_head *head, char *s)
{
    if (!s || !head)
        return false;
    // create new node and assign value to it
    element_t *new_node = malloc(sizeof(element_t));

    if (!new_node)
        return false;

    new_node->value = strdup(s);

    if (!new_node->value) {
        q_release_element(new_node);
        return false;
    }
    list_add_tail(&new_node->list, head);
    return true;
}

q_remove_head

  • 呼叫 list_first_entry 來取得第一個entry
  • 用 list_del 來確保前後節點在移除 entry 後仍然互相連接。
  • 複製字串到sp
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
    if (!head || list_empty(head))
        return NULL;

    element_t *p = list_first_entry(head, element_t, list);

    list_del_init(&(p->list));

    if (sp) {
        strncpy(sp, p->value, bufsize - 1);
        sp[bufsize - 1] = '\0';
    }
    
    return p;
}

q_release_element

  • 無變動
void q_release_element(element_t *e)
{
    free(e->value);
    free(e);
}

q_size

  • 遍歷整個 list 來計算size
int q_size(struct list_head *head)
{
    if (!head || list_empty(head))
        return 0;

    int size = 0;
    struct list_head *p = NULL;

    list_for_each (p, head)
        size++;
    return size;
}

q_delete_mid

  • 運用快/慢指標,找出中心點
  • 找出中心後,用 list_del_init 確保 linked list 串接正常
  • 釋放記憶體
bool q_delete_mid(struct list_head *head)
{
    if (head == NULL || list_empty(head))
        return false;

    struct list_head **p = &head->next;

    for (struct list_head *fast = head->next;
         fast != head && fast->next != head; fast = fast->next->next)
        p = &(*p)->next;

    struct list_head *to_delete = (*p);
    list_del_init(to_delete);

    q_release_element(container_of(to_delete, element_t, list));
    return true;
}

q_delete_dup

  • 首先讓 double pointer p 指向 head 的 next 指標
  • 如果遇到重複的節點,透過prev來暫存即將被刪除的節點,並讓curr往下移動,並刪除prev
  • 把 p 移動到下一個節點的next指標上
  • 如果 curr 或是 curr->next 為 head 就結束
bool q_delete_dup(struct list_head *head)
{
    if (!head || list_empty(head))
        return false;

    struct list_head **p = &(head->next);
    struct list_head *curr = head->next, *prev = NULL;

    while (curr != head && curr->next != head) {
        if (strcmp(container_of(curr, element_t, list)->value,
                   container_of(curr->next, element_t, list)->value) == 0) {
            do {
                prev = curr;
                curr = curr->next;
                list_del(prev);
                q_release_element(container_of(prev, element_t, list));
            } while (curr->next != head &&
                     strcmp(container_of(curr, element_t, list)->value,
                            container_of(curr->next, element_t, list)->value) ==
                         0);
        }
        p = &((*p)->next);
        curr = curr->next;
    }
    return true;
}

q_swap

  • 兩兩交換節點
  • 如果有a和b節點需要交換,首先把b抽出來,接著加入a的前面
  • 原本是使用 list_add_tail 而不是 list_add,會需要額外的指標來記錄 a 前面的節點,如果用 list_add_tail就可以省下
void q_swap(struct list_head *head)
{
    if (!head || list_empty(head))
        return;

    struct list_head *curr = head->next;
    struct list_head *next = NULL;
    while (curr && curr->next && curr != head && curr->next != head) {
        next = curr->next;
        list_del(next);
        list_add_tail(next, curr);
        curr = curr->next;
    }
}

q_reverse

  • 運用prev和next來紀錄前後位置,逐一反轉節點
void q_reverse(struct list_head *head)
{
    if (!head || list_empty(head))
        return;

    struct list_head *prev = head->prev, *curr = head, *next = NULL;

    while (next != head) {
        next = curr->next;
        curr->next = prev;
        curr->prev = next;
        prev = curr;
        curr = next;
    }
}

q_sort

  • 參考老師提供的merge sort,改成有prev的版本
  • 一開始要先把 head 拆出來,變成單向的 linking list ,最後串回來
  • merge sort相較於quick sort能有更穩定的速度(都是 nlogn)
  • 更新: 在進入q_sort後要先用 list_empty來檢查一次,不然 test17 會出問題
struct list_head *merge(struct list_head *L1, struct list_head *L2)
{
    struct list_head *head = NULL, **ptr = &head, **node = NULL;
    struct list_head *prev_node = NULL;

    for (node = NULL; L1 && L2; prev_node = *node, *node = (*node)->next) {
        // let node point to smaller node pointer(L1/L2)
        node = strcmp(container_of(L1, element_t, list)->value,
                      container_of(L2, element_t, list)->value) > 0
                   ? &L2
                   : &L1;
        *ptr = *node;
        (*node)->prev = prev_node;
        ptr = &(*ptr)->next;
    }
    *node = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2);
    *ptr = *node;
    (*node)->prev = prev_node;
    return head;
}

struct list_head *merge_sort(struct list_head *head)
{
    if (!head || !head->next)
        return head;

    struct list_head *fast = head->next;
    struct list_head *slow = head;

    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    fast = slow->next;
    slow->next = NULL;

    struct list_head *L1 = merge_sort(head);
    struct list_head *L2 = merge_sort(fast);

    return merge(L1, L2);
}


/*
 * Sort elements of queue in ascending order
 * No effect if q is NULL or empty. In addition, if q has only one
 * element, do nothing.
 */
void q_sort(struct list_head *head)
{
    if (!head || list_empty(head))
        return;

    struct list_head *last_node = head->prev;

    last_node->next = NULL;
    head->next->prev = NULL;

    struct list_head *sorted_list = merge_sort(head->next);

    INIT_LIST_HEAD(head);

    head->next = sorted_list;
    sorted_list->prev = head;

    // move last_node pointer to
    for (last_node = sorted_list; last_node->next != NULL;
         last_node = last_node->next) {
    }

    head->prev = last_node;
    last_node->next = head;
}

最後一個測試的 bug

  • 在本地有時會顯示 ERROR: Probably not constant time ,有時則會通過,這似乎只是本機上的問題,丟到遠端還是會通過

shuffle 命令

參考作業說明 的 qtest 命令直譯器介紹來實作

在 qtest.c 的 console_init()內加入這行,可以新增命令、function、訊息顯示

ADD_COMMAND(shuffle, "                | Shuffle every nodes in queue");

ADD_COMMAND 是把 add_cmd() 再包裝一次,由於巨集展開後會把 cmd 和 do_cmd 的 function 相連,因此要連結的 function名稱應該以 do_開頭

// console.h
void add_cmd(char *name, cmd_function operation, char *documentation);
#define ADD_COMMAND(cmd, msg) add_cmd(#cmd, do_##cmd, msg)

在 qtest.c 加入 do_shuffle 函式,有做 arguments 和 null 的檢查

// qtest.c
static bool do_shuffle(int argc, char *argv[])
{
    if (argc != 1) {
        report(1, "%s takes no arguments", argv[0]);
        return false;
    }

    if (!l_meta.l)
        report(3, "Warning: Calling sort on null queue");
    error_check();

    if (exception_setup(true))
        q_shuffle(l_meta.l);

    show_queue(3);

    return !error_check();
}

在 qtest.c 內實作 shuffle,這邊會檢查 head 是否為null或empty,接著每次從還沒被 shuffle 的節點中,選一個來移動到尾端

void q_shuffle(struct list_head *head)
{
    if (!head || list_empty(head))
        return;

    int n = q_size(head);

    if (n == 1)
        return;

    int i;
    for (i = n; i > 1; i--) {
        int randNum = rand() % i;
        struct list_head *tmp = head->next;
        while (randNum--)
            tmp = tmp->next;
        list_move_tail(tmp, head);
    }
}

引入 lib/list_sort.c

  • linux 的 list_sort 定義如下
    • priv 是要傳入給 cmp 的資料
    • cmp 為用來比較的 function
void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp)

cmp argument 的格式

// list_cmp_func_t is a function pointer, it takes (void *, const struct list_head *, const struct list_head *) as argument and return int
typedef int (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *)

要套用到本專案的話,單獨把 list_sort.h list_sort.c 抽出並放入專案目錄,並做以下修改

// list_sort.h
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_LIST_SORT_H
#define _LINUX_LIST_SORT_H

#include <linux/types.h>

// 從 linux/compiler.h 抽出 likely(x) 和 unlikely(x)
+ #define likely(x) __builtin_expect(!!(x), 1)
+ #define unlikely(x) __builtin_expect(!!(x), 0)

struct list_head;

typedef int
    __attribute__((nonnull(2, 3))) (*list_cmp_func_t)(void *,
                                                      const struct list_head *,
                                                      const struct list_head *);
  
// 測試程式中,會嘗試傳入 NULL 到 sort 內
// 因此刪除 __attribute__((nonnull(2,3))) 的 function attribute
- __attribute__((nonnull(2,3)))
void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp);

#endif
  • 簡化標頭檔的使用
// list_sort.c
- #include <linux/kernel.h>
- #include <linux/bug.h>
- #include <linux/compiler.h>
- #include <linux/export.h>
- #include <linux/string.h>
- #include <linux/list_sort.h>
- #include <linux/list.h>
+ #include "list_sort.h"
+ #include "list.h"
  • 刪除 function attribute並加入 null 判斷
// list_sort.c
- __attribute__((nonnull(2,3)))
void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp)
+    if (head == NULL)
+        return;
  • 加入 compare function 在 q_test.c , 也要 include 標頭檔
// q_test.c
+ #include "list_sort.h"

// the compare function of element_t compare
+ int compare_element_t(void *priv,
+                       const struct list_head *l,
+                       const struct list_head *r)
+ {
+     return strcmp(container_of(l, element_t, list)->value,
+                   container_of(r, element_t, list)->value);
+ }
  • 修改 qtest.c 的 do_sort() ,讓執行 sort 時後面加入 linux 參數可切換為 linux 版本的 sort
bool do_sort(int argc, char *argv[])
{
-   if (argc != 1) {
-        report(1, "%s takes no arguments", argv[0]);
+    if (argc > 2)
+        report(1, "%s takes <=2 arguments", argv[0]);

    if (!l_meta.l)
        report(3, "Warning: Calling sort on null queue");
    error_check();

    int cnt = q_size(l_meta.l);
    if (cnt < 2)
        report(3, "Warning: Calling sort on single node");
    error_check();

    set_noallocate_mode(true);

    if (exception_setup(true)) {
-        q_sort(l_meta.l);
+        if (argc == 2 && !strcmp(argv[1], "linux")) {
+            list_sort(NULL, l_meta.l, compare_element_t);
+        } else {
+            q_sort(l_meta.l);
+        }
    }
  • 在 makefile 的 OBJS 加入 list_sort.o
OBJS := qtest.o report.o console.o harness.o queue.o \
        random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \
-       linenoise.o
+       linenoise.o list_sort.o

比較效能

  • 執行命令檔來比較效能
# Comapre performance of my merge sort with linux sort
option fail 0
option malloc 0
# 30k sample
new
ih a 10000
ih b 10000
ih c 10000
time sort
reverse
time sort linux
free
# 300k sample
new
ih a 100000
ih b 100000
ih c 100000
time sort
reverse
time sort linux
free
# 3M sample
new
ih a 1000000
ih b 1000000
ih c 1000000
time sort
reverse
time sort linux
free
  • 在不同 sample 數量下的執行時間比較
my merge sort linux sort
30k samples 0.003 0.001
300k samples 0.044 0.023
3M samples 0.619 0.350

linux 的 sort 分析

篇幅過長,另外開筆記 link

tiny web server

運用 Valgrind 排除 qtest 實作的記憶體錯誤

  1. 用 valgrind 檢查記憶體,每種測試都有以下問題,都跟 linenoiseHistoryAddlinenoiseHistoryLoad 有關,而這兩個 function 都在 linenoise.h 被定義。

問題點的呼叫順序為 main -> linenoiseHistoryLoad -> linenoiseHistoryAdd -> linenoiseHistoryAdd -> strdup

$ make valgrind
# Test of insert_head and remove_head
==2070491== 10 bytes in 1 blocks are still reachable in loss record 1 of 3
==2070491==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==2070491==    by 0x4A5850E: strdup (strdup.c:42)
==2070491==    by 0x110A27: linenoiseHistoryAdd (linenoise.c:1236)
==2070491==    by 0x1115BA: linenoiseHistoryLoad (linenoise.c:1325)
==2070491==    by 0x10C82A: main (qtest.c:1010)
linenoiseHistoryAdd 的實做 (linenoise.c)
/* This is the API call to add a new entry in the linenoise history. * It uses a fixed array of char pointers that are shifted (memmoved) * when the history max length is reached in order to remove the older * entry and make room for the new one, so it is not exactly suitable for huge * histories, but will work well for a few hundred of entries. * * Using a circular buffer is smarter, but a bit more complex to handle. */ int linenoiseHistoryAdd(const char *line) { char *linecopy; if (history_max_len == 0) return 0; /* Initialization on first call. */ if (history == NULL) { history = malloc(sizeof(char *) * history_max_len); if (history == NULL) return 0; memset(history, 0, (sizeof(char *) * history_max_len)); } /* Don't add duplicated lines. */ if (history_len && !strcmp(history[history_len - 1], line)) return 0; /* Add an heap allocated copy of the line in the history. * If we reached the max length, remove the older line. */ linecopy = strdup(line); if (!linecopy) return 0; if (history_len == history_max_len) { free(history[0]); memmove(history, history + 1, sizeof(char *) * (history_max_len - 1)); history_len--; } history[history_len] = linecopy; history_len++; return 1; }
  1. 根據 linenoiseHistory 的描述,可以推斷 history 為一個 pointer array,裡面最多紀錄 history_max_len 個 char pointer。 valrind 的報告指出問題會出在 line29: strdup 這裡,當新加入了 line 後,程式有確實的把被移出的 line 給釋放掉,那問題或許會出在 history 在程式執行完成後沒全部釋放

在 linenoise.c 有宣告history

static char **history = NULL;
main at qtest.c
#define BUFSIZE 256 int main(int argc, char *argv[]) { /* sanity check for git hook integration */ if (!sanity_check()) return -1; /* To hold input file name */ char buf[BUFSIZE]; char *infile_name = NULL; char lbuf[BUFSIZE]; char *logfile_name = NULL; int level = 4; int c; while ((c = getopt(argc, argv, "hv:f:l:")) != -1) { switch (c) { case 'h': usage(argv[0]); break; case 'f': strncpy(buf, optarg, BUFSIZE); buf[BUFSIZE - 1] = '\0'; infile_name = buf; break; case 'v': { char *endptr; errno = 0; level = strtol(optarg, &endptr, 10); if (errno != 0 || endptr == optarg) { fprintf(stderr, "Invalid verbosity level\n"); exit(EXIT_FAILURE); } break; } case 'l': strncpy(lbuf, optarg, BUFSIZE); buf[BUFSIZE - 1] = '\0'; logfile_name = lbuf; break; default: printf("Unknown option '%c'\n", c); usage(argv[0]); break; } } srand((unsigned int) (time(NULL))); queue_init(); init_cmd(); console_init(); /* Trigger call back function(auto completion) */ linenoiseSetCompletionCallback(completion); linenoiseHistorySetMaxLen(HISTORY_LEN); linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ set_verblevel(level); if (level > 1) { set_echo(true); } if (logfile_name) set_logfile(logfile_name); add_quit_helper(queue_quit); bool ok = true; ok = ok && run_console(infile_name); ok = ok && finish_cmd(); return ok ? 0 : 1; }
  1. 回到 qtest.c 觀察 main ,在呼叫 linenoiseHistoryLoad 後,似乎沒有做任何事來讓 history 被釋放,因此第二點的推論或許是正確的

  2. 從 linenoice.h 中看到以下宣告,猜測這跟釋放 history 或 line 有關

void linenoiseFree(void *ptr);
  1. 回去 linenoice.c 中查找實做,發現了沒被 header file 宣告的函式,此函式可以把 history 釋放
/* Free the history, but does not reset it. Only used when we have to
 * exit() to avoid memory leaks are reported by valgrind & co. */
static void freeHistory(void)
{
    if (history) {
        int j;

        for (j = 0; j < history_len; j++)
            free(history[j]);
        free(history);
    }
}
  1. 而在 linenoise.c ,只有另一個函式呼叫 freeHistory
/* At exit we'll try to fix the terminal to the initial conditions. */
static void linenoiseAtExit(void)
{
    disableRawMode(STDIN_FILENO);
    freeHistory();
}
  1. 嘗試找出 linenoiseAtExitfreeHistory 在專案有哪些地方會被執行,結果完全沒有,證實了第 2 點的猜想,根本沒有去釋放 history


  1. 因此要修復記憶體問題,就只有手動呼叫 linenoiseAtExit 或是 freeHistory
  • linenoiseAtExit 從 static void 改成 void 來改變可視範圍,並移動宣告到 header file
// linenoise.c - static void linenoiseAtExit(void);
// linenoise.h + void linenoiseAtExit(void);
  • 由於 qtest.c 內並沒有 include linenoise.h ,因此嘗試加在finish_cmd() 內,會選擇加在這是因為: qtest.c 在 main 結束前有呼叫 finish_cmd() 來檢驗程式有沒有正確關閉
// console.c bool finish_cmd() { bool ok = true; if (!quit_flag) ok = ok && do_quit(0, NULL); has_infile = false; + linenoiseAtExit(); return ok && err_cnt == 0; }
  1. 重新執行 make valgrind 後問題解決,沒有任何記憶體洩漏