终端
概念
在UNIX系统中,用用户通过终端登录系统后得到一一个Shell进程,这个终端成为Shell进程的控制终端 (Controlling Terminal),控制终端是保存在PCB中的信息,而我们知道fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。
默认情况 下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示示器上。此外 在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表 示示SIGINT。
每个进程都可以通过一个特殊的设备文件/dev/tty访问它的控制终 端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty也可以通过该终端设备所对应的设备文文件来访 问。
ttyname函数可以由文件描述符查出对应的文件名,该文件描述符必须指向一个终端设备而不能是任意文件。
下面我们来验证:
下面是验证代码:
得到的结果是:
由结果我们可以看出
文件描述符:0(标准输入),1(标准输出),2(标准错误)的文件都在同一个终端下。
如果我们重新开一个终端,再次运行上面的代码,得到结果为:
进程组
概念
每一个进程除了有一个进程ID外,还属于一个进程组。
进程组是一个或多个进程的集合,通常情况下,他们是在同一作业中结合起来的,同一进程组的个进程接受来自同一终端的各种信号。
每一个进程组有一个唯一的进程ID。
进程组ID是一个正整数,可以使用getpgrp函数返回调用进程的进程组ID
#include
pid_t getpgrp(void);
组长进程
每个进程组都有一个组长进程,组长进程的进程组ID等于其进程ID。
进程组组长可以创建一个进程组,创建进程组中的进程然后种植。只要进程组中还有任意一个进程存在,那么这个进程组就存在。
从进程组的创建到最后一个进程离开的时间去成为进程组的生命周期。
进程组验证
首先我们编写验证进程组的代码:
在上面的代码中,我们首先创建两个进程,一个father父进程,一个child子进程,我们让father和child首先输出自己的pid,然后循环输出自己进程名称。
结果如下:
由结果我们看出,父进程pid为3306,子进程pid为3307,然后父子进程循环打印father,child。
重新打开一个终端,输入命令
ps -axj | grep "proc_rela"
首先我介绍一下各个标识的意义。
PID(Process ID 进程 ID号)
Linux系统中总是会分配一个号码用于在其命名空间中唯一地标识它们,即进程ID号(PID),用fork或者cline产生的每个进程都由内核自动地分配一个新的唯一的PID值
TPGID(显示前台进程组)
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值。
我们可以这么理解
对于一个多线程的进程来说,它实际上是一个进程组,每个线程在调用getpid()时获取到的是自己的tgid值,而线程组领头的那个领头线程的pid和tgid是相同的
对于独立进程,即没有使用线程的进程来说,它只有唯一一个线程,领头线程,所以它调用getpid()获取到的值就是它的pid
PGID(Process Group ID 进程组 ID号)
每个进程都会属于一个进程组(process group),每个进程组中可以包含多个进程。进程组会有一个进程组领导进程 (process group leader),领导进程的PID成为进程组的ID (process group ID, PGID),以识别进程组.
PPID( Parent process ID 父进程 ID号)
PPID是当前进程的父进程的PID
SID(Session ID 会话ID)
STAT(状态栏)
D 不可中断 Uninterruptible sleep (usually IO)
R 正在运行,或在队列中的进程
S 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
< 高优先级
N 低优先级
L 有些页被锁进内存
s 包含子进程
+ 位于后台的进程组;
l 多线程,克隆线程 multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
得到结果:
下面我们分析一下结果:
由程序输出结果我们已经得到,父进程的进程ID为3235,子进程的进程ID为3236。
于是我们看到第一个进程PID = 3235,为父进程,他的PGID为3235,就是他本身,即:
父进程就是进程组组长进程。
关于STAT位的意义在上面STAT有提到,所有标志相结合即可。
总结:
程序创建了一个进程,该进程又创建了子进程,于是父进程和子进程构成了一个进程组,进程组的组长为父进程。
那么如果父进程结束,子进程不结束,进程组存在不存在呢?
下面我们修改代码:
这次,我们让父进程创建子进程后就退出,当然父进程依然是组长进程,当父进程退出后,子进程依然在进行。
可以看到,子进程还在运行,并且组长进程ID就是父进程ID。
但是,即使子进程继续运行,我们还是可以在shell输入,并且ctrl+C键不能结束子进程,原因是什么呢?
这就是我下面要介绍的:
作业
Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。一个前台作业可以由多个进程组成,一个后台也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。
作业与进程组的区别:
如果作业中的某个进程又创建了子子进程,则子子进程不属于作业。一旦作业运行行结束,Shell就把自己提到前台,如果原来的前台进程还存在(如果这个子进程还没终止),它自动变为后台进程组。
shell中运行的是作业,当父进程创建了子进程后,虽然子进程属于进程组,但是子进程不属于当前作业 ,所以当父进程退出的时候,当前作业也随之结束掉了。子进程就到后台进行。然后当前作业就变成bash,我们可以输入命令。因为此时子进程在后台,所以我们给他发送ctrl+C信号,他接收不到。所以无法停止,我是用kill命令结束子进程的。
会话
会话(Session)是一个或多个进程组的集合。
一个会话可以有一个控制终端。这通常是登陆到其上的终端设备(在终端登陆情况下)或伪终端设备(在网络登陆情况下)。建立与控制终端连接的会话首进程被称为控制进程。
一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意后台进程组。
无论何时键入终端的终端键,都会将中断信号发送到前台进程组的所有进程
作业控制
作业控制允许在一个终端上启动多个作业(进程组),他控制哪一个作业可以访问该终端以及哪些作业在后台运行。
如上图,我们输入两个命令,可以看出,输入第一个命令时:
ps,cat进程同属于一个进程组,组长为ps,bash,ps,cat三个进程属于一个会话,Learder是bash。
当输入第二个命令时,在命令后在&符号,表示该进程在后台运行,[1]是作业编号,4439是该作业中某一个进程的ID,很明显,在上例中,4439是cat进程。
作业控制与三个信号
SIGINT(Ctrl + C)——中断字符
SIGQUIT(Ctrl +\)——退出字符
SIGTSTP(Ctrl + Z)——挂起字符