控制組

由 Paul Menage <menage@google.com> 基於 CPUSETS 編寫

源自 CPUSETS 的原始版權宣告

部分版權所有 (C) 2004 BULL SA.

部分版權所有 (c) 2004-2006 Silicon Graphics, Inc.

由 Paul Jackson <pj@sgi.com> 修改

由 Christoph Lameter <cl@gentwo.org> 修改

1. 控制組

1.1 什麼是 cgroup?

控制組提供了一種機制,用於將任務集及其所有未來子任務聚合/分割槽到具有特定行為的層次結構組中。

定義

一個 cgroup 將一組任務與一個或多個子系統的引數集關聯起來。

一個 子系統 是利用 cgroup 提供的任務分組功能,以特定方式處理任務組的模組。子系統通常是“資源控制器”,負責排程資源或應用每個 cgroup 的限制,但它也可以是任何希望對一組程序進行操作的模組,例如虛擬化子系統。

一個 層級 是一組以樹狀結構排列的 cgroup,系統中的每個任務都精確地位於該層級中的一個 cgroup 中,並且有一組子系統;每個子系統都將系統特定的狀態附加到該層級中的每個 cgroup。每個層級都關聯著一個 cgroup 虛擬檔案系統例項。

在任何給定時間,可能存在多個活躍的任務 cgroup 層級。每個層級都是系統中所有任務的一個分割槽。

使用者級程式碼可以在 cgroup 虛擬檔案系統例項中按名稱建立和銷燬 cgroup,指定和查詢任務分配給哪個 cgroup,以及列出分配給 cgroup 的任務 PID。這些建立和分配隻影響與該 cgroup 檔案系統例項關聯的層級。

cgroup 本身唯一的使用場景是簡單的作業跟蹤。其目的是讓其他子系統掛接到通用 cgroup 支援中,為 cgroup 提供新的屬性,例如核算/限制 cgroup 中程序可以訪問的資源。例如,cpuset(參見 CPUSETS)允許您將一組 CPU 和一組記憶體節點與每個 cgroup 中的任務關聯起來。

1.2 為什麼需要 cgroup?

Linux 核心中存在多種旨在提供程序聚合的努力,主要用於資源跟蹤目的。這些努力包括 cpusets、CKRM/ResGroups、UserBeanCounters 和虛擬伺服器名稱空間。它們都需要程序分組/分割槽這一基本概念,即新 fork 的程序最終與其父程序位於同一組(cgroup)中。

核心 cgroup 補丁提供了有效實現此類組所需的最低限度核心機制。它對系統快速路徑的影響最小,併為 cpusets 等特定子系統提供了鉤子,以便根據需要提供額外行為。

提供多層級支援,以應對不同子系統對任務到 cgroup 的劃分方式明顯不同的情況——擁有並行層級允許每個層級都是任務的自然劃分,而無需處理如果多個不相關的子系統需要被強制進入同一個 cgroup 樹時將出現的複雜任務組合。

一個極端情況是,每個資源控制器或子系統都可以位於獨立的層級中;另一個極端情況是,所有子系統都附加到同一個層級中。

作為一個可以從多層級中受益的場景示例(最初由 vatsa@in.ibm.com 提出),考慮一臺有各種使用者(學生、教授、系統任務等)的大型大學伺服器。該伺服器的資源規劃可以遵循以下思路:

CPU :          "Top cpuset"
                /       \
        CPUSet1         CPUSet2
           |               |
        (Professors)    (Students)

        In addition (system tasks) are attached to topcpuset (so
        that they can run anywhere) with a limit of 20%

Memory : Professors (50%), Students (30%), system (20%)

Disk : Professors (50%), Students (30%), system (20%)

Network : WWW browsing (20%), Network File System (60%), others (20%)
                        / \
        Professors (15%)  students (5%)

像 Firefox/Lynx 這樣的瀏覽器歸入 WWW 網路類別,而 (k)nfsd 則歸入 NFS 網路類別。

同時,Firefox/Lynx 將根據啟動者的身份(教授/學生)共享相應的 CPU/記憶體類別。

藉助為不同資源對任務進行不同分類的能力(透過將這些資源子系統置於不同的層級中),管理員可以輕鬆設定一個指令碼,該指令碼接收執行通知,並根據啟動瀏覽器的使用者進行操作,他可以

