Linux Kernel "task_struct" 徹底解説

task_structについて

プロセスに関する情報はtask_struct構造体に格納される。 プロセスディスクリプタとも呼ばれる。

プロセスの情報は具体的には以下のようなものがあげれる。

  • プロセスの属性
  • CPU上で実行されているか
  • あるいは何かしらの事象を待って止められているか
  • どのファイルへのアクセス権が許可されているか

などなどプロセス状態を格納するメンバがそろっている。 それだけでなく、情報が格納されているほかの構造体へのポインタが入っているサメンバもあります。

  • thread_info :プロセスに対応する低水準(低レイヤー)な情報
  • mm_struct :メモリリージョンディスクリプタへのポインタ
  • tty_struct :プロセスに対応するtty(制御端末)
  • fs_struct :カレントディレクト
  • files_struct:ファイルディスクリプタへのポインタ
  • signal_struct:受信シグナル

linuxkernelでは以下のファイルに存在する。

linux-3.12.6/include/linux/sched.h

struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    ...
    int exit_state;
    int exit_code, exit_signal;
    ...
    struct task_struct __rcu *real_parent; /* real parent process */
    struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
    ...
    struct list_head children;      /* list of my children */
    struct list_head sibling;       /* linkage in my parent's children list */
    struct task_struct *group_leader;       /* threadgroup leader */
    struct mm_struct       *mm;
    struct mm_struct       *active_mm;
    ...
    struct pid_link pids[PIDTYPE_MAX];
    ...
    const struct cred __rcu *cred;  /* effective (overridable) subjective task*/
    ...
    char comm[TASK_COMM_LEN]; /* executable name excluding path
    - access with [gs]et_task_comm (which lock
    it with task_lock())
    - initialized normally by setup_new_exec */
};

from https://www.coins.tsukuba.ac.jp/~yas/coins/os2-2013/2013-12-26/

参照:https://github.com/torvalds/linux/blob/692b7dc87ca6d55ab254f8259e6f970171dc9d01/include/linux/sched.h#L739

stateメンバについて

task_struct構造体のstateはその名が示す通り、プロセスの現在の状態を表します。

格納される値は以下の通りです。(多少編集有)

/* Used in tsk->state: */
#define TASK_RUNNING           0x00000000
#define TASK_INTERRUPTIBLE     0x00000001
#define TASK_UNINTERRUPTIBLE   0x00000002
#define TASK_STOPPED           0x00000004
#define TASK_TRACED            0x00000008
/* Used in tsk->exit_state: */
#define EXIT_DEAD          0x00000010
#define EXIT_ZOMBIE            0x00000020
#define EXIT_TRACE          (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED            0x00000040
#define TASK_DEAD          0x00000080
#define TASK_WAKEKILL      0x00000100
#define TASK_WAKING            0x00000200
#define TASK_NOLOAD            0x00000400
#define TASK_NEW           0x00000800
#define TASK_RTLOCK_WAIT   0x00001000
#define TASK_FREEZABLE     0x00002000
#define __TASK_FREEZABLE_UNSAFE        (0x00004000 * IS_ENABLED(CONFIG_LOCKDEP))
#define TASK_FROZEN            0x00008000
#define TASK_STATE_MAX         0x00010000

from https://github.com/torvalds/linux/blob/692b7dc87ca6d55ab254f8259e6f970171dc9d01/include/linux/sched.h#L86

  • TASK_STOPPPED:プロセスは停止中。SIGSTOP SIGTSTP SIGTTIN SIGTTOUを受け取るとこの状態になる。

  • TASK_TRACED:デバッガにより停止中。ptrace()システムコールなどによりほかのプログラムから監視されている場合は、どのシグナルを受信しても、この状態になります。

  • TASK_UNINTERRUPTIBLE:ある条件が成り立つのを一時停止して待ち合わせしている状態。次の条件でTASK_RUNNGINになる
    • ハードウェアの割り込み
    • プロセスが待っているシステム資源の解放
    • シグナルの受信

stateメンバの値は、以下のようにして単純に代入します。

p->state = TASK_RUNNNING

あるいは、set_task_stateマクロや、set_current_state(現在実行中のプロセスの状態を設定する)マクロを利用することもあります。 →(以前はset_task_stateは次の階層の中に定義されていたようですが、今はないようです)

include/linux/sched.h
#define set_current_state(state_value)               \
   do {                          \
       current->task_state_change = _THIS_IP_;      \
       smp_store_mb(current->state, (state_value));      \
   } while (0)
#endif

#define set_current_state(state_value)          \
   smp_store_mb(current->state, (state_value))
#endif

プロセスのしきべつ

プロセスディスクリプタポインタ

