NJUOS-13-系统调用和Shell

本文最后更新于:2 年前

这一节会比较有趣昂!Unix Shell和这些东西联系起来啦!!!

Shell

image-20221227104925319

这就是 Shell (内核 Kernel 提供系统调用;Shell 提供用户接口)

  • “与人类直接交互的第一个程序”
  • 帮助人类创建/管理进程 (应用程序)、数据文件……

The UNIX Shell

img

img

“终端” 时代的伟大设计

  • “Command-line interface” (CLI) 的巅峰

Shell 是一门 “把用户指令翻译成系统调用” 的编程语言

  • man sh (推荐阅读!), bash, …
  • 原来我们一直在编程
    • 直到有了 Graphical Shell (GUI)
    • Windows, Gnome, Symbian, Android

脾气有点小古怪的 UNIX 世界

“Unix is user-friendly; it’s just choosy about who its friends are.”

  • 但如果把 shell 理解成编程语言,“不好用” 好像也没什么毛病了
    • 你见过哪个编程语言 “好用” 的?

img

(UNIX 世界有很多历史遗留约定)

A Zero-dependency UNIX Shell (from xv6)

sh-xv6.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
// Linux port of xv6-riscv shell (no libc)
// Compile with "-ffreestanding"!

#include <fcntl.h>
#include <stdarg.h>
#include <stddef.h>
#include <sys/syscall.h>

// Parsed command representation
enum { EXEC = 1, REDIR, PIPE, LIST, BACK };

#define MAXARGS 10
#define NULL ((void *)0)

struct cmd {
int type;
};

struct execcmd {
int type;
char *argv[MAXARGS], *eargv[MAXARGS];
};

struct redircmd {
int type, fd, mode;
char *file, *efile;
struct cmd* cmd;
};

struct pipecmd {
int type;
struct cmd *left, *right;
};

struct listcmd {
int type;
struct cmd *left, *right;
};

struct backcmd {
int type;
struct cmd* cmd;
};

struct cmd* parsecmd(char*);

// Minimum runtime library
long syscall(int num, ...) {
va_list ap;
va_start(ap, num);
register long a0 asm ("rax") = num;
register long a1 asm ("rdi") = va_arg(ap, long);
register long a2 asm ("rsi") = va_arg(ap, long);
register long a3 asm ("rdx") = va_arg(ap, long);
register long a4 asm ("r10") = va_arg(ap, long);
va_end(ap);
asm volatile("syscall"
: "+r"(a0) : "r"(a1), "r"(a2), "r"(a3), "r"(a4)
: "memory", "rcx", "r8", "r9", "r11");
return a0;
}

size_t strlen(const char *s) {
size_t len = 0;
for (; *s; s++) len++;
return len;
}

char *strchr(const char *s, int c) {
for (; *s; s++) {
if (*s == c) return (char *)s;
}
return NULL;
}

void print(const char *s, ...) {
va_list ap;
va_start(ap, s);
while (s) {
syscall(SYS_write, 2, s, strlen(s));
s = va_arg(ap, const char *);
}
va_end(ap);
}

#define assert(cond) \
do { if (!(cond)) { \
print("Panicked.\n", NULL); \
syscall(SYS_exit, 1); } \
} while (0)

static char mem[4096], *freem = mem;

void *zalloc(size_t sz) {
assert(freem + sz < mem + sizeof(mem));
void *ret = freem;
freem += sz;
return ret;
}