# echo browser_pid > /sys/fs/cgroup/<restype>/<userclass>/tasks

如果只有一個層級,他現在可能不得不為每個啟動的瀏覽器建立一個單獨的 cgroup,並將其與適當的網路和其他資源類別關聯起來。這可能導致此類 cgroup 的泛濫。

此外,假設管理員希望暫時為學生的瀏覽器提供增強的網路訪問(因為現在是晚上,使用者想玩線上遊戲:D)或者給學生的某個模擬應用增強 CPU 能力。

能夠直接將 PID 寫入資源類別,這只是一個

# echo pid > /sys/fs/cgroup/network/<new_class>/tasks
(after some time)
# echo pid > /sys/fs/cgroup/network/<orig_class>/tasks

如果沒有這種能力,管理員將不得不將 cgroup 拆分成多個獨立的 cgroup,然後將新的 cgroup 與新的資源類別關聯起來。

1.3 cgroup 是如何實現的?

控制組透過以下方式擴充套件核心:

  • 系統中的每個任務都有一個指向 css_set 的引用計數指標。

  • css_set 包含一組指向 cgroup_subsys_state 物件的引用計數指標,系統中註冊的每個 cgroup 子系統都有一個。任務與它在每個層級中所屬的 cgroup 之間沒有直接連結,但可以透過 cgroup_subsys_state 物件上的指標進行確定。這是因為訪問子系統狀態是預期會頻繁發生且在效能關鍵程式碼中發生的操作,而需要任務實際 cgroup 分配的操作(特別是 cgroup 之間的移動)則不那麼常見。一個連結串列透過使用 css_set 的每個 task_struct 的 cg_list 欄位執行,錨定在 css_set->tasks。

  • cgroup 層級檔案系統可以從使用者空間掛載以進行瀏覽和操作。

  • 您可以列出附加到任何 cgroup 的所有任務(按 PID)。

cgroup 的實現需要在核心其餘部分中引入一些簡單的鉤子,這些鉤子都不在效能關鍵路徑上:

  • 在 init/main.c 中,在系統啟動時初始化根 cgroup 和初始 css_set。

  • 在 fork 和 exit 中,用於將任務附加和分離到其 css_set。

此外,可以掛載一個型別為“cgroup”的新檔案系統,以啟用對當前核心已知 cgroup 的瀏覽和修改。掛載 cgroup 層級時,您可以指定一個逗號分隔的子系統列表作為檔案系統掛載選項。預設情況下,掛載 cgroup 檔案系統會嘗試掛載包含所有已註冊子系統的層級。

如果已存在一個包含完全相同子系統集的活躍層級,它將被新掛載重用。如果沒有現有層級匹配,並且任何請求的子系統正在現有層級中使用,則掛載將因 -EBUSY 失敗。否則,將啟用一個新的層級,並將其與請求的子系統關聯起來。

目前不可能將新的子系統繫結到活躍的 cgroup 層級,或從活躍的 cgroup 層級中解綁子系統。將來可能會實現,但這充滿了棘手的錯誤恢復問題。

當一個 cgroup 檔案系統被解除安裝時,如果在頂級 cgroup 下建立了任何子 cgroup,即使解除安裝了,該層級仍將保持活躍;如果沒有子 cgroup,則該層級將被停用。

沒有為 cgroup 新增新的系統呼叫——所有查詢和修改 cgroup 的支援都透過這個 cgroup 檔案系統進行。

/proc 下的每個任務都有一個名為“cgroup”的附加檔案,顯示每個活躍層級的子系統名稱以及相對於 cgroup 檔案系統根目錄的 cgroup 名稱。

每個 cgroup 在 cgroup 檔案系統中由一個目錄表示,該目錄包含描述該 cgroup 的以下檔案:

  • tasks:附加到該 cgroup 的任務列表(按 PID)。此列表不保證排序。將執行緒 ID 寫入此檔案會將執行緒移動到此 cgroup。

  • cgroup.procs:cgroup 中執行緒組 ID 的列表。此列表不保證排序或沒有重複的 TGID,如果需要此屬性,使用者空間應自行排序/去重。將執行緒組 ID 寫入此檔案會將該組中的所有執行緒移動到此 cgroup。

  • notify_on_release 標誌:退出時是否執行釋放代理?

  • release_agent:用於釋放通知的路徑(此檔案僅存在於頂級 cgroup 中)

