Linux: linux 匿名管道

相信很多在linux平台工作的童鞋, 都很熟悉管道符 '|', 通过它, 我们能够很灵活的将几种不同的命令协同起来完成一件任务.就好像下面的命令:

echo 123 | awk '{print $0+123}'       # 输出246

不过这次咱们不来说这些用法, 而是来探讨一些更加有意思的, 那就是 管道两边的数据流"实时性"管道使用的小提示.

其实我们在利用管道的时候, 可能会不经意的去想, 我前一个命令的输出, 是全部处理完再通过管道传给第二个命令, 还是一边处理一边输出呢? 可能在大家是试验中或者工作经验中, 应该是左边的命令全部处理完再一次性交给右边的命令进行处理, 不光是大家, 我在最初接触管道时, 也曾有这么一个误会, 因为我们通过现象看到的就是这样.

但其实只要有简单了解过管道这工具, 应该都不难得出解释:

管道是两边是同时进行, 也就是说, 左边的命令输出到管道, 管道的右边将马上进行处理.

管道的定义

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会堵塞,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

管道工作流程图

通过上面的解释可以看到, 假设 COMMAND1 | COMMAND2, 那么COMMAND1的标准输出, 将会被绑定到管道的写端, 而COMMAND2的标准输入将会绑定到管道的读端, 所以当COMMAND1一有输出, 将会马上通过管道传给COMMAND2, 我们先来做个实验验证下:

# 1.py
import time
import sys
while 1:
    print '1111'
    time.sleep(3)
    print '2222'
    time.sleep(3)
[root@iZ23pynfq19Z ~]# python 1 | cat 

