0%

CS15-213labs-shell-lab(上)

内容

实现一个 Linux Shell

主要功能:

  • 执行二进制文件
  • 作业控制(前后台切换, 前后台运行, 显示当前所有作业)
  • 支持简单的信号(SIGINT, SIGTSTP, SIGCHLD, SIGQUIT)
  • IO重定向(stdin, stdout)

完整代码: code

前置知识

我会将此次 lab 涉及到的一些核心知识进行简单的阐述

  • Linux 信号
  • io 重定向
  • Linux 进程

Signal

定义

维基百科: 在计算机科学中,信号(英语:Signals)是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。

它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

信号类似于中断,不同之处在于中断由处理器调解并由内核处理,而信号由内核调解(可能通过系统调用)并由进程处理。内核可以将中断作为信号传递给导致中断的进程(典型的例子有SIGSEGV、SIGBUS、SIGILL和SIGFPE)。

所有的普通信号可以通过 kill-l 查看

1
2
ardxwe@ardxwe ~$ kill -l                                                       
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS

编号 1 - 31 是普通信号, 32 - 64 是实时信号, 普通信号不支持排队, 可能发生的情况是发送多次信号, 进程只收到了一次, 而实时信号支持排队, 保证所有的信号都将被处理而不会丢失, 通常用于用户。

1
2
3
#include <signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

可以通过 signal 函数向信号 signum 绑定处理函数, 在执行某一个信号的信号处理函数期间, 收到同样的信号, 此信号会被阻塞。也就是不会被处理。

阻塞信号

信号在内核中分为四类:

  • 信号递达 (delivery) 实际执行信号处理信号的动作。
  • 信号未决 (pending) 信号从产生到抵达之间的状态,信号产生了但是未处理。
  • 忽略,抵达之后的一种动作。
  • 阻塞 (block) 收到信号不立即处理,被阻塞的信号将保持未决状态,直到进程解除对此信号的阻塞,才执行抵达动作。

每个信号都由两个标志位分别表示阻塞和未决,以及一个函数指针表示信号的处理动作。

信号内核实现

  • SIGHUP 未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT 信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT 信号未产生过,一旦产生 SIGQUIT 信号将被阻塞,它的处理动作是用户自定义函数 sighandler。

信号产生但是不立即处理,前提条件是要把它保存在 pending 表中,表明信号已经产生。

也就是说, 内核根据信号对应的 block 和 pending 和 handle 来判断。

block pending signal comes
0 0 handle
1 0 pending = 1
1 1 throw away

信号捕捉

如果信号的处理动作是⽤户⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。

内核捕捉信号

信号集操作函数

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
#include <signal.h>
/* 所有信号的对应位清0 */
int sigemptyset(sigset_t *set);

/* 设置所有的信号,包括系统支持的所有信号 */
int sigfillset(sigset_t *set);

/* 在该信号集中添加有效信号 */
int sigaddset(sigset_t *set, int signo);

/* 在该信号集中删除有效信号 */
int sigdelset(sigset_t *set, int signo);

/* 判断一个信号集的有效信号中是否包含某种信号 */
int sigismember(const sigset_t *set, int signo);

/*
* how:
* SIG_BLOCK 信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集,set包含了希望阻塞的信号
* SIG_UNBLOCK 信号屏蔽字是其当前信号屏蔽字和set所指向信号集补集的交集,set包含了希望解除阻塞的信号
* SIG_SETMASK 信号屏蔽字将被set指向的信号集的值代替
*/
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

/*sigpending读取当前进程的未决信号集,通过set参数传出 */
int sigpending(sigset_t *set);

一些例子

first

验证两点:

  • 当 SIGINT 被阻塞时, 来了一个 SIGINT 只会置 pending 为 1
  • 阻塞解除会执行 siginthandle 一次
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
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>

void printsigset(const sigset_t *pset)
{
for (int i = 0; i < 64; i++)
{
if (sigismember(pset, i + 1))
putchar('1');
else
putchar('0');
fflush(stdout);
}
printf("\n");
fflush(stdout);
}

void siginthandle(int sig) {
printf("sigint handle call\n");
sleep(1);
return;
}

int main() {
signal(SIGINT, siginthandle);

sigset_t sigint, prev;
sigemptyset(&sigint);
sigaddset(&sigint, SIGINT);
sigprocmask(SIG_BLOCK, &sigint, &prev);

int count = 1;
while(true) {
sigset_t now, pending;

printf("block set:");
fflush(stdout);
sigprocmask(SIG_BLOCK, NULL, &now);
printsigset(&now);
sigpending(&pending);
printf("pending set:");
fflush(stdout);
printsigset(&pending);
sleep(1);
count++;
if (count == 5) {
sigprocmask(SIG_SETMASK, &prev, NULL);
}
else if (count == 10) {
break;
}
}
return 0;
}

先阻塞 SIGINT 4秒, 每秒打印 block 和 pending, 然后解除 SIGINT 阻塞

