Linux的控制台(TTY/PTY)与多任务(MULTI-TASKING)
Linux是一个多任务操作系统,可以方便的在一个控制台(或shell)下同时执行多条命令,达到这样的目标并不是一件容易的事情。本文帮助理解下面几个跟控制台有关的概念:tty/pty,control terminal,session,process group,signal;并设计实现一个多任务控制程序
bash 的多任务支持
熟悉Linux的同学都知道,bash支持同时跑多个任务,下面先简单演示一下 bash 的多任务。首先,运行一个 cat 命令,然后通过 Ctrl+Z 中断这个 cat 回到 bash:
$ cat ^Z [1]+ Stopped cat |
然后,我们可以再运行一个 cat 任务,这回让 cat 干点活,继续用 Ctrl+Z 中断:
$ cat /dev/urandom > /dev/null ^Z [2]+ Stopped cat /dev/urandom > /dev/null |
这样我们就有了两个 cat 任务在后台,通过 bash 内置的 jobs 命令可以查看后台的任务:
$ jobs [1]- Stopped cat [2]+ Stopped cat /dev/urandom > /dev/null |
大家可以看到,这两条命令都处于 Stopped 状态,现在我们通过 bg 命令让第二个 cat 在后台运行:
$ bg 2 [2]+ cat /dev/urandom > /dev/null & $ jobs [1]+ Stopped cat [2]- Running cat /dev/urandom > /dev/null & |
这时可以看到,第二个 cat 变成蓝 Running 状态,同时我机器的 CPU 温度也飚上来了。我们还可以通过 fg 命令让第一个 cat 到前台执行:
$ fg 1 cat |
现在这个 cat 又恢复到等待我的输入的状态了。这样看来,好像 multi-tasking 是一件非常简单的事情,那么请尝试回答下面几个问题:
Q1. 如何中断/继续程序的运行?
Q2. 如何防止后台运行的程序互相抢夺控制台输入?
TTY - signal 转换
首先,回答第一个问题
Q1. 如何中断/继续程序的运行?
很简单,通过两个 signal 就可以控制程序中断/运行:
$ man 7 signal ... SIGCONT 19,18,25 Cont Continue if stopped SIGSTOP 17,19,23 Stop Stop process ... |
于是某同学就在 bash 的代码里面找关于这两个 signal 的代码,但是他只找到了 fg/bg 命令给程序发送了 SIGCONT,没有找到关于 SIGSTOP 的代码,于是有了下面的问题:
Q1.1: 什么程序给 cat 发送了 SIGSTOP 信号?
通过本小节标题也可以看出,signal 其实是 TTY 发送的:
$ stty -a speed 38400 baud; rows 51; columns 185; line = 0; .... susp = ^Z; .... .... .... |
看到了里面的 susp = ^Z 了么,这个设置的意思就是一旦 TTY 收到了 Ctrl-Z 就会把其转换成 SIGSTOP 信号?同理我们可以设置 Ctrl-N 为挂起的快捷键
$ stty susp ^N $ cat ^N [1]+ Stopped cat |
由于所有的信号都是 tty 控制的,我们也可以修改 Ctrl-C/S/Q 等所有信号快捷键的行为,那么这里又有了下面这个问题:
Q3. 哪些程序会收到 TTY 来的信号?如果当前 TTY 来的信号所有程序都能收到,那么 Ctrl-C 会不会连后台程序一起杀掉?
这个问题先卖个关子,下面介绍一下process group。
process group
这里先启动一些进程:
$ google-chrome & [1] 26077 $ cat | cat |
我在这个 bash session 里面启动了3个进程,一个 google-chrome,两个 cat。chrome 又继续启动了很多进程。为了方便理解画张图,理清 TTY,session 与 process group 之间的关系:
|
每个 TTY 对应一个 session,session 是由 login (ssh/getty/…) 程序创建的,bash 仅仅是继承了这个 session。每个 session 里面有若干个 process group,每当 bash 创建(fork)一个进程的时候,在运行程序(exec*)前,都会为这个进程创建一个新的 process group。每个 process group 中只有一个 group 处于 active 状态,由 bash 控制哪个进程处于 active 状态。每个 process group 里面有若干个进程。为什么要这样设计,且听我慢慢道来。大家是否还记得前面留下的两个悬而未决的问题:
Q2. 如何防止后台运行的程序互相抢夺控制台输入?
Q3. 哪些程序会收到 TTY 来的信号?如果当前 TTY 来的信号所有程序都能收到,那么 Ctrl-C 会不会连后台程序一起杀掉?
有了 process group 的概念,回答这两个问题就非常简单了,只有处于 Active 内的进程能够获取控制台输入,并且 signal 只发送给 Active 内的进程。当有后台运行的进程想要读取控制台的时候(比如后台运行一个 cat),tty 会向其发送 SIGSTOP 信号,挂起此程序,你会发现,企图让 cat 后台运行是不可能的。当用户按下 Ctrl-C 或者 Ctrl-Z 的时候,只有前台的进程会收到相应的信号,并执行相应的操作。由于 bash 的特殊设计,用户是无法让两个程序同时读取控制台的,如果有人实在无聊,写了个程序 fork 出几个前台进程读取控制台(比如笔者),还是会出现抢占的现象:
int main() { if (fork()) execlp("awk", "awk", "{ print \"parent:\" $0 }", 0); else execlp("awk", "awk", "{ print \"child:\" $0 }", 0); } |
用户的输入会随机传给 parent 或者 child,并且结果是不可预测的。
多任务控制的实现
前面介绍的多任务控制的原理,下面我们设计一个多任务控制程序,实现和 bash 一样的功能:
-
在前台有任务执行的时候等待
set processGroups int startCommand(char *cmd) { int pid = fork(); if (pid) { int = waitpid(pid); // 等待命令返回(结束或者挂起) return status; } else { int mypid = getpid() pgid = setpgid(); // 建立新的 process group tcsetpgrp(pgid); // 设置当前 group 为 active processGroups.add(mypid) exec(cmd); // 运行命令 } } |
-
可以随时挂起前台执行的任务
挂起程序完全由 tty 完成,只需要在 waitpid 后面检查前台程序是结束了,还是被挂起了,如果是挂起了,需要把控制台输入权限返还给 bash:
... switch(startCommand(cmd)) { case exit: processGroups.remove(pid); break; case suspend: break; // do nothing } tcsetpgrp(getpgid()); // 拿回 active process group |
-
bg 命令切换后台执行任务
只需要向程序发送 SIGCONT 信号:
void bg(pid) { if (issuspended(pid)) kill(pid, SIGCONT); } |
-
fg 命令把任务切换到前台
先让程序继续执行,然后转移 active process group:
int fg(pid) { bg(pid); tcsetpgrp(getpgid(pid)); return waitpid(pid); } |
-
jobs 命令查看当前任务
for (pid in processGroups) print pid; |
这样我们就实现了一个多任务控制程序,由于 tty 设备的存在,使得实现多任务控制轻松了很多。Windows 下是没有 tty 设备的,于是控制台程序就有很多限制,比如无法实现一个能够获取 Ctrl-C 输入的控制台程序,只有去拦截 interrupt 信号。上面这些内容估计只有 bash 的设计师才会关注,但是下面的内容就是几乎每个 linux 程序员都会关注的内容。
daemon 程序的实现
daemon 进程就是要与 TTY 划开界限,所有东西都不依赖 TTY,那么结果就非常简单了,因为 TTY 和 session 是一一对应的关系,我们新建一个 session 就等于把与原来 TTY 有关的东西完全抛开了:
void daemonize(char *cmd) { close(0); close(1); close(2); if (fork()) exit(0); else { setsid(); exec(cmd); } } |
为什么这里需要 fork ? 请查阅此文章: http://stackoverflow.com/questions/2613104/why-fork-before-setsid