本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Assembler (x64,ARM).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
こういった私から学べます.
前回を読んでいない方はこちらからどうぞ.
Linuxカーネルの記事一覧はこちらからどうぞ.
LinuxカーネルはC言語で書かれています.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
今回のテーマはプロセス管理です.
プロセス管理により,プロセスのライフサイクルがわかります.
以下の記事を理解していることを前提とします.
目次
プロセス
プロセスはシステムで現在実行されているプログラムのことです.
プロセスは以下の情報で構成されています.
- CPUのレジスタ
- プログラムコード(テキストセクション)
- メモリセグメントの状態(データ,スタック,その他)
- カーネルリソース(開いているファイル,保留中のシグナルなど)
- スレッド
また,プロセスはプロセッサとメモリを仮想化します.
ユーザ空間からプロセスは以下の関数で操作します.
- fork関数:呼び出したプロセスを複製して新しいプロセスを作成する.
- execファミリー関数:現在のプロセスイメージを新しいプロセスイメージに置き換える.
- wait関数:呼び出し元プロセスの子プロセスの状態変化(終了,シグナルにより停止または再開)を待つ.
親子プロセスの実行フローは下図になります.
上記のページにあるfork関数のコードfork.cを実行してみましょう!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/* * Author: Hiroyuki Chishiro * License: 2-Clause BSD */ #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main(void) { pid_t pid; int status; int ret; if ((pid = fork()) < 0) { perror("fork"); exit(1); } else if (pid == 0) { sleep(1); printf("Child Process: %d\n", pid); } else { printf("Parent Process: Child PID = %d\n", pid); if ((ret = wait(&status)) < 0) { perror("wait"); exit(2); } printf("Parent Process end\n"); } return 0; } |
実行結果は以下になります.
1 2 3 4 5 |
$ gcc fork.c $ a.out Parent Process: Child PID = 4136 Child Process: 0 Parent Process end |
システムコールやシグナルをトレースするstraceコマンドで実行してみましょう!
2行目でexecve関数,9行目でclone関数を呼び出していることがわかります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
$ strace -f ./a.out execve("./a.out", ["./a.out"], 0x7ffe1ff189d8 /* 68 vars */) = 0 brk(NULL) = 0x5609a9640000 arch_prctl(0x3001 /* ARCH_??? */, 0x7ffdf0898780) = -1 EINVAL (Invalid argument) mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5e1ac49000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) ... munmap(0x7f5e1ac34000, 83491) = 0 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLDstrace: Process 4148 attached , child_tidptr=0x7f5e1aa09a10) = 4148 [pid 4147] newfstatat(1, "", <unfinished ...> [pid 4148] set_robust_list(0x7f5e1aa09a20, 24 <unfinished ...> [pid 4147] <... newfstatat resumed>{st_mode=S_IFIFO|0600, st_size=0, ...}, AT_EMPTY_PATH) = 0 [pid 4148] <... set_robust_list resumed>) = 0 [pid 4147] getrandom( <unfinished ...> [pid 4148] clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, <unfinished ...> [pid 4147] <... getrandom resumed>"\x50\xd4\xb7\x84\xaa\x3f\x08\x87", 8, GRND_NONBLOCK) = 8 [pid 4147] brk(NULL) = 0x5609a9640000 [pid 4147] brk(0x5609a9661000) = 0x5609a9661000 [pid 4147] wait4(-1, <unfinished ...> [pid 4148] <... clock_nanosleep resumed>0x7ffdf08987d0) = 0 [pid 4148] newfstatat(1, "", {st_mode=S_IFIFO|0600, st_size=0, ...}, AT_EMPTY_PATH) = 0 [pid 4148] getrandom("\xc9\x0b\xd4\x1a\xc1\x0e\xa4\x47", 8, GRND_NONBLOCK) = 8 [pid 4148] brk(NULL) = 0x5609a9640000 [pid 4148] brk(0x5609a9661000) = 0x5609a9661000 [pid 4148] write(1, "Child Process: 0\n", 17Child Process: 0 ) = 17 [pid 4148] exit_group(0) = ? [pid 4148] +++ exited with 0 +++ <... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 4148 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=4148, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --- write(1, "Parent Process: Child PID = 4148"..., 52Parent Process: Child PID = 4148 Parent Process end ) = 52 exit_group(0) = ? +++ exited with 0 +++ |
プロセスディスクリプタ「task_struct」
task_struct構造体
プロセスディスクリプタ「task_struct」は,プロセス情報を管理する構造体です.
linux/include/linux/sched.hでtask_structは構造体として定義されています.
※長いので一部省略しています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
struct task_struct { #ifdef CONFIG_THREAD_INFO_IN_TASK /* * For reasons of header soup (see current_thread_info()), this * must be the first element of task_struct. */ struct thread_info thread_info; #endif unsigned int __state; #ifdef CONFIG_PREEMPT_RT /* saved state for "spinlock sleepers" */ unsigned int saved_state; #endif /* * This begins the randomizable portion of task_struct. Only * scheduling-critical items should be added above here. */ randomized_struct_fields_start void *stack; refcount_t usage; /* Per task flags (PF_*), defined further below: */ unsigned int flags; unsigned int ptrace; #ifdef CONFIG_SMP int on_cpu; struct __call_single_node wake_entry; #ifdef CONFIG_THREAD_INFO_IN_TASK /* Current CPU: */ unsigned int cpu; #endif unsigned int wakee_flips; unsigned long wakee_flip_decay_ts; struct task_struct *last_wakee; /* * recent_used_cpu is initially set as the last CPU used by a task * that wakes affine another task. Waker/wakee relationships can * push tasks around a CPU where each wakeup moves to the next one. * Tracking a recently used CPU allows a quick search for a recently * used CPU that may be idle. */ int recent_used_cpu; int wake_cpu; #endif int on_rq; int prio; int static_prio; int normal_prio; unsigned int rt_priority; const struct sched_class *sched_class; struct sched_entity se; struct sched_rt_entity rt; struct sched_dl_entity dl; #ifdef CONFIG_SCHED_CORE struct rb_node core_node; unsigned long core_cookie; unsigned int core_occupation; #endif #ifdef CONFIG_CGROUP_SCHED struct task_group *sched_task_group; #endif #ifdef CONFIG_UCLAMP_TASK /* * Clamp values requested for a scheduling entity. * Must be updated with task_rq_lock() held. */ struct uclamp_se uclamp_req[UCLAMP_CNT]; /* * Effective clamp values used for a scheduling entity. * Must be updated with task_rq_lock() held. */ struct uclamp_se uclamp[UCLAMP_CNT]; #endif #ifdef CONFIG_PREEMPT_NOTIFIERS /* List of struct preempt_notifier: */ struct hlist_head preempt_notifiers; #endif #ifdef CONFIG_BLK_DEV_IO_TRACE unsigned int btrace_seq; #endif unsigned int policy; int nr_cpus_allowed; const cpumask_t *cpus_ptr; cpumask_t *user_cpus_ptr; cpumask_t cpus_mask; void *migration_pending; #ifdef CONFIG_SMP unsigned short migration_disabled; #endif unsigned short migration_flags; ... struct sched_info sched_info; struct list_head tasks; #ifdef CONFIG_SMP struct plist_node pushable_tasks; struct rb_node pushable_dl_tasks; #endif struct mm_struct *mm; struct mm_struct *active_mm; /* Per-thread vma caching: */ struct vmacache vmacache; #ifdef SPLIT_RSS_COUNTING struct task_rss_stat rss_stat; #endif int exit_state; int exit_code; int exit_signal; /* The signal sent when the parent dies: */ int pdeath_signal; /* JOBCTL_*, siglock protected: */ unsigned long jobctl; /* Used for emulating ABI behavior of previous Linux versions: */ unsigned int personality; /* Scheduler bits, serialized by scheduler locks: */ unsigned sched_reset_on_fork:1; unsigned sched_contributes_to_load:1; unsigned sched_migrated:1; #ifdef CONFIG_PSI unsigned sched_psi_wake_requeue:1; #endif /* Force alignment to the next boundary: */ unsigned :0; /* Unserialized, strictly 'current' */ /* * This field must not be in the scheduler word above due to wakelist * queueing no longer being serialized by p->on_cpu. However: * * p->XXX = X; ttwu() * schedule() if (p->on_rq && ..) // false * smp_mb__after_spinlock(); if (smp_load_acquire(&p->on_cpu) && //true * deactivate_task() ttwu_queue_wakelist()) * p->on_rq = 0; p->sched_remote_wakeup = Y; * * guarantees all stores of 'current' are visible before * ->sched_remote_wakeup gets used, so it can be in this word. */ unsigned sched_remote_wakeup:1; /* Bit to tell LSMs we're in execve(): */ unsigned in_execve:1; unsigned in_iowait:1; #ifndef TIF_RESTORE_SIGMASK unsigned restore_sigmask:1; #endif #ifdef CONFIG_MEMCG unsigned in_user_fault:1; #endif #ifdef CONFIG_COMPAT_BRK unsigned brk_randomized:1; #endif #ifdef CONFIG_CGROUPS /* disallow userland-initiated cgroup migration */ unsigned no_cgroup_migration:1; /* task is frozen/stopped (used by the cgroup freezer) */ unsigned frozen:1; #endif #ifdef CONFIG_BLK_CGROUP unsigned use_memdelay:1; #endif #ifdef CONFIG_PSI /* Stalled due to lack of memory */ unsigned in_memstall:1; #endif #ifdef CONFIG_PAGE_OWNER /* Used by page_owner=on to detect recursion in page tracking. */ unsigned in_page_owner:1; #endif #ifdef CONFIG_EVENTFD /* Recursion prevention for eventfd_signal() */ unsigned in_eventfd_signal:1; #endif unsigned long atomic_flags; /* Flags requiring atomic access. */ struct restart_block restart_block; pid_t pid; pid_t tgid; #ifdef CONFIG_STACKPROTECTOR /* Canary value for the -fstack-protector GCC feature: */ unsigned long stack_canary; #endif /* * Pointers to the (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->real_parent->pid) */ /* Real parent process: */ struct task_struct __rcu *real_parent; /* Recipient of SIGCHLD, wait4() reports: */ struct task_struct __rcu *parent; /* * Children/sibling form the list of natural children: */ struct list_head children; struct list_head sibling; struct task_struct *group_leader; ... /* Filesystem information: */ struct fs_struct *fs; /* Open file information: */ struct files_struct *files; #ifdef CONFIG_IO_URING struct io_uring_task *io_uring; #endif /* Namespaces: */ struct nsproxy *nsproxy; /* Signal handlers: */ struct signal_struct *signal; struct sighand_struct __rcu *sighand; sigset_t blocked; sigset_t real_blocked; /* Restored if set_restore_sigmask() was used: */ sigset_t saved_sigmask; struct sigpending pending; unsigned long sas_ss_sp; size_t sas_ss_size; unsigned int sas_ss_flags; struct callback_head *task_works; ... }; |
古いカーネルでは,task_struct(Linuxカーネルのバージョン4.9まではthread_info)は各プロセスのカーネルスタックの一番下に割り当てられています(上図).
現在のtask_structを取得するには,スタックポインタの最下位13ビットをマスクするだけです.
Linuxカーネルのバージョン4.9以降,カーネルスタックをオーバーフローさせると悪用される可能性があるため,task_structはヒープで動的に確保されます.
現在のtask_structに効率的にアクセスするために,カーネルはcurrent_taskというCPUごとの変数を保持します.
x86-64とARM64のget_current関数とcurrentマクロの実装は以下になります.
currentマクロを利用してcurrent_taskを取得していることがわかります.
1 2 3 4 5 6 7 8 |
DECLARE_PER_CPU(struct task_struct *, current_task); static __always_inline struct task_struct *get_current(void) { return this_cpu_read_stable(current_task); } #define current get_current() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/* * We don't use read_sysreg() as we want the compiler to cache the value where * possible. */ static __always_inline struct task_struct *get_current(void) { unsigned long sp_el0; asm ("mrs %0, sp_el0" : "=r" (sp_el0)); return (struct task_struct *)sp_el0; } #define current get_current() |
プロセス識別子(PID)
プロセスの管理は,プロセス識別子(PID)で行います.
PIDの型はpid_tになります.
- x86は最大32768(多くの場合はint型)
- x86-64では約400万まで増加可能
- 最大値に達すると折り返される
PIDの最大値を知りたいあなたは,以下のコマンドを実行しましょう.
私のPC環境ではPIDの最大値は4194304です.
1 2 |
$ cat /proc/sys/kernel/pid_max 4194304 |
また,システム全体でのスレッド数の上限の確認方法は以下になります.
私のPC環境ではシステム全体でのスレッド数の上限は62906です.
1 2 |
$ cat /proc/sys/kernel/threads-max 62906 |
プロセスの状態(task->state)
プロセスの状態(task->state)を紹介します.
- TASK_RUNNING
- タスクが実行可能である(実行状態またはCPUごとのスケジューラ実行キューにある(実行可能状態)).
- タスクはユーザ空間またはカーネル空間にある.
- TASK_INTERRUPTIBLE
- プロセスは何らかの条件を待ってスリープしている.
- 待機条件が成立するか,シグナルを受信するとTASK_RUNNINGに切り替わる.
- TASK_UNINTERRUPTIBLE
- シグナルで起床しないこと以外は,TASK_INTERRUPTIBLEと同じである.
- __TASK_TRACED
- 別プロセス(例:デバッガ)によるトレース中である.
- __TASK_STOPPED
- プロセスを一時停止させるためのシグナル(SIGSTOP等)を受信した結果,実行も待機もしない状態である.
※__TASK_TRACEDと__TASK_STOPPEDは通常とは異なる状態遷移であることに注意して下さい.
下図にプロセスの状態遷移図を示します.
プロセスコンテキストとプロセス家系図
カーネルはプロセスコンテキストまたは割り込みコンテキストで実行できます.
currentは,カーネルがシステムコールの実行などプロセスコンテキストで実行されるときのみ意味があります.
割り込みは独自のコンテキストを持ちます.
プロセスは家系図として管理され,initプロセスが全てのプロセスの親プロセス(root)になります.
initプロセスは,ブートプロセスの最終段階としてカーネルによって起動されるプロセスです.
システムのinitscriptsを読み,さらにプログラムを実行します.
デーモンなどのプログラムを実行し,最終的にブートプロセスを完了させます.
initプロセスのPIDは1で,そのtask_sturctはinit_taskというグローバル変数としてlinux/init/init_task.cで以下のように定義されています.
※長いので一部省略しています.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/* * Set up the first task table, touch at your own risk!. Base=0, * limit=0x1fffff (=2MB) */ struct task_struct init_task #ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK __init_task_data #endif __aligned(L1_CACHE_BYTES) = { #ifdef CONFIG_THREAD_INFO_IN_TASK .thread_info = INIT_THREAD_INFO(init_task), .stack_refcount = REFCOUNT_INIT(1), #endif .__state = 0, .stack = init_stack, .usage = REFCOUNT_INIT(2), .flags = PF_KTHREAD, .prio = MAX_PRIO - 20, .static_prio = MAX_PRIO - 20, .normal_prio = MAX_PRIO - 20, .policy = SCHED_NORMAL, .cpus_ptr = &init_task.cpus_mask, .user_cpus_ptr = NULL, .cpus_mask = CPU_MASK_ALL, .nr_cpus_allowed= NR_CPUS, .mm = NULL, .active_mm = &init_mm, .restart_block = { .fn = do_no_restart_syscall, }, .se = { .group_node = LIST_HEAD_INIT(init_task.se.group_node), }, .rt = { .run_list = LIST_HEAD_INIT(init_task.rt.run_list), .time_slice = RR_TIMESLICE, }, .tasks = LIST_HEAD_INIT(init_task.tasks), #ifdef CONFIG_SMP .pushable_tasks = PLIST_NODE_INIT(init_task.pushable_tasks, MAX_PRIO), #endif ... }; EXPORT_SYMBOL(init_task); |
psコマンドを実行するとinitプロセスがPID1で実行していることがわかります.
1 2 3 4 5 6 7 |
$ ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.1 166656 11964 ? Ss 16:22 0:02 /sbin/init splash root 2 0.0 0.0 0 0 ? S 16:22 0:00 [kthreadd] root 3 0.0 0.0 0 0 ? I< 16:22 0:00 [rcu_gp] root 4 0.0 0.0 0 0 ? I< 16:22 0:00 [rcu_par_gp] ... |
プロセスのツリーを表示するpstreeコマンドの実行結果は以下になります.
systemdという名前のinitプロセスが実行していることがわかります.
※systemdを含むinitプロセスの詳細はこちらがわかりやすいです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ pstree systemd-+-ModemManager---2*[{ModemManager}] |-NetworkManager---2*[{NetworkManager}] |-VGAuthService |-accounts-daemon---2*[{accounts-daemon}] |-acpid |-at-spi-bus-laun-+-dbus-daemon | `-3*[{at-spi-bus-laun}] |-at-spi2-registr---2*[{at-spi2-registr}] |-avahi-daemon---avahi-daemon |-bluetoothd |-colord---2*[{colord}] |-cron ... |
プロセスの生成
プロセスの生成は,fork関数ベースで実行されます.
currentとの関係は以下になります.
- currentの親タスク: current->parent
- currentのタスク: current->children
- currentの親タスクの下にある兄弟タスク: current->siblings
- currentのシステム内の全タスクのリスト: current->tasks
また,linux/include/linux/sched/signal.hで定義されているnext_task(t),for_each_process(t)マクロ等で簡単に調査できます.
1 2 3 4 5 |
#define next_task(p) \ list_entry_rcu((p)->tasks.next, struct task_struct, tasks) #define for_each_process(p) \ for (p = &init_task ; (p = next_task(p)) != &init_task ; ) |
Linuxでは,何もないところからタスクを生成する機能(例:Unixのspawn関数やWindowsのCreateProcess関数)は実装されていません.
fork関数とexec関数の組み合わせて新しいプロセスを実行します.
fork関数は親プロセスのコピーである子プロセスを生成します.
親プロセスと子プロセスの違いは,PID,PPID,いくつかのリソースや統計情報のみです.
exec関数は,プロセスのアドレス空間に新しい実行ファイルをロードします.
Copy-on-Write(CoW)は,「データの書き込み時にコピーする」機構です.
fork関数で,Linuxは親ページテーブルを複製し,新しいプロセスディスクリプタを生成します.
ページテーブルのアクセスビットを読み取り専用に変更します.
ページがデータの書き込み時にアクセスされると,そのページがコピーされ,対応するページテーブルのエントリが読み取り/書き込みに変更されます.
この機構がCoWとなります.
fork関数はデータのコピーを遅らせるか,完全に防ぐことで高速化することができます.
また,fork関数は,読み取り専用ページを子孫間で共有することにより,メモリを節約することができます.
fork関数はcloneシステムコールで実装されています.
sys_clone関数は,linux/kernel/fork.cにあるkernel_clone関数(旧do_fork関数)を呼び出し,kernel_clone関数はcopy_process関数を呼び出して新しいタスクを開始します.
copy_process関数では,以下の操作をします
- カーネルスタック,task_struct,thread_infoを複製するdup_task_struct関数を呼び出します.
- プロセス数の制限をオーバーフローしないようにチェックします.
- task_struct構造体の様々なメンバ変数をクリアします.
- sched_for関数を呼び出し,子タスクの状態をTASK_NEWに設定します.
- ファイル,シグナルハンドラなどの親情報をコピーします.
- alloc_pid関数を使用して新しいPIDを取得します.
- 新しい子プロセスのtask_struct構造体へのポインタを返します.
最後に,kernel_clone関数(旧_do_fork関数)がwake_up_new_task関数を呼び出します.
新しい子タスクはTASK_RUNNING状態になります.
スレッド
Linuxカーネルにおけるスレッドの定義
スレッドは,同じアドレス空間を共有し,同じプログラムに属する同時実行のフローです.
Linuxカーネルにおけるスレッドの定義を紹介します.
Linuxカーネルにはスレッドという概念がありません.
スレッド固有のスケジューリングはないです.
Linuxではすべてのスレッドを標準プロセスとして実装しています.
つまり,スレッドは他のプロセスと情報を共有しているだけのプロセスなので,各スレッドは独自のtask_struct構造体を持ちます.
スレッドは,cloneシステムコールで共有することを示す特定のフラグを付けて生成されます.
1 |
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0); |
カーネルスレッド
カーネルスレッドは,カーネルでバックグラウンド処理を行うために利用されます.
ユーザ空間のスレッドに非常によく似ていて,スケジューリング可能なエンティティ(通常のプロセスのようなもの)です.
ただし,独自のアドレス空間は持ちません.(task_structのmmはNULLになります.)
カーネルスレッドはすべてkthreaddカーネルスレッド(PID 2)からフォークされたものです.
「ps --ppid 2」でカーネルスレッド一覧を以下に表示します.
カーネルスレッドとして,ワークキュー(kworker)やCPU間のロードバランシング(migration)があることがわかります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
$ ps --ppid 2 PID TTY TIME CMD 3 ? 00:00:00 rcu_gp 4 ? 00:00:00 rcu_par_gp 6 ? 00:00:00 kworker/0:0H-events_highpri 7 ? 00:00:00 kworker/0:1-events 9 ? 00:00:00 mm_percpu_wq 10 ? 00:00:00 rcu_tasks_kthre 11 ? 00:00:00 rcu_tasks_rude_ 12 ? 00:00:00 rcu_tasks_trace 13 ? 00:00:02 ksoftirqd/0 14 ? 00:00:02 rcu_preempt 15 ? 00:00:00 rcub/1 16 ? 00:00:00 rcuc/0 17 ? 00:00:00 migration/0 18 ? 00:00:00 irq_work/0 19 ? 00:00:00 idle_inject/0 20 ? 00:00:00 cpuhp/0 21 ? 00:00:00 cpuhp/1 22 ? 00:00:00 idle_inject/1 23 ? 00:00:00 irq_work/1 24 ? 00:00:00 migration/1 25 ? 00:00:00 rcuc/1 26 ? 00:00:02 ksoftirqd/1 28 ? 00:00:00 kworker/1:0H ... |
カーネルスレッドの生成
カーネルスレッドを生成するには,linux/include/linux/kthread.hにあるkthread_createマクロ(実体はlinux/kernel/kthread.cにあるkthread_create_on_node関数)を利用します.
kthread_create関数で作成された場合,スレッドは実行可能状態ではありません.
スレッドを実行可能状態にするには以下のどちらの関数を呼び出す必要があります.
- wake_up_process関数
- linux/kernel/sched/core.cで定義
- kthread_runマクロ
- linux/include/linux/kthread.hで定義
- kthread_create関数とwake_up_process関数を呼び出し
他のスレッドはlinux/kernel/kthread.cにあるkthread_stop関数を利用してカーネルスレッドに停止を要求できます.
カーネルスレッドはlinux/kernel/kthread.cにあるkthread_should_stop関数を呼び出して,継続するか停止するかを決定する必要があります.
プロセスの終了
プロセスはexitシステムコールの呼び出しにより終了します.
コンパイラがmain関数から戻るときに暗黙的に挿入することができます.
sys_exitシステムコールはlinux/kernel/exit.cにあるdo_exit関数を呼び出します.
do_exit関数は以下を実行します.
- task_structにPF_EXITINGフラグを立てるexit_signals関数を呼び出します.
- 親プロセスから取得するtask_structのexit_codeフィールドに終了コードを設定します.
- exit_mm関数をコールして,タスクのmm_structを解放します.
- exit_sem関数を呼び出します.プロセスがセマフォ待ちのキューに入っている場合,ここでキューを解除します.
- exit_files関数およびexit_fs関数を呼び出して,それぞれファイルディスクリプタおよびファイルシステムデータの参照カウンタをデクリメントします.参照カウンタが0になると,そのオブジェクトはもはやどのプロセスからも利用されなくなり,破棄されます.
また,do_exit関数はexit_notify関数とdo_task_dead関数を呼び出します.
exit_notify関数は以下を実行します.
- 親にシグナルを送ります.
- 子プロセスをスレッドグループ内の他のスレッドまたはinitプロセスに再保存します.
- task_structのexit_stateをEXIT_ZOMBIEに設定します.
do_task_dead関数は以下を実行します.
- タスクの状態をTASK_DEADに設定します.
- 新しいプロセスに切り替えるためにschedule関数を呼び出します.プロセスはスケジュールされなくなったので,do_exit関数は決して返りません.
この時点で残っているtask_struct,thread_info,カーネルスタックは,wait関数により親プロセスに情報を提供するために必要です.
親プロセスが情報を取得した後,プロセスが保持していた残りのメモリが解放されます.
クリーンアップ処理はwait関数の実装から呼ばれるlinux/kernel/exit.cにあるrelease_task関数で実装されています.
タスクリストからタスクを削除し,残りのリソースを解放します.
ゾンビプロセス(親がない孤児プロセス)について解説します.
ゾンビプロセスは親タスクが子タスクより先に終了した場合に,小タスクがなる状態のことです.
この場合,子タスクは再親化(reparent)されなければなりません.
exit_notify関数はforget_original_parent関数を呼び出し,find_new_reaper関数を呼び出します.
スレッドグループに別のタスクが存在する場合はそのタスクのtask_structを返し,そうでない場合はinitを返します.
そして,現在死につつあるタスクの全ての子タスク(ゾンビプロセス)がreaper(刈り手)に再親化されます.
まとめ
今回はプロセス管理を紹介しました.
具体的には,プロセス,プロセスディスクリプタ「task_struct」,プロセスの生成,スレッド,プロセスの終了を解説しました.
プロセス管理を深く理解したいあなたは,以下の記事を読みましょう!
- Exploiting Stack Overflows in the Linux Kernel
- security things in Linux v5.10(本記事で利用しているLinuxカーネル5.15より少し古いです)
LinuxカーネルはC言語で書かれています.
私にC言語の無料相談をしたいあなたは,公式LINE「ChishiroのC言語」の友だち追加をお願い致します.
私のキャパシティもあり,一定数に達したら終了しますので,今すぐ追加しましょう!
独学が難しいあなたは,元東大教員がおすすめするC言語を学べるオンラインプログラミングスクール5社で自分に合うスクールを見つけましょう.後悔はさせません!
次回はこちらからどうぞ.