プロセスとプロセスディスクリプタは必ず一対一対応しており、カーネルtask_struct構造体の32ビットのアドレスを使用してプロセスを識別します。このアドレスを、プロセスディスクリプタポインタと呼びます。これにより、独立にスケジューリング可能な各実行コンテキストにはそれぞれディスクリプタが割り当てられております。

PID

PIDはプロセスディスクリプタpidメンバに格納されています。新しく生成されるPIDはその直前に生成されたプロセスのPIDに1を足した値になります。

struct task_struct{
    ...
    /* PID/PID hash table linkage. */
    struct pid         *thread_pid;
    ...
}

from https://github.com/torvalds/linux/blob/692b7dc87ca6d55ab254f8259e6f970171dc9d01/include/linux/sched.h#LL998C1-L999C27

PIDのMAX

ちなみにPIDにもMAXの値は存在し、デフォルトではPID_MAX_DEFAULT-1です カーネルはPIDがこの上限に達したときに、使われていないPIDの再利用を行う必要があります。

/*
 * This controls the default maximum pid allocated to a process
 */
#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)

from https://github.com/torvalds/linux/blob/e660abd551f1172e428b4e4003de887176a8a1fd/include/linux/threads.h#L28

さらに、システム管理者は/proc/sys/kernel/pid_maxファイルにより小さな値を書き込むことにより、PIDの最大値を下げることができます。

PIDの再利用

PIDを再利用するときは、pidmap_arrayビットマップを使う必要があります。pidmap_arrayビットマップは、使用中のPIDと未使用のPIDを表しています。

static pidmap_t pidmap_array[PIDMAP_ENTRIES] =
     { [ 0 ... PIDMAP_ENTRIES-1 ] = { ATOMIC_INIT(BITS_PER_PAGE), NULL } };

TGIDによるグループ化

これまで、PIDは一意の値であることを期待されていると説明してきました。

しかし、UNIXプログラマは同じグループのスレッドが同じPIDを持っていることを期待しまsう。

例えば、あるPIDにシグナルを送ると、そのグループ内のすべてのスレッドに効果を及ぼすなど。

この仕組みを体現するため、Linuxはスレッドグループを使用しています。 スレッド間で共有する識別子として、グループの最初の軽量プロセスであるスレッドグループリーダのPIDを使用するのです。 この識別子は各プロセスディスクリプタtgidメンバに格納されています。 そして、getpidシステムコールは、カレントプロセスのpidではなく、tgidの値を返すのです。

static pid_t getpid(void)
{
    return bpf_get_current_pid_tgid();
}

参照 https://github.com/torvalds/linux/blob/e660abd551f1172e428b4e4003de887176a8a1fd/tools/perf/examples/bpf/augmented_raw_syscalls.c#LL351C1-L354C2

この仕組みのおかげで、すべてのマルチスレッドアプリケーションは同じ識別子を共有できるのです。

プロセスディスクリプタの処理

Linuxは各プロセスに一つのメモリ領域を割り当て、プロセスディスクリプタとリンクします。

この領域には二つの異なるデータ構造であるthread_info構造体とカーネルモードプロセッサスタックを割り当てます。

カーネルは8KBのメモリ領域を、先頭ページフレームが2の13乗の倍数のアドレス協会に位置合わせ下二つの連続するページフレームに置きます。 これにより、メモリのフラグメンテーションを防ぐことができます。

linux kernel

from https://doc.lagout.org/operating%20system%20/linux/Understanding%20Linux%20Kernel.pdf

この図を見ると

  • thread_info構造体はメモリ領域の先頭に格納し、
  • スタックは末尾から低位アドレス方向に延びていきます。

この状態をc言語であらわすと、以下のようなunionとして表現できますね。

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[2048];
}

さらに、thread_infoとtask_struct構造体(プロセスディスクリプタ)がtaskメンバとthread_infoメンバで互いにリンクしているのが分かると思います。

実際のlinuxkernelのthread_infoコードは以下の通りです。

struct thread_info {
    struct task_struct         *task;      /* main task structure */
    unsigned long             flags;      /* low level flags */
    __u32                       cpu;        /* current CPU */
    int                            preempt_count;  /* 0 => preemptable,<0 => BUG */
    struct thread_info         *real_thread;    /* Points to non-IRQ stack */
    unsigned long               aux_fp_regs[FP_SIZE]; 
    /* auxiliary fp_regs to save/restore them out-of-band */
};

from https://github.com/torvalds/linux/blob/e660abd551f1172e428b4e4003de887176a8a1fd/arch/um/include/asm/thread_info.h#LL19C1-L28C3

epsレジスタの動き

スタックは末尾から低位アドレス方向に延びると説明しましたが、espレジ須賀はCPUのスタックポイントであり、この値が小さければ小さいほど、スタックが使われていることを表します。

プロセスのカーネルスタックは、ユーザーモードからカーネルモードへ切り替わった直後は空です。 この状態ではespレジスタの値はスタックの末尾+1のアドレスを示しています。