其他子系統,例如 cpusets,可以在每個 cgroup 目錄中新增額外的檔案。

新的 cgroup 使用 mkdir 系統呼叫或 shell 命令建立。cgroup 的屬性(例如其標誌)透過寫入該 cgroup 目錄中的相應檔案進行修改,如上所述。

巢狀 cgroup 的命名分層結構允許將大型系統劃分為巢狀的、動態可變的“軟分割槽”。

每個任務對 cgroup 的附件(在 fork 時自動由其子程序繼承)允許將系統上的工作負載組織成相關的任務集。如果必要 cgroup 檔案系統目錄上的許可權允許,任務可以重新附加到任何其他 cgroup。

當一個任務從一個 cgroup 移動到另一個 cgroup 時,它會獲得一個新的 css_set 指標——如果已經存在一個包含所需 cgroup 集合的 css_set,則該組將被重用,否則將分配一個新的 css_set。透過查詢雜湊表來定位適當的現有 css_set。

為了允許從 cgroup 訪問構成它的 css_set(以及任務),一組 cg_cgroup_link 物件形成一個格;每個 cg_cgroup_link 都連結到單個 cgroup 的 cg_cgroup_link 列表的 cgrp_link_list 欄位中,以及單個 css_set 的 cg_cgroup_link 列表的 cg_link_list 欄位中。

因此,cgroup 中的任務集可以透過遍歷引用該 cgroup 的每個 css_set,並對每個 css_set 的任務集進行子遍歷來列出。

使用 Linux 虛擬檔案系統 (vfs) 來表示 cgroup 層級結構,為 cgroup 提供了熟悉的許可權和名稱空間,同時最大限度地減少了額外的核心程式碼。

1.4 notify_on_release 有什麼用?

如果 cgroup 中的 notify_on_release 標誌被啟用 (1),那麼每當 cgroup 中的最後一個任務離開(退出或附加到其他 cgroup)並且該 cgroup 的最後一個子 cgroup 被移除時,核心會執行該層級根目錄中“release_agent”檔案內容指定的命令,並提供被廢棄 cgroup 的路徑名(相對於 cgroup 檔案系統的掛載點)。這使得廢棄的 cgroup 能夠自動移除。系統啟動時根 cgroup 中 notify_on_release 的預設值為停用 (0)。建立其他 cgroup 時,其預設值是其父級 notify_on_release 設定的當前值。cgroup 層級的 release_agent 路徑的預設值為空。

1.5 clone_children 有什麼用?

此標誌僅影響 cpuset 控制器。如果在 cgroup 中啟用了 clone_children 標誌 (1),則新的 cpuset cgroup 將在初始化期間從其父級複製其配置。

1.6 如何使用 cgroup?

要啟動一個包含在 cgroup 中的新作業,並使用“cpuset”cgroup 子系統,步驟大致如下:

1) mount -t tmpfs cgroup_root /sys/fs/cgroup
2) mkdir /sys/fs/cgroup/cpuset
3) mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset
4) Create the new cgroup by doing mkdir's and write's (or echo's) in
   the /sys/fs/cgroup/cpuset virtual file system.
5) Start a task that will be the "founding father" of the new job.
6) Attach that task to the new cgroup by writing its PID to the
   /sys/fs/cgroup/cpuset tasks file for that cgroup.
7) fork, exec or clone the job tasks from this founding father task.

例如,以下命令序列將設定一個名為“Charlie”的 cgroup,其中只包含 CPU 2 和 3,以及記憶體節點 1,然後在該 cgroup 中啟動一個子 shell ‘sh’。

mount -t tmpfs cgroup_root /sys/fs/cgroup
mkdir /sys/fs/cgroup/cpuset
mount -t cgroup cpuset -ocpuset /sys/fs/cgroup/cpuset
cd /sys/fs/cgroup/cpuset
mkdir Charlie
cd Charlie
/bin/echo 2-3 > cpuset.cpus
/bin/echo 1 > cpuset.mems
/bin/echo $$ > tasks
sh
# The subshell 'sh' is now running in cgroup Charlie
# The next line should display '/Charlie'
cat /proc/self/cgroup

2. 用法示例和語法

2.1 基本用法

