1、前言
2、终端登录
-
在早期的UNIX系统,用户用哑终端(用硬连接到主机)进行登录,因为连接到主机上的终端设备数是固定的,所以同时登录数也就有了已知的上限。
-
随着位映射图像终端的出现,开发出了窗口系统,它向用户提供了与主机系统进行交互的新方式。创建终端窗口的应用也被开发出来,它仿真了基于字符的终端,使得用户可用用熟悉的方式(即shell命令)与主机进行交互。
-
我们现在描述的过程用于经由终端登录至UNIX系统。该过程几乎与所使用的终端类型无关,所使用的终端可以是基于字符的终端、仿真基于字符的终端,或者运行窗口系统的图形终端。
-
这里说明两种平台的终端登录:
-
BSD终端登录
BSD的终端登录过程比较经典,linux也是其后继者。-
系统管理者创建通常名为
/etc/ttys
的文件,其中每个终端设备都有一行,每行说明设备名和传到getty
程序的参数。例如其中一个参数说明了波特率等等。 -
当系统自举时,内核创建进程ID为
1
的进程,也就是init
进程。 -
init
进程使系统进入多用户模式。 -
init
读取/etc/ttys
,对每一个允许登录的终端设备,init
调用一次fork
,它所生成的子进程则exec getty
(get teletypewriter) 程序。init
以空环境exec getty
程序。 -
getty
对终端设备调用open
函数,以读、写方式打开终端,此时会得到该终端的文件描述符。一旦该设备被打开,文件描述符0
、1
、2
就被通过dup2
函数关联到一起,从而共享终端设备的文件表项。然后getty
输出“login :”
之类的信息,并等待用户键入用户名。 -
当用户键入了用户名后,
getty
的工作就完成了。然后它以类似于以下方式调用login
程序:execle("/bin/login","login","-p",username,(char *)0,envp);
-
其中
envp
环境变量是根据gettytab
文件中的环境字符串生成的,“-p”
参数通知login
保留传递给它的环境,也可以将其它环境字符串添加到该环境中,但是不要替换它。
-
login
能处理多项工作。因为它得到了用户名,所以能调用getpwnam
取得相应用户的口令文件登陆项。然后login
调用getpass
以显示“Password:”
,接着读入用户键入的口令,它调用crypt
将口令加密,并与该用户在阴影口令文件中登录项的pw_passwd
字段相比较。 -
如果用户几次键入的口令都无效,则
login
以参数1
调用exit
表示登陆失败。父进程(init
)了解到子进程的终止情况后,将再次调用fork
,然后又执行了getty
,对此终端执行上述过程。
-
如果用户正确登录,login就将完成如下工作:
- 将当前工作目录更改为该用户的起始目录(
chdir
)。 - 调用
chown
更改该终端的所有权,使登录用户成为它的所有者。 - 将对该终端设备的访问权限改变成“用户读和写”。
- 调用
setgid
及initgroups
设置进程的组ID。 - 用
login
得到的所有信息初始化环境:起始目录、shell
、用户名、以及一个系统默认路径(PATH
). login
进程更改为登录用户的ID(setuid
)并调用登录用户的shell,类似于:execl("/bin/sh","-sh",(char *)0);
- 将当前工作目录更改为该用户的起始目录(
-
至此,用户登录的登录
shell
得以开始运行。其父进程是init
进程,所以此shell
终止时,init
会得到通知(接收到SIGCHLD
信号),它会对该终端重复全部上述过程。登录shell
的文件描述符0
、1
和2
设置为终端设备。 -
现在,登录shell读取其启动文件(.profile),这些启动文件通常更改某些环节变量设置他们自己的PATH,当执行完启动文件后,用户最后得到shell提示符,并能键入命令
-
-
Linux终端登录
linux
的终端登录过程非常类似于BSD
,它们的主要区别在于说明终端配置的方式。Ubuntu
使用的init
程序叫作“Upstart”
,并使用存放在/etc/init
目录的*.conf
命名的配置文件。例如:运行/dev/tty1
上的getty
需要的说明可能放在/etc/init/tty1.conf
文件中。
-
3、网络登录
-
通过串行终端登录至系统和经由网络登录至系统两者之间的主要区别是:网络登录时,在终端和计算机之间的连接不再是点到点的。在网络登录情况下,
login
仅仅是一种可用的服务,这与其他网络服务(如FTP
和SMTP
)性质相同。 -
在上述的终端登录中,
init
知道哪些终端设备可用用来登录,并为每个设备生成一个getty
进程。但是,对网络登录情况有所不同,因为事先并不知道有多少个这样的登录。因此必须等待一个网络连接请求的到达,而不是使一个进程等待每一个可能的登录。 -
为使同一个软件既能处理终端登录,又能处理网络登录,系统使用了一种称为伪终端的软件驱动程序,它仿真串行终端的运行行为,并将终端操作映射为网络操作,反之亦然。
-
BSD网络登录
-
在BSD中,有一个
inetd
进程(有时称为英特网超级服务器),它等待大多数网络连接。 -
作为系统启动的一部分,
init
调用一个shell
,使其执行shell
脚本/etc/rc
。由此shell
脚本启动一个守护进程inetd
。一旦此shell
脚本终止,inetd
的父进程就变成init
。 -
inetd
等待TCP/IP
连接请求到达主机,而当一个连接请求到达时,它执行一次fork
,然后子进程exec
适当的程序。
-
以
telnet
为例:- 主机A启动
telnet
客户端进程,通过telnet hostname
登录远端名为hostname
的主机B。 - 主机B的
inetd
进程收到来自主机A的请求。 - 主机B的
inetd
进程fork
一个子进程并exec
主机B上的TELNET
进程(被称为talnetd
)。 - 然后
talnetd
进程打开一个伪终端,并用fork
分成两个进程。父进程通过网络连接的通行,子进程执行login
程序。
- 主机A启动
-
需要理解的是:当通过终端或网络登录时,我们得到一个登录shell,其标准输入、标准输出、标准错误连接到一个终端设备或一个伪终端上。后面将会了解到这一登录shell是一个会话的开始,而此终端或伪终端则是会话的控制终端。
-
-
linux网络登录
- 除了有些版本使用扩展的因特网服务进程
xinetd
代替inetd
进程外,Linux网络登录的其他方面与BSD网络登录相同。xinetd
进程对它所启动的各种服务的控制比inetd
提供的控制更加精细。
- 除了有些版本使用扩展的因特网服务进程
4、进程组
-
每个进程除了有一个进程ID外,还属于一个进程组。
-
进程组是一个或多个进程的集合。通常,他们是在同一作业中结合起来的,同一进程组中的各进程接收来自同一终端的各种信号。每一个进程组有一个唯一的进程组ID。
-
进程组ID类似于进程ID,它是一个正整数,并可存放在pid_t数据类型中。
#include <unistd.h> pid_t getpgrp(void); //返回调用进程的进程组ID. pid_t getpgid(pid_t pid); //返回进程号为pid的进程组ID,若pid=0,则等价于getpgrp
-
每个进程组有一个组长进程。组长进程的进程ID等于该进程组的进程组ID。
-
只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到最后一个进程离开为止的时间区间称为进程组的生命周期。某个进程组中的最后一个进程可以终止,也可以转移到另一个新的进程组。
-
进程调用
setpgid
可以创建一个进程组也可以键入到一个现有的进程组。#include <unistd.h> int setpgid(pid_t pid,pid_t pgid);
setpgid
函数将pid
进程的进程组ID设置为pgid
,如果这两个参数相等,则由pid
指定的进程变成进程组组长;- 如果
pid=0
,则使用调用者的进程ID; - 如果
pgid=0
,则由pid
指定的进程ID用作进程组ID; - 一个进程只能为自己或它的子进程设置进程组ID。在它的子进程调用了
exec
后,他就不能更改该子进程的ID了。
-
在大多数作业控制
shell
中,在fork
之后调用此函数,使父进程设置其子进程进程组ID,并且也使子进程设置其自己的进程组ID,这两个调用中有一个是冗余的,但让父子进程都这样做可以保证,在父进程和子进程认为子进程已经进入了该进程组之前,这确实已经发生了。如果不这样做,在fork之后,由于父进程和子进程运行的先后顺序不确定,会因为子进程的组员身份取决于哪个进程首先执行而产生竞争条件。
5、 会话
-
会话(session)是一个或多个进程组的集合。其结构可以如下,在一个会话中有3个进程组:
-
通常是由shell的管道将几个进程编成一组的。上面的安排可能由下列形式的shell命令形成:
proc1 | proc2 & # 这是后台进程组 proc3 | proc4 | proc5 # 这是前台进程组
-
进程调用
setsid
函数建立一个新会话。#include <unistd.h> pid_t setsid(void);
-
如果调用此函数的进程不是一个进程组组长,则此函数创建一个新会话。具体会发生以下3件事:
- 该进程会变成新会话的首进程(
session leader
,会话首进程是创建该会话的进程)。此时,该进程是新会话中的唯一进程。 - 该进程成为一个新进程组的组长进程。新进程组ID是该调用进程的进程ID。
- 该进程没有控制终端,如果之前有一个控制终端,那么这种联系也被切断。
- 该进程会变成新会话的首进程(
-
如果该调用进程已经是一个进程组的组长,则此函数返回出错。 为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,这就保证了子进程不是一个进程组的组长。
-
getsid
函数返回会话首进程的进程组ID:#include <unistd.h> pid_t getsid(pid_t pid);
- 如果
pid
是0
,getsid
返回调用进程的会话首进程的进程组ID
。
- 如果
6、控制终端
-
控制终端对应的文件是
/dev/tty
-
这是一个逻辑概念,即用户正在控制的终端,可以为串行终端,虚拟终端和伪终端。
- 一个会话可以有一个控制终端(controlling terminal)。这通常是终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。
- 建立与控制终端连接的会话首进程被称为控制进程(controlling process) 。
一个会话中的几个进程组可以被分成一个前台进程组(foreground process group)以及一个或多个后台进程组(background process group)。 - 如果一个会话有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。
- 无论何时键入终端的退出键(常常是ctrl+\),都会将退出信号发送至前台进程组的所以进程。
- 如果终端接口检测到调制解调器或网络已经断开,则将挂断信号发送至控制进程(会话首进程)。
-
以用户登录系统为例,可能存在如下图所示的情况:
-
通常我们不必担心控制终端,登录时,将自动建立控制终端(如通过终端登录Unix时,getty通过open函数以读写方式打开该终端设备,把文件描述符0、1、2都指向该控制终端)。
-
如何为会话分配一个控制终端:
- 当会话首进程打开第一个尚未与一个会话相关联的终端设备时,只要在调用
open
时没有O_NOCTTY
,将会将此终端作为控制终端分配给该会话 - 当会话首进程以
TIOCSCTTY
作为request
参数调用ioctl
时,会为该会话分配控制终端。为了使此函数成功执行,此会话不能已经有一个控制终端(因此此操作通常跟在setsid调用之后,setsid保证此进程是一个没有控制终端的会话首进程)
- 当会话首进程打开第一个尚未与一个会话相关联的终端设备时,只要在调用
-
程序能与控制终端对话的方法是
open
文件/dev/tty
,如果程序没有控制终端,则打开此设备将失败。
7、 函数tcgetpgrp、tcsetpgrp和tcgetsid
-
需要一种方法来通知内核哪一个进程组是前台进程组,这样,终端设备驱动程序就能知道将终端输入和终端产生的信号发送到何处
#include <unistd.h> pid_t tcgetpgrp(int fd); //返回终端为fd的前台进程组ID int tcsetpgrp(int fd ,pid_t pgrpid); //将前台进程组ID设置为pgrpid,终端为fd。
tcgetpgrp
函数返回前台进程组ID,fd
引用该会话的控制终端tcsetpgrp
函数将前台进程组ID设为pgrp
。pgrp
应为同一会话中的一个进程组ID,fd引用该会话的控制终端
-
大多数应用程序不直接调用这两个函数,它们通常由作业控制
shell
调用 -
通过
tcgetsid
函数获取控制终端的会话首进程的进程组ID#include <termios.h> pid_t tcgetsid(int fd);