// Execute cmd. Never returns.
void runcmd(struct cmd* cmd) {
int p[2];
struct backcmd* bcmd;
struct execcmd* ecmd;
struct listcmd* lcmd;
struct pipecmd* pcmd;
struct redircmd* rcmd;

if (cmd == 0) syscall(SYS_exit, 1);

switch (cmd->type) {
case EXEC:
ecmd = (struct execcmd*)cmd;
if (ecmd->argv[0] == 0) syscall(SYS_exit, 1);
syscall(SYS_execve, ecmd->argv[0], ecmd->argv, NULL);
print("fail to exec ", ecmd->argv[0], "\n", NULL);
break;

case REDIR:
rcmd = (struct redircmd*)cmd;
syscall(SYS_close, rcmd->fd);
if (syscall(SYS_open, rcmd->file, rcmd->mode, 0644) < 0) {
print("fail to open ", rcmd->file, "\n", NULL);
syscall(SYS_exit, 1);
}
runcmd(rcmd->cmd);
break;

case LIST:
lcmd = (struct listcmd*)cmd;
if (syscall(SYS_fork) == 0) runcmd(lcmd->left);
syscall(SYS_wait4, -1, 0, 0, 0);
runcmd(lcmd->right);
break;

case PIPE:
pcmd = (struct pipecmd*)cmd;
assert(syscall(SYS_pipe, p) >= 0);
if (syscall(SYS_fork) == 0) {
syscall(SYS_close, 1);
syscall(SYS_dup, p[1]);
syscall(SYS_close, p[0]);
syscall(SYS_close, p[1]);
runcmd(pcmd->left);
}
if (syscall(SYS_fork) == 0) {
syscall(SYS_close, 0);
syscall(SYS_dup, p[0]);
syscall(SYS_close, p[0]);
syscall(SYS_close, p[1]);
runcmd(pcmd->right);
}
syscall(SYS_close, p[0]);
syscall(SYS_close, p[1]);
syscall(SYS_wait4, -1, 0, 0, 0);
syscall(SYS_wait4, -1, 0, 0, 0);
break;

case BACK:
bcmd = (struct backcmd*)cmd;
if (syscall(SYS_fork) == 0) runcmd(bcmd->cmd);
break;

default:
assert(0);
}
syscall(SYS_exit, 0);
}

int getcmd(char* buf, int nbuf) {
print("> ", NULL);
for (int i = 0; i < nbuf; i++) buf[i] = '\0';

while (nbuf-- > 1) {
// 就是用最基本的系统调用,而不调用别的库了!!!
int nread = syscall(SYS_read, 0, buf, 1);
if (nread <= 0) return -1;
if (*(buf++) == '\n') break;
}
return 0;
}

void _start() {
static char buf[100];

// Read and run input commands.
while (getcmd(buf, sizeof(buf)) >= 0) {
if (buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' ') {
// Chdir must be called by the parent, not the child.
buf[strlen(buf) - 1] = 0; // chop \n
if (syscall(SYS_chdir, buf + 3) < 0) print("cannot cd ", buf + 3, "\n", NULL);
continue;
}
if (syscall(SYS_fork) == 0) runcmd(parsecmd(buf));
syscall(SYS_wait4, -1, 0, 0, 0);
}
syscall(SYS_exit, 0);
}

// Constructors

struct cmd* execcmd(void) {
struct execcmd* cmd;

cmd = zalloc(sizeof(*cmd));
cmd->type = EXEC;
return (struct cmd*)cmd;
}

struct cmd* redircmd(struct cmd* subcmd, char* file, char* efile, int mode,
int fd) {
struct redircmd* cmd;

cmd = zalloc(sizeof(*cmd));
cmd->type = REDIR;
cmd->cmd = subcmd;
cmd->file = file;
cmd->efile = efile;
cmd->mode = mode;
cmd->fd = fd;
return (struct cmd*)cmd;
}

struct cmd* pipecmd(struct cmd* left, struct cmd* right) {
struct pipecmd* cmd;

cmd = zalloc(sizeof(*cmd));
cmd->type = PIPE;
cmd->left = left;
cmd->right = right;
return (struct cmd*)cmd;
}

struct cmd* listcmd(struct cmd* left, struct cmd* right) {
struct listcmd* cmd;

cmd = zalloc(sizeof(*cmd));
cmd->type = LIST;
cmd->left = left;
cmd->right = right;
return (struct cmd*)cmd;
}

struct cmd* backcmd(struct cmd* subcmd) {
struct backcmd* cmd;

cmd = zalloc(sizeof(*cmd));
cmd->type = BACK;
cmd->cmd = subcmd;
return (struct cmd*)cmd;
}

// Parsing

char whitespace[] = " \t\r\n\v";
char symbols[] = "<|>&;()";

int gettoken(char** ps, char* es, char** q, char** eq) {
char* s;
int ret;

s = *ps;
while (s < es && strchr(whitespace, *s)) s++;
if (q) *q = s;
ret = *s;
switch (*s) {
case 0:
break;
case '|': case '(': case ')': case ';': case '&': case '<':
s++;
break;
case '>':
s++;
if (*s == '>') {
ret = '+'; s++;
}
break;
default:
ret = 'a';
while (s < es && !strchr(whitespace, *s) && !strchr(symbols, *s)) s++;
break;
}
if (eq) *eq = s;

while (s < es && strchr(whitespace, *s)) s++;
*ps = s;
return ret;
}

