The Linux Process Principle,NameSpace, PID、TID、PGID、PPID、SID、TID、TTY

目錄html

0. 引言
1. Linux進程
2. Linux命名空間
3. Linux進程的相關標識
4. 進程標識編程示例
5. 進程標誌在Linux內核中的存儲和表現形式
6. 後記

 

0. 引言node

在進行Linux主機的系統狀態安全監控的過程當中,咱們經常會涉及到對系統進程信息的收集、聚類、分析等技術,所以,研究Linux進程原理能幫助咱們更好的明確如下幾個問題linux

1. 針對Linux的進程狀態,須要監控捕獲哪些維度的信息,哪些信息可以更好地爲安全人員描繪出黑客的入侵跡象
2. 監控很容易形成的現象就是會有短期內的大量數據產生(雜誌數據過濾後),如何對收集的數據進行聚類,使之表現出一些更高維度的、相對有用的信息
3. 要實現數據的聚類,就須要從如今的元數據中找到一些"連結標識",這些"連結標識"就是咱們可以將低緯度的數據聚類擴展到高緯度的技術基礎。

本文的技術研究會圍繞這幾點進行Linux操做系統進程的基本原理研究web

 

1. Linux進程算法

0x1: 進程的表示shell

進程屬於操做系統的資源,所以進程相關的元數據都保存在內核態RING0中,Linux內核涉及進程和程序的全部算法都圍繞task_struct數據結構創建,該結構定義在include/sched.h中,這是操做系統中主要的一個結構,task_struct包含不少成員,將進程與各個內核子系統聯繫起來,關於task_struct結構體的相關知識,請參閱另外一篇文章編程

http://www.cnblogs.com/LittleHann/p/3865490.html

0x2: 進程的產生方式數組

Linux下新進程是使用fork和exec系統調用產生的安全

1. fork
生成當前進程的一個相同副本,該副本稱之爲"子進程"。原進程的全部資源都以適當的方式複製到子進程,所以執行了該系統調用以後,原來的進程就有了2個獨立的實例,包括
    1) 同一組打開文件
    2) 一樣的工做目錄
    3) 內存中一樣的數據(2個進程各有一個副本)
    ..
2. exec
從一個可執行的二進制文件來加載另外一個應用程序,來"代替"當前運行的進程,即加載了一個新的進程。由於exec並不建立新進程,搜易必須首先使用fork複製一箇舊的程序,而後調用exec在系統上建立另外一個應用程序
//整體來講:fork負責產生空間、exec負責載入實際的須要執行的程序

除此以外,Linux還提供了clone系統調用,clone的工做原理基本上和fork相同,所區別的是bash

1. 新進程不是獨立於父進程,而是能夠和父進程共享某些資源
2. 能夠指定須要共享和複製的資源種類,例如
    1) 父進程的內存數據
    2) 打開文件或安裝的信號處理程序

關於Linux下進程建立的相關知識,請參閱另外一篇文章

http://www.cnblogs.com/LittleHann/p/3853854.html

 

2. Linux命名空間

0x1: Linux namespace基本概念

命名空間提供了虛擬化的一種輕量級形式,使得咱們能夠從不一樣的方面來查看運行系統的全局屬性,該機制相似於Solaris中的zone、或FreeBSD中的jail
首先要明白的是,Linux命名空間是一個總的概念,它體現的是一個資源虛擬隔離的思想,在這個思想下,Linux的內核實現了不少的命名空間虛擬化機制,命名空間的一個整體目標是支持輕量級虛擬化工具container的實現,container機制自己對外提供一組進程,這組進程本身會認爲它們就是系統惟一存在的進程,目前Linux實現了六種類型的namespace,每個namespace是包裝了一些全局系統資源的抽象集合,這一抽象集合使得在進程的命名空間中能夠看到全局系統資源

1. mount命名空間(CLONE_NEWS)
用於隔離一組進程看到的文件系統掛載點集合,即處於不一樣mount命名空間的進程看到的文件系統層次極可能是不同的。mount()和umount()系統調用的影響再也不是全局的而隻影響其調用進程指向的命名空間
mount命名空間的一個應用相似chroot,然而和chroot()系統調用相比,mount命名空間在安全性和擴展性上更好。其它一些更復雜的應用如: 不一樣的mount命名空間能夠創建主從關係,這樣可讓一個命名空間的事件自動傳遞到另外一個命名空間
mount命名空間是Linux內核最先實現的命名空間 

2. UTS命名空間(CLONE_NEWUTS)
隔離了兩個系統變量
    1) 系統節點名: uname()系統調用返回UTS
    2) 域名: 域名使用setnodename()和setdomainname()系統調用設置
從容器的上下文看,UTS賦予了每一個容器各自的主機名和網絡信息服務名(NIS)(Network Information Service),這使得初始化和配置腳本可以根據不一樣的名字進行裁剪。UTS源於傳遞給uname()系統調用的參數:struct utsname。該結構體的名字源於"UNIX Time-sharing System" 

3. IPC namespaces(CLONE_NEWIPC)
隔離進程間通訊資源,具體來講就是System V IPC objects and (since Linux2.6.30) POSIX message queues。每個IPC命名空間擁有本身的System V IPC標識符和POSIX消息隊列文件系統

