fork()システムコールとプロセスの識別子について調べた。@詳解Linuxカーネル読書会
ソースコードのバージョンはlinuxカーネルはv4.7-rc4、glibcはb6084a958をみている。
fork()のおおまかな仕組みは、currentのtask_structを元に子プロセスのtask_structを作って、スケジューラに登録する、というものだ。
プロセスに付与されるID
IDとは言っても、だいたいpid_t(=int)をインクリメントしてるだけなので、重複する可能性が無いというわけではない。カーネル内での呼び名と、ユーザ空間での呼び名が違う場合があるので結構ややこしい。
表はカーネルから見たIDでまとめた。
| ID | 読み方 | 実装上の意味 |
|---|---|---|
| PID | プロセスID | プロセスごとの識別子 |
| TGID | スレッドグループID | スレッドごと、シングルスレッドの場合はPIDと同一、ユーザ空間にgetpid()で帰ってくるのはなぜかこっち。 |
| PGID | プロセスグループID | グループリーダが持っているPGIDを継承 |
| SID | セッションID | グループリーダが持っているSIDを継承 |
| TID | スレッドID | gettidで取れるカーネルで管理されているもの(実はPID) or ユーザ空間で管理されているもの、ややこしい |
PIDとTGIDは、ここの図がわかりやすいので、引用する。
http://stackoverflow.com/questions/9305992/linux-threads-and-process
USER VIEW
<-- PID 43 --> <----------------- PID 42 ----------------->
+---------+
| process |
_| pid=42 |_
_/ | tgid=42 | \_ (new thread) _
_ (fork) _/ +---------+ \
/ +---------+
+---------+ | process |
| process | | pid=44 |
| pid=43 | | tgid=42 |
| tgid=43 | +---------+
+---------+
<-- PID 43 --> <--------- PID 42 --------> <--- PID 44 --->
KERNEL VIEW
_do_fork()の実装は次のようになっている。CLONE_THREADの場合は、TGIDにコピー元のtgidをコピーし、それ以外の場合はpidと同じものがセットされる。fork()は後者である。
group_leaderもtask_structが入るだけで伝わり方はほぼ同じだ。
/* ok, now we should be set up.. */
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -1;
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
if (clone_flags & CLONE_PARENT)
p->exit_signal = current->group_leader->exit_signal;
else
p->exit_signal = (clone_flags & CSIGNAL);
p->group_leader = p;
p->tgid = p->pid;
}
PGID,SIDはこのあたり。
if (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
init_task_pid(p, PIDTYPE_PID, pid);
if (thread_group_leader(p)) {
init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
init_task_pid(p, PIDTYPE_SID, task_session(current));
ユーザ空間で決めたTIDはこのあたり。
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
/*
* Clear TID on mm_release()?
*/
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr : NULL;
実はfork()システムコールそのものは使われていない
fork()はglibcでラップされていて、clone()システムコールで実装されている。
juntaki@ubuntu% cat forktest
ls; ls;
juntaki@ubuntu% strace -f bash forktest 2>&1 | grep "fork("
juntaki@ubuntu% strace -f bash forktest 2>&1 | grep "clone("
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f154401a9d0) = 26811
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f154401a9d0) = 26812
glibc実装とカーネルのfork()
glibcのx86の実装では、CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLDがclone_flagに入る。フラグの意味はman cloneにある。
# define ARCH_FORK() \
INLINE_SYSCALL (clone, 4, \
CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, 0, \
NULL, &THREAD_SELF->tid)
Linuxカーネルでは、フラグはSIGCHLDだけ。
# ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
# ifdef CONFIG_MMU
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
# else
/* can not support in nommu mode */
return -EINVAL;
# endif
}
# endif