Bash:创建匿名FIFO


38

我们都知道mkfifo管道。第一个创建一个命名管道,因此必须选择一个名称,最有可能使用mktemp,后来记得取消链接。另一个创建了一个匿名管道,没有麻烦的名称和删除操作,但是管道的末端与管道中的命令绑定在一起,以某种方式掌握文件描述符并在其余部分中使用它们并不十分方便的脚本。在编译的程序中,我只想做ret=pipe(filedes);在Bash中有exec 5<>file这样的期望- "exec 5<> -"或者"pipe <5 >6"Bash中有类似的东西吗?

Answers:


42

您可以在将命名管道附加到当前进程后立即取消链接,这实际上导致了匿名管道:

# create a temporary named pipe
PIPE=$(mktemp -u)
mkfifo $PIPE
# attach it to file descriptor 3
exec 3<>$PIPE
# unlink the named pipe
rm $PIPE
...
# anything we write to fd 3 can be read back from it
echo 'Hello world!' >&3
head -n1 <&3
...
# close the file descriptor when we are finished (optional)
exec 3>&-

如果您确实要避免使用命名管道(例如,文件系统是只读的),那么“掌握文件描述符”的想法也可以。请注意,由于使用了procfs,因此这是特定于Linux的。

# start a background pipeline with two processes running forever
tail -f /dev/null | tail -f /dev/null &
# save the process ids
PID2=$!
PID1=$(jobs -p %+)
# hijack the pipe's file descriptors using procfs
exec 3>/proc/$PID1/fd/1 4</proc/$PID2/fd/0
# kill the background processes we no longer need
# (using disown suppresses the 'Terminated' message)
disown $PID2
kill $PID1 $PID2
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptors when we are finished (optional)
exec 3>&- 4<&-

:您可以不使用的文件描述符的自动发现结合本stackoverflow.com/questions/8297415/...
CMCDragonkai

23

虽然我所知的任何一个外壳都无法制造没有分叉的管道,但有些外壳确实比基本的外壳管道更好。

在bash,ksh和zsh中,假设系统支持/dev/fd(当今大多数情况下),则可以将命令的输入或输出绑定到文件名:<(command)扩展为一个文件名,该文件名指定从到输出连接的管道command,然后>(command)扩展到指定连接到的输入的管道的文件名command。此功能称为流程替换。它的主要目的是将一个以上的命令通过管道传递到另一个命令中,例如,

diff <(transform <file1) <(transform <file2)
tee >(transform1 >out1) >(transform2 >out2)

这对于消除基本壳管的某些缺点也很有用。例如,command2 < <(command1)等于command1 | command2,但其状态为command2。另一个用例是exec > >(postprocessing),与将整个脚本的其余部分放到里面等效,但比它更易读{ ... } | postprocessing


我用diff尝试了一下,但是可以用kdiff3或emacs,但是没有用。我的猜测是在kdiff3读取之前,临时的/ dev / fd文件已被删除。还是kdiff3尝试两次读取文件,而管道仅发送一次?
2015年

@Eyal使用进程替换时,文件名是对管道(或不支持这些魔术变体的Unix变体上的临时文件)的“魔术”引用。魔术的实现方式取决于操作系统。Linux将它们实现为“魔术”符号链接,其目标不是有效的文件名(类似于pipe:[123456])。Emacs认为symlink的目标不是现有的文件名,并且使它很混乱,以至于它无法读取文件(尽管Emacs不喜欢将管道作为档案)。
吉尔斯(Gillles)“所以-别再邪恶了”

10

重击4具有协同处理

协同处理在子shell中异步执行,就好像命令已由'&'控制运算符终止,并且在执行外壳和协同处理之间建立了双向管道。

协同处理的格式为:

coproc [NAME] command [redirections] 

3

截至2012年10月,该功能似乎在Bash中仍然不存在,但是如果您需要与子进程进行通讯的所有未命名/匿名管道,都可以使用coproc。此时coproc的问题在于,显然一次仅支持一个。我不知道为什么coproc有这个限制。它们本来应该是现有任务背景代码(&op)的增强,但这是bash作者的一个问题。


不仅支持一个协同处理。您可以命名它们,只要您不提供简单的命令即可。而是给它一个命令列表:coproc THING { dothing; }现在,您的FD进入了${THING[*]},您可以运行coproc OTHERTHING { dothing; }并向彼此发送和接收事物。
clacke

2
@clacke在man bashBUGS标题下这样说:一次可能只有一个活动的协同进程。如果您再次启动coproc,则会收到警告。它似乎可以正常工作,但我不知道后台会发生什么爆炸。
Radu C

好的,所以它目前只能靠运气工作,而不是因为它是故意的。合理警告,谢谢。:-)
clacke

2