4. PID namespaces(CLONE_NEWPID)
隔離進程ID號命名空間,話句話說就是位於不一樣進程命名空間的進程能夠有相同的進程ID號,PID命名空間的最大的好處是在主機之間移植container時,能夠保留container內的ID號,PID命名空間容許每一個container擁有本身的init進程(PID=1),init進程是全部進程的祖先,負責系統啓動時的初始化和做爲孤兒進程的父進程 
從特殊的角度來看PID命名空間,就是一個進程有兩個ID,一個ID號屬於PID命名空間,一個ID號屬於PID命名空間以外的主機系統,此外,PID命名空間可以被嵌套。

5. Network namespaces(CLONE_NEWNET)
用於隔離和網絡有關的資源,這就使得每一個網絡命名空間有其本身的網絡設備、IP地址、IP路由表、/proc/net目錄、端口號等等
從網絡命名空間的角度看,每一個container擁有其本身的網絡設備(虛擬的)和用於綁定本身網絡端口號的應用程序。主機上合適的路由規則能夠將網絡數據包和特定container相關的網絡設備關聯。例如,能夠有多個web服務器,分別存在不一樣的container中,這就使得這些web服務器能夠在其命名空間中綁定80端口號

6. User namespaces(CLONE_NEWUSER) 
隔離用戶和組ID空間,換句話說,一個進程的用戶和組ID在用戶命名空間以外能夠不一樣於命名空間以內的ID,最有趣的是一個用戶ID在命名空間以外非特權,而在命名空間內卻能夠是具備特權的。這就意味着在命名空間內擁有所有的特權權限,在命名空間以外則不是這樣 

咱們重點學習一下"PID namespace"的相關概念

0x2: Linux PID namespace
傳統上,在Linux及其其餘衍生的Unix變體中,進程PID是全局管理的,系統中的全部進程都是經過PID標識的,這意味着內核必須管理一個全局的PID列表,全局ID使得內核能夠有選擇地容許或拒絕某些特權,即針對ID值進行權限劃分,可是在有些業務場景下,例如提供Web主機的供應商須要向用戶提供Linux計算機的所有訪問權限(包括root權限在內)。能夠想到的解決方案有

1. 爲每一個用戶都準備一臺計算機,可是代價過高
2. 使用KVM、VMWare提供的虛擬化環境,可是資源分配作的不是很好,計算機的各個用戶都須要提供一個獨立的內核、以及一份徹底安裝好的配套的用戶層應用

命名空間提供了一種較好的解決方案,而且所需資源較少,命名空間只使用一個內核在一臺物理計算機上運行,全部的全局資源都經過命名空間抽象起來,這使得能夠將一組進程放置到容器中,各個容器彼此隔離,隔離可使容器的成員與其餘容器毫無關係。但也能夠經過容許容器進行必定的共享,來下降容器之間的分隔。例如,容器能夠設置爲使用自身的PID集合,但仍然與其餘容器共享部分文件系統(這就是Docker的流行作法)

本質上,命名空間創建了系統的不一樣視圖。未使用命名空間以前的每一項全局資源都必須包裝到容器數據結構中,只有資源和包含資源的命名空間構成的二元組仍然是全局惟一的,即

1. 對於子命名空間來講: 本空間中的全局資源是本空間全局惟一的,可是在父空間及兄弟空間中不惟一
2. 對於父空間來講,本空間和子空間的全部全局資源都是惟一的

從圖中能夠看到:

1. 命名空間能夠組織爲層次,圖中一個命名空間爲父空間,衍生了兩個子命名空間
    1) 父命名空間: 宿主物理機
    2) 子命名空間: 虛擬機容器
2. 對於每一個虛擬機容器自身來講,它們看起來徹底和單獨的一臺Linux計算機同樣,有自身的init進程,PID爲0,其餘進程的PID以遞增次序分配
3. 對於父命名空間來講,全局的ID依然不變,而在子命名空間中,每一個子命名空間都有本身的一套ID命名序列
4. 雖然子容器(子命名空間)不瞭解系統中的其餘容器,但父容器知道子命名空間的存在,也能夠看到其中執行的全部進程
5. 在Linux的這種層次結構的命名空間的架構下,一個進程可能擁有多個ID值,至於哪個是"正確"的,則依賴於具體的上下文

命名空間可使用如下兩種方法建立

1. 在用fork或clone系統調用建立新進程時,傳入特定的選項
    1) 與父進程共享命名空間
    2) 創建新的命名空間

2. unshare系統調用將進程的某些部分從父進程分離,包括命名空間
/*
在進程使用上述兩種機制之一從父進程命名空間分離後,從該進程的角度來看,改變子進程命名空間的全局屬性不會傳播到父進程命名空間,而父進程的修改也不會傳播到子進程。可是,對於文件系統來講,這裏存在特例,在這種主機型虛擬機架構下文件系統經常伴隨着大量的共享
*/

命名空間的實現須要兩個部分

1. 每一個子系統的命名空間結構,用於將傳統的全部全局資源包裝到命名空間中
2. 將給定進程關聯到所屬各個命名空間的機制

從圖中能夠看到,每一個進程經過"struct nsproxy"的轉接,可使用位於不一樣的命名空間類別中,由於使用了指針,多個進程能夠共享一組子命名空間,所以,修改給定的命名空間,對全部屬於該命名空間的進程都是可見的
每一個進程都經過struct task_struct關聯到自身的命名空間視圖

struct task_struct
{
    ..
    struct nsproxy *nsproxy;
    ..
}

