Shell

$ ls

Shell本质上是一个用户态程序,运行于内核之上,可以与用户进行交互。当用户输入一些命令后,shell会对这个输入进行解析,找到对应的可执行文件并执行,然后显示输出。

shell基本工作原理

命令执行

shell的基本工作原理可以用下图概括:当shell运行后,会处在一个死循环中,等待用户输入命令;而一旦用户有命令输入,会调用fork产生一个子进程,父进程等待子进程完成,子进程调用exec替换内核镜像执行用户命令,结束后执行exit。

图片名称

管道命令

管道命令模型

利用管道组合而成的命令,实际上是构成了一棵命令树,例如当sh解析命令ls | (sort|tail -1)时,得到的命令树如下:

图片名称

实现

任务划分

现在我们来考虑如何实现管道功能,我们把整个任务划分为三个部分:

  • 创建管道
  • 左侧命令向管道中写入
  • 右侧命令从管道中读取

创建管道

我们使用pipe()函数创建管道,如果失败触发一个perror

1
2
3
4
if(pipe(p) < 0){
perror("create pipe error!");
_exit(-1);
}

处理左侧命令

创建管道后,我们开始处理左侧命令,我们首先创建了一个新的进程,然后将标准输出重定向至写管道,最后执行左侧命令

1
2
3
4
5
6
7
if(fork1() == 0){
close(1);
dup(p[1]); // 重定向stdout
close(p[0]);
close(p[1]);
runcmd(pcmd->left);
}

处理右侧命令

右侧命令类似,只不过方向是从读管道中读取

1
2
3
4
5
6
7
if(fork1() == 0){
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
runcmd(pcmd->right);
}

等待左右命令完成

最后我们的线程做一些收尾工作,等待左右命令完成

1
2
3
4
close(p[0]);
close(p[1]);
wait(&r);
wait(&r);

思考

根据上面的管道实现的过程,思考下面的问题:

  • 为何需要多次关闭pipe?
  • 为何要为左右命令都fork?能不能只为左侧或右侧命令创建进程?
  • 为何要在两个子进程都创建好之后才开始wait,能不能先创建一个wait一个再创建另一个wait另一个?
  • 当左右两侧读写速度不一致时会发生什么?如果左侧快会怎样?右侧快又会怎样?
  • 左右两侧的命令是如何决定何时退出的?
  • 如果右侧命令没有关闭写端,会发生什么?左侧命令没有关闭读端,又会发生什么?
  • 内核如何决定何时释放pipebuffer

为何需要多次关闭pipe?

每个进程的文件描述符表是相互独立的,所以每个进程都需要单独关闭自己的文件描述符。

为何要为左右命令都fork?能不能只为左侧或右侧命令创建进程?

根据管道命令构成的命令树我们可知,如果只为右侧命令创建进程,那么构成的命令树如下:

图片名称

这个显然是有问题的,shell会调用exec去执行左侧命令,如果不fork,那么意味着进程会执行runcmd(pcmd->left);,也不会返回,more命令不会有机会被执行,所以此时只有ls向管道中写入了命令,而没有命令从管道中读取。

而如果我们只为左侧命令创建进程,那么执行过程如下:

  • 先创建一个进程,子进程执行左侧命令
  • 父进程执行右侧命令,如果右侧命令还是带管道的命令,那么重复上述过程

看似没什么问题,执行起来也确实如此,但是这只是表象,如果我们执行下列命令

1
sleep 10 | echo hi

正常情况下,这个命令应该先打印hi,然后等待约10秒后退出,但是如果没有给右侧命令创建进程,那么会打印hi然后直接显示终端,没有睡眠10秒的过程,是因为右侧命令占用了我们的进程,改变了进程的执行分支,导致主进程没有办法执行wait指令,从而无法对子进程进行回收,直接抛弃了子进程继续执行而不是等待子进程结束后再显示终端。此时如果用ps ax查看,可以看到一个sleep 10的进程,说明左侧命令确实执行了,但是是以孤儿进程的方式执行的,父进程不再等待这个进程,任由其自生自灭。从上面两个实验我们可知,使用管道时,必须对左右命令单独创建子进程,然后主进程等待这两个命令执行完毕。

为何要在两个子进程都创建好之后才开始wait

如果我们先创建一个wait一个,再创建另一个wait另一个,这意味着我们明确规定管道左侧的命令先执行,等管道左侧命令完成后再执行管道右侧。如果左侧命令向管道中写入的数据不多,这是可以的。但是如果左侧命令向管道中写入大量数据直到写满了,而没有进程消耗这些数据,就会导致写堵塞,既写不进去,有没有进程读取数据,会导致程序被阻塞。

