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. 如何防止后台运行的程序互相抢夺控制台输入?
我按了break让进程停了,按什么键能发SIGCONT 让进程继续走啊? | 通常是通过shell来控制的, Ctrl+Z SIGSTOP 用bg或fg命令会发SIGCONT, 也可以用kill -SIGCONT PID |
Ctrl+c是强制中断程序的执行。
Ctrl+z的是将任务中断,但是此任务并没有结束,他仍然在进程中他只是维持挂起的状态。
用户可以使用fg/bg操作继续前台或后台的任务,
fg命令重新启动前台被中断的任务,
bg命令把被中断的任务放在后台执行.
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 之间的关系:
┌─────────────────────────────────────┐
│ 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 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 ?
First of all: setsid() will make your process a process group leader, but it will also make you the leader of a new session. If you are just interested in getting your own process group, then use setpgid(0,0).
Now for understanding the actual reason why setsid() returns EPERM if you already are process group leader or session leader you have to understand that process group and session ids are initialized from the process id of the process creating them (and hence leading them, i.e. for a session leader pid == sid and for a process group leader pid == pgid). Also process groups cannot move between sessions.
That means if you are a process group leader, and creating a new session would be allowed then the sid and the pgid would get set to your pid, leaving the other processes in your old process group in a weird state: their process group leader suddenly is in a different session then they themselves might be. And that cannot be allowed, hence the EPERM by the kernel.
Now if you fork() once you are neither session nor process group leader anymore and hence setting your sid and pgid to your pid is safe, because there are no other processes in such group.