子系統此前的全局屬性如今都封裝到命名空間中,每一個進程關聯到一個選定的命名空間中,每一個能夠感知命名空間的內核子系統都必須提供一個數據結構,將全部經過命名空間形式提供的對象集中起來,struct nsproxy用於聚集指向特定於子系統的命名空間包裝器的指針
/source/include/linux/nsproxy.h

/*
A structure to contain pointers to all per-process namespaces
1. fs (mount)
2. uts
3. network
4. sysvipc
5. etc
'count' is the number of tasks holding a reference. The count for each namespace, then, will be the number of nsproxies pointing to it, not the number of tasks.
The nsproxy is shared by tasks which share all namespaces. As soon as a single namespace is cloned or unshared, the nsproxy is copied.
*/
struct nsproxy 
{
    atomic_t count;

    /*
    1. UTS(UNIX Timesharing System)命名空間包含了運行內核的名稱、版本、底層體系結構類型等信息
    */
    struct uts_namespace *uts_ns;

    /*
    2. 保存在struct ipc_namespace中的全部與進程間通訊(IPC)有關的信息
    */
    struct ipc_namespace *ipc_ns;

    /*
    3. 已經裝載的文件系統的視圖,在struct mnt_namespace中給出
    */
    struct mnt_namespace *mnt_ns;

    /*
    4. 有關進程ID的信息,由struct pid_namespace提供
    */
    struct pid_namespace *pid_ns;

    /*
    5. struct net包含全部網絡相關的命名空間參數
    */
    struct net          *net_ns;
};
extern struct nsproxy init_nsproxy;

因爲在建立新進程時可使用fork創建一個新的命名空間,所以必須提供控制該行爲的適當的標誌,每一個命名空間都有一個對應的標誌
/source/include/linux/sched.h

..
#define CLONE_NEWUTS        0x04000000    /* New utsname group? */
#define CLONE_NEWIPC        0x08000000    /* New ipcs */
#define CLONE_NEWUSER        0x10000000    /* New user namespace */
#define CLONE_NEWPID        0x20000000    /* New pid namespace */
#define CLONE_NEWNET        0x40000000    /* New network namespace */
..

須要注意的是,對命名空間的支持必須在編譯時啓用,並且必須逐一指定須要支持的命名空間。這樣,每一個進程都會關聯到一個默認命名空間,這樣能夠感知命名空間的代碼老是可使用。可是若是內核編譯時沒有指定對具體命名空間的支持,默認命名空間的做用則相似不啓用命名空間,全部的屬性都至關於全局的
init_nsproxy定義了初始的全局命名空間,其中維護了指向各子系統的命名空間對象的指針
\linux-2.6.32.63\kernel\nsproxy.c

struct nsproxy init_nsproxy = INIT_NSPROXY(init_nsproxy);

\linux-2.6.32.63\include\linux\init_task.h

#define INIT_NSPROXY(nsproxy) {                        \
    .pid_ns        = &init_pid_ns,                    \
    .count        = ATOMIC_INIT(1),                \
    .uts_ns        = &init_uts_ns,                    \
    .mnt_ns        = NULL,                        \
    INIT_NET_NS(net_ns)                                             \
    INIT_IPC_NS(ipc_ns)                        \
}

0x3: 在命名空間模式下的進程ID號

Linux進程老是會分配一個PID用於在其命名空間中惟一地標識它們,用fork或clone產生的每一個進程都由內核自動的分配了一個新的惟一的PID值。可是命名空間增長了PID管理的複雜性,PID命名空間按層次組織。在創建一個新的命名空間時,該命名空間中的全部PID對父命名空間都是可見的,但子命名空間沒法看到父命名空間的PID。這意味着某些進程具備多個PID,凡是能夠看到該進程的命名空間,都會爲其分配一個PID,這必須反應在數據結構中,即咱們必須區分局部ID、全局ID

1. 全局ID
是在內核自己和初始命名空間中的惟一ID號,在系統啓動期間開始的init進程即屬於初始命名空間。對每一個ID類型,都有一個給定的全局ID,保證在整個系統中是惟一的
    1) 全局PID、TGID直接保存在task_struct中
    struct task_struct
    {
        ..
        pid_t pid;
        pid_t tgid;
        ..
    }
    2) 會話和進程組ID不是直接保存在task_struct自己中,而是保存在用於信號處理的結構中
    全局SID: task_struct->group_leader->pids[PIDTYPE_SID].pid; -> pid_vnr(task_session(current));
    全局PGID:  task_struct->group_leader->pids[PIDTYPE_PGID].pid; -> pid_vnr(task_pgrp(current));
    輔助含數據set_task_session、set_task_pgrp可用於修改這些值

2. 局部ID
屬於某個特定的命名空間,不具有全局有效性。對每一個ID類型,它們在所屬的命名空間內部有效,但類型相同、值也相同的ID可能出如今不一樣的命名空間中

0x4: 管理PID

除了內核態支持的數據結構以外,內核還須要找一個辦法來管理全部命名空間內部的局部ID。這須要幾個相互鏈接的數據結構、以及許多輔助函數互相配合

1. 數據結構

一個小型的子系統稱之爲PID分配器(pid allocator)用於加速新ID的分配。此外,內核須要提供輔助函數,以實現經過ID及其類型查找進程的task_struct的功能、以及將ID的內核表示形式和用戶空間可見的數值進行轉換的功能,關於命名空間下PID的相關數據結構及其之間的關係,請參閱另外一篇文章

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:10. 命名空間(namespace)相關數據結構