本地运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ardxwe@ardxwe ~/github/CS15-213labs/shlab-handout/signaltest$ ./signal
block set:0100000000000000000000000000000000000000000000000000000000000000
pending set:0000000000000000000000000000000000000000000000000000000000000000
block set:0100000000000000000000000000000000000000000000000000000000000000
pending set:0000000000000000000000000000000000000000000000000000000000000000
^Cblock set:0100000000000000000000000000000000000000000000000000000000000000
pending set:0100000000000000000000000000000000000000000000000000000000000000
^C^Cblock set:0100000000000000000000000000000000000000000000000000000000000000
pending set:0100000000000000000000000000000000000000000000000000000000000000
sigint handle call
block set:0000000000000000000000000000000000000000000000000000000000000000
pending set:0000000000000000000000000000000000000000000000000000000000000000
block set:0000000000000000000000000000000000000000000000000000000000000000
pending set:0000000000000000000000000000000000000000000000000000000000000000
block set:0000000000000000000000000000000000000000000000000000000000000000
pending set:0000000000000000000000000000000000000000000000000000000000000000
block set:0000000000000000000000000000000000000000000000000000000000000000
pending set:0000000000000000000000000000000000000000000000000000000000000000
block set:0000000000000000000000000000000000000000000000000000000000000000
pending set:0000000000000000000000000000000000000000000000000000000000000000

第二次打印结束的时候, 输入 CTRL+C, pending 对应位变为 1, 之后还输入了两次 CTRL+C, pending 不变, 解除阻塞时执行一次处理函数, 非常自然。

second

验证:

  • 系统调用会被信号打断
  • 如果当前正在执行信号一的处理函数, 此时信号二来是有可能打断信号一处理函数, 去执行信号二处理函数的, 类似于中断嵌套
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
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdbool.h>


void siginthandle(int sig) {
printf("sigint handle call\n");
unsigned int rs = sleep(5);
printf("remain seconds: %d\n", rs);
return;
}

void sigtstphandle(int sig) {
printf("sigtstp handle call\n");
sleep(1);
return;
}

int main() {
signal(SIGINT, siginthandle);
signal(SIGTSTP, sigtstphandle);

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTSTP);

unsigned int rs = sleep(5);
printf("main routine remain seconds: %d\n", rs);
return 0;
}

注册 SIGINT SIGTSTP 处理函数, 主程序 sleep

结果:

1
2
3
4
5
ardxwe@ardxwe ~/github/CS15-213labs/shlab-handout/signaltest$ ./breaksyscl
^Csigint handle call
^Zsigtstp handle call
remain seconds: 4
main routine remain seconds: 3

IO重定向

Linux文件描述符原理

0 代表 标准输入
1 代表 标准输出
2 代表 标准错误

1
2
3
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);

这些系统调用创建文件描述符 oldfd 的副本

dup 使用编号最小的未使用描述符作为新描述符

dup2 使newfd是oldfd的副本,如果有必要, 关闭 newfd

创建进程时, 0, 1, 2 已经指向对应的打开文件表:

文件描述符初始状态

当进程执行 dup(1) :

执行dup(1)之后

执行 打开一个文件时, 指向一个新的打开文件表 :

打开文件

此时 dup2(filefd, 1):

IO重定向

之后向标准输出写就是往文件写, 这就是 io重定向的原理

进程

定义

维基百科: 进程(英语:process),是指计算机中已运行的程序。进程曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。现代计算机系统可在同一段时间内以进程的形式将多个程序加载到存储器中,并借由时间共享(或称时分复用),以在一个处理器上表现出同时(平行性)运行的感觉。同样的,使用多线程技术(多线程即每一个线程都代表一个进程内的一个独立执行上下文)的操作系统或计算机体系结构,同样程序的平行线程,可在多CPU主机或网络上真正同时运行(在不同的CPU上)。

简单说, 当我们运行一个可执行文件 ./a.out 时, 操作系统就创建了进程

fork

在 Linux 中, 当我们调用 fork, 操作系统帮我们产生一个新的进程, 和原先的进程几乎完全相同

原先进程称为父进程, 派生的进程称为子进程

Linux 还采用了写时复制, 当子进程创建, 并不会完全拷贝内存, 而是共享内存页, 只有当父子进程写的时候, 才会给子进程分配新的内存页, 然后才会执行对应的写操作

fork

父子进程共享内存, 当然也共享代码段, 父子进程如何执行代码段?

检查 fork 返回值, 子进程返回 0, 父进程返回子进程 pid, 这样做的理由是, 子进程可以调用 getppid 获取父进程 pid, 一个进程只能有一个父进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pid_t pid;

if((pid=fork()) < 0) {
printf("fork error\n");
return -1;
}
else if (pid == 0) {
/* child process */
...
}
else {
/* father process */
...
}

有了这些前置知识, shell 的实现就会很容易, 见下章

参考:

https://gohalo.me/post/kernel-signal-introduce.html

http://c.biancheng.net/view/3066.html

https://zh.wikipedia.org/wiki/%E8%A1%8C%E7%A8%8B

https://zh.wikipedia.org/wiki/Signal_(%E8%BB%9F%E4%BB%B6)