int peek(char** ps, char* es, char* toks) {
char* s;

s = *ps;
while (s < es && strchr(whitespace, *s)) s++;
*ps = s;
return *s && strchr(toks, *s);
}

struct cmd* parseline(char**, char*);
struct cmd* parsepipe(char**, char*);
struct cmd* parseexec(char**, char*);
struct cmd* nulterminate(struct cmd*);

struct cmd* parsecmd(char* s) {
char* es;
struct cmd* cmd;

es = s + strlen(s);
cmd = parseline(&s, es);
peek(&s, es, "");
assert(s == es);
nulterminate(cmd);
return cmd;
}

struct cmd* parseline(char** ps, char* es) {
struct cmd* cmd;

cmd = parsepipe(ps, es);
while (peek(ps, es, "&")) {
gettoken(ps, es, 0, 0);
cmd = backcmd(cmd);
}
if (peek(ps, es, ";")) {
gettoken(ps, es, 0, 0);
cmd = listcmd(cmd, parseline(ps, es));
}
return cmd;
}

struct cmd* parsepipe(char** ps, char* es) {
struct cmd* cmd;

cmd = parseexec(ps, es);
if (peek(ps, es, "|")) {
gettoken(ps, es, 0, 0);
cmd = pipecmd(cmd, parsepipe(ps, es));
}
return cmd;
}

struct cmd* parseredirs(struct cmd* cmd, char** ps, char* es) {
int tok;
char *q, *eq;

while (peek(ps, es, "<>")) {
tok = gettoken(ps, es, 0, 0);
assert(gettoken(ps, es, &q, &eq) == 'a');
switch (tok) {
case '<':
cmd = redircmd(cmd, q, eq, O_RDONLY, 0);
break;
case '>':
cmd = redircmd(cmd, q, eq, O_WRONLY | O_CREAT | O_TRUNC, 1);
break;
case '+': // >>
cmd = redircmd(cmd, q, eq, O_WRONLY | O_CREAT, 1);
break;
}
}
return cmd;
}

struct cmd* parseblock(char** ps, char* es) {
struct cmd* cmd;

assert(peek(ps, es, "("));
gettoken(ps, es, 0, 0);
cmd = parseline(ps, es);
assert(peek(ps, es, ")"));
gettoken(ps, es, 0, 0);
cmd = parseredirs(cmd, ps, es);
return cmd;
}

struct cmd* parseexec(char** ps, char* es) {
char *q, *eq;
int tok, argc;
struct execcmd* cmd;
struct cmd* ret;

if (peek(ps, es, "(")) return parseblock(ps, es);

ret = execcmd();
cmd = (struct execcmd*)ret;

argc = 0;
ret = parseredirs(ret, ps, es);
while (!peek(ps, es, "|)&;")) {
if ((tok = gettoken(ps, es, &q, &eq)) == 0) break;
assert(tok == 'a');
cmd->argv[argc] = q;
cmd->eargv[argc] = eq;
assert(++argc < MAXARGS);
ret = parseredirs(ret, ps, es);
}
cmd->argv[argc] = 0;
cmd->eargv[argc] = 0;
return ret;
}

// NUL-terminate all the counted strings.
struct cmd* nulterminate(struct cmd* cmd) {
int i;
struct backcmd* bcmd;
struct execcmd* ecmd;
struct listcmd* lcmd;
struct pipecmd* pcmd;
struct redircmd* rcmd;

if (cmd == 0) return 0;

switch (cmd->type) {
case EXEC:
ecmd = (struct execcmd*)cmd;
for (i = 0; ecmd->argv[i]; i++) *ecmd->eargv[i] = 0;
break;

case REDIR:
rcmd = (struct redircmd*)cmd;
nulterminate(rcmd->cmd);
*rcmd->efile = 0;
break;

case PIPE:
pcmd = (struct pipecmd*)cmd;
nulterminate(pcmd->left);
nulterminate(pcmd->right);
break;

case LIST:
lcmd = (struct listcmd*)cmd;
nulterminate(lcmd->left);
nulterminate(lcmd->right);
break;

case BACK:
bcmd = (struct backcmd*)cmd;
nulterminate(bcmd->cmd);
break;
}
return cmd;
}

  • 零库函数依赖 (-ffreestanding 编译、ld 链接)
  • 可以作为最小 Linux 的 init 程序
  • 用到文件描述符:一个打开文件的 “指针”

上面这个编译好了之后,是直接可以跑的昂!!!

上面这些代码写的十分精巧,下去要读一读昂!!!


