Linux的控制台(TTY/PTY)与多任务(MULTI-TASKING)
原文: https://blog.kghost.info/2012/08/20/tty-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 1cat
现在这个 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 -aspeed 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 之间的关系:
┌─────────────────────────────────────┐ │ Session │┌─────┐One│┌────────────────────────────────┐ ││ TTY │<──on─>││ Process group │ │└─────┘One││ bash │ │ │└────────────────────────────────┘ │ │┌────────────────────────────────┐ │ ││ Active process group │ │ ││ cat │ │ ││ cat │ │ │└────────────────────────────────┘ │ │┌────────────────────────────────┐ │ ││ process group │ │ ││ /opt/google/chrome/chrome │ │ ││ \_ /opt/google/chrome/chrome│ │ ││ \_ /opt/google/chrome/chrome│ │ ││ \_ /opt/google/chrome/chrome│ │ │└────────────────────────────────┘ │ └─────────────────────────────────────┘
每个 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 processGroupsint 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);}}
页:
[1]