日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Linux 内核进程管理之进程ID

發布時間:2023/11/27 生活经验 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Linux 内核进程管理之进程ID 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Linux 內核使用?task_struct?數據結構來關聯所有與進程有關的數據和結構,Linux 內核所有涉及到進程和程序的所有算法都是圍繞該數據結構建立的,是內核中最重要的數據結構之一。該數據結構在內核文件?include/linux/sched.h?中定義,在Linux 3.8 的內核中,該數據結構足足有 380 行之多,在這里我不可能逐項去描述其表示的含義,本篇文章只關注該數據結構如何來組織和管理進程ID的。

進程ID類型

要想了解內核如何來組織和管理進程ID,先要知道進程ID的類型:

  • PID:這是 Linux 中在其命名空間中唯一標識進程而分配給它的一個號碼,稱做進程ID號,簡稱PID。在使用 fork 或 clone 系統調用時產生的進程均會由內核分配一個新的唯一的PID值。
  • TGID:在一個進程中,如果以CLONE_THREAD標志來調用clone建立的進程就是該進程的一個線程,它們處于一個線程組,該線程組的ID叫做TGID。處于相同的線程組中的所有進程都有相同的TGID;線程組組長的TGID與其PID相同;一個進程沒有使用線程,則其TGID與PID也相同。
  • PGID:另外,獨立的進程可以組成進程組(使用setpgrp系統調用),進程組可以簡化向所有組內進程發送信號的操作,例如用管道連接的進程處在同一進程組內。進程組ID叫做PGID,進程組內的所有進程都有相同的PGID,等于該組組長的PID。
  • SID:幾個進程組可以合并成一個會話組(使用setsid系統調用),可以用于終端程序設計。會話組中所有進程都有相同的SID。

PID 命名空間

命名空間是為操作系統層面的虛擬化機制提供支撐,目前實現的有六種不同的命名空間,分別為mount命名空間、UTS命名空間、IPC命名空間、用戶命名空間、PID命名空間、網絡命名空間。命名空間簡單來說提供的是對全局資源的一種抽象,將資源放到不同的容器中(不同的命名空間),各容器彼此隔離。命名空間有的還有層次關系,如PID命名空間,圖1 為命名空間的層次關系圖。

圖1 命名空間的層次關系

在上圖有四個命名空間,一個父命名空間衍生了兩個子命名空間,其中的一個子命名空間又衍生了一個子命名空間。以PID命名空間為例,由于各個命名空間彼此隔離,所以每個命名空間都可以有 PID 號為 1 的進程;但又由于命名空間的層次性,父命名空間是知道子命名空間的存在,因此子命名空間要映射到父命名空間中去,因此上圖中 level 1 中兩個子命名空間的六個進程分別映射到其父命名空間的PID 號5~10。

命名空間增大了 PID 管理的復雜性,對于某些進程可能有多個PID——在其自身命名空間的PID以及其父命名空間的PID,凡能看到該進程的命名空間都會為其分配一個PID。因此就有:

  • 全局ID:在內核本身和初始命名空間中唯一的ID,在系統啟動期間開始的 init 進程即屬于該初始命名空間。系統中每個進程都對應了該命名空間的一個PID,叫全局ID,保證在整個系統中唯一。
  • 局部ID:對于屬于某個特定的命名空間,它在其命名空間內分配的ID為局部ID,該ID也可以出現在其他的命名空間中。

進程ID管理數據結構

Linux 內核在設計管理ID的數據結構時,要充分考慮以下因素:

  1. 如何快速地根據進程的 task_struct、ID類型、命名空間找到局部ID
  2. 如何快速地根據局部ID、命名空間、ID類型找到對應進程的 task_struct
  3. 如何快速地給新進程在可見的命名空間內分配一個唯一的 PID

如果將所有因素考慮到一起,將會很復雜,下面將會由簡到繁設計該結構。

一個PID對應一個task_struct

如果先不考慮進程之間的關系,不考慮命名空間,僅僅是一個PID號對應一個task_struct,那么我們可以設計這樣的數據結構:

struct task_struct {//...struct pid_link pids;//...
};struct pid_link {struct hlist_node node;  struct pid *pid;          
};struct pid {struct hlist_head tasks;        //指回 pid_link 的 nodeint nr;                       //PIDstruct hlist_node pid_chain;    //pid hash 散列表結點
};

每個進程的 task_struct 結構體中有一個指向 pid 結構體的指針,pid 結構體包含了 PID 號。結構示意圖如圖2。

圖2 一個task_struct對應一個PID

