在类Unix系统中,用户通常会跟各种相关的进程打交道。虽然在登录的时候只有一个终端进程(用户对应的登录shell ,通过这个shell启动各种程序和服务),但通常不久以后就会产生许多相关的进程,例如进行如下动作:

  • 在后台运行无交互的程序(例如bash命令中末位的"&")
  • 通过shell的 job control在各种交互进程之间切换
  • 通过管道启动一组程序
  • 在图形环境下(例如X window system)启用多个终端窗口

为了管理这些进程,内核便对这些进程进行了分组,称其为进程组,几个进程组又构成一个会话。例如下图所示,lsless在一个进程组里,而grepwc在一个进程组里。这两个进程组又同属于一个会话。

下面具体讲一讲进程组和会话。

1.1 进程组

每一个进程都属于一个“进程组”,当一个进程被创建的时候,它默认是其父进程所在组的成员。传统上,一个进程的组ID(pgid)等于这个组的第一个成员(也称为进程组领导)。

可以使用ps j这个命令获取进程的PPID (父进程ID), PID (本进程 ID), PGID (进程组 ID) and SID (会话 ID)。

frank@under:~$ ps j
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
24120 24125 24125 24125 pts/5 24125 Ss+ 1000 0:00 bash
24120 30633 30633 30633 pts/6 31468 Ss 1000 0:00 bash
30633 31468 31468 30633 pts/6 31468 R+ 1000 0:00 ps j

当使用不具有工作管理(job control)的shell时(例如ash ),每一个shell创建的子进程都会和shell在同一个进程组和会话里。当使用具有工作管理的shell时(例如bash ),如果使用管道(参考:Pipes: A Brief Introduction), 那么这些管道连接起来的进程单独组成一个进程组,和shell在一个会话中。例如:

% cat paper | ideal | pic | tbl | eqn | ditroff > out

这几个程序运行后的进程都在一个进程组里面。

前台进程组

每一个会话最多有一个进程组是“前台进程组”,控制终端(下面的第二部分会讲)会将输入和信号 传给该进程组的成员(例如你在终端按下Ctrl+C就会向前台进程组发送SIGINT信号)。进程可以通过系统调用 tcgetpgrp(fd)获取所在会话的前台进程组ID,其中fd对应会话的控制终端的文件描述符;也可以通过tcsetpgrp(fd,pgrp)设置所在会话的前台进程组,其中fd对应会话的控制终端的文件描述符, pgrp是这个会话中的一个进程组。

那么如何得到fd呢? ctermid() 调用会返回控制终端的名字,在符合 POSIX标准的系统上,它会返回 /dev/tty 这个文件,然后我们就可以用系统调用open()打开这个文件从而得到文件描述符了。

后台进程组

在一个会话中,除前台进程组外的进程组都称为“后台进程组”,这些后台进程组的进程不参与终端的输入输出,如果它们尝试从终端读取数据,会收到SIGTTIN信号(其默认操作是Stop),同时终端会通知用户:

SIGTTIN   21,21,26    Stop    Terminal input for background process

但是如果后台进程忽略或者blockSIGTTIN信号了,或者它所在的进程组是一个“孤儿进程组”(下面有讲),那么读取终端就会得到一个EIO(error in operation)错误而非收到SIGTTIN信号。当后台进程尝试向终端写操作时,它可能会受到SIGTTOU信号:

SIGTTOU   22,22,27    Stop    Terminal output for background process

同样地,如果后台进程忽略或者blockSIGTTOU信号了,或者它所在的进程组是一个“孤儿进程组”,那么读取终端就会得到一个EIO(error in operation)错误而非收到SIGTTOU信号。

为了让后台进程加入前台进程组,可以使用fg命令。

相应操作

可以通过系统调用 setpgid()将进程加入到另一个进程组中:

int setpgid(pid_t pid, pid_t pgid);

其中pid是要操作的进程,0代表本进程;pgid是要加入的进程组,0代表要加入的进程组的pgid是这个进程的pid(也就是说,这个组的组领导就是这个进程)。

使用 setpgid() 要注意以下几点:

  1. 一个进程可能将pgid设置为自己或者它所在组的其他成员,这样的操作可能不会改变其他任意进程的进程组,即使这个进程有root权限。
  2. 会话头进程(第二部分会介绍)不能改变自己所在的进程组。
  3. 一个进程不能被加入到另一个会话中的进程组中,换句话说,setpgid只能在一个会话中使用。由于setpgid()只能将进程在本会话中“移动”,所以两个会话不可能有相同的进程组或者进程。