cgroup 的建立、修改和使用可以透過 cgroup 虛擬檔案系統完成。

要掛載包含所有可用子系統的 cgroup 層級,請鍵入:

# mount -t cgroup xxx /sys/fs/cgroup

“xxx”不會被 cgroup 程式碼解釋,但會出現在 /proc/mounts 中,因此可以是您喜歡的任何有用的標識字串。

注意:某些子系統在沒有使用者輸入之前無法工作。例如,如果啟用了 cpusets,使用者必須先為每個新建立的 cgroup 填充 cpus 和 mems 檔案,然後才能使用該組。

正如1.2 為什麼需要 cgroup?一節所解釋的,您應該為每個您想要控制的單個資源或資源組建立不同的 cgroup 層級。因此,您應該在 /sys/fs/cgroup 上掛載一個 tmpfs,併為每個 cgroup 資源或資源組建立目錄。

# mount -t tmpfs cgroup_root /sys/fs/cgroup
# mkdir /sys/fs/cgroup/rg1

要僅掛載 cpuset 和記憶體子系統的 cgroup 層級,請鍵入:

# mount -t cgroup -o cpuset,memory hier1 /sys/fs/cgroup/rg1

儘管目前支援重新掛載 cgroup,但建議不要使用。重新掛載允許更改繫結的子系統和 release_agent。重新繫結幾乎沒有用處,因為它只在層級為空時才起作用,而 release_agent 本身應該被傳統的 fsnotify 替換。對重新掛載的支援將在未來移除。

指定層級的 release_agent

# mount -t cgroup -o cpuset,release_agent="/sbin/cpuset_release_agent" \
  xxx /sys/fs/cgroup/rg1

請注意,多次指定“release_agent”將返回失敗。

請注意,目前僅在層級由單個(根)cgroup 組成時才支援更改子系統集。支援從現有 cgroup 層級任意繫結/解綁子系統的能力計劃在未來實現。

然後在 /sys/fs/cgroup/rg1 下,您可以找到一個與系統中 cgroup 樹相對應的樹。例如,/sys/fs/cgroup/rg1 是包含整個系統的 cgroup。

如果你想改變 release_agent 的值

# echo "/sbin/new_release_agent" > /sys/fs/cgroup/rg1/release_agent

也可以透過重新掛載來更改。

如果你想在 /sys/fs/cgroup/rg1 下建立一個新的 cgroup

# cd /sys/fs/cgroup/rg1
# mkdir my_cgroup

現在你想對這個 cgroup 做些什麼

# cd my_cgroup

在這個目錄中你可以找到幾個檔案

# ls
cgroup.procs notify_on_release tasks
(plus whatever files added by the attached subsystems)

現在將你的 shell 附加到這個 cgroup

# /bin/echo $$ > tasks

你也可以透過在這個目錄中使用 mkdir 在你的 cgroup 內部建立 cgroup

# mkdir my_sub_cs

要移除一個 cgroup,只需使用 rmdir

# rmdir my_sub_cs

如果 cgroup 正在使用中(內部有 cgroup,或附加有程序,或被其他子系統特定引用保持活躍),這將失敗。

2.2 附加程序

# /bin/echo PID > tasks

請注意是 PID,而不是 PIDs。您一次只能附加一個任務。如果您有多個任務要附加,則必須一個接一個地進行。

# /bin/echo PID1 > tasks
# /bin/echo PID2 > tasks
        ...
# /bin/echo PIDn > tasks

您可以透過 echoing 0 附加當前 shell 任務。

# echo 0 > tasks

您可以使用 cgroup.procs 檔案而不是 tasks 檔案一次移動執行緒組中的所有執行緒。將執行緒組中任何任務的 PID 回顯到 cgroup.procs 會導致該執行緒組中的所有任務附加到 cgroup。將 0 寫入 cgroup.procs 會移動寫入任務的執行緒組中的所有任務。

注意:由於每個任務在每個已掛載的層級中始終是且僅是一個 cgroup 的成員,要將任務從其當前 cgroup 中移除,您必須透過寫入新 cgroup 的 tasks 檔案將其移動到一個新的 cgroup(可能是根 cgroup)。

注意:由於某些 cgroup 子系統強制執行的限制,將程序移動到另一個 cgroup 可能會失敗。

2.3 按名稱掛載層級

