# 2022q1 Homework4 (quiz4) contributed by < `shawn5141` > ## [測驗1](https://hackmd.io/@sysprog/linux2022-quiz4#%E6%B8%AC%E9%A9%97-1) :::success 延伸第 3 週測驗題的測驗 7,已知輸入必為大於 0 的數值 (即 x > 0),以下程式碼可計算 ⌈log2(x)⌉,也就是 ceil 和 log2 的組合並轉型為整數: ```cpp int ceil_log2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (EXP1) + 1; } ``` 請補完,使其行為符合預期。作答規範: EXP1 應以最簡潔的形式撰寫,且符合作業一排版規範 (近似 Linux 核心程式碼排版風格) EXP1 為表示式,限制使用 ^, &, |, <<, >> 這幾個運算子,可能會出現常數 EXP1 不該包含小括號 (即 ( 和 )) 為了自動批改的便利,變數出現的順序 (可從缺) 從左到右為 r, shift, x EXP1 不可包含 , 和 ; 這樣的分隔符號 (separator) ::: ### 延伸問題: #### 解釋上述程式碼運作原理 跟測驗七類似,ceil_log2是在用二分法去找MSB的位置。當最後剩下兩個位元,可以使用 x >> 1 來確認最高位元是0還是1。然後把原本都會做的`r | shift` 也直接拿到最後去當作return值。 ```cpp return (r | shift |x>>1) +1 ``` #### 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless ```cpp= int ceil_log2(uint32_t x) { uint32_t r, shift; uint32_t tmp = ((uint32_t)(x-1) > x); x = x - 1 + tmp; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (r | shift | x >> 1) + ( ~tmp & 1); } ``` 使用 tmp 去紀錄 x 是否為0。如果是零的話,第 5 行就不要扣掉1,並且最後也把該加回來的 1 取消掉。 #### 在 Linux 核心原始程式碼找出 ceil 和 log2 的組合 (類似的形式),並解說應用案例 (提示: 在 Linux 核心排程器就有) ## [測驗2](https://hackmd.io/@sysprog/linux2022-quiz4#%E6%B8%AC%E9%A9%97-2) :::success 複習〈[你所不知道的 C 語言: bitwise 操作](https://hackmd.io/@sysprog/c-bitwise)〉,改寫[第 3 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz3)的測驗 `11` 裡頭的 fls 函式 (fls 意謂 “find last set”),使其得以計算 [Find first set](https://en.wikipedia.org/wiki/Find_first_set): ```cpp #define BITS_PER_BYTE 8 #define BITS_PER_LONG (sizeof(unsigned long) * BITS_PER_BYTE) #include <stddef.h> static inline size_t ffs(unsigned long x) { if (x == 0) return 0; size_t o = 1; unsigned long t = ~0UL; size_t shift = BITS_PER_LONG; shift >>= 1; t >>= shift; while (shift) { if ((EXP2) == 0) { x >>= shift; EXP3; } shift >>= 1; t >>= shift; } return o; } ``` 請補完,使其行為符合預期。作答規範: * `EXP2` 和 `EXP3` 應以最簡潔的形式撰寫,且符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範 (近似 Linux 核心程式碼排版風格) * `EXP2` 和 `EXP3` 限制使用 ^, &, |, <<=, >>=, +=, -= 這幾個運算子,可能會出現常數 * `EXP2` 和 `EXP3` 不該包含小括號 (即 `(` 和 `)`) * 為了自動批改的便利,變數出現的順序 (可從缺) 從左到右為 `x`, `o`, `t`, `shift`,也就是說,你應該寫 `x ^ t` 而非 `t ^ x` * `EXP2` 和 `EXP3` 不可包含 `,` 和 `;` 這樣的分隔符號 (separator) ::: #### 解釋上述程式碼運作原理,並迴避 while, for, goto 等關鍵字以改寫出功能等價的實作 `EXP2 = x & t` `EXP3 = o |= shift` ```cpp= #define BITS_PER_BYTE 8 #define BITS_PER_LONG (sizeof(unsigned long) * BITS_PER_BYTE) static inline size_t ffs(unsigned long x) { size_t o = 1; unsigned long t = ~0UL; size_t shift = BITS_PER_LONG; shift >>= 1; t >>= shift; if ((word & t) == 0){ x >>= shift; o |= shift } shift >>= 1; t >>= shift; if ((word & t) == 0){ x >>= shift; o |= shift } shift >>= 1; t >>= shift; if ((word & t) == 0){ x >>= shift; o |= shift } shift >>= 1; t >>= shift; if ((word & t) == 0){ x >>= shift; o |= shift } shift >>= 1; t >>= shift; if ((word & t) == 0){ x >>= shift; o |= shift } shift >>= 1; t >>= shift; return o; } ``` #### 在 Linux 核心原始程式碼的 include/asm-generic/bitops 目錄找出相關程式碼,並探討應用案例 在[tools/include/asm-generic/bitops/__ffs.h](https://elixir.bootlin.com/linux/latest/source/tools/include/asm-generic/bitops/__ffs.h#L14) line 14 ```cpp= /** * __ffs - find first bit in word. * @word: The word to search * * Undefined if no bit exists, so code should check against 0 first. */ static __always_inline unsigned long __ffs(unsigned long word) { int num = 0; #if __BITS_PER_LONG == 64 if ((word & 0xffffffff) == 0) { num += 32; word >>= 32; } #endif if ((word & 0xffff) == 0) { num += 16; word >>= 16; } if ((word & 0xff) == 0) { num += 8; word >>= 8; } if ((word & 0xf) == 0) { num += 4; word >>= 4; } if ((word & 0x3) == 0) { num += 2; word >>= 2; } if ((word & 0x1) == 0) num += 1; return num; } ``` ## [測驗3](https://hackmd.io/@sysprog/linux2022-quiz4#%E6%B8%AC%E9%A9%97-3) :::success 考慮以下改寫自 Linux 核心的程式碼: ```cpp struct foo_consumer { int (*handler)(struct foo_consumer *self, void *); struct foo_consumer *next; }; struct foo { struct foo_consumer *consumers; unsigned long flags; }; #include <stdbool.h> /* * For foo @foo, delete the consumer @fc. * Return true if the @fc is deleted sfccessfully * or return false. */ static bool consumer_del(struct foo *foo, struct foo_consumer *fc) { struct foo_consumer **con; bool ret = false; for (con = &foo->consumers; *con; EXP4) { if (*con == fc) { *con = EXP5; ret = true; break; } } return ret; } ``` 請補完,使 `consumer_del` 行為符合註解規範。作答規範: 1. `EXP4` 和 `EXP5` 應以最簡潔的形式撰寫,且符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範 (近似 Linux 核心程式碼排版風格) 2. `EXP4` 和 `EXP5` 都包含指標操作 (如 `->`) ::: `EXP4 = *con = &(*con->next)` `EXP5 = con = fc->next` ```graphviz digraph g{ rankdir=LR; node [shape=record]; prev [label = "{<data>prev |<ref>}"]; fc [label = "{<data>fc |<ref>}"]; next [label = "{<data>next |<ref>}"]; node [shape=none] none1 [label = ""]; none2 [label = ""]; con; edge[weight=2]; none1 -> prev [arrowhead=vee]; prev:ref:c -> fc [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; fc:ref:c -> next [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; next:ref:c -> none2 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; edge[weight=next]; con -> prev:ref; } ``` ### 1. 解釋上述程式碼運作原理,並探討這樣區分 foo 和 foo_consumer 的好處 因為使用indirect pointer `con`,所以在`*con == fc`時,其實`con`是指向`prev`的`next`。所以要跳過fc這一點的話,必須要讓`con`現在指到的`next`跳過`fc`,直接指到`next`,也就是fc->next。所以EXP5可以等於 `con = fc->next` 或者是 `con = (*con)->next` - 探討這樣區分 foo 和 foo_consumer 的好處 類似`list_head`的做法,能夠類似linked list的功能直接放到struct當中。這樣就只要實作一套foo_consumer的操作就可以了。 ### 在 Linux 核心原始程式碼相似技巧的程式碼 (例如 uprobe),並探討應用案例 > what is [upprobe](https://docs.kernel.org/trace/uprobetracer.html): 本質上是在指定的探測點(比如函數的某行, 函數的入口地址和出口地址, 或者內核的指定地址處)插入一組處理程序. 內核執行到這組處理程序的時候就可以獲取到當前正在執行的上下文信息, 比如當前的函數名, 函數處理的參數以及函數的返回值, 也可以獲取到寄存器甚至全局數據結構的信息 good article: https://blog.px.dev/ebpf-function-tracing/ ![](https://i.imgur.com/VSkAr5O.png) ```cpp= struct trace_probe { struct list_head list; struct trace_probe_event *event; ssize_t size; /* trace entry size */ unsigned int nr_args; struct probe_arg args[]; }; ``` --- ## [測驗4](https://hackmd.io/@sysprog/linux2022-quiz4#%E6%B8%AC%E9%A9%97-4) :::success 以下嘗試用 [lab0](https://hackmd.io/@sysprog/linux2022-lab0) 提及的 [setjmp](https://man7.org/linux/man-pages/man3/setjmp.3.html) 和 [longjmp](https://man7.org/linux/man-pages/man3/setjmp.3.html),用以實作〈[你所不知道的 C 語言: goto 和流程控制篇](https://hackmd.io/@sysprog/c-control-flow)〉闡述的 [coroutine](https://en.wikipedia.org/wiki/Coroutine),參考的程式執行輸出如下: ``` Task 0: n = 3 Task 1: n = 3 Task 0: resume Task 1: resume Task 0: resume Task 0: complete Task 1: resume Task 1: complete ``` 原始程式碼如下: (檔名 `jmp.c`) ```cpp= #include <setjmp.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "list.h" struct task { jmp_buf env; struct list_head list; }; static LIST_HEAD(tasklist); static void (**tasks)(void *); static int ntasks; static jmp_buf sched; static void task_add(struct list_head *tasklist, jmp_buf env) { struct task *t = malloc(sizeof(*t)); memcpy(t->env, env, sizeof(jmp_buf)); INIT_LIST_HEAD(&t->list); list_add_tail(&t->list, tasklist); } static void task_switch(struct list_head *tasklist) { jmp_buf env; if (!list_empty(tasklist)) { struct task *t = list_first_entry(tasklist, struct task, list); EXP6; memcpy(env, t->env, sizeof(jmp_buf)); free(t); longjmp(env, 1); } } static void task_join(struct list_head *tasklist) { jmp_buf env; while (!list_empty(tasklist)) { struct task *t = list_first_entry(tasklist, struct task, list); EXP7; memcpy(env, t->env, sizeof(jmp_buf)); free(t); longjmp(env, 1); } } void schedule(void) { static int i; srand(0xCAFEBABE ^ (uintptr_t) &schedule); /* Thanks to ASLR */ setjmp(sched); while (ntasks-- > 0) { int n = rand() % 5; tasks[i++](&n); printf("Never reached\n"); } task_join(&tasklist); } /* A task yields control n times */ void task0(void *arg) { jmp_buf env; static int n; n = *(int *) arg; printf("Task 0: n = %d\n", n); if (setjmp(env) == 0) { task_add(&tasklist, env); EXP8; } for (int i = 0; i < n; i++) { if (setjmp(env) == 0) { task_add(&tasklist, env); task_switch(&tasklist); } printf("Task 0: resume\n"); } printf("Task 0: complete\n"); longjmp(sched, 1); } void task1(void *arg) { jmp_buf env; static int n; n = *(int *) arg; printf("Task 1: n = %d\n", n); if (setjmp(env) == 0) { task_add(&tasklist, env); EXP9; } for (int i = 0; i < n; i++) { if (setjmp(env) == 0) { task_add(&tasklist, env); task_switch(&tasklist); } printf("Task 1: resume\n"); } printf("Task 1: complete\n"); longjmp(sched, 1); } #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) int main(void) { void (*registered_task[])(void *) = {task0, task1}; tasks = registered_task; ntasks = ARRAY_SIZE(registered_task); schedule(); return 0; } ``` 請補完,使行為符合註解規範。作答規範: 1. `EXP6`, `EXP7`, `EXP8`, `EXP9` 應以最簡潔的形式撰寫,且符合[作業一](https://hackmd.io/@sysprog/linux2022-lab0)排版規範 (近似 Linux 核心程式碼排版風格) 2. `EXP6` 和 `EXP7` 都包含 List API 的使用 3. `EXP8` 和 `EXP9` 應有函式呼叫 ::: ### 1. 解釋上述程式碼運作原理,舉出上述 [coroutine](https://en.wikipedia.org/wiki/Coroutine) 實作的限制 (注意 stack) ###### `task_add`: 除了init list_head,也讓每個task都有自己的jmp_buf env。最後透過 `list_add_tail` 把list_head加入list tail。 ###### `task_switch` 做的事情就是把放在第一個的list_head 取出來,然後透過longjmp跳過去。然後`task_switch` will use `longjmp(env, 1);` to jump to where setjmp(env) ###### `schedule` 透過`setjmp(sched);` 把當下的context存到shed這個jmp_buf中。並且開始呼叫task0 or task1 ###### `task0` 和`task1` 基本上長得一樣。一開始呼叫`setjmp(env)` 把local context存到 local的env中,然後呼叫`task_addf` 把自己這個task加到link list中,並把local env 的資料也一併放進去。(因為是`setjmp(env)==0`所以一開始呼叫時就會執行) 真的要分析的話,還是必須從ouput下手。 ``` Task 0: n = 3 (step1) Task 1: n = 3 (step2) Task 0: resume (step3) Task 1: resume (step4) Task 0: resume (step5) Task 0: complete (step6) Task 1: resume (step7) Task 1: complete (step8) ``` 從output可以發現 `Task 0` 印出 `n=3` (step1) 之後做了一次`context switch`,所以讓 `EXP8=longjmp(sched, 1)` 就會跳到scheduler的一開始,然後進到 `task1` 並印出 `n=3` (step2)後又跳回 `task0` ,代表 `EXP9=longjmp(sched, 1);`。 當 `task0` 呼叫 `task_switch` 的時候,會取出存放在 linked list head 的 task,然後在 `free(t)` 後,就跳到這個`env`存放的`context`。(跳回 `task0` 的時候,`task list` 中還有著 `task1` 的 list_head)。再跳到task0之後,就會進到for迴圈,然後透過 `setjmp` 再次設定 `env` ,並透過 `task_add` 把自己加到 `task list` 中,並透過 `task_switch` 取出 `task1` 並跳過去。`task1` 也執行同樣的事情,進到回圈然後設定 `setjmp(env)` ,然後再跳回`task0`。這次因為 `longjmp(env,1)` 所以 `setjmp(env)==0` 會是`false`,也就進到`printf("Task 0: resume\n");`這一行(step3)。然後再次迴圈,並設定 `setjmp(env)` 和把自己放到task list tail和進行 `context switch`到 task1。 接著因為是從longjmp跳過來的,所以就不進到 `setjmp(env)==0` 的`condition` 中,而是直接進到 `printf("Task 1: resume\n");` (step4)。並且開始第二次的回圈。因為n這邊是設定3,所以總共 `task0` 和 `task1` 共會印出 `resume` 兩次(第一次還沒印出來就context switch了)。最後在執行完回圈後,就會印出 `complete` (step6)。然後使用 `longjmp(sched, 1);` 跳回 `schedule` 最開始。這時候因為 `ntasks<0` 了,所以不會進到迴圈,而是直接跳到task_join,把剩餘的task處理完。 > EXP6= list_del(&list) > EXP7= list_del(&list) > EXP8= longjmp(sched, 1) > EXP9= longjmp(sched, 1) <details> <summary>完整程式</summary> ```cpp= #include <setjmp.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include "list.h" struct task { jmp_buf env; struct list_head list; }; static LIST_HEAD(tasklist); static void (**tasks)(void *); static int ntasks; static jmp_buf sched; static void task_add(struct list_head *tasklist, jmp_buf env) { struct task *t = malloc(sizeof(*t)); memcpy(t->env, env, sizeof(jmp_buf)); INIT_LIST_HEAD(&t->list); list_add_tail(&t->list, tasklist); } static void task_switch(struct list_head *tasklist) { jmp_buf env; if (!list_empty(tasklist)) { struct task *t = list_first_entry(tasklist, struct task, list); list_del(&t->list); memcpy(env, t->env, sizeof(jmp_buf)); free(t); longjmp(env, 1); } } static void task_join(struct list_head *tasklist) { jmp_buf env; while (!list_empty(tasklist)) { struct task *t = list_first_entry(tasklist, struct task, list); list_del(&t->list); memcpy(env, t->env, sizeof(jmp_buf)); free(t); longjmp(env, 1); } } void schedule(void) { static int i; srand(0xCAFEBABE ^ (uintptr_t) &schedule); /* Thanks to ASLR */ setjmp(sched); while (ntasks-- > 0) { int n = 3; tasks[i++](&n); printf("Never reached\n"); } task_join(&tasklist); } /* A task yields control n times */ void task0(void *arg) { jmp_buf env; static int n; n = *(int *) arg; printf("Task 0: n = %d\n", n); if (setjmp(env) == 0) { task_add(&tasklist, env); longjmp(sched, 1); } for (int i = 0; i < n; i++) { if (setjmp(env) == 0) { task_add(&tasklist, env); task_switch(&tasklist); } printf("Task 0: resume\n"); } printf("Task 0: complete\n"); longjmp(sched, 1); } void task1(void *arg) { jmp_buf env; static int n; n = *(int *) arg; printf("Task 1: n = %d\n", n); if (setjmp(env) == 0) { task_add(&tasklist, env); longjmp(sched, 1); } for (int i = 0; i < n; i++) { if (setjmp(env) == 0) { task_add(&tasklist, env); task_switch(&tasklist); } printf("Task 1: resume\n"); } printf("Task 1: complete\n"); longjmp(sched, 1); } #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) int main(void) { void (*registered_task[])(void *) = {task0, task1}; tasks = registered_task; ntasks = ARRAY_SIZE(registered_task); schedule(); return 0; } ``` </details> ### 2. 對比 [concurrent-programs](https://github.com/sysprog21/concurrent-programs) 裡頭的 coroutine 程式碼,如 [tinync](https://github.com/sysprog21/concurrent-programs/blob/master/tinync) 和 [preempt_sched](https://github.com/sysprog21/concurrent-programs/blob/master/preempt_sched),嘗試引入 UNIX Signal 來做到 preemptive scheduling ### 3. 對比〈[A brief history of the Linux Kernel’s process scheduler: The very first scheduler, v0.01](https://dev.to/satorutakeuchi/a-brief-history-of-the-linux-kernel-s-process-scheduler-the-very-first-scheduler-v0-01-9e4)〉,解說早期 Linux 核心 CPU 排程器的行為,並利用上述程式碼來模擬 ## [測驗5](https://hackmd.io/@sysprog/linux2022-quiz4#%E6%B8%AC%E9%A9%97-5) :::success 〈[你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor)〉已提過若干 Linux 核心的巨集,不難發現這些巨集經歷多次演變。Linux 核心一度有個巨集 `ACCESS_ONCE`,其作用是確實地讀取所指定記憶體位址的內容值,且限這一次,其原始定義如下: ```cpp #define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x)) ``` > 注意 `volatile` 關鍵字的使用 `ACCESS_ONCE` 巨集的使用情境,可參照 Linux v4.0 的 `kernel/locking/mutex.c`: ```cpp while (true) { struct task_struct *owner; ... /* * If there's an owner, wait for it to either * release the lock or go to sleep. */ owner = ACCESS_ONCE(lock->owner); if (owner && !mutex_spin_on_owner(lock, owner)) break; ``` 然而,如果不使用 `ACCESS_ONCE` 巨集,程式碼如下: ```cpp while (true) { struct task_struct *owner; ... /* * If there's an owner, wait for it to either * release the lock or go to sleep. */ owner = lock->owner; if (owner && !mutex_spin_on_owner(lock, owner)) break; ``` 由於,編譯器偵測到第 8 行的 `owner = lock->owner` 在迴圈中沒有被更改,所以其最佳化機制可能將第 8 行的程式碼搬出 while 迴圈之外,如此不用每次都實際地讀取 `lock->owner`,其程式碼變成: ```cpp struct task_struct *owner = lock->owner; while (true) { ... if (owner && !mutex_spin_on_owner(lock, owner)) break; ``` 但問題來了,`lock->owner` 有可能被其它核心執行緒修改,從而造成資料不一致。因此使用 `ACCESS_ONCE` 巨集可以防止編譯器做相關最佳化工作,並確保每次都能到實體記憶體位址讀取。其做法便是將某參數暫時性地轉換成具備 `volatile` 的型態。如此,存取該參數在非引入 `ACESS_ONCE` 巨集之處 (不具 `volatile` 特性),仍可享用編譯器最佳化的好處。 在 [ACCESS_ONCE() and compiler bugs](https://lwn.net/Articles/624126/) 則提及 Linux 核心捨棄上述 `ACCESS_ONCE` 巨集,改為 `READ_ONCE` 巨集。在 Linux 核心原始程式碼曾存在以下註解: > `ACCESS_ONCE` will only work on scalar types. For union types, ACCESS_ONCE on a union member will work as long as the size of the member matches the size of the union and the size is smaller than word size. > > The major use cases of `ACCESS_ONCE` used to be > > 1. Mediating communication between process-level code and irq/NMI handlers, all running on the same CPU, and > 2. Ensuring that the compiler does not fold, spindle, or otherwise mutilate accesses that either do not require ordering or that interact with an explicit memory barrier or atomic instruction that provides the required ordering. 以下是可能的實作: ```cpp= #include <stdint.h> #include <string.h> #include <stdlib.h> #define __READ_ONCE_SIZE \ ({ \ switch (size) { \ case 1: \ *(uint8_t *) res = *(volatile uint8_t *) p; \ break; \ case 2: \ *(uint16_t *) res = *(volatile uint16_t *) p; \ break; \ case 4: \ *(uint32_t *) res = *(volatile uint32_t *) p; \ break; \ case 8: \ *(uint64_t *) res = *(volatile uint64_t *) p; \ break; \ default: \ memcpy((void *) res, (const void *) p, size); \ } \ }) static inline void __read_once_size(const volatile void *p, void *res, int size) { __READ_ONCE_SIZE; } #define READ_ONCE(x) \ ({ \ union { \ typeof(x) __val; \ DECL0; \ } __u; \ __read_once_size(&(x), __u.__c, sizeof(x)); \ __u.__val; \ }) ``` `READ_ONCE` 巨集會判斷變數的寬度,針對寬度為 1, 2, 4, 8 位元組的變數,將其轉換為對應的純量 (scalar) 型態並增加 volatile 關鍵字,而對於其他資料寬度的類型,改呼叫 `memcpy` 來避免編譯器對該變數進行最佳化。 請補完程式碼,使其運作符合預期。書寫規範: 1. 符合一致的程式碼風格,使用最精簡的方式撰寫 2. `DECL0` 為變數宣告,不該存在 `;` 和 `,` 分隔字元 ::: ### 1. 解釋上述程式碼運作原理,並佐以〈[ACCESS_ONCE() and compiler bugs](https://lwn.net/Articles/624126/)〉及〈[你所不知道的 C 語言:編譯器和最佳化原理篇](https://hackmd.io/@sysprog/c-compiler-optimization)〉進行說明 #### [volatile](https://blog.csdn.net/Roland_Sun/article/details/107365134) > 在C语言中,volatile关键字的作用是:声明这个变量易变,不要把它当成一个普通的变量,做出错误的优化。保证CPU每次都从内存重新读取变量的值,而不是用寄存器中暂存的值。注意,这里说的是寄存器中缓存的值,而不是CPU缓存中存的值。很多英文文档里面都说了Cache,容易让人产生误解。 #### DECL0 = char[1] __c 這邊的 `__u.__c` 是拿來當作指標儲存被`volatile`標注的`union`,所以使用char array。 ### 2. 研讀〈[Why the “volatile” type class should not be used](https://docs.kernel.org/process/volatile-considered-harmful.html)〉和〈[並行程式設計: Atomics 操作](https://hackmd.io/@sysprog/concurrency-atomics)〉,並分析近期 [include/asm-generic/rwonce.h](https://github.com/torvalds/linux/blob/master/include/asm-generic/rwonce.h) 中 `READ_ONCE` / `WRITE_ONCE` 巨集的實作和其演化 (使用 `git log` 觀察) :::danger 注意書寫規範:中英文間用一個半形空白字元或標點符號區隔 :notes: jserv ::: :::info 了解,感謝提醒。 :notes: Shawn :::