2. 操做函數

內核提供了一些輔助函數,用於操做命名空間下和PID相關的數據結構,本質上內核必須完成下面2件事

1. 給出局部數字ID和對應的命名空間,查找此二元組描述的task_struct對應的PID實例(即在虛擬機沙箱中看到的局部PID)
    1) 爲肯定pid實例(這是PID的內核表示),內核必須採用標準的散列方案,首先根據PID和命名空間指針計算在pid_hash數組中的索引,而後遍歷散列表直至找到所須要的元素,這是經過輔助函數find_pid_ns處理的
    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)
            if (pnr->nr == nr && pnr->ns == ns)
                return container_of(pnr, struct pid,
                        numbers[ns->level]);

        return NULL;
    }

    2) pid_task取出pid->tasks[type]散列表中的第一個task_struct實例,這兩個步驟能夠經過輔助函數find_task_by_pid_type_ns完成
/*
一些簡單一點的輔助函數基於最通常性的find_task_by_pid_type_ns
struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns): 根據給出的數字PID和進程的命名空間拉力查找task_struct實例
struct task_struct *find_task_by_vpid(pid_t vnr): 經過局部數字PID查找進程
struct task_struct *find_task_by_pid(pid_t vnr): 經過全局數組PID查找進程
內核中許多地方都須要find_task_by_pid,由於不少特定於進程的操做(例如 kill發送一個信號)都經過PID標識目標進程
*/

2. 給出task_struct、ID類型、命名空間,取得命名空間局部的數字ID
    1) 得到與task_struct關聯的pid實例
    static inline struct pid *task_pid(struct task_struct *task)
    {
        return task->pids[PIDTYPE_PID].pid;
    }

    2) 得到pid實例以後,從struct pid的numbers數組中的uid信息,便可得到數字ID
    pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns)
    {
        struct upid *upid;
        pid_t nr = 0;

        /*
        由於父命名空間能夠看到子命名空間中的PID,反過來卻不行,內核必須確保當前命名空間的level小於或等於產生局部PID的命名空間的level,即當前必定是父命名空間在讀子命名空間
        */
        if (pid && ns->level <= pid->level) 
        {
            upid = &pid->numbers[ns->level];
            if (upid->ns == ns)
                nr = upid->nr;
        }
        return nr;
    }
/*
內核使用了幾個輔助函數,合併了前述的步驟
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_pgrp_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns);
*/

3. 生成惟一的PID

除了管理PID以外,內核還負責提供機制來生成惟一的PID(還沒有分配),爲了跟蹤已經分配和仍然可用的PID,內核使用一個大的位圖,其中每一個PID由一個bit標識,PID的值可經過對應bit位在位圖中的位置計算而來
所以,分配一個空閒的PID,本質上就等同於尋找位圖中第一個值爲0的bit,接下來將該bit設置爲1.反之,釋放一個PID可經過將對應的bit從1設置爲0便可

1. static int alloc_pidmap(struct pid_namespace *pid_ns)
2. static void free_pidmap(struct upid *upid)

在創建一個新的進程時,進程可能在多個命名空間中是可見的,對每一個這樣的命名空間,都須要生成一個局部PID,這是在alloc_pid中處理的。起始於創建進程的命名空間,一直到初始的全局命名空間,內核會爲此間的每一個命名空間都分別建立一個局部PID,包含在strcut pid中的全部upid都用從新生成的PID更新其數據,每一個upid實例都必須置於PID散列表中

Relevant Link:

深刻linux內核架構(中文版).pdf 第2章
http://guojing.me/linux-kernel-architecture/posts/process-type-and-namespace/
http://blog.csdn.net/linuxkerneltravel/article/details/5303863
http://bbs.chinaunix.net/thread-4165157-1-1.html
http://blog.csdn.net/dog250/article/details/9325017
http://blog.csdn.net/shichaog/article/details/41378145
http://linux.cn/article-5019-weibo.html

 

2. Linux進程的相關標識

如下是Linux和進程相關的標識ID值,咱們先學習它們的基本概念,在下一節咱們會學習到這些ID值間的關係、以及Linux是如何保存和組織它們的。須要明白的是,Linux採起了向下兼容、統一視角的設計思想

1. Linux全部的進程都被廣義地當作一個組的概念,差異在於
    1) 對於進程來講,進程自身就是惟一的組員,它本身表明本身,也表明這個組
    2) 對於線程來講,線程由進程建立,線程屬於領頭進程所屬的組中
2. Linux的標識採起逐級聚類的思想,不斷擴大組範圍

0x1: PID(Process ID 進程 ID號)
Linux系統中老是會分配一個號碼用於在其命名空間中惟一地標識它們,即進程ID號(PID),用fork或者clone產生的每一個進程都由內核自動地分配一個新的惟一的PID值: current->pid
值得注意的是,命名空間增長了PID管理的複雜性,PID命名空間按照層次組織,在創建一個新的命名空間時

1. 該命名空間中的全部PID對父命名空間都是可見的
2. 但子命名空間沒法看到父命名空間的PID

這也就是意味着在"多層次命名空間"的狀態下,進行具備多個PID,凡是可能看到該進程的的命名空間,都會爲其分配一個PID,這種特徵反映在了Linux的數據結構中,即局部ID、和全局ID