在掛載 cgroup 層級時傳遞 name=<x> 選項會將給定名稱與該層級關聯起來。這可以在掛載預先存在的層級時使用,以便透過名稱而不是其活躍子系統集來引用它。每個層級要麼沒有名稱,要麼有一個唯一的名稱。

名稱應匹配 [w.-]+

當為新層級傳遞 name=<x> 選項時,您需要手動指定子系統;當您為子系統指定名稱時,不支援在未明確指定子系統時掛載所有子系統的傳統行為。

子系統的名稱作為層級描述的一部分出現在 /proc/mounts 和 /proc/<pid>/cgroups 中。

3. 核心 API

3.1 概述

每個希望掛接到通用 cgroup 系統的核心子系統都需要建立一個 cgroup_subsys 物件。該物件包含各種方法,這些方法是來自 cgroup 系統的回撥,以及一個將由 cgroup 系統分配的子系統 ID。

cgroup_subsys 物件中的其他欄位包括:

  • subsys_id:子系統唯一的陣列索引,指示該子系統應管理 cgroup->subsys[] 中的哪個條目。

  • name:應初始化為唯一的子系統名稱。長度不應超過 MAX_CGROUP_TYPE_NAMELEN。

  • early_init:指示子系統是否需要在系統啟動時進行早期初始化。

系統建立的每個 cgroup 物件都帶有一個指標陣列,透過子系統 ID 進行索引;這個指標完全由子系統管理;通用的 cgroup 程式碼絕不會觸及這個指標。

3.2 同步

cgroup 系統使用一個全域性互斥鎖 cgroup_mutex。任何想要修改 cgroup 的操作都應該持有此鎖。它也可以用來防止 cgroup 被修改,但在那種情況下,更具體的鎖可能更合適。

更多詳情請參閱 kernel/cgroup.c。

子系統可以透過 cgroup_lock()/cgroup_unlock() 函式獲取/釋放 cgroup_mutex。

訪問任務的 cgroup 指標可以透過以下方式完成:- 在持有 cgroup_mutex 時 - 在持有任務的 alloc_lock(透過 task_lock())時 - 在 rcu_read_lock() 區域內透過 rcu_dereference()

3.3 子系統 API

每個子系統都應:

  • 在 linux/cgroup_subsys.h 中新增一個條目

  • 定義一個名為 _cgrp_subsys 的 cgroup_subsys 物件

每個子系統可以匯出以下方法。唯一強制的方法是 css_alloc/free。任何其他為空的方法都被假定為成功的無操作。

struct cgroup_subsys_state *css_alloc(struct cgroup *cgrp) (呼叫者持有 cgroup_mutex)

呼叫此函式以為一個 cgroup 分配子系統狀態物件。子系統應為傳入的 cgroup 分配其子系統狀態物件,成功時返回指向新物件的指標,或返回 ERR_PTR() 值。成功時,子系統指標應指向 cgroup_subsys_state 型別的結構(通常嵌入在更大的子系統特定物件中),該結構將由 cgroup 系統初始化。請注意,這將在初始化時呼叫以建立此子系統的根子系統狀態;這種情況可以透過傳入的 cgroup 物件具有 NULL 父級(因為它是層級的根)來識別,並且可能是初始化程式碼的合適位置。

int css_online(struct cgroup *cgrp) (呼叫者持有 cgroup_mutex)

在 @cgrp 成功完成所有分配並對 cgroup_for_each_child/descendant_*() 迭代器可見後呼叫。子系統可以選擇透過返回 -errno 來使建立失敗。此回撥可用於實現沿層級的可靠狀態共享和傳播。有關詳細資訊,請參閱 cgroup_for_each_live_descendant_pre() 上的註釋。

void css_offline(struct cgroup *cgrp); (呼叫者持有 cgroup_mutex)

這是 css_online() 的對應函式,並且僅在 css_online() 在 @cgrp 上成功後才呼叫。這標誌著 @cgrp 結束的開始。@cgrp 正在被移除,子系統應該開始釋放其對 @cgrp 的所有引用。當所有引用都釋放後,cgroup 移除將進入下一步 - css_free()。在此回撥之後,@cgrp 對子系統而言應被視為已死亡。

void css_free(struct cgroup *cgrp) (呼叫者持有 cgroup_mutex)