在上面的命令, 我们可以猜测下输出结果: 究竟是 睡眠6秒之后, 输出"1111222", 还是输出 "1111" 睡眠3秒, 再输出 "2222", 然后再睡眠3秒, 再输出"1111" 呢? 答案就是: 都不是! what! 这不可能, 大家可以尝试下, 我们会看到终端没反应了, 为什么呢? 这就要涉及到文件IO的缓冲方式了,关于文件IO, 可以参考我的另一篇文章: 浅谈文件描述符1和2, 在最下面的地方提到文件IO的三种缓冲方式:

  • 全缓冲: 直到缓冲区被填满,才调用系统I/O函数, (一般是针对文件)
  • 行缓冲: 遇到换行符就输出(标准输出)
  • 无缓冲: 没有缓冲区,数据会立即读入或者输出到外存文件和设备上(标准错误

因为python是默认采用带缓冲的fputs(参考py27源码: fileobject.c: PyFile_WriteString函数), 又因为标准输出被改写到管道, 所以将会采取全缓冲的方式(shell 命令具体要看实现, 因为有些是用不带缓冲write实现,如果不带缓冲区,会直接写入管道), 所以将会采取全缓冲的方式, 也就是说, 直到缓冲区被填满, 或者手动显示调用flush刷入,才能看到输出.那我们可以将代码改写成下面两种方式吧

# 方式1: 填满缓冲区, 我这边大小是4096字节, 你们也可以试下这个值, 估计都一样
import time
import sys
while 1:
    print '1111' * 4096
    time.sleep(3)
    print '2222' * 4096
    time.sleep(3)

# 方式2: 手动刷入写队列
import time
import sys
while 1:
    print '1111'
    sys.stdout.flush()    // 因为是标准输出, 所以直接通过sys的接口去flush
    time.sleep(3)
    print '2222' 
    sys.stdout.flush()
    time.sleep(3)

输出结果:

# 第一种方式:
[root@iZ23pynfq19Z ~]# python 1 | cat 
1111.....(超多1, 刷屏了..)
睡眠3秒..
2222.....(超多2, 刷屏了..)

# 第二种方式:
[root@iZ23pynfq19Z ~]# python 1 | cat 
1111
睡眠3秒..
2222
睡眠3秒..
1111
....

在这里我们已经能够得出结果, 如果像我们以前所想的那样, 要等到COMMAND1全部执行完才一次性输出给COMMAND2, 那么结果应该是无限堵塞..因为我的程序一直没有执行完..这样应该是不符合老前辈们设计初衷的, 因为这样可能会导致管道越来越大..然而管道也是有大小的~ 具体可以去看posix标准, 所以我们得出结论是: 只要COMMAND1的输出写入管道的写端(不管是缓冲区满还是手动flush), COMMAND2都将立刻得到数据并且马上处理.

那么 管道两边的数据流"实时性" 讨论到就先暂告一段落, 接下来将在这个基础上继续讨论: 管道使用的小提示.

在开始讨论前, 我想先引入一个专业术语, 也是我们偶尔会遇到的, 那就是: SIGPIPE

或者是一个更加具体的描述: broken pipe (管道破裂)

上面的专业术语都是跟管道读写规则息息相关的, 那咱们来看下 管道的读写规则吧:

  • 当没有数据可读时 O_NONBLOCK (未设置):read调用阻塞,即进程暂停执行,一直等到有数据来到为止。 O_NONBLOCK ( 设置 ) :read调用返回-1,errno值为EAGAIN。
  • 当管道满的时候 O_NONBLOCK (未设置): write调用阻塞,直到有进程读走数据 O_NONBLOCK ( 设置 ):调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

在上面我们可以看到, 如果我们收到SIGPIPE信号, 那么一般情况就是读端被关闭, 但是写端却依旧尝试写入

咱们来重现下 SIGPIPE

#!/usr/bin/python
import time
import sys
while 1:
    time.sleep(10)   # 手速不够快的童鞋可以将睡眠时间设置长点
    print '1111'
    sys.stdout.flush()

这次执行命令需要考验手速了, 因为我们要赶在py醒过来之前, 将读端进程杀掉

 python 1 | cat 

------------------------
# 另一个终端
[root@iZ23pynfq19Z ~]# ps -fe | grep -P 'cat|python'
root     10775  4074  0 00:05 pts/2    00:00:00 python 1
root     10776  4074  0 00:05 pts/2    00:00:00 cat        # 读端进程
root     10833 32581  0 00:06 pts/0    00:00:00 grep -P cat|python

[root@iZ23pynfq19Z ~]# kill 10776  

输出结果

[root@iZ23pynfq19Z ~]# python 1 | cat 
Traceback (most recent call last):
  File "1", line 6, in <module>
    sys.stdout.flush()
IOError: [Errno 32] Broken pipe
Terminated

从上图我们可以验证两个点:

  1. 当我们杀掉读端时, 写端会收到SIGPIPE而默认退出, 管道结束
  2. 当我们杀掉读端时, 写端的程序并不会马上收到SIGPIPE, 相反的, 只有真正写入管道写端时才会触发这个错误

如果写入一个 读端已经关闭的管道, 将会收到一个 SIGPIPE, 那读一个写端已经关闭的管道又会这样呢?

import time
import sys

# 这次我们不需要死循环, 因为我们想要写端快点关闭退出
time.sleep(5)   
print '1111'
sys.stdout.flush()
# 因为我们想要 读端 等到足够长的时间, 让写端关闭, 所以我们需要利用awk先睡眠10秒
[root@iZ23pynfq19Z ~]# python 1.py | awk '{system("sleep 10");print 123}' 

------------------------
[root@iZ23pynfq19Z ~]# ps -fe | grep -P 'awk|python'
root     11717  4074  0 00:20 pts/2    00:00:00 python 1.py
root     11718  4074  0 00:20 pts/2    00:00:00 awk {system("sleep 10");print 123}
root     11721 32581  0 00:20 pts/0    00:00:00 grep -P awk|python

# 5秒过后
[root@iZ23pynfq19Z ~]# ps -fe | grep -P 'awk|python'
root     11685  4074  0 00:20 pts/2    00:00:00 awk {system("sleep 10");print 123}
root     11698 32581  0 00:20 pts/0    00:00:00 grep -P awk|python

# 10秒过后
[root@iZ23pynfq19Z ~]# python 1 | awk '{system("sleep 10");print 123}' 
123

在上面也已经证明了上文提到的读写规则: 如果所有管道写端对应的文件描述符被关闭,将产生EOF结束标志,read返回0, 程序退出

总结

通过上面的理论和实验, 我们知道在使用管道时, 两边命令的数据传输过程, 以及对管道读写规则有了初步的认识, 希望我们以后在工作时, 再接触管道时, 能够更加有把握的去利用这一强大的工具. 欢迎各位大神指点交流

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张善友的专栏

ASP.NET Web API 处理架构

这篇文章主要是介绍ASP.NET Web API的处理架构:当一个HTTP请求到达直到产生一个请求的过程。ASP.NET Web API 的处理架构图如下,主要...

2838
来自专栏Java帮帮-微信公众号-技术文章全总结

day07.HDFS学习【大数据教程】

分而治之:将大文件、大批量文件,分布式存放在大量服务器上,以便于采取分而治之的方式对海量数据进行运算分析;

1874
来自专栏云霄雨霁

死锁、饥饿和活锁

3144
来自专栏Spark学习技巧

深入了解HBase架构

2012
来自专栏北京马哥教育

深入浅出:Linux设备驱动之中断与定时器

“我叮咛你的 你说 不会遗忘 你告诉我的 我也全部珍藏 对于我们来说 记忆是飘不落的日子 永远不会发黄 相聚的时候 总是很短 期待的时候 总是很长 岁月的溪水边...

3979
来自专栏Seebug漏洞平台

TP-LINK WR941N路由器研究

是TP-Link WR940N后台的RCE, 手头上正好有一个TP-Link WR941N的设备,发现也存在相同的问题,但是CVE-2017-13772文章中给...

4168
来自专栏技术点滴

远程线程注入引出的问题

远程线程注入引出的问题 一、远程线程注入基本原理 远程线程注入——相信对Windows底层编程和系统安全熟悉的人并不陌生,其主要核心在于一个Windows AP...

28010
来自专栏java思维导图

缓存穿透,缓存击穿,缓存雪崩解决方案分析

设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。

1503
来自专栏安富莱嵌入式技术分享

【RL-TCPnet网络教程】第37章 RL-TCPnet之FTP客户端

本章节为大家讲解RL-TCPnet的FTP客户端应用,学习本章节前,务必要优先学习第35章的FTP基础知识。有了这些基础知识之后,再搞本章节会有事半功倍的效果。

832
来自专栏互联网大杂烩

操作系统-进程和线程

进程线程的区别 1、进程是什么? 是具有一定独立功能的程序、它是系统进行资源分配和调度的一个独立单位,重点在系统调度和单独的单位,也就是说进程是可以独立运行...

1154

扫码关注云+社区

领取腾讯云代金券