1. 全局ID
是在內核自己和"初始命名空間"中的惟一ID號,在系統啓動期間開始的init進程即屬於"初始命名空間"。對每一個ID類型,都有一個給定的全局ID,保證在整個系統中是惟一的

2. 局部ID
屬於某個特定的命名空間,不具有全局有效性。對每一個ID類型,它們"只能"在所屬的命名空間內部有效

//the system call getpid() is defined to return an integer that is the current process's PID.
asmlinkage long sys_getpid(void)
{
    return current->tgid;
} 

0x2: TGID(Thread Group ID 線程組 ID號)

處於某個線程組(在一個進程中,經過標誌CLONE_THREAD來調用clone創建的該進程的不一樣的執行上下文)中的全部進程都有統一的線程組ID(TGID)

1. 若是進程沒有使用線程,則它的PID和TGID相同
線程組中的"主線程"(Linux中線程也是進程)被稱做"組長(group leader)",經過clone建立的全部線程的task_struct的group_leader成員,都會指向組長的task_struct。
2. 在Linux系統中,一個線程組中的全部線程使用和該線程組的領頭線程(該組中的第一個輕量級進程)相同的PID(本質是tgid),並被存放在tgid成員中。只有線程組的領頭線程的pid成員纔會被設置爲與tgid相同的值 

梳理一下這段概念,咱們能夠這麼理解

1. 對於一個多線程的進程來講,它其實是一個進程組,每一個線程在調用getpid()時獲取到的是本身的tgid值,而線程組領頭的那個領頭線程的pid和tgid是相同的
2. 對於獨立進程,即沒有使用線程的進程來講,它只有惟一一個線程,領頭線程,因此它調用getpid()獲取到的值就是它的pid

0x3: TID

#define gettid() syscall(__NR_gettid)

/* Thread ID - the internal kernel "pid" */
asmlinkage long sys_gettid(void)
{
    return current->pid;
}

對於進程來講,取TID、PID、TGID都是同樣的,取到的值都是相同的,可是對於線程來講

1. tid: 線程ID
2. pid: 線程所屬進程的ID
3. tgid: 等同於pid,也就是線程組id,即線程組領頭進程的id

爲了更好地闡述這個概念,咱們能夠運行下列這個程序

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/syscall.h>

struct message
{
    int i;
    int j;
};

void *hello(struct message *str)
{
    printf("child, the tid(pthread_self())=%lu, tid(SYS_gettid)=%d\n",pthread_self(),syscall(SYS_gettid));
    //printf("the arg.i is %d, arg.j is %d\n",str->i,str->j);
    printf("child, getpid()=%d\n",getpid());
    while(1);
}

int main(int argc, char *argv[])
{
    struct message test;
    pthread_t thread_id;
    test.i=10;
    test.j=20;
    pthread_create(&thread_id,NULL,hello,&test);
    printf("parent, the tid(pthread_self())=%lu, tid(SYS_gettid)=%d\n",pthread_self(),syscall(SYS_gettid));
    printf("parent, getpid()=%d\n",getpid());
    pthread_join(thread_id,NULL);
    return 0;
}

Relevant Link:

http://www.cnblogs.com/lakeone/p/3789117.html
http://blog.csdn.net/pppjob/article/details/3864020

0x3: PGID(Process Group ID 進程組 ID號)

瞭解了進行ID、線程組(就是單線程下的進程)ID以後,咱們繼續學習"進程組ID",能夠看出,Linux就是在將作原子的因素不斷組合成更大的集合。
每一個進程都會屬於一個進程組(process group),每一個進程組中能夠包含多個進程。進程組會有一個進程組領導進程 (process group leader),領導進程的PID成爲進程組的ID (process group ID, PGID),以識別進程組。

圖中箭頭表示父進程經過fork和exec機制產生子進程。ps和cat都是bash的子進程。進程組的領導進程的PID成爲進程組ID。領導進程能夠先終結。此時進程組依然存在,並持有相同的PGID,直到進程組中最後一個進程終結

進程組簡化了向組內的全部成員發送信號的操做,進程組中的全部進程都會收到該信號,例如,用管道鏈接的進程包含在同一個進程組中(管道的原理就是在建立2個子進程)

或者輸入pgrp也能夠,pgrp和pgid是等價的

0x4: PPID(Parent process ID 父進程 ID號)

PPID是當前進程的父進程的PID

ps -o pid,pgid,ppid,comm | cat

由於ps、cat都是由bash啓動的,因此它們的ppid都等於bash進程的pid

0x5: SID(Session ID 會話ID)

更進一步,在shell支持工做控制(job control)的前提下,多個進程組還能夠構成一個會話 (session)。bash(Bourne-Again shell)支持工做控制,而sh(Bourne shell)並不支持

1. 每一個會話有1個或多個進程組組成,可能有一個領頭進程((session leader)),也可能沒有 
2. 會話領導進程的PID成爲識別會話的SID(session ID) 
3. 會話中的每一個進程組稱爲一個工做(job)
4. 會話能夠有一個進程組成爲會話的前臺工做(foreground),而其餘的進程組是後臺工做(background)
5. 每一個會話能夠鏈接一個控制終端(control terminal)。當控制終端有輸入輸出時,都傳遞給該會話的前臺進程組。由終端產生的信號,好比CTRL+Z, CTRL+\,會傳遞到前臺進程組。
6. 會話的意義在於將多個job(進程組)囊括在一個終端,並取其中的一個job(進程組)做爲前臺,來直接接收該終端的輸入輸出以及終端信號。 其餘工做在後臺運行