cgroup 系統即將釋放 @cgrp;子系統應該釋放其子系統狀態物件。當此方法被呼叫時,@cgrp 已完全未使用;@cgrp->parent 仍然有效。(注意——如果在此子系統的 create() 方法為新 cgroup 呼叫後發生錯誤,也可以為新建立的 cgroup 呼叫此方法)。

int can_attach(struct cgroup *cgrp, struct cgroup_taskset *tset) (呼叫者持有 cgroup_mutex)

在將一個或多個任務移動到 cgroup 之前呼叫;如果子系統返回錯誤,這將中止附加操作。@tset 包含要附加的任務,並且保證至少包含一個任務。

如果任務集中有多個任務,那麼:
  • 保證所有任務都來自同一個執行緒組

  • @tset 包含執行緒組中的所有任務,無論它們是否正在切換 cgroup

  • 第一個任務是leader

每個 @tset 條目還包含任務的舊 cgroup,並且可以使用 cgroup_taskset_for_each() 迭代器輕鬆跳過未切換 cgroup 的任務。請注意,這不會在 fork 上呼叫。如果此方法返回 0(成功),則在呼叫者持有 cgroup_mutex 的同時,此狀態應保持有效,並且確保將來會呼叫 attach() 或 cancel_attach()。

void css_reset(struct cgroup_subsys_state *css) (呼叫者持有 cgroup_mutex)

一個可選操作,應將 @css 的配置恢復到初始狀態。目前僅在統一層級中當子系統透過“cgroup.subtree_control”在 cgroup 上被停用但由於其他子系統依賴它而應保持啟用時使用。cgroup 核心透過刪除關聯的介面檔案使此類 css 不可見,並呼叫此回撥,以便隱藏的子系統可以返回到初始中立狀態。這可以防止來自隱藏 css 的意外資源控制,並確保配置在稍後再次可見時處於初始狀態。

void cancel_attach(struct cgroup *cgrp, struct cgroup_taskset *tset) (呼叫者持有 cgroup_mutex)

在 can_attach() 成功後,當任務附加操作失敗時呼叫。如果一個子系統的 can_attach() 具有副作用,則應提供此函式,以便子系統可以實現回滾。否則,則不是必需的。這僅針對 can_attach() 操作已成功的子系統呼叫。引數與 can_attach() 相同。

void attach(struct cgroup *cgrp, struct cgroup_taskset *tset) (呼叫者持有 cgroup_mutex)

在任務附加到 cgroup 後呼叫,以允許任何需要記憶體分配或阻塞的附加後活動。引數與 can_attach() 相同。

void fork(struct task_struct *task)

當一個任務 fork 到一個 cgroup 中時呼叫。

void exit(struct task_struct *task)

在任務退出時呼叫。

void free(struct task_struct *task)

當 task_struct 被釋放時呼叫。

void bind(struct cgroup *root) (呼叫者持有 cgroup_mutex)

當 cgroup 子系統重新繫結到不同的層級和根 cgroup 時呼叫。目前,這僅涉及在預設層級(從不包含子 cgroup)與正在建立/銷燬的層級(因此不包含子 cgroup)之間進行移動。

4. 擴充套件屬性使用

cgroup 檔案系統支援其目錄和檔案中的某些型別的擴充套件屬性。當前支援的型別是:

  • 受信任 (XATTR_TRUSTED)

  • 安全 (XATTR_SECURITY)

兩者都需要 CAP_SYS_ADMIN 能力才能設定。

與 tmpfs 類似,cgroup 檔案系統中的擴充套件屬性使用核心記憶體儲存,建議將其使用量保持在最低限度。這就是不支援使用者定義的擴充套件屬性的原因,因為任何使用者都可以這樣做,並且對值大小沒有限制。

目前已知此功能的使用者包括 SELinux,用於限制容器中的 cgroup 使用;以及 systemd,用於各種元資料,例如 cgroup 中的主 PID(systemd 為每個服務建立一個 cgroup)。

5. 問題

Q: what's up with this '/bin/echo' ?
A: bash's builtin 'echo' command does not check calls to write() against
   errors. If you use it in the cgroup file system, you won't be
   able to tell whether a command succeeded or failed.

Q: When I attach processes, only the first of the line gets really attached !
A: We can only return one error code per call to write(). So you should also
   put only ONE PID.