前言
在翻 futex
man document 的時候,不小心看到 Linux Futex的设计与实现 這篇文章。文章中有提到在執行 sem_post
的時候,雖然沒有與其他 thread 競爭,還是會用到 fuxtex system call。當然文章中有提到原因,不過看了原因,覺得這看起來很明顯的效能問題,應該會被提出來並改進吧?後來翻了一下目前的 source code ,看到這個是 GLIBC_2_0
的版本(那篇文章也很久了,所以能想見當時的版本應該蠻舊的),而現在 GLIBC_2_1
就有把這部分改進,因此就來簡單記錄一下差異在哪裡吧~
Version
glibc 2.29
GLIBC_2_0 version
1int attribute_compat_text_section __old_sem_post (sem_t *sem)
2{
3 int *futex = (int *) sem;
4 atomic_write_barrier ();
5 (void) atomic_increment_val (futex);
6 int err = lll_futex_wake(futex, 1, LLL_SHARED);
7 if (__builtin_expect (err, 0) < 0)
8 {
9 __set_errno (-err);
10 return -1;
11 }
12 return 0;
13}
從 source code 可以看到,在將 sem value + 1 後,就直接使用 lll_futex_wake
呼叫 futex wake system call,其原因在文章內也有提到,是因為當 sem value 加 1 後,當時的機制並無法確認是否有其他 thread 正在 wait。
也因此,才會有原文提到的:
120533 futex(0xb7db1be8, FUTEX_WAIT, 20534, NULL <unfinished ...> // pthread_join
220534 futex(0x8049870, FUTEX_WAKE, 1) = 0 // sem_post
320533 <... futex resumed> ) = 0
這樣的情形發生。
GLIBC_2_1 version
1// 這邊假設為 64 bit processor
2int __new_sem_post (sem_t *sem)
3{
4 struct new_sem *isem = (struct new_sem *) sem;
5 int private = isem->private;
6 uint64_t d = atomic_load_relaxed (&isem->data);
7 do
8 {
9 // SEM_VALUE_MASK = 32
10 // SEM_VALUE_MAX = 2147483647 (Maximum value the semaphore can have)
11 if ((d & SEM_VALUE_MASK) == SEM_VALUE_MAX)
12 {
13 __set_errno (EOVERFLOW);
14 return -1;
15 }
16 }
17 while (!atomic_compare_exchange_weak_release (&isem->data, &d, d + 1));
18 if ((d >> SEM_NWAITERS_SHIFT) > 0)
19 futex_wake (((unsigned int *) &isem->data) + SEM_VALUE_OFFSET, 1, private);
20 return 0;
21}
在新版可以看到,為了解決無法確認當前是否有其他 threads 正在 wait的問題, sem value 被分為兩個部分: nwaiters
and value
,然後使用 bit shift 來取出 nwaiters number ,只有當 nwaiters > 0 時,才會呼叫 futex_wake 了。
接著就使用原文中的實驗程式,來看一下新版的結果:
1set_tid_address(0x7feb89cd39d0) = 3771
2set_robust_list(0x7feb89cd39e0, 24) = 0
3rt_sigaction(SIGRTMIN, {sa_handler=0x7feb898a0bd0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7feb898ac0e0}, NULL, 8) = 0
4rt_sigaction(SIGRT_1, {sa_handler=0x7feb898a0c60, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7feb898ac0e0}, NULL, 8) = 0
5rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
6getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
7mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7feb88cfb000
8brk(NULL) = 0x5599ebd5e000
9brk(0x5599ebd7f000) = 0x5599ebd7f000
10mprotect(0x7feb88cfb000, 4096, PROT_NONE) = 0
11clone(child_stack=0x7feb894faff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7feb894fb9d0, tls=0x7feb894fb700, child_tidptr=0x7feb894fb9d0) = 3772
12futex(0x7feb894fb9d0, FUTEX_WAIT, 3772, NULLsem value = 0 // pthread_join
13) = 0
14exit_group(0)
可以清楚看到,除了 pthread_join
的 futex wait 之外,並沒有發現如同舊版的 futex wake 了。
結論
目前看到很多大神的 source code 分析。其實都有點年代了,建議還是要再翻一次當前版本的 source code ,才不會只學到舊觀念,而沒跟上新版本呀!
附錄
實驗 source code
1#include <stdio.h>
2#include <unistd.h>
3#include <semaphore.h>
4#include <pthread.h>
5
6sem_t sem_a;
7void *task1();
8
9int main(void){
10 int ret=0;
11 pthread_t thrd1;
12 sem_init(&sem_a,0,1);
13 ret=pthread_create(&thrd1,NULL,task1,NULL);
14 pthread_join(thrd1,NULL);
15}
16
17void *task1()
18{
19 int sval = 0;
20 sem_wait(&sem_a);
21 sleep(1);
22 sem_getvalue(&sem_a,&sval);
23 printf("sem value = %d\n",sval);
24
25 (&sem_a);
26}