一個命令能夠經過在末尾加上&方式讓它在後臺運行:

$ping localhost > log &
[1] 10141
//括號中的1表示工做號,而10141爲PGID

信號能夠經過kill的方式來發送給工做組

1. $kill -SIGTERM -10141
//發送給PGID(經過在PGID前面加-來表示是一個PGID而不是PID)

2. $kill -SIGTERM %1
//發送給工做1(%1) 

一個工做能夠經過$fg從後臺工做變爲前臺工做:

$cat > log &
$fg %1
//當咱們運行第一個命令後,因爲工做在後臺,咱們沒法對命令進行輸入,直到咱們將工做帶入前臺,才能向cat命令輸入。在輸入完成後,按下CTRL+D來通知shell輸入結束

進程組(工做)的概念較爲簡單易懂。而會話主要是針對一個終端創建的。當咱們打開多個終端窗口時,實際上就建立了多個終端會話。每一個會話都會有本身的前臺工做和後臺工做。這樣,咱們就爲進程增長了管理和運行的層次

Relevant Link:

http://www.cnblogs.com/vamei/archive/2012/10/07/2713023.html
http://blog.csdn.net/zmxiangde_88/article/details/8027431

 

3. 進程標識編程示例

瞭解了進程標識的基本概念以後,接下來咱們經過API編程方式來直觀地瞭解下

0x1: 父子進程、組長進程和組員進程的關係

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() 
{
    pid_t pid;

    /*
    計算機程序設計中的分叉函數。返回值,若成功調用一次則返回兩個值1
    1. 子進程返回0
    2. 父進程返回子進程標記
    3. 不然,出錯返回-1
    */
    if ((pid = fork())<0) 
    {//error
        printf("fork error!");
    }
    else if (pid==0) 
    {//child process
        printf("The Child Process PID is %d.\n", getpid());
        printf("The Parent Process PPID is %d.\n", getppid());
        printf("The Group ID PGID is %d.\n", getpgrp());
        printf("The Group ID PGID is %d.\n", getpgid(0));
        printf("The Group ID PGID is %d.\n", getpgid(getpid()));
        printf("The Session ID SID is %d.\n", getsid());
        exit(0);
    }
    
    printf("\n\n\n");

    //parent process
    sleep(3);
    printf("The Parent Process PID is %d.\n", getpid());
    printf("The Group ID PGID is %d.\n", getpgrp());
    printf("The Group ID PGID is %d.\n", getpgid(0));
    printf("The Group ID PGID is %d.\n", getpgid(getpid()));
    printf("The Session ID SID is %d.\n", getsid());

    return 0;
}

從運行結果來看,咱們能夠得出幾個結論

1. 組長進程
    1) 組長進程標識: 其進程組ID == 其進程ID
      2) 組長進程能夠建立一個進程組,建立該進程組中的進程,而後終止
      3) 只要進程組中有一個進程存在,進程組就存在,與組長進程是否終止無關
      4) 進程組生存期: 進程組建立到最後一個進程離開(終止或轉移到另外一個進程組)
 
2. 進程組id == 父進程id,即父進程爲組長進程

0x2: 進程組更改

咱們繼續拿fork()產生父子進程的這個code example做爲示例,由於正常狀況下,父子進程具備相同的PGID,這個代碼場景能幫助咱們更好地去了解PGID的相關知識

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() 
{
    pid_t pid;

    if ((pid=fork())<0) 
    {
        printf("fork error!");
        exit(1);
    }
    else if (pid==0) 
    {//child process
        printf("The child process PID is %d.\n",getpid());
        printf("The Group ID of child is %d.\n",getpgid(0)); // 返回組id
        sleep(5);
        printf("The Group ID of child is changed to %d.\n",getpgid(0));
        exit(0);
    }
    
    sleep(1);
    /*
    1. parent process:
    will first execute the code blow
    對於父進程來講,它的PID就是進程組ID: PGID,因此setpgid對父進程來講沒有變化
    
    2. child process 
    will also execute the code blow after 5 seconds
    對於子進程來講,setpgid將改變子進程所屬的進程組ID
    */
    // 改變子進程的組id爲子進程自己
    setpgid(pid, pid); 
    
    sleep(5);
    printf("\n");
    printf("The process PID is %d.\n",getpid()); 
    printf("The Group ID of Process is %d.\n",getpgid(0)); 

    return 0;
}

從程序的運行結果能夠看出

1. 一個進程能夠爲本身或子進程設置進程組ID
2. setpgid()加入一個現有的進程組或建立一個新進程組

0x3: 會話ID Session ID

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() 
{
    pid_t pid;

    if ((pid=fork())<0) 
    {
        printf("fork error!");
        exit(1);
    }
    else if (pid==0) 
    {//child process
        printf("The child process PID is %d.\n",getpid());
        printf("The Group ID of child is %d.\n",getpgid(0));
        printf("The Session ID of child is %d.\n",getsid(0)); 
        setsid(); 
        /*
        子進程非組長進程
        1. 故其成爲新會話首進程(sessson leader) 
        2. 且成爲組長進程(group leader) 
        因此
        1. pgid = pid_child
        2. sid = pid_child
        */
        printf("Changed:\n");
        printf("The child process PID is %d.\n",getpid());
        printf("The Group ID of child is %d.\n",getpgid(0));
        printf("The Session ID of child is %d.\n",getsid(0)); 
        exit(0);
    }

    return 0;
}

