- Git
forkとは|GitHubのフォークとプロセスのfork()を実物で
結論
fork(フォーク)とは「枝分かれして複製を作る」ことです。GitHub では他人のリポジトリを自分のアカウントに丸ごと複製する操作、Linux などの OS では実行中のプロセスが自分の複製(子プロセス)を作るシステムコール fork()、オープンソースでは元のプロジェクトから分かれて別名で開発を続けることを指します。
この記事でわかること
- GitHub のフォークと clone・branch の違い、fork→clone→upstream→同期→差分提出の流れを git で再現した実物
- C と Python の fork() で確かめた PID・返り値と、コピーオンライトの実測(200MB の親を fork しても子の専有メモリは 0MB)
- fork と exec・vfork・posix_spawn・スレッドの違い、ゾンビ・二重出力などのつまずきの原文
「fork」は検索すると、GitHub の操作・OS のプロセス・プロジェクトの分裂と、まったく違う話が混ざって出てきます。どれも「同じものから枝分かれした複製を作り、以後は別々に進む」という点は共通です。先に3つの使い分けを押さえ、その後で GitHub とプロセスのそれぞれを、実際に動かした結果で見ていきます。
%%{init: {"flowchart": {"nodeSpacing": 18, "rankSpacing": 36}}}%%
flowchart TD
F[fork = 枝分かれして複製を作る] --> G[GitHub の<br>リポジトリ]
F --> P[OS の<br>プロセス]
F --> O[OSS の<br>プロジェクト]
G --> X[複製の後は別々に進む]
P --> X
O --> X
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| fork とは(意味) | 枝分かれして複製を作ること。GitHub・プロセス・プロジェクトで使う | forkとは |
| github fork とは | 他人のリポジトリを自分のアカウントに複製し、元へ変更を提案できる | GitHubのフォークとは |
| fork と clone の違い | fork は GitHub 上の複製、clone は手元への複製。両方使う | GitHubのフォークとは |
| fork と branch の違い | branch は同じリポジトリの中の枝、fork は別のリポジトリ | GitHubのフォークとは |
| フォークの手順・流れ | Fork→clone→upstream 追加→ブランチ→push→プルリクエスト | git で再現したフォークの流れ |
| フォークを最新にする | Sync fork か git fetch upstream→merge→push |
フォークを元のリポジトリに追従させる |
| fork プロセスとは | 親の複製を作る。親には子の PID、子には 0 が返る | プロセスのfork()とは |
| fork と exec・スレッドの違い | fork は複製、exec は中身の入れ替え、スレッドは同じメモリ | forkとexec・vfork・posix_spawn・スレッドの違い |
確認環境:Ubuntu 24.04.5 LTS(Linux 6.18.44、2コア・メモリ 8GB)の上で実行しました。GitHub のフォークの流れは、GitHub には書き込まず、手元のベアリポジトリ2つ(元の作者側と自分側)をリモートに見立てて git 2.43.0 で再現しています。GitHub の画面の操作とボタン名は GitHub Docs(日本語版)で確かめた範囲だけを書いています。プロセスの fork は gcc 13.3.0 でコンパイルした C と Python 3.13.18 の os.fork()・multiprocessing・subprocess で実行し、PID は ps・pstree、メモリは /proc/self/status と /proc/self/smaps_rollup、システムコールは strace で確かめました。fork(2) の仕様は man7.org のマニュアル、Python の開始方式は公式ドキュメントで確かめています(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「forkとは|GitHubのフォークとプロセスのfork()を実物で」
forkとは(3つの意味の使い分け)
この節では、IT で「fork」と言われたときに、どの意味なのかを見分ける方法が分かります。
fork は英語で「食器のフォーク」「道の分かれ目」を指す言葉で、IT では「ある時点の状態を複製して枝分かれし、以後は別々に進むこと」を指します。 話している相手が何を複製しているかで、3つに分かれます。
| 場面 | 何を複製するか | 複製した後 |
|---|---|---|
| GitHub のフォーク | 他人のリポジトリ(履歴ごと)を自分のアカウントに | 自分の側で自由に変更し、元へプルリクエストで提案できる |
| プロセスの fork(fork(2)) | 実行中のプロセスのメモリ・開いているファイルなど | 親と子が同じ場所から並行して動く。子は別のプログラムに置き換わることが多い |
| プロジェクトのフォーク | オープンソースのソースコードと、ときに開発者の一部 | 別の名前で別のプロジェクトとして開発を続ける |
| ブロックチェーンのフォーク | 台帳(チェーン)が仕様変更などで枝分かれ | ソフトフォークは収束、ハードフォークは別のチェーンになる |
「フォークして PR を出す」
GitHub のフォーク。他人のリポジトリに変更を提案する流れの話。
git・GitHub の文脈
「fork して exec する」
プロセスの fork。シェルがコマンドを起動する仕組みや、C・Python のコードの話。
OS・Linux の文脈
「〇〇は△△のフォーク」
プロジェクトのフォーク。製品名・ライセンス・コミュニティの話。
OSS の文脈
「ハードフォーク」
ブロックチェーンのフォーク。互換性のない仕様変更でチェーンが分かれる話。
暗号資産の文脈
Mac の以前のファイルシステム(HFS)にも「データフォーク・リソースフォーク」という用語がありますが、1つのファイルの中身を2つに分けて持つ仕組みのことで、この記事では扱いません。
GitHubのフォークとは(clone・branchとの違い)
この節では、GitHub のフォークが何をするものかと、clone・branch との違いが分かります。
GitHub のフォークは、他人のリポジトリを、自分のアカウント(または組織)の下に新しいリポジトリとして複製する操作です。 GitHub Docs は「リポジトリをフォークすると、アップストリーム リポジトリに影響を与えずにプロジェクトに変更を提案できます」と説明しています。元のリポジトリを upstream(アップストリーム)、複製したほうをフォークと呼びます。
フォークが要るのは、元のリポジトリに書き込む権限が無いときです。オープンソースのプロジェクトに修正を送りたい、他人のサンプルを土台に自分の版を作りたい、というときに使います。自分や自分のチームのリポジトリなら、フォークではなくブランチで済みます。
| fork(フォーク) | clone(クローン) | branch(ブランチ) | |
|---|---|---|---|
| どこに複製するか | GitHub 上の自分のアカウント | 自分の PC(手元) | 同じリポジトリの中 |
| 元との関係 | 別のリポジトリ。GitHub が「〇〇からフォーク」と記録する | リモート(origin)として元を覚えている | 同じ履歴の中の枝 |
| 元への書き込み権限 | 無くてよい(提案はプルリクエストで) | 読めれば clone できる。push には権限が要る | 要る |
| 手元で測った大きさ | リポジトリ丸ごと(120KB) | リポジトリ丸ごと | 41 バイトのファイル1つ |
| 使う場面 | 他人のプロジェクトへの貢献・自分の版を持つ | どのリポジトリでも、手元で作業を始めるとき | 同じリポジトリでの機能追加・修正 |
fork と clone は「どちらか」ではない
見分ける問い:「元のリポジトリに push できるか」。できないならフォーク、できるならブランチ
clone 自体の使い方(ブランチ指定・浅い clone)は、関連記事「git cloneの使い方|ブランチ指定とshallow clone」で扱っています。
GitHubでフォークする手順(画面の操作・公式の範囲)
この節では、GitHub の画面でフォークを作る手順と、作成画面の項目の意味が分かります。
GitHub の画面での操作は、GitHub Docs(日本語版)で確かめた範囲で書いています。 この記事では GitHub に書き込んでいないため、画面の文言は公式ドキュメントのとおりです。
元のリポジトリを開き、右上の「Fork」を選ぶ
GitHub Docs の練習用リポジトリは octocat/Spoon-Knife です。右上のボタンから、フォークの作成画面に進みます。
所有者(Owner)を選ぶ
自分のアカウントか、所属する組織かを選びます。リポジトリ名と説明は必要なら変えられます。
「既定のブランチだけをコピーする」を決める
既定(main など)だけを複製するかの選択です。GitHub Docs は、オープンソースへの貢献では既定のブランチだけのコピーが一般的としています。
作成を確定する
自分のアカウントの下に、同じ名前のリポジトリができます。画面には「forked from 元の名前」のように元が表示されます。
手元に clone し、upstream を足す
フォーク側の「Code」から URL を取り git clone。続けて git remote add upstream 元のURL で元のリポジトリも登録します(次の節で実物)。
GitHub CLI を使うなら、gh repo fork リポジトリ --clone=true でフォークの作成と clone を一度に行えます(GitHub Docs)。gh コマンド自体の使い方は、GitHub CLI の記事で扱っています。
gitで再現したフォークの流れ(fork→clone→upstream→ブランチ→差分提出)
この節では、フォークから変更の提出までの一連の流れを、手元のベアリポジトリで再現した実物のコマンドと出力で追えます。
GitHub の「Fork」ボタンがサーバー側でやっていることは、リポジトリの複製です。 そこで、元の作者側 gh/original-owner/spoon-knife.git と、自分側 gh/your-username/spoon-knife.git という2つのベアリポジトリ(作業ファイルの無い、サーバー用の形)を手元に作り、GitHub の代わりにしました。URL を GitHub のものに置き換えれば、同じ手順がそのまま使えます。
flowchart TD
U[(upstream<br>元の作者のリポジトリ)] -->|1 Fork| O[(origin<br>自分のフォーク)]
O -->|2 clone| L[手元の作業コピー]
L -->|3 ブランチで変更して push| O
O -->|4 プルリクエスト| U
U -->|5 fetch upstream で同期| L
1. フォークする(サーバー側の複製)
$ git clone --bare gh/original-owner/spoon-knife.git gh/your-username/spoon-knife.git
$ git -C gh/your-username/spoon-knife.git log --oneline --all
9972926 最初のコミット
履歴ごと複製されるので、フォーク側にも同じコミット(9972926)があります。
2. フォークを clone し、upstream を登録する
$ git clone gh/your-username/spoon-knife.git spoon-knife
Cloning into 'spoon-knife'...
done.
$ cd spoon-knife
$ git remote -v
origin gh/your-username/spoon-knife.git (fetch)
origin gh/your-username/spoon-knife.git (push)
$ git remote add upstream gh/original-owner/spoon-knife.git
$ git remote -v
origin gh/your-username/spoon-knife.git (fetch)
origin gh/your-username/spoon-knife.git (push)
upstream gh/original-owner/spoon-knife.git (fetch)
upstream gh/original-owner/spoon-knife.git (push)
origin が自分のフォーク、upstream が元のリポジトリという2つのリモートを持つのが、フォークして作業するときの基本の形です。GitHub Docs も、git remote -v で origin がフォーク、upstream が元になっていることを確かめるよう書いています。リモートの追加・変更は、関連記事「git remoteの変更・追加・削除とpush rejectedの対処法」で扱っています。
3. ブランチを切って変更し、自分のフォークに push する
$ git switch -c fix-typo
Switched to a new branch 'fix-typo'
$ (app.py を編集)
$ git commit -am "挨拶に world を足す"
$ git push -u origin fix-typo
To gh/your-username/spoon-knife.git
* [new branch] fix-typo -> fix-typo
branch 'fix-typo' set up to track 'origin/fix-typo'.
push 先は origin(自分のフォーク)です。upstream には書き込み権限が無い前提なので、push しません。手元で、upstream 側に書き込みを拒む設定(pre-receive フック)を入れて試すと、次のように拒まれました。
$ git push upstream doc-fix
remote: 書き込み権限がありません(upstream は読むだけ)
To gh/original-owner/spoon-knife.git
! [remote rejected] doc-fix -> doc-fix (pre-receive hook declined)
error: failed to push some refs to 'gh/original-owner/spoon-knife.git'
$ git push -u origin doc-fix # 自分のフォークには通る
* [new branch] doc-fix -> doc-fix
4. プルリクエスト(差分の提出)
GitHub では、フォーク側のブランチから元のリポジトリへ「プルリクエスト」を作ります。プルリクエストの中身は「upstream の main と自分のブランチの差分」なので、提出前に手元で同じものを確かめられます。
$ git fetch upstream
$ git diff upstream/main...fix-typo --stat
app.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git diff upstream/main...fix-typo
- return "hello"
+ return "hello, world"
ドット3つ(A...B)は「A と B の分岐点から B までの差分」で、プルリクエストの表示と同じ範囲になります。説明文の書き方は、関連記事「プルリクエストの書き方|レビューされやすい説明文とテンプレート」で扱っています。
5. 元の作者側でマージされる
元の作者は、フォークのブランチを取り込んでマージします(GitHub の「Merge pull request」にあたる操作を、手元の作者側リポジトリで行いました)。
$ git fetch gh/your-username/spoon-knife.git fix-typo
$ git merge --no-ff -m "Merge pull request #1 from your-username/fix-typo" FETCH_HEAD
Merge made by the 'ort' strategy.
app.py | 2 +-
$ git log --oneline --graph
* d713426 Merge pull request #1 from your-username/fix-typo
|\
| * ef31da7 挨拶に world を足す
* | c2bfdcc LICENSE を追加
|/
* 9972926 最初のコミット
フォークを元のリポジトリに追従させる(Sync fork・fetch upstream)
この節では、元のリポジトリが先に進んだときに、自分のフォークを最新にする3つの方法と、実行した結果が分かります。
フォークは複製した時点で止まっています。元のリポジトリが進んでも、自分で同期しない限り追いつきません。 GitHub Docs は、同期の方法として画面・GitHub CLI・コマンドラインの3つを挙げています。
| 方法 | 操作 | 更新されるもの |
|---|---|---|
| GitHub の画面 | フォークのファイル一覧の上の「Sync fork」→「Update branch」 | GitHub 上のフォーク(手元は別途 pull) |
| GitHub CLI | gh repo sync 自分/リポジトリ -b ブランチ |
GitHub 上のフォーク |
| コマンドライン | git fetch upstream→git merge upstream/main→git push |
手元→push で GitHub 上のフォーク |
手元では、元の作者が LICENSE を追加した後に、コマンドラインで同期しました。
$ git fetch upstream
From gh/original-owner/spoon-knife
* [new branch] main -> upstream/main
$ git log --oneline main..upstream/main # どれだけ遅れているか
c2bfdcc LICENSE を追加
$ git switch main
$ git merge upstream/main
Updating 9972926..c2bfdcc
Fast-forward
LICENSE | 1 +
1 file changed, 1 insertion(+)
$ git push origin main
9972926..c2bfdcc main -> main
自分のフォークに独自のコミットを入れていなければ Fast-forward(早送り)になり、マージコミットはできません。 GitHub Docs も、コマンドラインでの同期は手元を更新するだけで、GitHub 上のフォークを更新するには push が要ると書いています。マージ後にブランチを片付けるところまでの一連は次のとおりです。
$ git branch -d fix-typo
Deleted branch fix-typo (was ef31da7).
$ git push origin --delete fix-typo
- [deleted] fix-typo
$ git branch -r
origin/HEAD -> origin/main
origin/main
upstream/main
- 1main には自分のコミットを入れないmain は upstream を映すだけにし、変更は毎回ブランチで行うと、同期が常に Fast-forward になる
- 2作業を始める前に fetch upstream遅れた main からブランチを切ると、プルリクエストで衝突しやすい
- 3push 先は originupstream への push は権限が無ければ拒まれる。提案はプルリクエストで
- 4取り込まれたブランチは消す手元は branch -d、フォーク側は push --delete
プロセスのfork()とは(Cで確かめたPIDと返り値)
この節では、OS のシステムコール fork() が何をするか、親と子で何が違うかを、C で実行した結果で確かめられます。
fork() は、呼び出したプロセスを複製して新しいプロセスを作るシステムコールです。 man ページ(fork(2))は「fork() creates a new process by duplicating the calling process.」と定義し、返り値は「親には子の PID、子には 0、失敗なら親に -1 で子は作られない」としています。複製された直後、親と子はメモリの内容が同じで、fork() の次の行から別々に動きます。
pid_t pid = fork(); /* ここでプロセスが2つになる */
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) { /* 子プロセス:fork() は 0 を返す */
counter += 1;
printf("子 : fork()の返り値=%d pid=%d ppid=%d counter=%d\n",
pid, getpid(), getppid(), counter);
exit(3); /* 終了コード 3 で終わる */
} else { /* 親プロセス:fork() は子の PID を返す */
int status;
waitpid(pid, &status, 0); /* 子の終了を待つ */
printf("親 : fork()の返り値=%d pid=%d counter=%d\n", pid, getpid(), counter);
printf("親 : 子の終了コード=%d\n", WEXITSTATUS(status));
}
$ gcc -O2 -Wall -o fork1 fork1.c && ./fork1
fork() の前: pid=3024 ppid=3008 counter=10
子 : fork()の返り値=0 pid=3025 ppid=3024 counter=11
親 : fork()の返り値=3025 pid=3024 counter=10(子の変更は見えない)
親 : 子の終了コード=3
| 見るもの | 親(pid 3024) | 子(pid 3025) |
|---|---|---|
| fork() の返り値 | 3025(子の PID) | 0 |
| getppid()(親の PID) | 3008(シェル) | 3024(fork した親) |
| counter に +1 した結果 | 10 のまま | 11 |
| 終了 | waitpid で子の終了コード 3 を受け取る | exit(3) |
返り値で親と子を見分けるのが fork の使い方の中心です。同じコードを2つのプロセスが実行していて、pid == 0 の側だけが子です。子で足した counter が親では 10 のままなのは、fork の時点でメモリが分かれるためです(仕組みは次の節)。
親プロセスが wait()/waitpid() で子の終了を回収するのは、子の終了コードを受け取るためと、後述のゾンビプロセスを残さないためです。ps・pstree で親子を見ると次のようになります(子を3つ fork して 2 秒待たせた例)。
tree(4569)-+-sh(4573)---pstree(4575)
|-tree(4570)
|-tree(4571)
`-tree(4572)
プロセスの見方・止め方は、関連記事「Linuxプロセス確認と停止|ps・top・kill逆引きコマンド一覧」で扱っています。
コピーオンライトの実測(200MBの親をforkしても子は0MB)
この節では、fork が「メモリを丸ごとコピーする」のではなく「書き込むまで共有する」仕組み(コピーオンライト)であることを、/proc の数値で確かめられます。
man ページは「Linux では fork() はコピーオンライトのページで実装されているので、かかるコストは親のページテーブルを複製する時間とメモリだけ」と説明しています。 200MB を確保して書き込んだ親を fork し、子が書き込む前後で、専有している(Private_Dirty)メモリと共有している(Shared)メモリを /proc/self/smaps_rollup で読みました。
$ ./cow
親: fork() の前 RssAnon= 200 MB Shared_Clean+Dirty= 1 MB Private_Dirty= 200 MB
子: 書き込む前 RssAnon= 200 MB Shared_Clean+Dirty= 201 MB Private_Dirty= 0 MB
子: 100MB 書いた後 RssAnon= 200 MB Shared_Clean+Dirty= 101 MB Private_Dirty= 100 MB
子: buf[0]=B buf[209715199]=A
親: fork() にかかった時間 = 2.617 ms
親: 子が終わった後 RssAnon= 200 MB Shared_Clean+Dirty= 1 MB Private_Dirty= 200 MB
親: buf[0]=A(子の書き込みは親に見えない)
RssAnon(プロセスが使っている無名メモリ)は親も子も 200MB と表示されますが、fork 直後の子はその全部が「共有」で、専有は 0MB です。子が前半 100MB に書き込むと、その分だけ子の専有になり、共有は 101MB に減りました。親の buf[0] は A のままで、子の書き込みは親に影響しません。
fork() の時間は親のメモリに比例する
コピーはしなくても、ページテーブル(どのアドレスがどの物理メモリかの対応表)は複製します。親のメモリを変えて fork() の時間を測ると、ほぼ比例して増えました。比較として、ページテーブルも複製しない vfork() と、新しいプログラムを直接起動する posix_spawn() も並べています。
| 親のメモリ | fork() | vfork() | posix_spawn(/bin/true) |
|---|---|---|---|
| 0MB | 0.031ms | 0.012ms | 0.070ms |
| 100MB | 0.943ms | 0.018ms | 0.121ms |
| 500MB | 3.977ms | 0.017ms | 0.110ms |
| 1,000MB | 8.043ms | 0.016ms | 0.124ms |
| 2,000MB | 17.104ms | 0.019ms | 0.149ms |
9回の中央値です。メモリを多く使うプロセス(データを大量に読み込んだサーバーなど)から外部コマンドを起動するとき、fork() を使うと親のメモリに比例した時間がかかります。 外部コマンドを起動するだけなら、posix_spawn や vfork を使うライブラリ(後述の Python の subprocess など)のほうが速い理由がこの表です。
forkとexec・vfork・posix_spawn・スレッドの違い
この節では、fork とよく並べられる4つの言葉の違いと、シェルがコマンドを起動する仕組みが分かります。
fork は「同じプログラムの複製」、exec は「今のプロセスの中身を別のプログラムに入れ替える」操作です。 シェルで ls を打ったときに起きているのは「fork して子を作り、子が exec で ls に変わり、親のシェルが wait で終わりを待つ」の3つです。
pid_t pid = fork();
if (pid == 0) { /* 子:別のプログラムに置き換わる */
execlp("uname", "uname", "-r", (char *)NULL);
perror("execlp"); /* ここに来るのは失敗したときだけ */
_exit(127);
}
int st; waitpid(pid, &st, 0);
printf("親: 子(pid=%d) が終了。終了コード=%d\n", pid, WEXITSTATUS(st));
$ ./exec1
6.18.44-fc-v185
親: 子(pid=3991) が終了。終了コード=0
$ strace -f -e trace=clone,execve,wait4 ./exec1
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f735cffda10) = 3999
wait4(3999, <unfinished ...>
[pid 3999] execve("/usr/bin/uname", ["uname", "-r"], 0x7ffe69e2d4b8 /* 230 vars */) = 0
strace で見ると、C の fork() は clone システムコールとして呼ばれています。man ページによると、glibc 2.3.3 以降の fork() はカーネルの fork システムコールではなく clone を使い、その前後で pthread_atfork() で登録したハンドラーを呼びます。execlp の後ろのコードが「失敗したときだけ動く」のは、成功すればプロセスの中身が uname に入れ替わって戻ってこないためです。
| 操作 | 何が起きるか | PID | メモリ |
|---|---|---|---|
| fork() | 同じプログラムの複製(子)ができる | 新しい PID | コピーオンライトで別 |
| exec 系 | 今のプロセスが別のプログラムに入れ替わる | 変わらない | 入れ替わる |
| vfork() | exec するまで親を止め、メモリを借りる fork | 新しい PID | 親と共有(exec か _exit まで) |
| posix_spawn() | fork+exec を1回で行う | 新しい PID | 新しいプログラムのもの |
| スレッド(pthread_create) | 同じプロセスの中に実行の流れを増やす | 同じ PID、別の TID | 共有 |
fork(プロセス)に向く
別のプログラムを起動する(fork+exec)
1つが落ちても他に影響させたくない
CPU を複数コアで使う(Python では GIL の制約を避けられる)
スレッドに向く
同じデータを頻繁に共有する
起動を軽くしたい
通信・待ち時間の多い処理を並行させる
man ページは「子プロセスは1つのスレッド(fork() を呼んだスレッド)だけを持って作られる」としています。複数のスレッドを持つプロセスが fork すると、子では他のスレッドが消えた状態になり、そのスレッドが握っていたロックが解放されないままになります。次の節の Python の警告は、この問題への対策です。
Pythonのos.fork・multiprocessing・subprocessでのforkの使われ方
この節では、Python で fork が直接・間接に使われている場所と、開始方式(fork・spawn・forkserver)の速さの違いが分かります。
Python の os.fork() は C の fork() とまったく同じ意味で、返り値で親と子を見分けます。
pid = os.fork() # ここでプロセスが2つになる
if pid == 0: # 子:os.fork() は 0 を返す
counter += 1
print(f"子 : 返り値={pid} pid={os.getpid()} ppid={os.getppid()} counter={counter}", flush=True)
os._exit(3) # 子は os._exit で終わる
else: # 親:返り値は子の PID
_, status = os.waitpid(pid, 0)
print(f"親 : 返り値={pid} counter={counter} 子の終了コード={os.waitstatus_to_exitcode(status)}")
$ python3 fork_basic.py
fork の前: pid=5070 ppid=5069 counter=10
子 : 返り値=0 pid=5071 ppid=5070 counter=11
親 : 返り値=5071 pid=5070 counter=10 子の終了コード=3
子を sys.exit() ではなく os._exit() で終えるのは、sys.exit() だと親から受け継いだ finally や atexit の処理が子でも動き、二重に実行されるためです。
Python ではコピーオンライトが効きにくい(読むだけで 92MB がコピーされた)
C では「書くまでコピーされない」でしたが、Python のオブジェクトは参照カウント(何か所から参照されているかの数)をオブジェクトの中に持っていて、読むだけでもその数が書き換わります。int 300万個のリストを持つ親を fork し、子で sum() しただけの結果です。
$ python3 cow_py.py
親: fork の前 RssAnon=118MB Private_Dirty=118MB
子: 読む前 Private_Dirty=0MB
子: sum した後 Private_Dirty=92MB(0.18秒)
親: 子の後 Private_Dirty=118MB
書き込みをしていないのに、子の専有メモリが 0MB から 92MB に増えました。「親で大きなデータを読み込み、fork した子で参照だけする」という設計で、子の数だけメモリが増えていくのはこのためです。
multiprocessing の開始方式:fork・forkserver・spawn
multiprocessing は子プロセスの作り方を3つ持ちます。公式ドキュメントによると、Linux での既定は Python 3.13 までが fork、3.14 から forkserverに変わりました(複数スレッドを持つプロセスでの不具合を避けるため)。手元の 3.13 で、子プロセス1個の起動から終了までを測りました。
$ python3 mp_methods.py
既定の開始方式: fork
fork 子1個の起動〜終了 8.9 ms(5回の中央値) 子から見た BIG の長さ=1000000
forkserver 子1個の起動〜終了 14.7 ms(5回の中央値) 子から見た BIG の長さ=False
spawn 子1個の起動〜終了 119.7 ms(5回の中央値) 子から見た BIG の長さ=False
| 方式 | 仕組み | 手元の時間 | 親のグローバル変数 |
|---|---|---|---|
| fork | os.fork() で親を複製 | 8.9ms | 見える(BIG=100万) |
| forkserver | 単一スレッドのサーバーに fork してもらう | 14.7ms | 見えない(import し直し) |
| spawn | 新しい Python を起動して必要なものだけ渡す | 119.7ms | 見えない(import し直し) |
fork 方式だけが、親が作った変数を子でそのまま見られます。 3.14 以降で「子で変数が None になった」「if __name__ == '__main__': が無いとエラーになった」という変化は、既定が forkserver に変わったためで、multiprocessing.get_context('fork') と書けば以前の動きになります。複数スレッドがあるプロセスで fork すると、3.12 以降は次の警告が出ます。
DeprecationWarning: This process (pid=5386) is multi-threaded, use of fork() may lead to deadlocks in the child.
subprocess は fork ではなく vfork を使っていた
外部コマンドを起動する subprocess.run() を strace で見ると、fork ではなく vfork が呼ばれ、すぐ execve していました。前の節の表のとおり、親のメモリが大きくても起動が遅くならない作りです。
$ strace -f -e trace=clone,clone3,vfork,execve python3 -c 'import subprocess; subprocess.run(["/bin/true"])'
vfork( …
[pid 5299] execve("/bin/true", ["/bin/true"], 0x2ef55c80 /* 231 vars */ <unfinished ...>
[pid 5298] <... vfork resumed>) = 5299
subprocess の run・Popen の使い分けは、関連記事「Python subprocessの使い方|run・Popenの違いと例外処理の実例」で扱っています。
スレッドと fork を同じコードで比べる
$ python3 thread_vs_fork.py
スレッド: pid=5383 tid=5384 shared=1
メイン : pid=5383 tid=5383 shared=1(スレッドの変更が見える)
子プロセス: pid=5385 shared=2
親プロセス: pid=5383 shared=1(子の変更は見えない)
スレッドは同じ PID の中で動き、変数の変更が共有されます。fork した子は別の PID で、変数の変更は親に戻りません。
forkの落とし穴(二重出力・ゾンビ・孤児)
この節では、fork を使ったプログラムで起きる3つの典型的な現象を、再現した出力で確かめられます。
1. fork の前の printf が2回出る
標準出力がファイルやパイプに向いているとき、printf の出力はバッファーにためられます。バッファーごと fork で複製されるので、親と子の両方が同じ行を書き出します。
printf("fork の前に出した文字\n"); /* 端末なら改行で出力されるが、ファイルやパイプではバッファーに残る */
if (fork() == 0) { printf("[子]\n"); return 0; }
wait(NULL); printf("[親]\n");
$ ./buf # 端末へ
fork の前に出した文字
[子]
[親]
$ ./buf | cat # パイプへ(ファイルへのリダイレクトも同じ)
fork の前に出した文字
[子]
fork の前に出した文字
[親]
端末では改行のたびに書き出されるため1回、パイプでは2回出ました。fork の直前に fflush(stdout)(Python なら print(..., flush=True))を入れると防げます。「ログに同じ行が2回出る」の原因の1つです。
2. 親が wait しないとゾンビになる
終了した子は、親が wait() で終了コードを受け取るまで、プロセス表に「終わったという記録」だけが残ります。これがゾンビプロセス(STAT が Z、<defunct>)です。
$ ./zombie
--- wait() の前
PID PPID STAT COMMAND
4496 4495 Z zombie
--- wait() の後
PID PPID STAT COMMAND
(消えた)
ゾンビは CPU もメモリもほぼ使いませんが、PID を1つ占有します。長く動くサーバーが子を作りっぱなしにすると、PID を使い切って fork が失敗します。SIGCHLD を受けて waitpid(-1, …, WNOHANG) で回収するか、signal(SIGCHLD, SIG_IGN) で自動回収にします。ゾンビは kill では消えず、親が回収するか親が終わるかで消えます。この現象は、関連記事「Linuxの怪現象8選|rmしても減らない・ゾンビ・kill」でも扱っています。
3. 親が先に終わると子は PID 1 の子になる(孤児)
$ ./orphan
子: 生まれた直後の ppid=4542
親: pid=4542 子=4543。親は先に終わる
子: 親が終わった後の ppid=1
親が先に終わった子は、PID 1(init・systemd など。この環境では別の親プロセスでした)に引き取られます。デーモンを作るときに「fork して親を終える」のは、この仕組みでシェルから切り離すためです。
プロジェクトのフォーク(OSS)とブロックチェーンのフォーク
この節では、ソフトウェアのプロジェクトが「フォークした」と言われるときの意味と、代表的な例、ブロックチェーンでの用語が分かります。
プロジェクトのフォークは、オープンソースのソースコードをある時点で複製し、別の名前・別の運営で開発を続けることです。 オープンソースのライセンスは複製と改変を認めているので、ライセンスの条件を守る限りフォークは正当な行為です。開発方針やライセンスの変更、運営元の買収をきっかけに起きることが多く、次のような例があります(各プロジェクトの公式サイト・発表の範囲)。
| フォーク | 元 | 公式の説明 |
|---|---|---|
| MariaDB | MySQL | MySQL の元の開発者たちが作ったと公式サイトが説明 |
| LibreOffice | OpenOffice.org | 公式サイトが「LibreOffice は OpenOffice.org を基にした」と説明 |
| Valkey | Redis | Linux Foundation の発表(2024年3月)で Redis 7.2.4 を基に開発を続けると説明 |
| OpenTofu | Terraform | 公式サイトが「Terraform のフォーク」で Linux Foundation の管理下と説明 |
GitHub のフォークとプロジェクトのフォークは、操作としては同じ「複製」ですが、GitHub のフォークの多くは「変更を元に戻す(プルリクエスト)」ためのもので、プロジェクトのフォークは「戻さずに別々に進む」ものです。GitHub 上の個人のフォークが、そのまま独立したプロジェクトに育つこともあります。
ブロックチェーンのフォークは、台帳(チェーン)の枝分かれです。以前の規則とも互換性があり、一時的な分岐で収束するものをソフトフォーク、互換性が無く別々のチェーンとして続くものをハードフォークと呼びます。「分岐して別々に進む」という点は、この記事の他の fork と同じです。
よくあるエラー・つまずき(原文つき)
この節では、GitHub のフォークとプロセスの fork で出るエラーや意外な動きの原文と、原因・直し方が分かります。
| 原文・現象 | 原因 | 直し方 |
|---|---|---|
! [remote rejected] … (pre-receive hook declined)/GitHub では Permission to … denied |
upstream(元のリポジトリ)に push した | origin(自分のフォーク)に push し、プルリクエストで提案する |
fatal: 'upstream' does not appear to be a git repository |
upstream リモートを登録していない | git remote add upstream 元のURL |
| プルリクエストに関係ないコミットが大量に混ざる | 遅れた main からブランチを切り、同期をマージコミットで行った | main は upstream を映すだけにし、ブランチを切る前に fetch upstream |
| 「Sync fork」で競合が出る | フォークの main に独自のコミットがある | 手元で merge upstream/main して競合を解消し push |
| fork の前の出力がログに2回出る | stdio のバッファーごと複製された | fork の直前に fflush/flush=True |
ps に <defunct>(STAT Z)が増える |
親が wait していない | SIGCHLD で waitpid、または SIG_IGN |
fork: Resource temporarily unavailable(EAGAIN) |
プロセス数の上限(ulimit -u・cgroup の pids.max)に達した |
ゾンビや残った子を回収する。上限を見直す |
fork: Cannot allocate memory(ENOMEM) |
カーネルの構造を確保できない | メモリの使用量を減らす(man の記述) |
Python: DeprecationWarning: This process … is multi-threaded, use of fork() may lead to deadlocks |
スレッドがある状態で fork した | 開始方式を forkserver か spawn にする。fork はスレッドを作る前に |
| Python 3.14 で子プロセスから親の変数が見えなくなった | 既定が forkserver に変わった | get_context('fork')、または引数で渡す |
判断表:fork・clone・branch、fork・spawn・スレッド
この節では、「GitHub でフォークするか」と「プロセスを fork するか」を、場面ごとに決められます。
- 1元のリポジトリに push できる → ブランチ自分や自分のチームのリポジトリ。フォークは要らない
- 2push できないが変更を提案したい → フォーク+プルリクエストオープンソースへの貢献。origin と upstream の2つを持つ
- 3自分の版を持ち続けたい → フォーク(戻さない)元の更新は必要なときだけ同期する
- 4読んで試すだけ → clone だけフォークすると元の作者に通知される。学習だけなら clone だけで済む場面が多い
| やりたいこと(プロセス) | 向く手段 | 理由(手元の実測) |
|---|---|---|
| 外部コマンドを起動する | subprocess/posix_spawn | 親が 2GB でも 0.15ms。fork は 17ms |
| CPU を複数コアで使う(Python) | multiprocessing | 別プロセスなら GIL の影響を受けない |
| 親の大きなデータを子で読むだけ | C なら fork、Python は共有メモリや Arrow 等を検討 | Python は読むだけで 92MB がコピーされた |
| 通信待ちの多い処理を並行 | スレッド・asyncio | 起動が軽く、データ共有が要らない |
| スレッドがあるプログラムで子を作る | forkserver/spawn | fork は 3.12 以降 DeprecationWarning |
1フォークは「権限が無いとき」の道具
push できるならブランチ。フォークは別リポジトリになるので、同期の手間が増える。
2fork() はコピーしないが、ページテーブルは複製する
親のメモリに比例して遅くなる。コマンド起動なら posix_spawn・vfork。
3Python の fork は「読むだけでコピー」
参照カウントの書き込みでページが専有になる。子に渡すデータは引数で明示するほうが安全。
よくある質問
Q. fork とは何ですか? A. 「枝分かれして複製を作る」ことです。GitHub では他人のリポジトリを自分のアカウントに複製する操作、OS では実行中のプロセスが自分の複製(子プロセス)を作るシステムコール、オープンソースでは元から分かれて別名で開発を続けることを指します。
Q. GitHub の fork と clone の違いは? A. fork は GitHub 上に自分のリポジトリとして複製すること、clone は GitHub から手元の PC に複製することです。他人のプロジェクトに変更を提案するときは、フォークしてから、そのフォークを clone します。
Q. fork と branch の違いは? A. branch は同じリポジトリの中の枝で、手元では 41 バイトのファイル1つでした。fork は別のリポジトリ(手元の例で 120KB)で、元に書き込む権限が無くても作れます。権限があるならブランチで済みます。
Q. フォークしたリポジトリを最新にするには?
A. GitHub の画面の「Sync fork」→「Update branch」、GitHub CLI の gh repo sync、またはコマンドラインで git fetch upstream→git merge upstream/main→git push origin main です。コマンドラインの場合、push するまで GitHub 上のフォークは変わりません。
Q. フォークすると元の作者に分かりますか? A. 分かります。フォークは元のリポジトリのフォーク一覧に載り、通知されます。読んで試すだけなら clone だけで済みます。
Q. プロセスの fork() とは何ですか? A. 呼び出したプロセスを複製して子プロセスを作るシステムコールです。親には子の PID、子には 0 が返り、その値で親と子の処理を分けます。手元では親 pid 3024 に 3025 が返り、子には 0 が返りました。
Q. fork するとメモリは2倍になりますか? A. なりません。Linux はコピーオンライトなので、書き込むまで親と子はページを共有します。200MB の親を fork した直後の子の専有メモリは 0MB で、100MB 書き込むと 100MB になりました。ただしページテーブルの複製は要るので、fork() の時間は親のメモリに比例しました(2GB で 17ms)。
Q. fork と exec の違いは? A. fork は同じプログラムの複製を作ること、exec は今のプロセスの中身を別のプログラムに入れ替えることです。シェルがコマンドを起動するときは、fork で子を作り、子で exec し、親が wait します。
Q. fork とスレッドの違いは? A. fork は別のプロセス(別の PID・別のメモリ)、スレッドは同じプロセスの中(同じ PID・同じメモリ)です。手元では、スレッドが変えた変数はメインに見え、fork した子が変えた変数は親に見えませんでした。
Q. Python の multiprocessing で fork を使うと警告が出ます。 A. 3.12 以降、スレッドがあるプロセスで fork すると DeprecationWarning が出ます。3.14 からは Linux の既定が forkserver に変わりました。スレッドを使うプログラムでは forkserver か spawn にし、子に渡すデータは引数で渡します。
Q. ゾンビプロセスは fork と関係がありますか? A. あります。fork した子が終わっても、親が wait() で回収しないと、終了の記録だけがプロセス表に残ります。これがゾンビ(STAT が Z)です。親で SIGCHLD を受けて waitpid するか、SIG_IGN で自動回収にします。
Q. 「〇〇は△△のフォーク」とはどういう意味ですか? A. オープンソースのソースコードをある時点で複製し、別の名前・別の運営で開発を続けているという意味です。MariaDB(MySQL から)、LibreOffice(OpenOffice.org から)、Valkey(Redis から)などがあります。
まとめ
- fork は「枝分かれして複製を作る」こと。GitHub のフォーク・プロセスの fork()・プロジェクトのフォークの3つを、文脈で見分ける。
- GitHub のフォークは、元に push できないときの道具。origin(フォーク)と upstream(元)の2つのリモートを持ち、ブランチで変更して origin に push し、プルリクエストで提案する。同期は
fetch upstream→merge→push。 - プロセスの fork() は親には子の PID、子には 0 を返す。親と子のメモリは別で、手元では子の変更が親に見えなかった。
- Linux の fork はコピーオンライト。200MB の親を fork しても子の専有は 0MB、書いた分だけコピーされる。ただし fork() の時間は親のメモリに比例し、2GB で 17ms。コマンド起動は posix_spawn・vfork(subprocess はこちら)が速い。
- Python は読むだけで参照カウントが書き換わり、92MB がコピーされた。3.14 から multiprocessing の既定は forkserver。二重出力は fork 前の flush、ゾンビは wait で防ぐ。
参考・出典
確認日はいずれも 2026年10月29日です。
- GitHub Docs「リポジトリをフォークする」:https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo
- GitHub Docs「フォークを同期する」:https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork
- GitHub Docs「フォークについて」:https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks
- Git「git-remote Documentation」:https://git-scm.com/docs/git-remote
- man7.org「fork(2) — Linux manual page」:https://man7.org/linux/man-pages/man2/fork.2.html
- man7.org「vfork(2)」:https://man7.org/linux/man-pages/man2/vfork.2.html
- man7.org「posix_spawn(3)」:https://man7.org/linux/man-pages/man3/posix_spawn.3.html
- man7.org「proc_pid_smaps(5)」:https://man7.org/linux/man-pages/man5/proc_pid_smaps.5.html
- Python ドキュメント「multiprocessing — プロセスベースの並列処理」(開始方式):https://docs.python.org/ja/3/library/multiprocessing.html
- Python ドキュメント「os.fork」:https://docs.python.org/ja/3/library/os.html#os.fork
- Python ドキュメント「subprocess」:https://docs.python.org/ja/3/library/subprocess.html
- The Linux Foundation「Linux Foundation Launches Open Source Valkey Community」(2024年3月28日):https://www.linuxfoundation.org/press/linux-foundation-launches-open-source-valkey-community
- MariaDB Foundation「About MariaDB Server」:https://mariadb.org/about/
- The Document Foundation(公式サイト):https://www.documentfoundation.org/
- OpenTofu「OpenTofu」:https://opentofu.org/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「forkとは|GitHubのフォークとプロセスのfork()を実物で」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。