--- tags: Linux Kernel --- # Linux CPU Scheduler Notes contributed by < [steven1lung](https://github.com/steven1lung) > Repo : https://github.com/steven1lung/cfs-visualizer-react Website : https://steven1lung.github.io/cfs-visualizer-react/ ## Chapter 1 Basic of Linux Kernel 我覺得 Linus Torvalds 解釋的作業系統很精準:「作業系統唯一的任務是要幫助程式運行,作業系統一直都在等著程式向它要求資源。」作業系統可以看成是硬體跟使用者的仲介,工作包含管理硬體資源跟應用程式,而應用程式會再與使用者進行互動。而作業系統核心可以執行特權指令,讓每個任務都可以安全地被執行。 作業系統核心會在開機的時候被初始化,並且透過 boot loader 載入到記憶體中,核心從開機載入後就會一直待在記憶體中。記憶體儲存核心重要程式碼的位置也會跟一般的程式分開,防止應用程式或是其他作業系統程式對重要部分進行存取。 核心的任務是要管理硬體資源,像是操作 CPU、記憶體階層、I/O 裝置等等。為了達成這些任務,核心要有權限對所有的資源進行存取。所以將 kernel code 跟 user application code 分開。實作的方式為: 1. 核心的 image 會被存到 RAM 中低或是高的位址 2. 剛剛儲存位址隔壁有一塊已經被定義的 RAM 會被留給核心 3. 剩下的 RAM 可以被使用者自由存取 > 存核心的區塊稱為 kernel space,而存使用者資料的區塊稱為 user space。 因為有這個機制,user space 會被高權限的系統管理。每個行程可以運行在 user mode 或是 kernel mode。User mode 下運行的行程可以使用「System Call」來使用高權限的功能,可以將 system call 視為使用者跟核心之間的 API。當一個行程呼叫 system call: 1. 會暫時運行在 kernel mode 下 2. 執行需要高權限的動作 3. 再切換回 user mode 舉例來說,x86 架構的兩位元 code selector (cs) 會顯示現在的權限等級 (current privilege level) 。0 代表 kernel mode,3 代表 user mode,每個系統呼叫都會暫時更改 cs 裡面的值。 Linux 核心是一塊非常特別的軟體,其中一個特性是核心自己沒有 error checking,因為核心非常信任自己,核心裡的函式都會被當成是沒有錯誤的。因為錯誤偵測跟處理需要花費不少心力,所以核心裡都是使用 assertion 來確認硬體跟軟體的一致性,一旦出現錯誤核心就會暫停或 `panic()`。但是程式錯誤或是硬體錯誤還會有機會在核心出現的,例如:在 user space 操作 null 指標會產生一個 segmentaion fault,而在 kernel space 操作 null 指標或產生一個 oops。核心產生 oops 之後,情況嚴重點還會 `panic()` ,這下這個核心就不能被信任跟繼續執行,所以通常都會重啟,以免出現破壞記憶體的問題。 核心有不同的設計方式,以光譜兩端為例:一邊會是 monolithic kernel,而一邊是 microkernel。在 monolithic 的設計裡,每個核心的服務都位在核心裡,而相反的,microkernel 目的是要將核心的大小縮小,盡量將大部分的服務移到 user space,只在 kernel space 留下最重要的部分。 > Linux 的設計偏向 monolithic kernel,所以許多程式都在 kernel space。 >現在也有 hybrid 處在兩種概念之間的設計,像是 macOS。 Microkernel 會這樣做的原因不外乎就是將受到信任的程式碼減少,因為信任的程式碼減少,所以需要做的 assertion 就會減少,不像是 monolithic 需要考慮到許多因素。另一個原因是將服務移到 user space 比較有維護性,可以做到直接在核心上開發測試,直接將要測試的服務編譯完後重啟就好了。 但是 Monolithic 還會一直被留著是有原因的,因為 microkernel 的效率太差。Monolithic 可以直接跟底層硬體溝通,而 microkernel 因為將服務移到 user space,所以會有許多行程之間的溝通,造成這個問題有兩個主因: 1. 呼叫大量的 system call 2. 許多 context switch Linux 核心試著解決這種可擴充性的問題是透過 kernel modules,可以在執行期間進行 `insmod` 跟 `rmmod` 載入跟移除核心模組。但因為模組是運行在 kernel space,所以在開發的時候會比較困難。 但是 kernel modules 也不能完全達到 microkernel 的功能,像是要在執行期間替換一個排程器是不允許的,除非有多個排程器可以在 kernel code 進行切換。 **所以 modules 並不是要實作核心的程式,而是當一個擴充額外功能的工具。** ## Chapter 2 The Linux CPU scheduler ### 排程器的演化: > [Brief History of Linux CPU Scheduler](https://docs.google.com/presentation/d/1L4MJaTv0_-ep2ejqE60QdOd6B75wbXg8bSiIPJHTwso/edit?usp=sharing) 1. 最早期的排程器 2. $O(n)$ 排程器 3. $O(1)$ 排程器 4. CFS ### 最早的排程器 當初這個排程器被設計出來並不是為了應付多處理器的架構,那時候使用了最簡單的設計。當初只使用了單一佇列存放任務,並迭代這個佇列來選取任務。在那時這個方法可行,是因當初有限制行程數量 (`NR_TASKS` 最初被設為 64)。 運作方法如下: 1. 在 runqueue 中以反向順序找尋最大 runtime 且大於 0 的行程。 2. 如果沒有找到,就重置所有行程的 runtime,並且會給目前在睡覺的行程加上一半 current time slice 的時間。 ### $O(n)$ 排程器 $O(n)$ 排程器跟最早的排程器其實概念差不多,會取這個名字是因為挑選任務要迭代過整個佇列,為了區隔在 Linux v2.6 出現的 $O(1)$ 排程器,這裡就命名早期的實作為 $O(n)$。不同的地方是這邊多了優先權的概念,從最高到最低為:即時行程(realtime process)、互動式行程(interactive process)、批次行程(batch process)。優先權越高的行程通常會拿到越大的 timeslice。 $O(n)$ 排程器介紹了一個新的概念:優先權,在程式碼裡使用的名字是:`goodness`。越「好」的行程優先權越高。 > 好人不長命,禍害遺千年的概念 xD。好的行程一下就被挑出來執行掉了。 :warning: 要注意到這邊的 goodness 跟 nice 是不一樣的! ### $O(1)$ 排程器 會取名 $O(1)$ 不外乎就是因為對任務的挑選跟加入所耗費的時間為 $O(1)$,上一個 $O(n)$ 排程器在處理數量小的行程時還不錯,但是若行程數量多起來,會浪費許多時間來迭代過如此多的行程。 並且 O(n) 排程器也在 SMP 系統有些困難: 1. 單一 runqueue > 造成行程被分配到不同處理器時會有大量的 cache miss (affinity)。 2. 單一 runqueue lock > 執行效率會因為一個 global 的 lock 而拖慢。 3. 無法搶佔正在執行的行程 > 在 pre-2.6 linux 版本,搶佔是不被允許的。造成一些低優先權行正在執行時,有許多高優先權行程在等待。 在 $O(1)$ 排程器中有一個 global scheduler,會在多個處理器 runqueue 中進行 load balance(work stealing)。這個策略是透過初始化時,各自佇列上的工人會先挑選自己住列的任務,如果沒有任務可以挑選就會變成「小偷」跑去其他住列偷任務。 $O(1)$ 排程器的優先權陣列實作是透過 bitmap 來達成,每個優權權等級都會有一個住列(共 `MAX_PRIO` (140) 個住列),所以要使用為元表達 140 個優先權,一個 word 32 位元,所以 `BITMAP_SIZE` 就為 5。Bitmap 作用是要指出哪些優先權是空的(1 為有任務,0 為空的住列),來達到快速找出任務的效果。 $O(1)$ 排程器中有兩個優先權陣列:Active 跟 Expired。Active 會儲存目前還有剩餘 timeslice 的行程,Expried 會儲存用完 timeslice 的行程。任務的排程會在 Active 中執行,若優先權最高的住列裡有多個行程,那就會使用 round-robin 來執行。會一路執行到 Active 裡的行程都跑完 timeslice 並加入到 Expired 中,這時候兩個優先權陣列就會互換,再繼續執行下去。 優先權的計算在這裡會依據任務的表現,一個行程的優先權是由 static priority 跟 bonus priority 去計算的。Static 對應到 nice 值,從 -20 到 +19。Bonus 則是從 -5 到 +5,並且是被任務的行為決定的:如果行程一直都在睡覺,那 bonus 會被設為 -5 提高優先權,反之亦然。 但是 $O(1)$ 排程器依舊存在著一些問題:因為 nice 值高的行程佔用太多處理器,造成許多抗議。所以 $O(1)$ 依賴一個最小的執行時間 "jiffy" 來分配 timeslice,jiffy 的大小取決於 tick rate。但是現今處理器都可以達到 1000 Hz,使得 jiffy 只有 1ms,就造成 nice 值 19 的任務 timeslice 只有 1ms,導致排程器很快又要挑選下一個任務。 還有問題是 starvation,低優先權的任務要等到所有高優先權任務執行完,才有機會被執行,導致一些即時行程會因為遲遲等不到處理器而緊張(priority inversion)。 ### Completely Fair Scheduler > 這邊要先知道 RSDL 排程器,可以去看老師的書 2.2.4 章,簡潔明瞭。 CFS 跟 RSDL 不同的地方在於 CFS 並沒有使用固定的 timeslice,也沒有使用任何 heuristic 函示來計算優先權,而是試著去在硬體上製作一個理想的多工 CPU。假設我們有一個很小很小的 timeslice,那我們就可以很理想地分給每個行程 $\frac{1}{n}$ 的處理器時間。 但是真正的處理器會在這種排程下做非常多的 context switch,所以現實世界不太可能會有這種完全平等的演算法。退而求其次的方法是使用 virtual runtime(`vruntime`),代表每個行程在理想情況下應該執行的時間。CFS 就是透過 vruntime 來選擇下一個任務,目標是維持每個任務的 vruntime ,讓每個 vruntime 都差不多大。所以 CFS 會挑選 vruntime 最小的任務來執行。 執行完的任務會將 vruntime 加上 ${time}\times{weight}$,weight 的值會依據優先權決定。CFS 會紀錄一個最小的 vruntime,也就是任務佇列中最小的 vruntime,來達到快速找到下一個執行目標。總而言之,CFS 會挑選最小 vruntime 的任務,並且 dynamic timeslice 會依據上述的理想排程方法計算。 * nice 跟實際執行時間有關 * vruntime 會被保持平衡 ## Chapter 3 Implementation of the Linux scheduler 參考資料: 1. 老師的書 2. https://elixir.free-electrons.com/linux/v5.10/source/include/linux/sched.h 3. https://developer.ibm.com/tutorials/l-completely-fair-scheduler/ ## CFS 機制介紹 跟上面 $O(n)$ 或是 $O(1)$ 排程器所不同的地方是,CFS 使用的資料結構並不是一個 queue,而是一個紅黑樹。會使用任務的 virtual runtime 來產生紅黑樹,並且每次都挑選最小的 virtual runtime 來執行,達到公平的目的(因為 vruntime 就是每個任務理想中要被執行的時間,所以最小 vruntime 的任務就會是目前為止理想執行時間最少的任務)。 > 紅黑樹的特性這邊就不做介紹了,[網路](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree)上很多。 排程器會使用到一些參數,這些參數通常都會存在 `task_struct` 裡面的 `sched_enitity` 中,`sched_enitity` 裡面會有 `load_weight`、 `struct rb_node run_node`、`vruntime` 等等的重要資訊。 ![](https://i.imgur.com/P1HBdPp.png) 計時器會在每個 tick period 透過 `do_timer()` 跟 `update_process_times()` 去更新系統的 uptime 還有各個行程的資訊。 在 `update_process_times()` 裡會呼叫到 `scheduler_tick()`,就是排程器每次更新 task 資訊的地方。`scheduler_tick()` 有幾個重要的呼叫: 1. `sched_clock_tick()` > 更新 per_CPU `sched_clock_data`。 2. `update_rq_clock(rq)` > 更新 runqueue 裡的 `clock_task`,會在 `update_curr()` 使用到 `clock_task`。 3. `task_tick(rq, curr, 0)` > 更新每個 task 的資料。 `task_tick` 會依照在 `sched_class` 所定義好的函示執行對應的動作,這邊就以老師的書裡的例子做使用,就會是 `.task_tick = task_tick_fair` (p.71),所以會執行 `task_tick_fair`。 `task_tick_fair` 會從傳入的 `rq` 拿到 `sched_entity`,並且透過一個 `for` 來對每一個 entity 進行更新。 ```c for_each_sched_entity(se) { cfs_rq = cfs_rq_of(se); entity_tick(cfs_rq, se, queued); } ``` 真正去更變更一個 enitity 的 `vruntime` 是 `enitity_tick()` 會呼叫到的第一個函式,`update_curr()`。更新完 `vruntime` 就會檢查現在的 enitity 有沒有用完分配的時間或是有沒有其他 entity 需要被執行。 一個 enitity 的 `vruntime` 的更新會發生在 `update_curr()` 中,所執行的運算如下: $$ vruntime = vruntime + weight\_of\_nice\_0 * \frac{delta\_exec}{task\_weight} $$ 其中的 `delta_exec` 是目前這個 enitity 所執行的時間,透過 `curr->exec_start` 減去現在的時間得到。而 `weight` 計算的公式是:$weight = \frac{1024}{1.25^{nice}}$,在 linux kernel 是直接用陣列將每個 nice 值都對應到一個權重。 分配給一個任務的 `timeslice` 是在 `entitiy_tick()` 最後幾行計算的,透過呼叫 `check_preempt_tick()`,而 `check_preempt_tick()` 會呼叫 `sched_slice(cfs_rq, curr)` 去計算要分配給任務的 `timeslice`。 $$ slice = target\_latency * \frac{task\_weight}{total\_weight} $$ 這邊要注意的是要計算出 `timeslice` 跟 `vruntime` 所呼叫的函式都是 `calc_delta()` 因為這兩個都差別只有傳進去的參數不同,運算是一樣的。`target_latency` 是每個任務執行一次所需要的最小時間。 > `sum_exec_runtime` 會紀錄任務目前所執行的時間 ## 模擬 CFS 這邊會列出一些等等會使用到的變數: 1. sched_latency > 排程器的週期 2. sched_min_granularity > 一個任務最少會被執行到的時間 3. sched_wakeup_granularity > 剛醒來的任務的 `vruntime` 如果大於現在任務的 `vruntime`,照理來說會選擇原本的任務繼續執行,但如果兩個任務 `vruntime` 之間的差距沒有超過 sched_wakeup_granularity 就會讓醒來的任務執行。 4. target_latency > 確保讓每個任務都執行到一次,所以會取 $max(sched\_latency,n*sched\_min\_granularity)$。 ### 建立一個任務會需要三個參數,分別是: 1. Arrival Time 2. Burst Time 3. Nice value (-20~+19) > 這邊的 nice 值是在說明對系統友善的程度,nice 值越大,priority 越低。 之後會將建立的任務加入紅黑樹中,加入紅黑樹的動作是在 `nr_running` 被增加之前完成的,詳細程式碼在 [kernel/sched/fair.c 中的 enqueue_task_fair](https://elixir.bootlin.com/linux/v5.10/source/kernel/sched/fair.c#L5475)。 ### 當排程器需要進行排程時: 1. 挑選 `vruntime` 最小的任務,也就是在紅黑樹最左邊的節點。 2. 被挑選的任務會被從黑紅樹中移除。 3. 任務會被賦予 timeslice。 > $slice = target\_latency * \frac{task\_weight}{total\_weight}$ ### 當任務正在被執行時: 1. 更新被執行的時間 `sum_exec_runtime += delta_exec` > `delta_exec` 就是現在的時間減去任務開始執行的時間:`delta_exec = now - curr->exec_start`。 2. 更新 vruntime `vruntime += delta_exec/task_weight * 1024` 從上方的運算式可以看出來如果一個任務的 `weight` 比較大,那這個任務 vruntime 的增加速度就會比較慢,讓排程器更容易挑選到這個任務。 ### 當一個任務的 timeslice 執行完時: 1. 如果任務還沒有完成,那他會被加回紅黑樹中。 2. 排程器進行排程。 ### 範例 輸入: ``` 3 11 A 1 3 0 B 2 4 -2 C 2 3 2 ``` 第一行的參數分別為任務數量跟總執行時間,接下來的每一行就是任務的名稱、開始的時間、需要執行的時間長度、nice 值。 > A 會在第一個單位到達,並且總共要執行 3 個單位,nice 值是 0。以此類推... #### Time = 0 Scheduler 開始,上述開頭有一些變數我們先假設為下列值: * sched_latency = 6ms * sched_min_granularity = 0.75ms * sched_wakeup_granularity = 1ms * target_latency = max(sched_latency,n * sched_min_granularity) #### Time = 1 任務 A 被加入排程器(burst time = 3, nice = 0),A 的 timeslice: $slice = target\_latency * \frac{task\_weight}{total\_weight} = sched\_latency * \frac{1024}{1024} = 6$ 這時候的 `vruntime` 是 0,因為根本沒有執行時間。 > `timeslice` = 6, `vruntime` = 0 所以任務 A 會被選中,並且從紅黑樹中移除。 #### Time = 2 任務 B(burst time = 4, nice = -2)、C(burst time = 3, nice = 2)被加入排程器。 A 的 timeslice: $slice = target\_latency * \frac{task\_weight}{total\_weight} = 6 * \frac{1024}{1024+655+1586} = 6 * 0.31 = 1.86$ B 的 timeslice: $slice = sched\_latency * \frac{1586}{1024+655+1586} = 2.91$ C 的 timeslice: = 1.2 $slice = sched\_latency * \frac{655}{1024+655+1586} = 1.2$ #### Time = 3 A 的 timeslice 跑完,但是 A 還沒有完成,所以會被加回紅黑樹中,新的 vruntime: $vruntime = vruntime + \frac{delta\_exec}{task\_weight} * 1024 = \frac{3-1}{1024} * 1024 = 2$ 排程器會挑選任務 B 執行,給予 timeslice = 2.91。 #### Time = 4, 5 任務 B 還在 timeslice 內,也沒有新的任務被加入。 #### Time = 6 B 的 timeslice 跑完,且 B 還沒完成,所以會加回紅黑樹中,新的 vruntime: $vruntime = \frac{6-3}{1586} * 1024 = 1.93$ 排程器會挑選任務 C 並且將其從紅黑樹移除,給予 timeslice = 1.2。 #### Time = 7 任務 C 還在 timeslice 內,也沒有新的任務被加入。 #### Time = 8 C 的 timeslcie 跑完,且 C 還沒完成,所以會加回紅黑樹中,新的 vruntime: $vruntime = \frac{8-6}{655} * 1024 = 3.12$ > A: vruntime = 2 B: vruntime = 1.91 C: vruntime = 3.12 排成器會挑選任務 B(vruntime = 1.91 最小),並給予 timeslice = 2.91。 #### Time = 9 B 執行完成。 A 的 timeslice: $slice = sched\_latency * \frac{1024}{1024+655} = 3.66$ C 的 timeslice: $slice = sched\_latency * \frac{655}{1024+655} = 2.34$ > A: vruntime = 2 C: vruntime = 3.12 排程器挑選任務 A,給予 timeslice = 3.66。 #### Time = 10 A 執行完成。 C 的 timeslice: $slice = sched\_latency * \frac{655}{655} = 6$ 排程器挑選任務 C,給予 timeslice = 6。 #### Time = 11 C 執行完成。 ## 實作 使用 reactjs 來實作,詳細程式碼在[這裡](https://github.com/steven1lung/cfs-visualizer-react)。目前將排程邏輯輸出在 console,可以透過 f12 看目前狀態。 ### Linux 紅黑樹 ```c struct rb_node { unsigned long __rb_parent_color; struct rb_node *rb_right; struct rb_node *rb_left; } __attribute__((aligned(sizeof(long)))); ``` > 看了 `rb_node` 跟 `list_head` 之後,發現 Linux 風格的資料結構都不會包含 data,而是會將 `rb_node` 或是 `list_head` 這些資料結構包在一個更大的結構體裡面。 一個紅黑樹節點包含:父節點、左節點、右節點、顏色。而在 Linux 宣告的程式碼中,很巧妙地將父節點跟顏色一起宣告,使用 `unsigned long __rb_parent_color` 將指向父節點的指標跟自己的顏色合併。 透過 `__attribute__((aligned(sizeof(long))))` 讓編譯器知道 `struct rb_node` 會對齊 `sizeof(long)`,這樣就會讓指標最右兩個位元是沒有使用到的,就可以把顏色的資料放進去其中一個位元中。 舉例來說,一個節點指標如果指向 `0xF724315C` ,這個位址轉成二進制會變成 `...1100`,最右邊兩個位元會是 0,所以開發者就把這其中一個沒用到的位元拿來儲存顏色。 > 更詳細的內容可以去看我的[紅黑樹筆記](https://hackmd.io/@steven1lung/linux-rbt) ### 紅黑樹 用 javascript 實作紅黑樹裡的插入跟刪除功能,節點的架構如下: ```javascript class Node { constructor(key, value) { this.key = key; this.value = value; this.left = null; this.right = null; this.color = RED; this.parent = null; } } ``` 紅黑樹的需要考慮到的實作有:插入、刪除、平衡,插入是我花比較多時間的地方。 > 原本考慮想說找找看網路上有沒有現成的套件,但還是覺得自己寫會對這個資料結構更熟悉。 ### 架構 ```javascript function click_next_clock(){ schedule_tick(); add_tasks(); update_new_tasks(); schedule(); } ``` 架構簡單來看會像上面那樣,每次點 next clock 按鈕意思是進入下一個 clock。每一個 clock 都會去呼叫 `sched_tick()` 更新 schedule entity 裡的一些資訊。如果有超過 timeslice 或是任務完成的話,就會呼叫 `schedule()` 選擇下一個任務。 如果有新的任務被加進 `readyqueue` 也會呼叫排程器,重新計算所有任務的 `timeslice`。如果有任務執行完 `timeslice` 但還沒有完成,就會更新 `vruntime` 並加回紅黑樹。 會執行的動作: 1. `schedule_tick` 會更新目前正在被處理器執行的 schedule entity 的資料,例如:`sum_exec_runtime`。如果執行完成或是用完 timeslice 的情況會在這邊判斷等等要不要 `schedule()`。用完 timeslice 的任務也會需要重新計算 `vruntime` 並且加回紅黑樹中。 2. `add_tasks` 會將這個 clock 要進入 ready queue 的任務初始化他們的 schedule entity 資料。 3. `update_new_tasks` 上述所新加入 ready queue 的 schedule entity 中,我們會需要重新計算所有任務的 timeslice,並且將這些新加入的任務加入紅黑樹。 4. `schedule` 如果目前任務執行完成(或是用完 timeslice),就會從紅黑樹拿最小 `vruntime` 的任務執行,並從紅黑樹中移除。 ### 目前功能 * 有 3、4、5 個任務的預設任務選擇 * 紅黑樹的狀態是在每個 clock 的最後,也就是排程器挑選完任務並將其從紅黑樹移除後的紅黑樹 * 右上角會顯示目前正在執行的任務 * 左上角會顯示現在的 clock * 左邊 clock 下方會顯示紅黑樹節點的資訊(透過滑鼠選擇要看的節點資訊) * 每次排程邏輯會顯示在右邊 clock 下方,比如說這個 clock 有沒有觸發排程、timeslice 結束的任務加回紅黑樹的 vruntime 是多少、選擇到的任務給多少 timeslice 等等 * 每次排程的邏輯在整個模擬結束後會顯示在最下方的 result 裡 ## Chapter 4 Group scheduling and cgroups 普通的 CFS 會去從任務的角度去分配執行時間,比如說:使用者 A 有 49 跟任務,使用者 B 有 1 個任務,那這樣 CFS 會分配這 50 個任務各 1/50 的執行時間。但是這樣從使用者的角度來看,A 被分配到快所有的時間,這樣對 B 不公平。 這個章節介紹了 groups 的概念,可以把任務分到一個群組,對一個群組去做排程,達到從使用者角度的公平。 Linux 在 v2.6.24 加入 group scheduling,當核心被設定 `CONFIG_FAIR_GROUP_SCHED` 就會被使用。 ## 其他 ### 嘗試發 patch 在看老師的書 Demystifying the Linux CPU Scheduler 的時候,突然好奇說每一個任務的 loading 是如何運算的,畢竟排程器上的任務如此多,計算出 load 然後依據每個 CPU 的狀態去做 task migration 應該是很重要的課題。所以我就跑去 [pelt.c ](https://elixir.bootlin.com/linux/latest/source/kernel/sched/pelt.c) 看了一下實際的運算是如何做到的。 > pelt 全名是 Per Entity Load Tracking 在看程式碼的時候,發現有個地方的中括號打反了變成 `[1002..1024[`,我就想說試試看發人生第一個 patch。 結果自己在摸索如何發 patch 花了好多時間...,轉眼間就早上了。所以就記錄了一下自己發 patch 的過程來紀念逝去的時間。雖然這個 patch 最後是我誤會中括號的用法,所以並沒有被採納,但跟這些厲害的人交流讓人有點興奮>< > 結果發現反過來的中括號是法文的用法,是我自己搞錯 xD 我發的 patch 在[這裡](https://lore.kernel.org/lkml/3193fcc9-c672-19d9-a2e2-ad67809dd20b@infradead.org/T/)。 記錄我發 patch 的過程可以點[這裡](https://hackmd.io/@steven1lung/submitting-patches),就不放在排程器筆記這裡了,佔空間。