從程序的運行結果能夠獲得以下結論

1. 會話: 一個或多個進程組的集合
    1) 開始於用戶登陸
    2) 終止與用戶退出
      3) 此期間全部進程都屬於這個會話期

2. 創建新會話: setsid()函數
    1) 該調用進程是組長進程,則出錯返回 
      2) 該調用進程不是組長進程,則建立一個新會話
        2.1) 先調用fork父進程終止,子進程調用
        2.2) 該進程變成新會話首進程(領頭進程)(session header)
        2.3) 該進程成爲一個新進程組的組長進程。
        2.4) 該進程沒有控制終端,若是以前有,則會被中斷
    3) 組長進程不能成爲新會話首進程,新會話首進程一定會成爲組長進程 

下面這張圖對整個PID、PGID、SID的關係作了一個梳理

Relevant Link:

http://www.cnblogs.com/nysanier/archive/2011/03/10/1979321.html
http://www.cnblogs.com/forstudy/archive/2012/04/03/2427683.html

 

4. 進程標誌在Linux內核中的存儲和表現形式

瞭解了Linux中進程標識的基本概念和API編程方式後,咱們接下來繼續研究一下Linux在內核中是如何去存儲、組織、表現這些標識的

在task_struct中,和進程標識ID相關的域有

struct task_struct
{
    ...
    pid_t pid;
    pid_t tgid;
    struct task_struct *group_leader;
    struct pid_link pids[PIDTYPE_MAX];
    struct nsproxy *nsproxy;
    ...
};

若是顯示不完整,請另存到本地看

0x1: Linux內核Hash表

要談Linux內核中進程標識的存儲和組織,咱們首先要了解Linux內核的Hash表機制,在內核中,查找是必不可少的,例如

1. 內核管理這麼多用戶進程,如今要快速定位某一個進程,這兒須要查找
2. 一個進程的地址空間中有多個虛存區,內核要快速定位進程地址空間的某個虛存區,這兒也須要查找
..

查找技術屬於數據結構算法的範疇,經常使用的查找算法有以下幾種

1. 基於樹的查找: 紅黑樹
2. 基於計算的查找: 哈希查找
//二者(基於樹、基於計算)的查找的效率高,並且適應內核的狀況

3. 基於線性表的查找: 二分查找
//儘管效率高但不能適應內核裏面的狀況,如今版本的內核幾乎不可能使用數組管理一些數據 

本文主要學習的進程標識的存儲和查找就是基於計算的HASH表查找方式

0x2: Linux pid_hash散列表

在內核中,常常須要經過進程PID來獲取進程描述符,例如
kill命令: 最簡單的方法能夠經過遍歷task_struct鏈表並對比pid的值來獲取,但這樣效率過低,尤爲當系統中運行不少個進程的時候
linux內核經過PIDS散列表來解決這一問題,能快速的經過進程PID獲取到進程描述符

PID散列表包含4個表,由於進程描述符包含了表示不一樣類型PID的字段,每種類型的PID須要本身的散列表

//Hash表的類型   字段名     說明
1. PIDTYPE_PID   pid      進程的PID
2. PIDTYPE_TGID  tgid     線程組領頭進程的PID
3. PIDTYPE_PGID  pgrp     進程組領頭進程的PID
4. PIDTYPE_SID   session  會話領頭進程的PID

0x3: 進程標識在內核中的存儲

一個PID只對應着一個進程,可是一個PGID,TGID和SID可能對應着多個進程,因此在pid結構體中,把擁有一樣PID(廣義的PID)的進程放進名爲tasks的成員表示的數組中,固然,不一樣類型的ID放在相應的數組元素中。
考慮下面四個進程:

1. 進程A: PID=12345, PGID=12344, SID=12300
2. 進程B: PID=12344, PGID=12344, SID=12300,它是進程組12344的組長進程
3. 進程C: PID=12346, PGID=12344, SID=12300

4. 進程D: PID=12347, PGID=12347, SID=12300

分別用task_a, task_b, task_c和task_d表示它們的task_struct,則它們之間的聯繫是:

1. task_a.pids[PIDTYPE_PGID].pid.tasks[PIDTYPE_PGID]指向有進程A-B-C構成的列表
2. task_a.pids[PIDTYPE_SID].pid.tasks[PIDTYPE_SID]指向有進程A-B-C-D構成的列表

內核初始化期間動態地爲4個散列表分配空間,並把它們的地址存入pid_hash數組(就是struct->pids[PIDTYPE_MAX]中)

0x4: 進程標識ID在內核中的表示和使用

內核用pid_hashfn宏把PID轉換爲表索引

kernel/pid.c

#define pid_hashfn(nr, ns)    \
    hash_long((unsigned long)nr + (unsigned long)ns, pidhash_shift)

這個宏就負責把一個PID轉換爲一個index,咱們繼續跟進hash_long這個函數

\include\linux\hash.h

/* 2^31 + 2^29 - 2^25 + 2^22 - 2^19 - 2^16 + 1 */
#define GOLDEN_RATIO_PRIME_32 0x9e370001UL
/*  2^63 + 2^61 - 2^57 + 2^54 - 2^51 - 2^18 + 1 */
#define GOLDEN_RATIO_PRIME_64 0x9e37fffffffc0001UL