@DavidAnderson的答案涵盖了所有基础并提供了一些不错的保护措施,但它揭示的最重要的一点是,<(:)只要您继续使用Linux,就可以轻松地访问匿名管道。

因此,最简单的答案是:

exec 5<> <(:)

在macOS上,它将无法正常工作,然后您需要创建一个临时目录来存放命名的fifo,直到您将其重定向到该目录为止。我不了解其他BSD。


您确实意识到答案仅因Linux中的错误而有效。此错误在macOS中不存在,因此需要更复杂的解决方案。我发布的最终版本将在linux中运行,即使linux中的错误已修复。
David Anderson

@DavidAnderson听起来您比我更深入地了解这一点。为什么Linux行为是bug?
clacke

1
如果exec已通过,并且匿名fifo仅打开以供读取,则exec不应允许使用自定义文件描述符打开此匿名fifo以进行读取和写入。您应该期望得到一条-bash: /dev/fd/5: Permission denied消息,这就是macOS出现的问题。我相信错误是Ubuntu不会产生相同的消息。如果有人可以出示exec 5<> <(:)允许的文件说明,我愿意改变主意。
David Anderson

@DavidAnderson哇,这很有趣。我以为bash在内部做某事,但事实证明,这是Linux,它允许仅open(..., O_RDWR)在替换提供的一个单向管道末端执行操作,并将其转换为一个FD中的双向管道。您可能是对的,不应依赖此。:-D使用execline的piperw创建管道,然后使用bash重新设置管道的输出<>libranet.de/display/0b6b25a8-195c-84af-6ac7-ee6696661765
clacke

没关系,但是如果您想在Ubuntu下查看传递给了什么exec 5<>,请输入fun() { ls -l $1; ls -lH $1; }; fun <(:)
David Anderson

1

使用进行了以下功能的测试GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)。操作系统是Ubuntu18。此函数采用单个参数,该参数是匿名FIFO所需的文件描述符。

MakeFIFO() {
    local "MakeFIFO_upper=$(ulimit -n)" 
    if [[ $# -ne 1 || ${#1} -gt ${#MakeFIFO_upper} || -n ${1%%[0-9]*} || 10#$1 -le 2
        || 10#$1 -ge MakeFIFO_upper ]] || eval ! exec "$1<> " <(:) 2>"/dev/null"; then
        echo "$FUNCNAME: $1: Could not create FIFO" >&2
        return "1"
    fi
}

使用进行了以下功能的测试GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)。操作系统为macOS High Sierra。此功能首先在仅创建它的进程知道的临时目录中创建一个命名FIFO 。接下来,将文件描述符重定向到FIFO。最后,通过删除临时目录将FIFO与文件名断开链接。这使FIFO为匿名。

MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
        MakeFIFO_file="$MakeFIFO_directory/pipe"
        mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
        ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}

可以将以上功能组合为在两个操作系统上都可以使用的单个功能。以下是此类功能的示例。在这里,尝试创建一个真正的匿名FIFO。如果未成功,那么将创建一个命名的FIFO并将其转换为匿名FIFO。

MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        if eval ! exec "$1<> " <(:) 2>"/dev/null"; then
            MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
            MakeFIFO_file="$MakeFIFO_directory/pipe"
            mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
            ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        fi
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}

这是一个创建匿名FIFO,然后将一些文本写入同一FIFO的示例。

fd="6"
MakeFIFO "$fd"
echo "Now is the" >&"$fd"
echo "time for all" >&"$fd"
echo "good men" >&"$fd"

以下是读取匿名FIFO的全部内容的示例。

echo "EOF" >&"$fd"
while read -u "$fd" message; do
    [[ $message != *EOF ]] || break
    echo "$message"
done

这将产生以下输出。

Now is the
time for all
good men

下面的命令关闭匿名FIFO。

eval exec "$fd>&-"

参考:
创建匿名管道以供以后使用
可公开编写目录中的文件是危险的
Shell脚本安全性


0

使用htamas的出色回答,我对其进行了一些修改,以便在一个衬套中使用它,这里是:

# create a temporary named pipe
PIPE=(`(exec 0</dev/null 1</dev/null; (( read -d \  e < /proc/self/stat ; echo $e >&2 ; exec tail -f /dev/null 2> /dev/null ) | ( read -d \  e < /proc/self/stat ; echo $e  >&2 ; exec tail -f /dev/null 2> /dev/null )) &) 2>&1 | for ((i=0; i<2; i++)); do read e; printf "$e "; done`)
# attach it to file descriptors 3 and 4
exec 3>/proc/${PIPE[0]}/fd/1 4</proc/${PIPE[1]}/fd/0
...
# kill the temporary pids
kill ${PIPE[@]}
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptor when we are finished (optional)
exec 3>&- 4<&-

7
我不禁注意到您的一线客车有多条线路。
德米特里·格里戈里耶夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.