终端登录:
当系统自举时,内核创建ID为1的进程,也就是init进程,init进程系统进入多用户状态。
init进程读取/etc/inittab,对每一个允许登录的终端设备,init调用一次fork,它所生成的子进程执行(exec)getty程序。
getty为终端设备调用open函数,如果没有请求则阻塞,如果有请求,则文件描述符0,1,2就设置到该设备,然后getty输出”login“等的信息并等带用户输入用户名。
当用户键入用户名后,getty的工作就完成了。
然后以类似于这样的方式调用login程序
execle(“/bin/login”,“login”,“-p”,username,(char*)0,envp);
init以空环境(即无envp参数)调用一个getty,gettty以终端名和gettytab中说明的环境字符串。-p标识通知login保留传递给它的环境,也可将其他环境字符串加到该环境中,但是不要替换它。
进程组
一个或多个进程的集合,进程组属于一个会话。Fork并不改变进程组id
进程组组长
PID与PGID相等的进程。组长可以改变子进程的进程组ID,使其转移到另一个进程组
例如一个shell进程,当使用管道线时,如echo “hello” |cat,bash以第一个命令的进程id
为该管道线内所有进程设置进程组ID。此时echo和cat的进程组id都设置成ehco的进程id
前台进程组
该进程组中的进程能够向终端设备进行读写
登录shell通过调用tcsetpgrp()函数设置前台进程组,该函数将终端设备的fd与指定进程组相关联。成为前台进程组的进程其PGID=TPGID,常常可以通过比较他们来判断前台进程组
后台进程组
一个会话中,除前台进程组,会话首进程以外的所有进程组。该进程组中的终端设备写,但是当试图读终端设备时将会收到SIGTTIN信号,并停止。登录shell可以根据设置在终端上发出一条消息通知用户有进程需要读终端。
前台进程组只有一个,而后台进程组同时可存在多个。
孤儿进程组
POSIX定义为:该组中的每个成员要么是该组的一个成员,要么不是该组所属会话的成员
简单来说:有bash产生的新进程组中,至少有一个进程的PPID指向该bash,否则该进程组成为孤儿进程组,无法将进程状态改变通知bash
POSIX要求系统向孤儿进程中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)对挂断信号的默认动作是终止该进程(注意终止进程和停止进程是有区别的,停止指暂停,终止指退出)
为什么有孤儿进程组
有孤儿进程,对应的也有孤儿进程组的概念。为何引入这个概念以及这个概念的引入需要OS的实现者作些什么呢?先看两个前提,首先,posix用一个session的概念来描述一次用户的登录以及该用户在此次登录后的操作,然后用作业的概念描述不同操作的内容,最后才用进程的概念描述不同操作中某一个具体的工作;其次,unix最初将所有的进程组织成了树的形式,这样就便于追踪每个进程也便于管理,有了上述两个前提事情就很明白了,一切都是为了便于管理,一切都是为了登录用户的安全,即此次登录用户的作业是不能被下个登录用户所控制的,即使它们的用户名一致也是不行的,因此所谓的孤儿进程组简单点说就是脱离了创造它的session控制的,离开其session眼线的进程组,unix中怎样控制进程,怎样证明是否在自己的眼线内,那就是树形结构了,只要处于以自己为根的子树的进程就是自己眼线内的进程,这个进程就是受到保护的,有权操作的,而在别的树枝上的进程原则上是触动不得的,unix中建立进程使用fork,自然地这么一“叉子”就形成了自己的一个树枝,当然在自己眼线了,一般对于登录用户而言一个会话起始于一次login之后的shell,只要该用户不logout,在该终端的shell上执行的所有的非守护进程都是该shell的后代进程,因此它们组成一个会话,全部在shell的眼线中,一个会话终止于会话首长的death。现在考虑一下终端上的shell退出后的情景,按照规定,该终端上所有的进程都过继给了别的进程,大多数情况是init进程,然后紧接着另外一个用户登录了这个终端或者知道前一个登录用户密钥的另一个有不好念头的人登录了该终端,当然为其启动的shell创建了一个新的session,由于之前登录的用户已退出,现在登录的用户由于之前用户的进程组都成了孤儿进程组,所以它再有恶意也不能控制它们了,那些孤儿进程组中的成员要么继续安全的运行,要么被shell退出时发出的SIGHUP信号杀死。
需要注意的
一般一个登录shell就是一个会话首进程,会话首进程获得一个控制终端给前台进程组用,会话首进程也只能通过控制终端来控制别的进程,所谓的控制就是发送信号(如ctl+c等)
一些函数
获得进程组id
#include
pid_t getpgrp(void); 返回值:调用进程的进程组id
#include
int setpgid(pid_t pid,pid_t pgid); 返回值:成功返回0,出错返回-1
Setpgid函数将pid进程的进程组id设置为pgid,
(1)如果两个参数相等,则由pid指定的进程变成进程组组长。
(2)如果pid为0,则使用调用者的进程id
(3)如果pgid为0,则由pid指定的进程id用作进程组id
#include
pid_t setsid(void); 返回值:成功返回进程组id,若出错返回-1
如果调用此函数的进程不是一个进程组的组长,则此函数就会创建一个新会话,结果会发生下面三件事:
(1)该进程变为会话首进程。此时,该进程是新会话中的唯一进程
(2)该进程成为一个新进程组的组长进程。新进程的组id是调用进程的进程id。
(3)该进程没有控制终端。如果在调用setsid之前该进程有一个控制终端,那么也会被中断
如果调用进程调用进程是一个进程组的组长,则此函数返回出错。为了保证不会出错,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组id,而其进程id则是重新分配的,两个不肯能相等,所以这就保证子进程不会是一个进程组的组长。
#include
pid_t tcgetpgrp(int filedes);返回值:成功则返回前台进程组id,出错返回-1
int tcsetpgrp(int fileds ,pid_t pgrpid);返回值:成功返回0,出错返回-1
tcgetpgrp返回前台进程组的进程组id,该前台进程组与在fileds上打开的终端相关联
tcsetpgrp将前台进程组id设置为pgrpid,pgrpid应是在同一会话中的一个进程组的id。fileds必须引用该会话的控制终端。
#include
pid_t tcgetsid(int filedes); 返回值:成功返回会话首进程的进程组id,出错返回-1.
需要管理控制终端的应用程序可以调用tcgetsid函数识别出控制终端的会话首进程的会话id
观察进程组id与进程id需要用到的命令
ps -o pid,ppid,pgrp,session,tpgid,comm
其中pgrp表示进程组id,tpgid表示前台进程组id,comm表示启动该进程的命令。