支持的功能:

  • 命令执行 ls
  • 重定向 ls > a.txt
  • 管道 ls | wc -l
  • 后台 ls &
  • 命令组合 (echo a ; echo b) | wc -l

A Zero-dependency UNIX Shell (from xv6)

img

我们应该如何阅读 sh-xv6.c 的代码? -> 这里以fork()为例子

  • strace + gdb!
    • set follow-fork-mode, set follow-exec-mode

Pipe的实现理解

  • 表达式求值:

image-20221227111733215

  • shell解析:

image-20221227111849691

这里的echo可能涉及系统调用啊,list;,这里的;又涉及到执行顺序。语法树中,先执行左边的语法树,再执行右边的语法树。可能还会涉及到编译原理相关的一些内容。

Pipe的形式语义 & 实现

image-20221227112506638

image-20221227112756432

1是管道的读口,0是管道的写口。

image-20221227112945829

执行fork(),会完整把进程状态拷贝一份(包括这里的文件描述符,意味着,父子进程其实共享了一份管道。管道不会被复制一份哈!!!因为fork()完全拷贝的是进程的状态和享有的资源(例如printf缓冲区),而pipe是操作系统的资源,因此复制的时候,只能复制进程中的对应的管道创建的文件描述符昂!!!)

下面有点代码的解析,首先这里有几个概念:0 -> stdin,1 -> stdout, p[0] -> 管道输入,p[1] -> 管道输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
case PIPE:
// 创建一个管道
pcmd = (struct pipecmd*)cmd;
assert(syscall(SYS_pipe, p) >= 0);
// 子进程
if (syscall(SYS_fork) == 0) {
// 把编号为1的文件描述符给关掉了(也就是刚刚的stdout被关掉了)
syscall(SYS_close, 1);
// 把刚刚关掉的stdout,指向了p[1],也就是管道的写口
syscall(SYS_dup, p[1]);
// 把p[0]关掉
syscall(SYS_close, p[0]);
// 把p[1]关掉
syscall(SYS_close, p[1]);
// 其实相当于把子进程的输出接入管道的输入,其他的文件描述符都关掉啦!!!
runcmd(pcmd->left);
}

// 下面这里是类似的,把管道的写口导向到进程的stdin。然后把这个进程中,多余的文件描述符都关掉啦!!!
if (syscall(SYS_fork) == 0) {
syscall(SYS_close, 0);
syscall(SYS_dup, p[0]);
syscall(SYS_close, p[0]);
syscall(SYS_close, p[1]);
runcmd(pcmd->right);
}
syscall(SYS_close, p[0]);
syscall(SYS_close, p[1]);
syscall(SYS_wait4, -1, 0, 0, 0);
syscall(SYS_wait4, -1, 0, 0, 0);
break;

image-20221227115333229

  1. 子进程执行管道左边的指令,并且把输入塞进管道。
  2. 子进程执行管道右边的指令,取出管道中的内容,并计算得出结果。
  3. 触发管道操作本身的父进程,wait上面两个子进程的结束,这就是管道!!!优雅干净!!!
  • 本质还是上面形式语义的那一棵树树!!!|变成了管道,左右两边的命令变成了子进程执行而已昂,通过创建的管道进行了进程间的同步!!!

关键点

  • 命令的执行、重定向、管道和对应的系统调用
  • 这里用到 minimal.S 会简化输出
1
echo './a.out > /tmp/a.txt' | strace -f ./sh
  • 还可以用管道过滤不想要的系统调用

如何理解底层源码调用

  1. 我们应该如何阅读 sh-xv6.c 的代码? -> 这里以fork()为例子
    • strace + gdb!
      • set follow-fork-mode, set follow-exec-mode

一步一步跟着走

  1. 系统调用追踪

Strace -f -o /tmp/strace.log ./sh

Tail -f /tmp/strace.log

image-20221227120511392

  • 仔细看看执行命令的时候,系统调用时如何运行的,也会对于我们理解指令底层的执行,有更好的帮助昂!!!
  • 小技巧:如何想要系统调用的序列干净一点,可以把所有的进程绑定到同一个CPU上(Shell),干净!!!

The Shell Programming Language

基于文本替换的快速工作流搭建

  • 重定向: cmd > file < file 2> /dev/null
  • 顺序结构: cmd1; cmd2, cmd1 && cmd2, cmd1 || cmd2
  • 管道: cmd1 | cmd2
  • 预处理: $(), <()
  • 变量/环境变量、控制流……

