文章目录
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
提供的控制更加精细。
- 除了有些版本使用扩展的因特网服务进程
补充A:线程组
学习Linux内核的时候会涉及到线程组、进程组和会话的概念,本书没有介绍到线程组,所以在此进行补充。
- 线程组:就是一组线程或者线程组,可以包含线程,也可以包含线程组
- 线程对象关联
- 1级关联:就是线程组中全部是线程,没有线程组,这样往下就没有可延伸的了。
- 多级关联:就是线程组中,还有线程组,可以向下延伸
- 思考:这种线程组中嵌套线程组的方式,可不可以理解为父进程
fork
出的子进程?
- 线程对象关联
- POSIX 标准规定,进程的所有线程共享同一进程 ID(即每个线程调用
getpid()
都应返回相
同值),Linux 从 2.4 版本开始引入了线程组(threads group
),以满足这一需求。
- 线程组就是共享同一线程组标识(
TGID
)(thread group identifier
)的一组内核调度实体(KSE,kernel scheduling entity
) getpid()
所返回的就是调用者的TGID
。换言之,TGID
和进程ID
是一回事。(这句话可能有问题,在《深入Linux内核架构》中介绍到:没有使用线程的进程,TGID
和PID
才相同…有点绕获取可以忽略)。- 一个线程组内的每个线程都拥有一个唯一的线程标识符(thread identifier,
TID
),用以标识自身。 - 线程
ID
在整个系统中是唯一的,且除了线程担当进程中线程组首线程的情况之外,内核能够保证系统中不会出现线程ID
与进程ID
相同的情况。 - 线程组中首个线程的线程
ID
与其线程组ID
相同,也将该线程称之为线程组首线程(thread group leader
)。 - 线程组中的所有线程拥有同一父进程
ID
,即与线程组首线程ID
相同。仅当线程组中的所有线程都终止后,其父进程才会收到SIGCHLD
信号(或其他终止信号)。
- 线程组就是共享同一线程组标识(
4、进程组
-
每个进程除了有一个进程ID外,还属于一个进程组。
-
进程组是一个或多个进程的集合。通常,他们是在同一作业中结合起来的,同一进程组中的各进程接收来自同一终端的各种信号。每一个进程组有一个唯一的进程组ID。例如:为了执行命令行
ls | sort | more
,Shell为三个相应的进程ls
、sort
、more
创建了一个新的组。Shell以这种方式作用于这三个进程,就好像它们是一个单独的实体(更准确地说是作业) -
进程组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);
8、作业控制
-
它允许在一个终端上启动多个作业(进程组),控制哪一个作业可以访问该终端,以及哪些作业在后台运行。
-
我们可以键入一个影响前台作业的特殊字符:挂起键(一般采用
Ctrl+Z
)与终端进行交互作用。键入此字符使终端驱动程序将信号SIGTSTP
送至前台进程组中的所有进程,后台进程组作业则不受影响。实际上有三个特殊字符可使终端驱动程序产生信号,并将它们送至前台进程组,它们是:- 中断字符(一般采用
DELETE
或Ctrl+C
)产生SIGINT
。 - 退出字符(一般采用
Ctrl+\
)产生SIGQUIT
。 - 挂起字符(一般采用
Ctrl+Z
)产生SIGTSTP
。
- 中断字符(一般采用
-
注意只有前台作业接受终端输入,因为每一个对话只有一个前台作业。当后台试图去读取时,终端驱动会使其STOPPED信号(SIGTTIN)停止这个后台作业,而shell则向用户提供相关信息。然后用户就可以用shell命令(fg)将此作业转为前台作业
-
shell 在后台起动 cat 进程,但是当 cat 试图读其标准输入(控制终端)时,终端驱动程序知道它是个后台作业,于是将 SIGTTIN 信号送至该后台作业。shell 检测到其子进程的状态改变(回忆对 wait 和 waitpid 的讨论),并通知我们该作业已被停止。然后,**用 shell 的 fg 命令将此停止的作业送入前台运行。这样做使 shell 将此作业转为前台进程组( tcsetpgrp),并将继续信号( SIGCONT )送给该进程组。**因为该作业现在前台进程组中,所以它可以读控制终端。
-
对于后台作业写入控制终端,我们可以允许或禁止后台作业输出到控制终端。
-
在用户禁止后台作业向控制终端写时,该作业的cat命令试图写其标准输出,此时终端驱动程序识别出该写操作来源于后台进程,于是向该作业SIGTTOU信号,cat进程阻塞。与之前例子一样,用户使用shell的fg命令将该作业转为前台时,该作业继续完成。
9、 shell执行程序(以bash为例)
-
对于以下指令:
$ ps -o pid,ppid,pgid,sid,comm PID PPID PGID SID COMMAND 565 453 565 565 bash 588 565 588 565 ps
- 可见ps的父进程是shell。ps和shell位于同一会话中(会话首进程即为shell进程),且ps位于前台进程组中。
-
对于管道命令,执行以下命令:
$ ps -o pid,ppid,pgid,sid,comm | cat | cat PID PPID PGID SID COMMAND 565 453 565 565 bash 1007 565 1007 565 ps 1008 565 1007 565 cat 1009 565 1007 565 cat
- 可见管道两边的进程都是shell的子进程,即shell对于管道中的每一条命令fork一个子进程。且上面的ps和两个cat都位于前台进程组。
-
但如果是下面的情况:ps和两个cat都位于同一后台进程组。
$ ps -o pid,ppid,pgid,sid,comm | cat | cat & [1] 1204 $ PID PPID PGID SID COMMAND 565 453 565 565 bash 1202 565 1202 565 ps 1203 565 1202 565 cat 1204 565 1202 565 cat
-
如果一个后台进程试图读其控制终端:
$ cat > ttt & [1] 1497 $ [1]+ Stopped cat > ttt $ fg %1 cat > ttt wudi 123456 ^D
-
如果后台作业试图读控制终端,则会产生信号SIGTTIN。导致后台进程停止。之后通过fg命令将指定1号作业设为前台作业,此时该作业可以从控制终端中获取输入。
-
注意:不同shell对创建各个进程的方式不同。
-
本节中例子是以bash作为示例,而如果是经典的sh,那么对于管道命令则是管道的最后一个进程是shell的子进程,而管道的其他进程则是最后一条命令的子进程。除此之外还有其他区别,具体看书
10、孤儿进程组
-
孤儿进程:
- 一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
-
僵尸进程:
- 如果子进程先退出了,父进程还未结束并且没有调用
wait
或者waitpid
获取 子进程的状态信息,则子进程残留的状态信息会变成僵尸进程。 - 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
- 孤儿进程不会占用系统资源,最终是由init进程托管,由init进程来释放。
- 如果子进程先退出了,父进程还未结束并且没有调用
-
孤儿进程组: 整个进程组也可以成为孤儿
- 该组中每个成员的父进程要么是该组的一个成员,要么在其它会话中。
- 或者说,一个进程组不是孤儿进程组的条件是:该组中有一个进程,其父进程在属于同一个会话的另一个组中。
-
孤儿进程组提出原因(作用):
- 当一个终端控制进程(即会话首进程)终止后,那么这个终端可以用来建立一个新的会话。这可能会产生一个问题,当该终端控制进程终止后,原来旧的会话(一个或者多个进程组的集合)中的任一进程可再次访问这个的终端。
-
为了防止这类问题的产生,**在会话首进程(也是控制进程)终止后仍继续运行的进程组被标记为孤儿进程组。**当一个进程组成为孤儿进程组时,posix.1要求向孤儿进程组中处于停止状态的进程发送SIGHUP(挂起)信号,系统对于这种信号的默认处理是终止进程,然而如果无视这个信号或者另行处理的话那么这个挂起进程仍可以继续执行。但它仍然无法再访问终端。
-
父进程终止后,进程组成为了孤儿进程组。那么新的孤儿进程组中处于停止(stopped)状态的每一个进程都会收到挂断(SIGHUP)信号,接着又收到继续(SIGCONT)信号。
-
也就是说,进程组成为孤儿进程组后,孤儿进程组中的状态为stopped的进程会被激活。前提是需要对SIGHUP信号自处理,对挂断信号系统默认的动作是终止进程。
-
孤儿进程组成为后台进程组,且没有控制终端
-
孤儿进程组去读控制终端时,read返回出错并将errno设置为EIO。
-
11、FreeBSD中会话、进程组、进程、控制终端之间的关系实现
-
session结构体:每个会话都分配一个session结构(如调用setsid时)
- s_count:进程组数
- s_leader:会话首进程
- s_ttyvp:指向控制终端设备文件的vnode结构(进程对/dev/tty的所有访问都通过vnode结构)
- s_ttyp:指向控制终端tty结构
- s_sid:会话ID(会话首进程ID)
-
tty结构:每个终端设备和每个伪终端设备均在内核分配这样一种结构
- t_session:指向将此终端用作控制终端的会话
- t_pgrp:前台进程组,由此可见前台进程组是终端的属性而不是进程的属性
- t_termios:包含与该终端的有关信息(如波特率等)
- t_winsize:包含终端窗口当前大小等信息
-
pgrp结构:包含进程组信息
- pg_id:进程组ID
- pg_session:此进程组所属会话
- pg_members:此进程组的进程成员
-
proc结构:包含一个进程的所有信息
- p_pid:进程pid
- p_pptr:指向父进程
- p_pgrp:指向所属进程组
- p_pglist:指向所属进程组中上一个和下一个进程
为了找到特定会话的前台进程组,内核从session结构开始,然后用s_ttyp得到控制终端的tty结构体,然后从t_pgrp得到前台进程组的pgrp结构体。
-
s_leader:会话首进程
-
s_ttyvp:指向控制终端设备文件的vnode结构(进程对/dev/tty的所有访问都通过vnode结构)
-
s_ttyp:指向控制终端tty结构
-
s_sid:会话ID(会话首进程ID)
-
tty结构:每个终端设备和每个伪终端设备均在内核分配这样一种结构
- t_session:指向将此终端用作控制终端的会话
- t_pgrp:前台进程组,由此可见前台进程组是终端的属性而不是进程的属性
- t_termios:包含与该终端的有关信息(如波特率等)
- t_winsize:包含终端窗口当前大小等信息
-
pgrp结构:包含进程组信息
- pg_id:进程组ID
- pg_session:此进程组所属会话
- pg_members:此进程组的进程成员
-
proc结构:包含一个进程的所有信息
- p_pid:进程pid
- p_pptr:指向父进程
- p_pgrp:指向所属进程组
- p_pglist:指向所属进程组中上一个和下一个进程
为了找到特定会话的前台进程组,内核从session结构开始,然后用s_ttyp得到控制终端的tty结构体,然后从t_pgrp得到前台进程组的pgrp结构体。