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

目录

0. 引言
1. Linux进程
2. Linux进程的相关标识
3. 进程标识编程示例
4. 进程标志在Linux内核中的存储和表现形式
5. 后记

0. 引言

在进行Linux主机的系统状态安全监控的过程中,我们常常会涉及到对系统进程信息的收集、聚类、分析等技术,因此,研究Linux进程原理能帮助我们更好的明确以下几个问题

1. 针对Linux的进程状态,需要监控捕获哪些维度的信息,哪些信息能够更好地为安全人员描绘出黑客的入侵迹象
2. 监控很容易造成的现象就是会有短时间内的大量数据产生(杂志数据过滤后),如何对收集的数据进行聚类,使之表现出一些更高维度的、相对有用的信息
3. 要实现数据的聚类,就需要从现在的元数据中找到一些"连结标识",这些"连结标识"就是我们能够将低纬度的数据聚类扩展到高纬度的技术基础。

本文的技术研究会围绕这几点进行Linux操作系统进程的基本原理研究

1. Linux进程

0x1: 进程的表示

进程属于操作系统的资源,因此进程相关的元数据都保存在内核态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相同,所区别的是

1. 新进程不是独立于父进程,而是可以和父进程共享某些资源
2. 可以指定需要共享和复制的资源种类,例如
    1) 父进程的内存数据
    2) 打开文件或安装的信号处理程序

关于Linux下进程创建的相关知识,请参阅另一篇文章

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

0x3: 命名空间

命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全局属性,本质上,命名空间建立了系统的不同视图。未使用命名空间之前的每一项全局资源都必须包装到容器数据结构中,而资源和包含资源的命名空间构成的二元组仍然是全局唯一的。

从图中可以看到:

1. 对于父命名空间来说,全局的ID依然不变,而在子命名空间中,每个子命名空间都有自己的一套ID命名序列
2. 虽然子容器(子命名空间)不了解系统中的其他容器,但父容器知道子命名空间的存在,也可以看到其中执行的所有进程
3. 在Linux的这种层次结构的命名空间的架构下,一个进程可能拥有多个ID值,至于哪一个是"正确"的,则依赖于具体的上下文

命名空间机制是Linux的一个重要的技术,对命名空间的支持已经有很长的时间了,主要是chroot系统调用,该方法可以将进程限制到文件系统的某一部分,因此是一种简单的命名空间机制。但真正的命名空间能够控制的功能远远超过"文件系统视图"

Relevant Link:

深入linux内核架构(中文版).pdf 第2章

2. Linux进程的相关标识

以下是Linux和进程相关的标识ID值,我们先学习它们的基本概念,在下一节我们会学习到这些ID值间的关系、以及Linux是如何保存和组织它们的

0x1: PID(Process ID 进程 ID号)

Linux系统中总是会分配一个号码用于在其命名空间中唯一地标识它们,即进程ID号(PID),用fork或者cline产生的每个进程都由内核自动地分配一个新的唯一的PID值

值得注意的是,命名空间增加了PID管理的复杂性,PID命名空间按照层次组织,在建立一个新的命名空间时

1. 该命名空间中的所有PID对父命名空间都是可见的
2. 但子命名空间无法看到父命名空间的PID

这也就是意味着在"多层次命名空间"的状态下,进行具有多个PID,凡是可能看到该进程的的命名空间,都会为其分配一个PID,这种特征反映在了Linux的数据结构中,即局部ID、和全局ID

1. 全局ID
是在内核本身和"初始命名空间"中的唯一ID号,在系统启动期间开始的init进程即属于"初始命名空间"。对每个ID类型,都有一个给定的全局ID,保证在整个系统中是唯一的

2. 局部ID
属于某个特定的命名空间,不具备全局有效性。对每个ID类型,它们"只能"在所属的命名空间内部有效

0x2: TGID(Thread Group ID 线程组 ID号)

处于某个线程组(在一个进程中,通过标志CLONE_THREAD来调用clone建立的该进程的不同的执行上下文)中的所有进程都有统一的线程组ID(TGID)

如果进程没有使用线程,则它的PID和TGID相同

线程组中的"主线程"(Linux中线程也是进程)被称作"组长(group leader)",通过clone创建的所有线程的task_struct的group_leader成员,都会指向组长的task_struct。

在Linux系统中,一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的PID,并被存放在tgid成员中。只有线程组的领头线程的pid成员才会被设置为与tgid相同的值。注意,getpid()系统调用

返回的是当前进程的tgid值而不是pid值。

梳理一下这段概念,我们可以这么理解

1. 对于一个多线程的进程来说,它实际上是一个进程组,每个线程在调用getpid()时获取到的是自己的tgid值,而线程组领头的那个领头线程的pid和tgid是相同的
2. 对于独立进程,即没有使用线程的进程来说,它只有唯一一个线程,领头线程,所以它调用getpid()获取到的值就是它的pid

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操作系统本身提供的特性,来对指令序列进行聚合,从而从低维的序列信息中发现更高伟的行为模式

  • 8
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值