Job control

  • 类比窗口管理器里的 “叉”、“最小化”
    • jobs, fg, bg, wait
    • (今天的 GUI 并没有比 CLI 多做太多事)

UNIX Shell: Traps and Pitfalls

img

在 “自然语言”、“机器语言” 和 “1970s 的算力” 之间达到优雅的平衡

  • 平衡意味着并不总是完美

  • 操作的 “优先级”?
    • ls > a.txt | cat (bash/zsh)
  • 文本数据 “责任自负”
    • 有空格?后果自负!(PowerShell: 我有 object stream pipe 啊喂) -> touch “a b.txt” -> vim a b.txt -> 寄了,同时操作两个文件,OS操作的时候,当成两个文件orz。Unix的设计里面,空格会具有二义性,既可能是命令的分割,也可能是文件的分割,这是“自然语言的缺陷!!!”,本身我们也会引发歧义昂,不是操作系统的问题昂!!!
  • 行为并不总是 intuitive
1
2
3
4
5
$ echo hello > /etc/a.txt
bash: /etc/a.txt: Permission denied
$ sudo echo hello > /etc/a.txt
bash: /etc/a.txt: Permission denied
# 先执行echo hello > /etc/a.txt,再去执行sudo,导致出现问题!!!执行命令的时候,还没有获得sudo的权限昂!!!

Unix Shell在做啥?

命令 -> 一颗树(Semantics) -> 根据这棵树,执行系统调用!!!

展望未来

Open question: 我们能否从根本上改变命令行的交互模式?

Shell 连接了用户和操作系统

  • 是 “自然语言”、“机器语言” 之间的边缘地带!
  • 非常适合 BERT 这样的语言模型

已经看到的一些方向

终端和 Job Control

Shell 还有一些未解之谜

img

为什么 Ctrl-C 可以退出程序?

为什么有些程序又不能退出?

  • 没有人 read 这个按键,为什么进程能退出?
  • Ctrl-C 到底是杀掉一个,还是杀掉全部?
    • 如果我 fork 了一份计算任务呢?
    • 如果我 fork-execve 了一个 shell 呢?
      • Hmmm……

为什么 fork-printf.c 会在管道时有不同表现?

  • libc 到底是根据什么调整了缓冲区的行为?

为什么 Tmux 可以管理多个窗口?

  • tmux是怎么实现的呢?

答案:终端

终端是 UNIX 操作系统中一类非常特别的设备!

  • RTFM: tty, stty, …

img

tmux开的每一个新的端口,都是一个终端!!!

image-20221228115554210

虽然三个窗口在一个终端上,但是其实有三个状态!!!

有意思的操作!!!!

  • 在tty1 -> echo hello > /dev/pts/2。在tty1中将hello打印到第二个终端上。
  • 甚至可以vim /dev/pts/2,编辑终端!!!,:%!ls,然后保存到vim中。实质上保存过程中,vim就会将数据写入我们的设备中。

观察 Tmux 的实现

img

首先,我们可以 “使用” tmux

  • 在多个窗口中执行 tty,会看到它们是不同的终端设备!

然后,我们也可以把 tmux “打开”

  • strace (-o) 可以看到一些关键的系统调用 (以及 man 7 pty)

tmux把你按键的信息捕捉下来,并且转发给对应的terminal,就可以实现了昂!!!

终端相关的 API

为什么 fork-printf 能识别 tty 和管道?

  • 当然是观察 strace 了!
    • 找到是哪个系统调用 “识别” 出了终端?

1
2
3
4
5
#include <stdio.h>

int main() {
printf("Hello, World\n");
}

image-20221228120508216

通过fstat系统调用,查看是不是终端!!!

Session, Process Group 和信号

img

参考 signal-handler.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handler(int signum) {
switch (signum) {
case SIGINT:
printf("Received SIGINT!\n");
break;
case SIGQUIT:
printf("Received SIGQUIT!\n");
exit(0);
break;
}
}

void cleanup() {
printf("atexit() cleanup\n");
}

int main() {
signal(SIGINT, handler);
signal(SIGQUIT, handler);
atexit(cleanup);

while (1) {
char buf[4096];
int nread = read(STDIN_FILENO, buf, sizeof(buf));
buf[nread - 1] = '\0';
printf("[%d] Got: %s\n", getpid(), buf);
if (nread < 0) {
perror("read");
exit(1);
}
sleep(1);
}
}

