理解操作系统中的终端设备和编程
学习 Shell 的实现
今天的计算机软件需要
输入/输出设备的变迁
终端是一个可以读写的对象
tty 命令查看当前终端
tty
tty < /dev/null
strace tty
终端默认是 “cooked (canonical) mode”,即自带一个 “行编辑器”
set -o vi
(bash); bindkey -v
(zsh)另一个 “raw mode” 按键即返回 (tty-raw.c)
一系列终端 “密码” (in-band 控制信号)
ls --color | less
,看到好多 ESC#define ESC "\033["
#define move(x, y) put(ESC "%d;%dH", y + 1, x + 1)
#define setbg(c) put(ESC "48;5;%dm", c)
#define setfg(c) put(ESC "38;5;%dm", c)
#define clear() put(ESC "2J")
#define reset() put(ESC "39m" ESC "49m")
打印二进制文件,“偶然” 的 escape code 可能改变终端的行为
以为故事结束了?诡异的事情发生了……
ls --color=auto
ls --color=auto | less
ls --color | cat
还记得 fork 时候的例子吗?
./a.out
和 ./a.out | wc -l
得到的行数不同两个命令执行的唯一差别是 stdout 输出对象不同
比较 strace/ltrace 的 diff!
strace ls --color=auto 2> strace-a.log
strace ls --color=auto > /dev/null 2> strace-b.log
破案结果
isatty(1)
- stdout 是否是 tty (竟然看懂了)ioctl(1, TCGETS, ...)
termios (3)
有兴趣的同学:
$ stty -a
speed 38400 baud; rows 24; columns 80; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R;
werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
...
我们是操作系统的用户。操作系统是系统调用的 API。那我们怎么用操作系统??
sh
-compatible command language interpreter that executes commands read from the standard input or from a file.”“Unix is user-friendly; it's just choosy about who its friends are.”
在 “自然语言”、“机器语言” 和 “1980s 的算力” 之间的优雅平衡
cmd > file < file 2> /dev/null
cmd1; cmd2
, cmd1 && cmd2
, cmd1 || cmd2
cmd1 | cmd2
$()
, <()
1980s 设计的问题
echo hello > a.txt | cat
(试试 bash 和 zsh)while (1) {
char *cmd = readline(get_env("PS1"));
if (!cmd) {
parse_and_execute(cmd);
free(cmd);
}
}
Shell 的 “命令提示符”
-x
)./hello
的实现fork-execve
void run_cmd(const char *cmd) {
if (fork1() == 0) {
execve(
parse_file(cmd),
parse_arg(cmd),
shell_env
);
} else {
wait(0);
}
}
void fork1() {
int pid = fork();
if (pid < 0) panic("cannot fork");
}
./hello > /dev/null
的实现fork-dup-execve
if (fork1() == 0) {
if (redirect) {
fd = open(redirect_path, ...);
dup2(fd, STDOUT_FILENO);
close(fd);
}
execve(parse_file(cmd), parse_arg(cmd), shell_env);
} else {
wait(0);
}
System 的行为一旦丰富起来,坑就多了
$ echo hello > /etc/a.txt
bash: /etc/a.txt: Permission denied
$ sudo echo hello > /etc/a.txt
bash: /etc/a.txt: Permission denied
./hello | wc -l
的实现pipe(2): 在操作系统中创建一个管道对象和它的读/写口
pipefd[0]
- read end;pipefd[1]
- write endRTFSC: xv6-sh.c
如果你仔细想一想,Ctrl-C 似乎很复杂!
Ctrl-C 并不能杀掉所有程序
Ctrl-C 是在程序运行的时候按下的
read(STDIN_FILENO, buf, size);
read(STDIN_FILENO, buf, size);
涉及若干操作系统中的对象
echo hello > $(tty)
(我们甚至可以向其他 tty 打印)tty 向前台的进程组 (process group) 发送 SIGINT
信号
操作系统内异步地通知进程的机制
ANSI C signal handling
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
为编号为 signum
的信号设置处理程序 handler
SIG_IGN
(ignore), SIG_DFL
(default), 或是一个函数的地址使用 kill 系统调用发送信号
kill -SIGNAME pid
SIGINT
, SIGQUIT
, ...SIGKILL
(9) 是不可忽略的信号 (强行终止,因此不推荐)大家熟悉的 Segmentation Fault/Floating point exception (core dumped)
UNIX (System V) 信号其实是有一些 dark corners 的
SIGSEGV
里再次 SIGSEGV
?sigaction(2)
, which provides explicit control of the semantics when a signal handler is invoked; use that interface instead of signal()
. 如果一个进程 fork()
了很多份
Shell 的一个重要功能: Job Control
任务管理
jobs
: 查看 (+
: most current job; -
: previous job)kill %1
杀掉 job (/bin/kill %1
就不行,为什么?)SIGINT
前台/后台运行
./a.out &
创建 job 后台运行bg
/fg
可以切换前台后台执行RTFM: setpgid/getpgid(2),它解释了 process group, session, controlling terminal 之间的关系。
./a.out &
创建新的进程组 (使用 setpgid)SIGINT
), that signal is sent to the foreground process group.SIGTTIN
signal, which suspends it.cat &
时你看到的 “suspended (tty input)”setpgid()
and getpgrp()
calls are used by programs such as bash(1) to create process groups in order to implement shell job control.ps -eo pid,pgid,cmd
可以查看进程的 pgid理解终端和 Shell
Take-away messages
./a.out
的执行a.out