一个进程可以通过 getpgrp()系统调用获得自己所在组的ID,也可以通过 getpgid(p) 获得pid为p的进程所在组的组ID,当p为0时,获得本进程的组ID:

pid_t getpgid(pid_t pid)
pid_t getpgrp(void)

下面是一个简单的示例图:

#### 断开连接(我对于Linux下的终端、shell通信机制不了解,这个地方暂时贴上参考资料里的原文,以后懂了再翻译)

If the terminal goes away by modem hangup, and the line was not local, then a SIGHUP is sent to the session leader. Any further reads from the gone terminal return EOF. (Or possibly -1 with errno set to EIO.)

If the terminal is the slave side of a pseudotty, and the master side is closed (for the last time), then a SIGHUP is sent to the foreground process group of the slave side.

When the session leader dies, a SIGHUP is sent to all processes in the foreground process group. Moreover, the terminal stops being the controlling terminal of this session (so that it can become the controlling terminal of another session).

Thus, if the terminal goes away and the session leader is a job control shell, then it can handle things for its descendants, e.g. by sending them again a SIGHUP. If on the other hand the session leader is an innocent process that does not catch SIGHUP, it will die, and all foreground processes get a SIGHUP.

1.2 孤儿进程组

现在我们来讨论当会话消失的时候进程是如何终止的。

假设有一个在终端下运行的会话,其会话头是一个shell。当这个shell存在时,会话中的进程组处于不同的环境中,可能在运行,也可能被挂起了。当终端关闭时,如果它正在运行,当终端关闭后它就无法读入或者输出了;如果它被挂起了,则它可能永远不会被唤醒(也不会终止)。在这种情况下,原会话的进程组就被称为“孤儿进程组”。POSIX定义为该进程组的父进程也是该进程组的成员或者是别的会话的成员。总之,只要一个进程组的父进程在同一会话的不同组中,它就不是孤儿进程组。

当一个进程组成为孤儿进程组之后,在这个进程组中的每一个进程都会被发送一个SIGHUP信号——通常进程将会被正常关闭。对于收到SIGHUP信号后,选择不终止的程序将会被发送一个SIGCONT,这个信号将会重启任何被挂起的进程。这个信号流程能够关闭大多数的进程并保证剩下的是正在运行的进程(不被挂起)。

当进程被“遗弃”后,它被强制和控制终端分离(其他的用户便可以使用这个终端)。原来的会话ID继续被保留(不作为新生进程的PID)直到每一个会话中的程序退出。

2.1 会话

当一个用户注销的时候,内核会终止用户之前启动的所有进程(不然这些进程会在那一直运行并等待输入的到来)。为了简化这个任务,内核将几个进程组并为一个“会话”。会话的ID就是通过setsid()启动这个会话的进程的PID (也就是这个会话的第一个进程,通常是用户的shell),这个进程也称为“会话头”,它随后产生的所有子孙进程都默认在这个会话里。

进程也可以使用setsid使自己离开自己的会话,其参数为空,返回新的会话的ID:

#include <unistd.h>

pid_t setsid(void);

2.2 控制终端

每一个会话有且仅有一个对应的终端,会话中的进程从这个终端得到输入并输出,该终端被称为“控制终端”(controlling terminal)。这个终端可能是机器本地的控制台、桌面环境的伪终端、网络上的伪终端等等。

虽然一个会话对应的控制终端是可以改变的,但这通常都是由初始化用户登录环境的那个进程设定的。


主要参考:

  1. The Process Model of Linux Application Development
  2. Processes (里面有关于进程和线程的简略介绍)
  3. 《深入理解计算机系统》第三版