圖中還有兩個結構上面未提及:

  • pid_hash[]: 這是一個hash表的結構,根據 pid 的 nr 值哈希到其某個表項,若有多個 pid 結構對應到同一個表項,這里解決沖突使用的是散列表法。這樣,就能解決開始提出的第2個問題了,根據PID值怎樣快速地找到task_struct結構體:
    • 首先通過 PID 計算 pid 掛接到哈希表 pid_hash[] 的表項
    • 遍歷該表項,找到 pid 結構體中 nr 值與 PID 值相同的那個 pid
    • 再通過該 pid 結構體的 tasks 指針找到 node
    • 最后根據內核的 container_of 機制就能找到 task_struct 結構體
  • pid_map:這是一個位圖,用來唯一分配PID值的結構,圖中灰色表示已經分配過的值,在新建一個進程時,只需在其中找到一個為分配過的值賦給 pid 結構體的 nr,再將pid_map 中該值設為已分配標志。這也就解決了上面的第3個問題——如何快速地分配一個全局的PID。

至于上面的第1個問題就更加簡單,已知 task_struct 結構體,根據其 pid_link 的 pid 指針找到 pid 結構體,取出其 nr 即為 PID 號。

進程ID有類型之分

如果考慮進程之間有復雜的關系,如線程組、進程組、會話組,這些組均有組ID,分別為 TGID、PGID、SID,所以原來的 task_struct 中pid_link 指向一個 pid 結構體需要增加幾項,用來指向到其組長的 pid 結構體,相應的 struct pid 原本只需要指回其 PID 所屬進程的task_struct,現在要增加幾項,用來鏈接那些以該 pid 為組長的所有進程組內進程。數據結構如下:

enum pid_type
{PIDTYPE_PID,PIDTYPE_PGID,PIDTYPE_SID,PIDTYPE_MAX
};struct task_struct {//...pid_t pid;     //PIDpid_t tgid;    //thread group idstruct task_struct *group_leader;   // threadgroup leaderstruct pid_link pids[PIDTYPE_MAX];//...
};struct pid_link {struct hlist_node node;  struct pid *pid;          
};struct pid {struct hlist_head tasks[PIDTYPE_MAX];int nr;                         //PIDstruct hlist_node pid_chain;    // pid hash 散列表結點
};

上面 ID 的類型 PIDTYPE_MAX 表示 ID 類型數目。之所以不包括線程組ID,是因為內核中已經有指向到線程組的 task_struct 指針 group_leader,線程組 ID 無非就是 group_leader 的PID。

假如現在有三個進程A、B、C為同一個進程組,進程組長為A,這樣的結構示意圖如圖3。

圖3 增加ID類型的結構

關于上圖有幾點需要說明:

  • 圖中省去了 pid_hash 以及 pid_map 結構,因為第一種情況類似;
  • 進程B和C的進程組組長為A,那么 pids[PIDTYPE_PGID] 的 pid 指針指向進程A的 pid 結構體;
  • 進程A是進程B和C的組長,進程A的 pid 結構體的 tasks[PIDTYPE_PGID] 是一個散列表的頭,它將所有以該pid 為組長的進程鏈接起來。

再次回顧本節的三個基本問題,在此結構上也很好去實現。

增加進程PID命名空間

若在第二種情形下再增加PID命名空間,一個進程就可能有多個PID值了,因為在每一個可見的命名空間內都會分配一個PID,這樣就需要改變 pid 的結構了,如下:

struct pid
{unsigned int level;  /* lists of tasks that use this pid */struct hlist_head tasks[PIDTYPE_MAX];struct upid numbers[1];
};struct upid {int nr;struct pid_namespace *ns;struct hlist_node pid_chain;
};

在 pid 結構體中增加了一個表示該進程所處的命名空間的層次level,以及一個可擴展的 upid 結構體。對于struct upid,表示在該命名空間所分配的進程的ID,ns指向是該ID所屬的命名空間,pid_chain 表示在該命名空間的散列表。

舉例來說,在level 2 的某個命名空間上新建了一個進程,分配給它的 pid 為45,映射到 level 1 的命名空間,分配給它的 pid 為 134;再映射到 level 0 的命名空間,分配給它的 pid 為289,對于這樣的例子,如圖4所示為其表示:

圖4 增加PID命名空間之后的結構圖

圖中關于如果分配唯一的 PID 沒有畫出,但也是比較簡單,與前面兩種情形不同的是,這里分配唯一的 PID 是有命名空間的容器的,在PID命名空間內必須唯一,但各個命名空間之間不需要唯一。

至此,已經與 Linux 內核中數據結構相差不多了。

進程ID管理函數

有了上面的復雜的數據結構,再加上散列表等數據結構的操作,就可以寫出我們前面所提到的三個問題的函數了:

獲得局部ID

