satan 通过 Google 阅读器发送给您的内容:
一个致命字符串
传说中,存在这么一串神秘的字符,你把它们放到终端,然后回车,不消太久,你的机器就变植物人只能低电平复位重启了。这串神秘的字符看起来是这样的,
1 | dutor@home: ~$ :(){ :|:; };:& |
这是神马玩意儿呢?好的,现在听我的,把你的脑袋面对显示器逆时针旋转四分之一圆周,像不像一个张着血盆大口的长袍老怪?
严肃点,你看懂它的真相了吗?换种等价的写法,
1 | dutor@home: ~$ foo(){ foo|foo; };foo& |
其实就是"声明"了一个函数,然后在后台执行这个函数。在函数体内部,以管道的形式调用递归调用自身。第一种写法只是把函数名换成":"产生的怪胎。
就这么简单吗?函数体里面,以管道连接的第二个foo会被执行吗?这需要了解Unix终端下的管道机制,在这之前,先观察一下这些进程的进程树。为了不致于死的那么快,对这个函数稍加修改,
1 2 | dutor@home: ~$ :(){ sleep 30;:|:; };:& [1] 12345 |
接下来使用pstree命令,查看进程12345的子进程树。
1 | dutor@home: ~$ watch pstree -c 12345 |
每隔30秒左右,进程树变化一次,依次,
bash---sleep bash-+-bash---sleep `-bash---sleep bash-+-bash-+-bash---sleep | `-bash---sleep `-bash-+-bash---sleep `-bash---sleep bash-+-bash-+-bash-+-bash---sleep | | `-bash---sleep | `-bash-+-bash---sleep | `-bash---sleep `-bash-+-bash-+-bash---sleep | `-bash---sleep `-bash-+-bash---sleep `-bash---sleep
可见,该进程树以二叉树的形式进行扩展。间接说明了,以管道连接的两个命令并不是一次执行的,即两个命令的执行是并发的,时间上是有交叠的。
终端下的管道机制
Unix终端下管道的实现使用了进程间通讯的一种,即匿名管道。由于匿名管道是匿名的,所以只能用在有亲缘关系的进程之间。通常是父进程创建匿名管道,然后fork出子进程,这时父子进程共享匿名管道的描述符。然后父进程向管道中写数据,子进程从管道中读数据。
具体到bash中的管道:bash首先解析用户的输入,比如foo | bar。然后,bash使用fork创建子进程P1,必要时使用wait族系统调用等待子进程结束。由P1创建匿名管道,然后P1再次fork出P2进程。接着,P1关闭匿名管道的读端,并将写端的文件描述符复制到标准输出(dup系统调用);P2关闭匿名管道的写端,并将读端的文件描述符复制到标准输入。最后,P1使用exec族系统调用执行foo,覆盖当前进程空间;P2使用执行bar。这样,最终foo的标准输出就被管道连接到了bar的标准输入上了。
要注意的是,这里foo和bar谁是父谁是子是取决于实现的,不变的是管道流始终是自左向右的。
下面是一段示例代码,
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 | //~ foo.c int main() { write(1, "message\n", 8); return 0; } //~ bar.c int main() { char buf[9] = {0}; read(0, buf, sizeof(buf)); buf[0] -= 32; write(1, buf, 8); return 0; } //~ main.c int main() { pid_t pid; if ((pid = fork()) == 0) { //~ child will establish a pipe int fd[2]; pipe(fd); if (fork() == 0) { //~ child of child close(fd[1]); //~ only read dup2(fd[0], 0); //~ dupliate 'in-endian' of the pipe to 'stdin' execv("./bar", NULL); //~ execute 'bar' } close(fd[0]); //~ only write dup2(fd[1] , 1); //~ duplicate 'out-endian' of the pipe to 'stdout' execv("./foo", NULL); //~ execute 'foo' wait(NULL); } //~ parent will wait. wait(NULL); return 0; } |
1 2 3 4 5 | dutor@home: ~$ cc foo.c -o foo && cc bar.c -o bar && cc main.c -o main dutor@home: ~$ ./foo message dutor@home: ~$ ./main Message |
总结
关于终端下的管道,它的简洁和强大自不必说。需要清楚的是,管道中各进程的启动是没有一定的次序的,执行时间是交叠的。一旦知道这些,下面的命令的结果就不会出乎意料。
1 | dutor@home: ~$ cat main.c | sed 's#//.*##' > main.c #warning: main.c may be truncated. |
这个命令可以并行的执行某个程序,
1 2 3 4 | # 如果run.sh产生大量标准输出时,只有最后一个进程的输出会显示到终端 # 其他的都会'堆积'到匿名管道中(如果run.sh不从标准输入读取数据的话) # 这可能导致进程阻塞 dutor@home: ~$ ./run.sh | ./run.sh | ./run.sh | ./run.sh |
可从此处完成的操作:
- 使用 Google 阅读器订阅Dutor
- 开始使用 Google 阅读器,轻松地与您喜爱的所有网站保持同步更新
没有评论:
发表评论