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

相关文章
相关标签/搜索