根據進程的 task_struct、ID類型、命名空間,可以很容易獲得其在命名空間內的局部ID:

  1. 獲得與task_struct 關聯的pid結構體。輔助函數有 task_pid、task_tgid、task_pgrp和task_session,分別用來獲取不同類型的ID的pid 實例,如獲取 PID 的實例:

    static inline struct pid *task_pid(struct task_struct *task)
    {return task->pids[PIDTYPE_PID].pid;
    }
    

    獲取線程組的ID,前面也說過,TGID不過是線程組組長的PID而已,所以:

    static inline struct pid *task_tgid(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_PID].pid;
    }
    

    而獲得PGID和SID,首先需要找到該線程組組長的task_struct,再獲得其相應的 pid:

    static inline struct pid *task_pgrp(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_PGID].pid;
    }static inline struct pid *task_session(struct task_struct *task)
    {return task->group_leader->pids[PIDTYPE_SID].pid;
    }
    
  2. 獲得 pid 實例之后,再根據 pid 中的numbers 數組中 uid 信息,獲得局部PID。

    pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
    {struct upid *upid;pid_t nr = 0;if (pid && ns->level <= pid->level) {upid = &pid->numbers[ns->level];if (upid->ns == ns)nr = upid->nr;}return nr;
    }
    

    這里值得注意的是,由于PID命名空間的層次性,父命名空間能看到子命名空間的內容,反之則不能,因此,函數中需要確保當前命名空間的level 小于等于產生局部PID的命名空間的level。
    除了這個函數之外,內核還封裝了其他函數用來從 pid 實例獲得 PID 值,如 pid_nr、pid_vnr 等。在此不介紹了。

結合這兩步,內核提供了更進一步的封裝,提供以下函數:

pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);

從函數名上就能推斷函數的功能,其實不外于封裝了上面的兩步。

查找進程task_struct

根據局部ID、以及命名空間,怎樣獲得進程的task_struct結構體呢?也是分兩步:

  1. 獲得 pid 實體。根據局部PID以及命名空間計算在 pid_hash 數組中的索引,然后遍歷散列表找到所要的 upid, 再根據內核的 container_of 機制找到 pid 實例。代碼如下:

    struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
    {struct hlist_node *elem;struct upid *pnr;//遍歷散列表hlist_for_each_entry_rcu(pnr, elem,&pid_hash[pid_hashfn(nr, ns)], pid_chain)     //pid_hashfn() 獲得hash的索引if (pnr->nr == nr && pnr->ns == ns)     //比較 nr 與 ns 是否都相同return container_of(pnr, struct pid,     //根據container_of機制取得pid 實體numbers[ns->level]);return NULL;
    }
    
  2. 根據ID類型取得task_struct 結構體。

    struct task_struct *pid_task(struct pid *pid, enum pid_type type)
    {struct task_struct *result = NULL;if (pid) {struct hlist_node *first;first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),lockdep_tasklist_lock_is_held());if (first)result = hlist_entry(first, struct task_struct, pids[(type)].node);}return result;
    }
    

內核還提供其它函數用來實現上面兩步:

struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns);
struct task_struct *find_task_by_vpid(pid_t vnr);
struct task_struct *find_task_by_pid(pid_t vnr);

具體函數實現的功能也比較簡單。

生成唯一的PID

內核中使用下面兩個函數來實現分配和回收PID的:

static int alloc_pidmap(struct pid_namespace *pid_ns);
static void free_pidmap(struct upid *upid);

在這里我們不關注這兩個函數的實現,反而應該關注分配的 PID 如何在多個命名空間中可見,這樣需要在每個命名空間生成一個局部ID,函數 alloc_pid 為新建的進程分配PID,簡化版如下:

struct pid *alloc_pid(struct pid_namespace *ns)
{struct pid *pid;enum pid_type type;int i, nr;struct pid_namespace *tmp;struct upid *upid;tmp = ns;pid->level = ns->level;// 初始化 pid->numbers[] 結構體for (i = ns->level; i >= 0; i--) {nr = alloc_pidmap(tmp);            //分配一個局部IDpid->numbers[i].nr = nr;pid->numbers[i].ns = tmp;tmp = tmp->parent;}// 初始化 pid->task[] 結構體for (type = 0; type < PIDTYPE_MAX; ++type)INIT_HLIST_HEAD(&pid->tasks[type]);// 將每個命名空間經過哈希之后加入到散列表中upid = pid->numbers + ns->level;for ( ; upid >= pid->numbers; --upid) {hlist_add_head_rcu(&upid->pid_chain,&pid_hash[pid_hashfn(upid->nr, upid->ns)]);upid->ns->nr_hashed++;}return pid;
}

參考資料

  • 深入Linux 內核架構(以前不覺得這本書寫得多好,現在倒發現還不錯,本文很多都是照抄上面的)
  • 周徐達師弟的PPT(讓我受益匪淺的一次討論,周由淺入深告訴我們該數據結構是如何設計出來的,本文主思路就是按照該PPT,在此?特別感謝!)

轉載于:https://www.cnblogs.com/hazir/p/linux_kernel_pid.html

總結

以上是生活随笔為你收集整理的Linux 内核进程管理之进程ID的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。