终端登录(串行终端登录)
BSD终端登录
系统自举时,内核创建PID为1的init进程,init进程使系统进入多用户模式。每当有终端连接请求时,init进程则fork一个子进程,子进程调用exec函数执行getty程序。
getty程序:用来开启终端、进行终端的初始化和设置终端
图中涉及所有进程的实际用户ID和有效用户ID都是0(也就是说它们都具有超级用户特权)。
流程图详解:
- 首先系统自举时将创建PID=1的init进程,init进程读取文件/etc/ttys,对每一个允许登录的终端设备,init调用一次fork创建一个子进程,并创建一个空环境。接下来子进程将以空环境调用getty程序。
- 子进程调用exec执行getty程序,getty对请求连入的终端设备调用open函数,以读、写方式将终端打开。一旦设备被打开,则文件描述符0、1、2就被设置到该设备(回忆UNIX操作系统设计里,每创建一个进程,内核就在用户描述符表里为其分配0、1、2三个文件描述符)。
execle("/bin/login", "login", "-p", username, (char*)0, envp);
然后getty输出“login:”之类的信息,等待用户键入用户名。这个过程中getty以终端名和在gettytab中说明的环境字符串为login创建一个环境(envp参数)。-p通知login保留传递给它的环境。 - login得到由getty传递来的终端名(用户名)之后,调用getpwnam取得相应用户的口令文件登录项(登录密码),然后调用getpass(3)以显示提示“Password:”,接着读入用户键入的口令并调用crypt(3)将用户键入的口令加密,并与该用户在阴影口令文件中登录项的pw_passwd字段相比较。
例如:(tty为teletype)
我利用程序打开一个终端时,init为我创建了一个PID=2328的子进程,可以看到这个终端对应的登录名为hupac,而这个用户名将会作为环境传递给getty程序。这个进程对应teletype号为ttys001,而再打开一个终端则会得到ttys002。注意PID=2328进程的uid==0,这就充分说明了这个进程是由init进程fork而来。
如果用户正确登录,login就将完成如下工作:
- 将当前工作目录更改为该用户的起始目录
- 调用chown更改该终端的所有权,使登录用户成为它的所有者
- 将对该终端设备的访问权限改变成“用户读和写”
- 调用setgid及initgroups设置进程的组ID
- 用login得到的所有信息初始化环境:起始目录(HOME)、shell(SHELL)、用户名(USER和LOGNAME)以及一个系统默认路径(PATH)
- login进程更改为登录用户的用户ID并调用该用户的登录shell
网络登录
在上面所述的终端登录中,init知道哪些终端设备可用来进行登录,并未每个设备生成一个getty进程,所有登录都经由内核的网络接口驱动程序,而且事先并不知道将会有多少个这样的登录,因此必须等待一个网络连接请求的到达,而不是使一个进程等待每一个可能的登录。
BSD网络登录
inetd进程:因特网超级服务器
流程图详解:
- 作为系统启动的一部分,init调用一个shell,使其执行shell脚本/etc/rc,由此shell脚本启动一个守护进程inetd,一旦此shell脚本终止,inetd的父进程就变成init。
- inetd等待连接TCP/IP连接请求到达主机,而当一个连接请求到达时,它执行
- 当一个连接请求到达时,它执行一次fork,然后生成的子进程exec telnetd程序
- telnetd进程打开一个伪终端设备,并用fork分成两个进程,父进程处理通过网络连接的通信,子进程则执行login程序,父进程和子进程通过伪终端相连接。
- 如果子进程中的login程序执行正确,则类似串行终端登录成功的操作。
注意:当通过终端或网络登录时,我们得到一个登录shell,其标准输入、标准输出和标准错误要么连接到一个终端设备,要么连接到一个伪终端设备上。
进程组
进程组时一个或多个进程的集合,每个进程组有一个唯一的进程组ID。一般地,进程组中的组长进程PID就是这个进程组的组ID。
#include <unistd.h>
pid_t getpgrp(void); //返回调用进程的进程组ID
pid_ getpgid(pid_t pid); //返回PID进程的进程组ID,为0时等同于getpgrp()
进程组组长可以创建一个进程组、创建该组中的进程。只要在某个进程组中有一个进程没终止,则该进程组就存在,这与组长进程是否终止无关。从进程组创建开始到其中最后一个进程终止为止的时间区间称为进程组的生命周期。
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
//进程调用setpgid可以加入一个现有的进程组或创建一个新进程组
注:1.当pid=pgid时可能会创建一个新的进程组,而调用此函数的进程将成为该进程组的组长
2. 一个进程只能为它自己或它的子进程设置进程组ID
会话
会话(session)是一个或多个进程组的集合,进程调用setsid函数创建一个新的会话。
#include <unistd.h>
pid_t setsid(void);
如果调用次函数的进程不是一个进程组的组长,则次函数创建一个新回话
- 该进程变成新回话的会话新进程,此时该进程是新会话中的唯一进程
- 该进程成为一个新进程组的组长进程,新进程组ID就是该进程ID
- 该进程没有控制终端,如果在调用setsid之前该进程有一个控制终端,那么这种联系也被切断
- 如果该进程已经是一个进程组的组长,则此函数返回出错
控制终端
会话和进程组还有一些其他特性
- 一个会话可以有一个控制终端
- 建立与控制终端连接的会话首进程被称为控制进程 例如: 这里tty代表的就是控制终端,pts/0和pts/2分别对一个控制终端程序,也对应一个会话。pts/2对应会话3838,3838会话中有两个进程组,分别是3874和3880,每个进程组包含一个进程,且这两个各自在进程组担任组长进程
- 一个会话中的几个进程组可被分成一个前台进程组以及一个或多个后台进程组
- 如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组
- 无论何时键入终端的中断键,都会将中断信号发送至前台进程组的所有进程
- 无论何时键入终端的退出键,都会讲退出信号发送至前台进程组的所有进程
函数tcgetpgrp、tcsetpgrp和tcgetsid
需要有一种方法来通知内核哪一个进程组时前台进程组
#include <unistd.h>
pid_t tcgetpgrp(int fd); //与在fd上打开的终端相关联,返回该终端上的前台进程组ID
int tcsetpgrp(int fd, pid_t pgrpid);//将前台进程组ID设置为pgrpid
#include <termios.h>
pid_t tcgetsid(int fd);//得到控制终端会话首进程的会话ID(等价于会话首进程的进程组组ID)
作业控制
允许一个终端上启动多个作业(进程组),它控制哪一个作业可以访问该终端以及哪些作业在后台运行。
我们必须明白,只有前台程序可以接受终端输入,后台作业试图读终端,并不是一个错误,但是终端驱动程序会检测出来,然后发送一个特定信号SIGTTIN给后台作业,就会暂停后台作业。我们想要它运行,就必须转到前台来。
例如,执行
[root@localhost proc]# cat > test &
[3] 7543
[3]+ Stopped cat > test
上面的程序表示在后台启动,从标准输入流读。&表示要求shell命令在后台执行。至此发现cat操作停止了,因为后台进程不能接受终端输入。继续:fg %1表示将1号作业转为前台,bg %4表示将4号作业转为后台。
//先用jobs查看作业号
[root@localhost proc]#jobs
[2] Stopped cat > mytest
[3]- Stopped cat > test
[4]+ Stopped cat > test
//将作业4恢复至前台运行
[root@localhost proc]# fg %4
cat > test
//接受终端输入,并重定向至test
作业控制要求以下三种形式的支持:
- 支持作业控制的shell
- 内核中的终端驱动程序必须支持作业控制
- 内核必须提供对某些作业控制信号的支持
shell执行程序
…
孤儿组进程
一个其父进程已终止的进程称为孤儿进程,这种进程由init进程“收养”。而整个进程组也可称为“孤儿”。
#include "apue.h"
#include <errno.h>
static void
sig_hup(int signo){
printf("SIGHUP received, pid = %ld\n", (long)getpid());
}
static void
pr_ids(char *name){
printf("%s:pid = %ld, ppid = %ld, pgrp = %ld, tpgrp = %ld\n",
name, (long)getpid(), (long)getppid(), (long)getpgrp(),
(long)tcgetpgrp(STDIN_FILENO));
fflush(stdout);
}
int main(int argc, char **argv){
char c;
pid_t pid;
pr_ids("parent");
if((pid = fork()) < 0){
err_sys("fork error");
}else if (pid > 0){
sleep(5);
}else {
pr_ids("child");
signal(SIGHUP, sig_hup);
kill(getpid(), SIGTSTP);
pr_ids("child");
if(read(STDIN_FILENO, &c, 1) != 1)
printf("read error %d on controlling TTY\n", errno);
}
exit(0);
}
执行结果:
[root@localhost proc]# ./orphan
parent:pid = 8300, ppid = 4826, pgrp = 8300, tpgrp = 8300
child:pid = 8301, ppid = 8300, pgrp = 8300, tpgrp = 8300
SIGHUP received, pid = 8301
child:pid = 8301, ppid = 1, pgrp = 8300, tpgrp = 4826
read error 5 on controlling TTY
开始时子进程和父进程处于同一进程组8300中。
父进程终止后,进程组8300成为孤儿进程组子进程的父进程ID变为1,即已经被init进程收养。
而且父进程终止时,子进程变成后台进程组,因为父进程是由shell作为前台作业执行的。
FreeBSD实现
…