进程组与会话 Linux Process Groups and Sessions的更多相关文章

  1. Linux进程组和会话

    Linux的进程相互之间有一定的关系.比如说,在Linux进程基础中,我们看到,每个进程都有父进程,而所有的进程以init进程为根,形成一个树状结构.我们在这里讲解进程组和会话,以便以更加丰富的方式了 ...

  2. linux内核之进程的基本概念(进程,进程组,会话关系)

    进程是操作系统的一个核心概念.每个进程都有自己唯一的标识:进程ID,也有自己的生命周期.一个典型的进程的生命周期如图4-1所示. 进程都有父进程,父进程也有父进程,这就形成了一个以init进程为根的家 ...

  3. Linux 的进程组、会话、守护进程

    一.进程组ID 每个进程都属于一个进程组.每个进程组有一个领头进程.进程组是一个或多个进程的集合,通常它们与一组作业相关联,可以接受来自同一终端的各种信号.每个进程组都有唯一的进程组ID(整数,也可以 ...

  4. 进程 、进程组、会话、控制终端之间的关系 (转载 http://blog.csdn.net/yh1548503342/article/details/41891047)

    一个进程组可以包含多个进程 进程组中的这些进程之间不是孤立的,他们彼此之间或者存在者父子.兄弟关系,或者在功能有相近的联系. 那linux为什么要有进程组呢?其实提供进程组就是方便管理这些进程.假设要 ...

  5. 重读APUE(8)-进程、进程组、会话

    进程: 是系统中一段程序执行的实体,也是资源分配和调度的基本单位: 进程组: 为了方便管理多个进程,可以将多个进程加入到一个进程组内: 每个进程都属于一个进程组,但是同一个进程组内可以有多个进程: 每 ...

  6. APUE 2 - 进程组(process group) 会话(session) job

    进程组(process group) 进程组顾名思义是指一个或多个进程的集合.他们通常与同一个job(可以从同一个终端接收信号)相关联.每个进程组拥有一个唯一的Process Group Id.可以使 ...

  7. linux 会话 进程组 守护进程

    Linux 下每个进程都会有一个非负整数表示的唯一进程 ID ,简称 pid . Linux 提供了 getpid 函数来获取 进程的 pid ,同时还提供了 getppid 函数来获取父进程的 pi ...

  8. 进程的基本属性:进程ID、父进程ID、进程组ID、会话和控制终端

    摘要:本文主要介绍进程的基本属性,基本属性包含:进程ID.父进程ID.进程组ID.会话和控制终端. 进程基本属性 1.进程ID(PID) 函数定义:      #include <sys/typ ...

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

    目录 . 引言 . Linux进程 . Linux命名空间 . Linux进程的相关标识 . 进程标识编程示例 . 进程标志在Linux内核中的存储和表现形式 . 后记 0. 引言 在进行Linux主 ...

随机推荐

  1. VMware三种网络模式

    VMware网络配置详解一:三种网络模式简介安装好虚拟机以后,在网络连接里面可以看到多了两块网卡: 其中VMnet1是虚拟机Host-only模式的网络接口,VMnet8是NAT模式的网络接口,这些后 ...

  2. js中的浅复制和深复制

    浅复制:浅复制是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响 深复制:深复制不是简单的复制引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深复 ...

  3. jQuery 插件格式 规范

    方式一(自定义对象): (function($, window, document) {  var Plugin, defaults, pluginName; 调用时的函数名:     pluginN ...

  4. 【Kafka源码】SocketServer启动过程

    SocketServer主要用于接收外部的网络请求,并把请求添加到请求队列中. 一.入口 在KafkaServer.scala中的start方法中,有这样的入口: socketServer = new ...

  5. oracle 归档模式开启后数据库宕机解决过程

    首先按照网友说的shutdown immediately,结果hang了半个小时也么反应. 然后检查日志,全盘搜索.trc,发现 (D:\app\oracle\diag\rdbms\cms1u\cms ...

  6. Java多线程由易到难

    线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现Runnable接口并编写run方法,使得该任务可以执行你的命令. public class ...

  7. CentOS卸载原有旧版OPENJDK并安装SUNJDK解决方案

    PS:由于图片是安装后进行截图所以出现的是1.8版本,步骤是完全正确的. 一.检测JDK 首先你要打开终端,为避免可能出现的权限问题建议直接su进入root.(PS:密码可以输入但不显示) 然后输入j ...

  8. C#中简单的this与get的用法(string,decimal)

    代码 namespace First{publicpartialclass Form1 : Form{public Form1(){InitializeComponent();} privatevoi ...

  9. java多线程创建-Thread,Runnable,callable和threadpool

    java创建多线程的方式有许多种,这里简要做个梳理 1. 继承Thread类 继承java.lang.Thread类,创建本地多线程的类,重载run()方法,调用Thread的方法启动线程.示例代码如 ...

  10. Java面试之框架篇(九)

    spring现在无疑是Java中最火的框架,使用范围广,几乎每个公司面试都会涉及spring和数据库,你可以对Struts不熟悉,但一定不能表现出对spring不了解.第九篇赢在面试全篇介绍sprin ...