左右两侧读取速度不一致会发生什么?

根据pipe(7) - Linux manual page (man7.org)可知,如果试图往一个满的pipe写入,那么write会被阻塞,直到pipe被读取后有空间写入后才会继续执行;同理,如果试图读取空的pipe,read也会被阻塞,直到有数据可读。

左右两侧的命令如何决定何时退出?

管道只是起了重定向的作用,我认为命令的退出还是和命令本身的退出机制有关,例如ls,如果读取的文件结束并且输出也结束,那么ls就会退出。(这里还不是特别清楚)

如果右侧命令没有关闭写端,会发生什么?左侧命令没有关闭读端,又会发生什么?

  • 如果右侧(读)命令没有关闭写端,那么左侧命令执行完后会退出,右侧不会

因为右侧命令还会等待其他命令对管道的写入,直到发现管道写端也关闭后,才会结束读取的动作并退出。但是左侧命令没关写管道就结束了,留下右侧命令以为还有进程会写pipe,所以它会傻傻地等待。

  • 如果左侧(写)命令没有关闭读端,会发生什么?

这个情况比较复杂,首先要了解一个概念broken pipe

一个程序如果试图向一个没有任何读者的管道中写入内容,那么这个程序会收到一个SIGPIPE 信号,告诉这个程序别瞎忙活了,没有人读管道,然后这个程序就会结束写操作。

一个经典的例子如下:

1
yes | true

yes命令会无限地向管道中写’y’,而true命令判断管道中内容是否为真,为真就退出。按照我们的理解,true会判断yes写入的内容为真,立刻退出,然后yes会一直向管道中写内容,直到写阻塞。但是实际并不是这样,yes会在true结束后立刻结束。因为true结束后,管道没人读了,yes如果继续写,会导致broken pipe并收到一个SIGPIPE 信号,然后导致yes退出。现在来理解一下问题:

如果左侧(写)命令没有关闭读端,会发生什么?

如果写命令没有关闭读端,那么他会以为管道有读者(实际上这个读者就是他自己),从而不会触发broken pipe,写命令会傻傻地将管道写满至堵塞,然后等那个不存在的读者读取,所以会导致阻塞的发生。

IO重定向

解释

在解释IO重定向前,我们先了解一下shell中最终要的三个文件描述符:

  • 标准输入:stdin 0
  • 标准输出:stdout 1
  • 标准错误:stderr 2

shell一般从标准输入获取命令,然后将运行结果发送至标准输出或错误,而一般标准输出就是终端。shell一般保证这三个描述符总是打开的。现在我们想要重定向输入输出,从其他的文件中读取,再发送到普通文件,这就需要进行IO重定向。

命令列表

命令 解释
command > file 将输出重定向至file
command < file 将输入重定向至file
command >> file 将输出追加至file

实现

函数原型

  • open
1
int open (const char* Path, int flags [, int mode ]);   // flags表示了文件处理方式

代码

shell对命令解析后,为了实现重定向,我们需要做下面几件事:

  • 关闭原有的fd
  • 打开新文件,替换原有fd
  • 执行命令

所以我们的代码如下:

1
2
3
4
5
6
7
8
9
10
case '>':
case '<':
rcmd = (struct redircmd*)cmd;
close(rcmd->fd); // 0 or 1 now closed
if(open(rcmd->file, rcmd->mode) < 0){ // 0 or 1 now ----> rcmd->file
printf(2, "open %s failed\n", rcmd->file); //这里没有使用dup,而是利用关闭fd的方式实现了重定向,很巧妙
exit();
}
runcmd(rcmd->cmd);
break;

现在当我们执行下面语句是,就会将给定文件重定向至标准输入/输出

1
2
3
6.828$ /bin/echo "Helloworld" > result  
6.828$ /bin/cat < result
"Helloworld"

其他运算符

这里总结一下其他常见的一些shell中的运算符:

&&运算符

格式

1
cmd1 && cmd2

功能

只有在左侧命令返回true时,才执行右侧命令

||运算符

格式

1
cmd1 || cmd2

功能

只有在左侧命令返回false时,才执行右侧命令

()运算符

格式

1
(cmd1; cmd2; cmd3)

功能

命令组,括号中的命令会新开一个子shell顺序执行。

参考文献

0%