2012年4月18日星期三

一个致命字符串的解析

 
 

satan 通过 Google 阅读器发送给您的内容:

 
 

于 11-11-1 通过 Dutor 作者:dutor

一个致命字符串

  传说中,存在这么一串神秘的字符,你把它们放到终端,然后回车,不消太久,你的机器就变植物人只能低电平复位重启了。这串神秘的字符看起来是这样的,

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

 
 

可从此处完成的操作:

 
 

没有评论:

发表评论