#if BITS_PER_LONG == 32
#define GOLDEN_RATIO_PRIME GOLDEN_RATIO_PRIME_32
#define hash_long(val, bits) hash_32(val, bits)
#elif BITS_PER_LONG == 64
#define hash_long(val, bits) hash_64(val, bits)
#define GOLDEN_RATIO_PRIME GOLDEN_RATIO_PRIME_64
#else
#error Wordsize not 32 or 64
#endif

static inline u64 hash_64(u64 val, unsigned int bits)
{
    u64 hash = val;

    /*  Sigh, gcc can't optimise this alone like it does for 32 bits. */
    u64 n = hash;
    n <<= 18;
    hash -= n;
    n <<= 33;
    hash -= n;
    n <<= 3;
    hash += n;
    n <<= 3;
    hash -= n;
    n <<= 4;
    hash += n;
    n <<= 2;
    hash += n;

    /* High bits are more random, so use them. */
    return hash >> (64 - bits);
}

static inline u32 hash_32(u32 val, unsigned int bits)
{
    /* On some cpus multiply is faster, on others gcc will do shifts */
    u32 hash = val * GOLDEN_RATIO_PRIME_32;

    /* High bits are more random, so use them. */
    return hash >> (32 - bits);
}

static inline unsigned long hash_ptr(const void *ptr, unsigned int bits)
{
    return hash_long((unsigned long)ptr, bits);
}
#endif /* _LINUX_HASH_H */

對這個算法的簡單理解以下

1. 讓key乘以一個大數,因而結果溢出
2. 把留在32/64位變量中的值做爲hash值3
3. 因爲散列表的索引長度有限,取這hash值的高几位做爲索引值,之因此取高几位,是由於高位的數更具備隨機性,可以減小所謂"衝突(collision)",即減小hash碰撞
4. 將結果乘以一個大數
    1) 32位系統中這個數是0x9e370001UL
    2) 64位系統中這個數是0x9e37fffffffc0001UL 
/*
取這個數字的數學意義
Knuth建議,要獲得滿意的結果
1. 對於32位機器,2^32作黃金分割,這個大樹是最接近黃金分割點的素數,0x9e370001UL就是接近 2^32*(sqrt(5)-1)/2 的一個素數,且這個數能夠很方便地經過加運算和位移運算獲得,由於它等於2^31 + 2^29 - 2^25 + 2^22 - 2^19 - 2^16 + 1
2. 對於64位系統,這個數是0x9e37fffffffc0001UL,一樣有2^63 + 2^61 - 2^57 + 2^54 - 2^51 - 2^18 + 1
*/
5. 從程序中能夠看到
    1) 對於32位系統計算hash值是直接用的乘法,由於gcc在編譯時會自動優化算法
    2) 對於64位系統,gcc彷佛沒有相似的優化,因此用的是位移運算和加運算來計算。首先n=hash, 而後n左移18位,hash-=n,這樣hash = hash * (1 - 2^18),下一項是-2^51,而n以前已經左移過18位了,因此只須要再左移33位,因而有n <<= 33,依次類推
6. 最終算出了hash值

如今咱們已經能夠經過pid_hashfn把PID轉換爲一個index了,接下來咱們再來想想其中的問題

1. 首先,對於內核中所用的hash算法,不一樣的PID/TGID/PGRP/SESSION的ID(沒作特殊聲明前通常用PID做爲表明),有可能會對應到相同的hash表索引,也就是衝突(colliding)
2. 因而一個index指向的不是單個進程,而是一個進程的列表,這些進程的PID的hash值都同樣
3. task_struct中pids表示的四個列表,就是具備一樣hash值的進程組成的列表。好比進程A的task_struct中的
    1) pids[PIDTYPE_PID]指向了全部PID的hash值都和A的PID的hash值相等的進程的列表
    2) pids[PIDTYPE_PGID]指向全部PGID的hash值與A的PGID的hash值相等的進程的列表

須要注意的是,與A同組的進程,他們具備一樣的PGID,更具上面所解釋的,這些進程構成的鏈表是存放在A的pids[PIDTYPE_PGID].pid.tasks指向的列表中

下面的圖片說明了hash和進程鏈表的關係,圖中TGID=4351和TGID=246具備一樣的hash值。(圖中的字段名稱比較老,但大意是同樣的,只要把pid_chain看作是pid_link結構中的node,把pid_list看作是pid結構中的tasks便可)

Relevant Link:

http://blog.chinaunix.net/uid-24683784-id-3297992.html
http://blog.chinaunix.net/uid-28728968-id-4189105.html
http://blog.csdn.net/fengtaocat/article/details/7025488
http://blog.csdn.net/gaopenghigh/article/details/8831312
http://blog.csdn.net/bysun2013/article/details/14053937
http://blog.csdn.net/zhanglei4214/article/details/6765913
http://blog.csdn.net/gaopenghigh/article/details/8831692
http://blog.csdn.net/yanglovefeng/article/details/8036154
http://www.oschina.net/question/565065_115167

 

5. 後記

在Linux的進程的標識符中有不少"組"的概念,Linux從最原始的PID開始,進行了逐層的封裝,不斷嵌套成更大的組,這也意味着,Linux中的進程序列之間並非徹底獨立的關係,而是包含着不少的組合關係的,咱們能夠充分利用Linux操做系統自己提供的特性,來對指令序列進行聚合,從而從低維的序列信息中發現更高偉的行爲模式

 

Copyright (c) 2014 LittleHann All rights reserved

相關文章
相關標籤/搜索