ctrl + c,本质上我们终端会给我们的前台进程发送一个信号,进程收到这个信号后,可以有相应的信号的handler。(ctrl + c -> SIGINT, ctrl + / -> SIGQUIT)。

如果进程没有自定义handler的话,SIGINT默认对应的就是退出!

上面的程序会从标准输入里面读入数据,并且打印出来。ctrl + c没有用!!!

image-20221228121014810

终端给进程发信号!!!

  • 如果fork拷贝了一份呢??? -> 对应同一个外设tty,对于多个进程,怎么去发送SIGINT等信号呢(是不是应该把所有的fork(),都发送一次呢???)

image-20221228121251900

  • 任何时候,只能有一个前台的进程组(Terminal里面),因此ctrl + c杀不掉后台的进程。
  • 如果我们启动一个shell,会启动一个session
  • 一个session里面,有很多“进程组”的概念。进程组可以放前台,可以放后台。无论fork多少个进程,所有的进程都是属于一个进程组(Process Group ID是一样的)!
  • ctrl + c或者ctrl + /等terminal信号,会发送给前台进程组里所有的进程。(这样就可以一起退出)
  • 有些进程可以拒绝退出,转为后台执行(收到SIGINT之后,自己Handle就可以了嘛,本质上就是:

img

SIGSEGV 和 SIGFPE

大家熟悉的 Segmentation Fault/Floating point exception (core dumped)

  • #GP, #PF 或 #DIV
    • UNIX 系统会给进程发送一个信号
    • 此时可以生成一个 “core” 文件 (ELF 格式),能用 gdb 调试

UNIX (System V) 信号其实是有一些 dark corners 的

  • 如果SIGSEGV里再次SIGSEGV?
    • POSIX.1 solved the portability mess by specifying sigaction(2), which provides explicit control of the semantics when a signal handler is invoked; use that interface instead of signal()
      • 支持多线程 (早期的 UNIX 还没有多线程)、信号屏蔽、……

Job Control 背后的机制

RTFM: setpgid/getpgid(2),它解释了 process group, session, controlling terminal 之间的关系

$ man setpgid

——你神奇地发现,读手册不再是障碍了!

  • The PGID (process-group ID) is preserved across an execve(2) and inherited in fork(2)…
  • Each process group is a member of a session

img

Job Control: RTFM (cont’d)

  • A session can have a controlling terminal
    • At any time, one (and only one) of the process groups in the session can be the foreground process group for the terminal; the remaining process groups are in the background.
      • ./a.out & 创建新的进程组 (使用 setpgid)
    • If a signal is generated from the terminal (e.g., typing the interrupt key to generate SIGINT), that signal is sent to the foreground process group.
      • Ctrl-C 是终端 (设备) 发的信号,发给 foreground 进程组
      • 所有 fork 出的进程 (默认同一个 PGID) 都会收到信号
      • 可以修改 signal-handler.c 观察到这个行为

Job Control: RTFM (cont’d)

  • Only the foreground process group may read(2) from the terminal; if a background process group tries to read(2) from the terminal, then the group is sent a SIGTTIN signal, which suspends it.
    • 这解释了 cat & 时你看到的 “suspended (tty input)”
    • 同一个进程组的进程 read tty 会竞争
    • signal-handler.c 同样可以观察到这个行为

  • The setpgid() and getpgrp() calls are used by programs such as bash(1) to create process groups in order to implement shell job control.
    • 如果希望从进程组里 detach, 使用 setpgid
    • ps -eo pid,pgid,cmd 可以查看进程的 pgid

shell在执行的时候,可以通过fg命令,把controlling terminal交给某个前台进程,由进程执行,这个时候,shell程序就不能接受来自用户的输入了,转为这个前台进程使用controlling terminal。

References

  1. 不怕读手册:man sh
  2. shell可以直接把指令丢到后台运行,jobs可以看到其中的内容昂!!!

image-20221228110548122

1970年代的Shell就很酷啦。

  1. tmux也可以完成类似的功能昂!!!把任务放在后台执行昂!!!
  2. 重定向和管道啊之类,多种命令的组合的优先级,在不同的shell之间,可能是不一样的昂!!!(bash和zsh就不一样昂!!!)
  3. tty可以查看当前终端
  4. man setpgid , get-bid

NJUOS-13-系统调用和Shell
https://alexanderliu-creator.github.io/2022/12/27/njuos-13-xi-tong-diao-yong-he-shell/
作者
Alexander Liu
发布于
2022年12月27日
许可协议