(´-`).。oO(ふと気になったのでpthread_exit(3)を調べてみる。
まず、man 3 pthread_exitによる基本的な部分はこのような感じ。
- pthread_exit()を呼び出したスレッドを終了する
- cleanupハンドラでスレッド終了時に実行させたい処理を登録できる
- スレッド終了時はatexit()で登録した関数は呼ばれない
- プロセスの最後のスレッドが終了する時にexit(3)が呼ばれる
- この時はatexit()で登録した関数が呼ばれる
- mainスレッドがchildスレッドを残して終了する場合はexit(3)ではなくpthread_exit(3)を呼ぶ必要がある
その他に、proc(5)でもpthread_exit()について言及されていて、mainスレッドが終了していた場合は制限が出てくる。
例えば以下のファイルなどは参照できなくなる
- /proc/[pid]/cwd
- /proc/[pid]/exe
実際に試してみるためこんなプログラムを用意します。開くのは/proc/net/tcpで/proc/netは/proc/selfへのシンボリックリンクです。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> int do_open(char *name) { FILE *fp; printf("open file %s\n", name); fp = fopen(name, "r"); if (!fp) return -1; fclose(fp); return 0; } int open_by_pid(void) { char buf[32] = { 0 }; sprintf(buf, "/proc/%d/net/tcp", getpid()); return do_open(buf); } int open_by_self(void) { return do_open("/proc/net/tcp"); } void *test(void *arg) { printf("child thread's pid %d\n", getpid()); sleep(1); if (open_by_self()) printf("open_by_self failed\n"); else printf("open_by_self success\n"); if (open_by_pid()) printf("open_by_pid failed\n"); else printf("open_by_pid success\n"); system("ls /proc"); return NULL; } int main(int argc, char **argv) { pthread_t th; printf("main thread's pid %d\n", getpid()); pthread_create(&th, NULL, &test, NULL); pthread_exit(0); }
上記コードをコンパイルして実行すると以下のようになり、/proc/net/tcp、/proc/[pid]/net/tcpともにファイル開くのに失敗します。/procには17775が見ているけどchildスレッドからアクセスできないんですね。
masami@saga:~$ ./a.out main thread's pid 17775 child thread's pid 17775 open file /proc/net/tcp open_by_self failed open file /proc/17775/net/tcp open_by_pid failed 1 1088 12105 133 14223 168 181 221 268 320 355 427 51 534 658 688 71 747 79 93 consoles iomem lockdep_stats scsi uptime 10 11 123 13344 14232 169 19 226 27 328 36 428 514 55 659 690 716 75 794 94 cpuinfo ioports locks self version 1040 1111 125 134 14257 17 2 228 28 329 37 43 52 56 66 691 718 76 8 97 crypto irq meminfo slabinfo vmallocinfo 1046 1119 126 135 143 170 20 23 29 33 38 45 525 57 661 693 719 763 80 9722 devices kallsyms misc softirqs vmstat 1054 1138 127 136 15 171 202 230 3 331 40 46 526 58 668 694 72 768 81 9821 diskstats kcore modules stat zoneinfo 1055 1160 128 13773 16 17129 209 234 30 335 41 461 528 60 669 696 723 77 83 acpi dma key-users mounts swaps 1057 11826 129 138 16019 17471 21 24 303 336 413 47 529 61 67 698 724 7768 838 asound driver kmsg mtrr sys 1061 119 12905 13930 163 177 210 25 304 34 414 48 53 62 674 7 729 778 84 buddyinfo execdomains kpagecount net sysrq-trigger 1062 11997 13 14 164 17775 211 26 307 348 416 489 530 621 676 70 73 78 85 bus fb kpageflags pagetypeinfo sysvipc 10642 12 130 14018 165 17777 213 260 31 35 42 490 531 622 68 701 730 781 9 cgroups filesystems latency_stats partitions timer_list 10643 12074 131 14019 166 18 219 264 318 351 420 5 532 63 684 703 733 787 90 cmdline fs loadavg sched_debug timer_stats 1083 121 132 14147 167 180 22 265 32 353 424 50 533 65 687 708 74 789 92 config.gz interrupts lockdep schedstat tty
次にこれをstraceで見てみようと思うんだけど、ファイルオープンとかの余計な処理は見る必要ないのでシンプル版で実行。
#include <unistd.h> #include <pthread.h> #include <unistd.h> void *test(void *arg) { sleep(1); return NULL; } int main(int argc, char **argv) { pthread_t th; pthread_create(&th, NULL, &test, NULL); pthread_exit(0); return 0; }
実行するとトレースは以下のように。
19988 execve("./a.out", ["./a.out"], [/* 53 vars */]) = 0 19988 brk(0) = 0x113a000 19988 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) 19988 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 19988 fstat(3, {st_mode=S_IFREG|0644, st_size=119644, ...}) = 0 19988 mmap(NULL, 119644, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f66c44aa000 19988 close(3) = 0 19988 open("/usr/lib/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3 19988 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000`\0\0\0\0\0\0"..., 832) = 832 19988 fstat(3, {st_mode=S_IFREG|0755, st_size=149301, ...}) = 0 19988 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f66c44a9000 19988 mmap(NULL, 2217104, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f66c408a000 19988 mprotect(0x7f66c40a2000, 2097152, PROT_NONE) = 0 19988 mmap(0x7f66c42a2000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18000) = 0x7f66c42a2000 19988 mmap(0x7f66c42a4000, 13456, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f66c42a4000 19988 close(3) = 0 19988 open("/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 19988 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20\1\2\0\0\0\0\0"..., 832) = 832 19988 fstat(3, {st_mode=S_IFREG|0755, st_size=2047384, ...}) = 0 19988 mmap(NULL, 3858192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f66c3cdc000 19988 mprotect(0x7f66c3e80000, 2097152, PROT_NONE) = 0 19988 mmap(0x7f66c4080000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a4000) = 0x7f66c4080000 19988 mmap(0x7f66c4086000, 16144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f66c4086000 19988 close(3) = 0 19988 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f66c44a8000 19988 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f66c44a7000 19988 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f66c44a6000 19988 arch_prctl(ARCH_SET_FS, 0x7f66c44a7700) = 0 19988 mprotect(0x7f66c4080000, 16384, PROT_READ) = 0 19988 mprotect(0x7f66c42a2000, 4096, PROT_READ) = 0 19988 mprotect(0x7f66c44c8000, 4096, PROT_READ) = 0 19988 munmap(0x7f66c44aa000, 119644) = 0 19988 set_tid_address(0x7f66c44a79d0) = 19988 19988 set_robust_list(0x7f66c44a79e0, 24) = 0 19988 rt_sigaction(SIGRTMIN, {0x7f66c408fb10, [], SA_RESTORER|SA_SIGINFO, 0x7f66c40994b0}, NULL, 8) = 0 19988 rt_sigaction(SIGRT_1, {0x7f66c408fba0, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x7f66c40994b0}, NULL, 8) = 0 19988 rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0 19988 getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0 19988 mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f66c34db000 19988 brk(0) = 0x113a000 19988 brk(0x115b000) = 0x115b000 19988 mprotect(0x7f66c34db000, 4096, PROT_NONE) = 0 19988 clone(child_stack=0x7f66c3cdaff0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f66c3cdb9d0, tls=0x7f66c3cdb700, child_tidptr=0x7f66c3cdb9d0) = 19989
ここでclone()が呼ばれているのでpthread_create()の処理をしているはず。さらに見ていって、
19989 set_robust_list(0x7f66c3cdb9e0, 24 <unfinished ...> 19988 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC <unfinished ...> 19989 <... set_robust_list resumed> ) = 0 19988 <... open resumed> ) = 3 19989 rt_sigprocmask(SIG_BLOCK, [CHLD], <unfinished ...> 19988 fstat(3, <unfinished ...> 19989 <... rt_sigprocmask resumed> [], 8) = 0 19988 <... fstat resumed> {st_mode=S_IFREG|0644, st_size=119644, ...}) = 0 19989 rt_sigaction(SIGCHLD, NULL, <unfinished ...> 19988 mmap(NULL, 119644, PROT_READ, MAP_PRIVATE, 3, 0 <unfinished ...> 19989 <... rt_sigaction resumed> {SIG_DFL, [], 0}, 8) = 0 19988 <... mmap resumed> ) = 0x7f66c44aa000 19989 rt_sigprocmask(SIG_SETMASK, [], <unfinished ...> 19988 close(3 <unfinished ...> 19989 <... rt_sigprocmask resumed> NULL, 8) = 0 19988 <... close resumed> ) = 0 19989 nanosleep({1, 0}, <unfinished ...>
ここがsleep(1)の処理。{1, 0}というのはstruct timespecのtv_secに1、tv_nsecに0で1秒のsleepを設定。
19988 open("/usr/lib/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3 19988 read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260*\0\0\0\0\0\0"..., 832) = 832 19988 fstat(3, {st_mode=S_IFREG|0644, st_size=90088, ...}) = 0 19988 mmap(NULL, 2185952, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f66c32c5000 19988 mprotect(0x7f66c32db000, 2093056, PROT_NONE) = 0 19988 mmap(0x7f66c34da000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x15000) = 0x7f66c34da000 19988 close(3) = 0 19988 munmap(0x7f66c44aa000, 119644) = 0 19988 futex(0x7f66c34da850, FUTEX_WAKE_PRIVATE, 2147483647) = 0 19988 _exit(0) = ?
この_exit()はpthread_exit()かな。
19989 <... nanosleep resumed> 0x7f66c3cdad60) = 0
ここでchildスレッドがsleep(1)から復帰。
19989 exit_group(0) = ? 19989 +++ exited with 0 +++ 19988 +++ exited with 0 +++
最後にexit()ではなくてexit_group()で終了。 なぜexit()ではなく、exit_group()なのか?というのはman exit_groupに答えが書いてあり「glibc 2.3 以降では、 exit(2) のラッパー関数が呼び出された際に、 このシステムコールが起動される。」ということらしい。
じゃあ、実際にglibcのコードでも見てみようということで、まずはpthread_exit()を。
ファイルはnptl/pthread_exit.c
void __pthread_exit (value) void *value; { THREAD_SETMEM (THREAD_SELF, result, value); __do_cancel (); }
THREAD_SETMEM()はスレッドの戻り値をpthread_exitに渡された引数に設定するだけだと思うのでdo_cancel()を探す。 do_cancel()はnptl/pthreadP.hにあり、inline関数で書かれている
/* Called when a thread reacts on a cancellation request. */ static inline void __attribute ((noreturn, always_inline)) __do_cancel (void) { struct pthread *self = THREAD_SELF; /* Make sure we get no more cancellations. */ THREAD_ATOMIC_BIT_SET (self, cancelhandling, EXITING_BIT); __pthread_unwind ((__pthread_unwind_buf_t *) THREAD_GETMEM (self, cleanup_jmp_buf)); }
__pthread_unwind()はnptl/unwind.cに存在。処理はHAVE_FORCED_UNWINDの定義によって変わっているので、とりあえずHAVE_FORCED_UNWINDが定義されていない場合を見てみよう。c++の例外処理に使ってそう。
void __cleanup_fct_attribute __attribute ((noreturn)) __pthread_unwind (__pthread_unwind_buf_t *buf) { struct pthread_unwind_buf *ibuf = (struct pthread_unwind_buf *) buf; struct pthread *self = THREAD_SELF; #ifdef HAVE_FORCED_UNWIND /* This is not a catchable exception, so don't provide any details about the exception type. We do need to initialize the field though. */ THREAD_SETMEM (self, exc.exception_class, 0); THREAD_SETMEM (self, exc.exception_cleanup, &unwind_cleanup); _Unwind_ForcedUnwind (&self->exc, unwind_stop, ibuf); #else
ここはpthread_cleanup_push()で登録した関数を実行してるのかな。
/* Handle the compatibility stuff first. Execute all handlers registered with the old method. We don't execute them in order, instead, they will run first. */ struct _pthread_cleanup_buffer *oldp = ibuf->priv.data.cleanup; struct _pthread_cleanup_buffer *curp = THREAD_GETMEM (self, cleanup); if (curp != oldp) { do { /* Pointer to the next element. */ struct _pthread_cleanup_buffer *nextp = curp->__prev; /* Call the handler. */ curp->__routine (curp->__arg); /* To the next. */ curp = nextp; } while (curp != oldp); /* Mark the current element as handled. */ THREAD_SETMEM (self, cleanup, curp); }
次にlongjmpしているけどこれはどこでsetjmpしているはず。
/* We simply jump to the registered setjmp buffer. */ __libc_unwind_longjmp ((struct __jmp_buf_tag *) ibuf->cancel_jmp_buf, 1); #endif /* NOTREACHED */
とりあえず先に進んでみると、ここは辿り着いたらダメなのでabort()で終了させる。
/* We better do not get here. */
abort ();
}
hidden_def (__pthread_unwind)
先ほど飛ばしたsejmpしている箇所だけど、これはnptl/pthread_create.cのstart_thread()というところでセット。細かいところはpthread_create()の話になってくるのでこれ以上は調べませんが。
/* This is where the try/finally block should be created. For compilers without that support we do use setjmp. */ struct pthread_unwind_buf unwind_buf; /* No previous handlers. */ unwind_buf.priv.data.prev = NULL; unwind_buf.priv.data.cleanup = NULL; int not_first_call; not_first_call = setjmp ((struct __jmp_buf_tag *) unwind_buf.cancel_jmp_buf);
ここまでの流れをgdbで確認してみよう。まず_exit、abort、pthread_exitにbreakpointを張って、それから実行。
masami@saga:~$ gdb ./a.out Reading symbols from ./a.out...done. (gdb) b _exit Function "_exit" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 1 (_exit) pending. (gdb) b abort Function "abort" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 2 (abort) pending. (gdb) b pthread_exit Breakpoint 3 at 0x400550 (gdb) r Starting program: /home/masami/a.out warning: Could not load shared library symbols for linux-vdso.so.1. Do you need "set solib-search-path" or "set sysroot"? [Thread debugging using libthread_db enabled] Using host libthread_db library "/usr/lib/libthread_db.so.1". [New Thread 0x7ffff780f700 (LWP 31248)]
最初に止まるのはpthread_exit()ですね。
Breakpoint 3, 0x00007ffff7bc63d0 in pthread_exit () from /usr/lib/libpthread.so.0 (gdb) bt #0 0x00007ffff7bc63d0 in pthread_exit () from /usr/lib/libpthread.so.0 #1 0x00000000004006b7 in main (argc=1, argv=0x7fffffffe688) at test.c:16
引き続き実行してみるとexit()に着て、ここでbacktraceを見るとこんな感じに。exit(3)が呼ばれてそこからexit()にたどり着く。
(gdb) c Continuing. [Thread 0x7ffff780f700 (LWP 31248) exited] Breakpoint 1, 0x00007ffff78c8d80 in _exit () from /usr/lib/libc.so.6 (gdb) bt #0 0x00007ffff78c8d80 in _exit () from /usr/lib/libc.so.6 #1 0x00007ffff784682f in __run_exit_handlers () from /usr/lib/libc.so.6 #2 0x00007ffff78468d5 in exit () from /usr/lib/libc.so.6 #3 0x00007ffff7830007 in __libc_start_main () from /usr/lib/libc.so.6 #4 0x0000000000400599 in _start () (gdb) c Continuing. [Inferior 1 (process 31244) exited normally]
これをglibcのコードで見るとstdlib/exit.cでexit()は下記のように書かれていて__run_exit_handlers()を呼ぶだけ。
void exit (int status) { __run_exit_handlers (status, &__exit_funcs, true); }
_run_exit_handlers()も同じくstdlib/exit.cにある。略したところはatexit()の呼び出しとかを実行している部分。最後の最後でexit()を呼び出して終わり。
/* Call all functions registered with `atexit' and `on_exit', in the reverse of the order in which they were registered perform stdio cleanup, and terminate program execution with STATUS. */ void attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit) { 〜略〜 _exit (status); }
_exit()はシステムコールなので此処から先はカーネルになる。 ここまで読んでstraceで見たpthread_exit()が_exit()を呼ぶフローは分かったけど、/proc/selfにアクセスできなくなるという部分に関してはここまで全く出てきてないので(まあ、当然と言えば当然なんだけど)次はsys_exit()でも読もうかな。というか、そちらのほうが気になる。。
並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング
- 作者: Clay Breshears,千住治郎
- 出版社/メーカー: オライリージャパン
- 発売日: 2009/12/21
- メディア: 大型本
- 購入: 12人 クリック: 598回
- この商品を含むブログ (38件) を見る