【问题标题】:Sub-shell differences between bash and kshbash 和 ksh 之间的子 shell 区别
【发布时间】:2013-01-19 03:50:55
【问题描述】:

我一直认为子shell不是子进程,而是另一个 shell环境在同一进程中。

我使用了一组基本的内置插件:

(echo "Hello";read)

在另一个终端上:

ps -t pts/0
  PID TTY          TIME CMD
20104 pts/0    00:00:00 ksh

因此,kornShell (ksh) 中没有子进程。

输入 bash,给定相同的命令,它的行为似乎有所不同:

  PID TTY          TIME CMD
 3458 pts/0    00:00:00 bash
20067 pts/0    00:00:00 bash

因此,bash 中的子进程。
通过阅读 bash 的手册页,很明显为子 shell 创建了另一个进程, 但是它伪造了$$,这很狡猾。

这是预期的 bash 和 ksh 之间的差异,还是我错误地阅读了症状?

编辑:附加信息: 在 bash 上运行 strace -f 并在 Linux 上运行 ksh 表明 bash 为示例命令调用了两次 clone(它不调用 fork)。所以 bash 可能正在使用线程(我试过 ltrace 但它核心转储了!)。 KornShell 既不调用forkvfork,也不调用clone

【问题讨论】:

    标签: linux bash shell scripting ksh


    【解决方案1】:

    bash 手册页内容如下:

    管道中的每个命令都作为单独的进程执行(即,在子 shell 中)。

    虽然这句话是关于管道的,但它强烈暗示子外壳是一个单独的进程。

    Wikipedia's disambiguation page 还用子进程术语描述了一个子shell。子进程本身当然也是一个进程。

    ksh 手册页(一目了然)并没有直接说明它自己对子 shell 的定义,因此它并不意味着子shell 是一个不同的进程。

    Learning the Korn Shell 表示它们是不同的进程。

    我会说你遗漏了一些东西(或者这本书是错误的或过时的)。

    【讨论】:

    • 好的,如果它们是不同的进程,那为什么ps中只显示一个?
    • 此外,strace 表明 ksh 不会为示例子 shell 调用 forkvforkclone(在 Linux 上)。
    • 公平地说,它可能是特定于版本的。我已经在 [ksh-users] 上询问过,我会在此处发布任何回复。
    • @cdarke 这在某种程度上取决于您的操作系统以及 ksh 的构建方式。如果可用,我相信它会使用 posix_spawn。
    【解决方案2】:

    在 ksh 中,子 shell 可能会或可能不会导致新进程。我不知道条件是什么,但是外壳针对fork() 比通常在Linux 上更昂贵的系统上的性能进行了优化,因此它尽可能避免创建新进程。规范说的是“新环境”,但环境分离可以在过程中完成。

    另一个隐约相关的区别是管道使用了新流程。在 ksh 和 zsh 中,如果管道中的最后一个命令是内置命令,它会在当前 shell 进程中运行,所以这样可以:

    $ unset x
    $ echo foo | read x
    $ echo $x
    foo
    $
    

    在 bash 中,第一个之后的所有管道命令都在子 shell 中运行,因此上述不起作用:

    $ unset x
    $ echo foo | read x
    $ echo $x
    
    $
    

    正如@dave-thompson-085 指出的那样,如果您关闭作业控制 (set +o monitor) 并打开 lastpipe 选项 (shopt -s lastpipe),则可以在 bash 4.2 版及更高版本中获得 ksh/zsh 行为)。但我通常的解决方案是使用进程替换:

    $ unset x
    $ read x < <(echo foo)
    $ echo $x
    foo
    

    【讨论】:

    • 我真的很在意条件是什么。您能否提供一个参考,您发现外壳经过优化以避免fork
    • 除常识外无参考; fork 曾经是一项非常昂贵的操作。由于现在 ksh 是开源的,所以我只会查看代码以了解它何时决定分叉。所有 Korn 的书都说:“子 shell 是一个单独的环境,它是父 shell 环境的副本。......子shell 环境不必是一个单独的进程。”
    • bash 4.2 up 中,如果您 shopt -s lastpipe 并且作业控制已关闭(通常意味着非交互式)并且管道没有后台运行,它会尝试运行最后一个“联合”在当前的外壳中。 unix.stackexchange.com/questions/143958/…gnu.org/software/bash/manual/html_node/…
    【解决方案3】:

    ksh93 非常努力地避免子shell。部分原因是避免使用 stdio 并广泛使用 sfio,它允许内置程序直接通信。另一个原因是 ksh 理论上可以有这么多的内置函数。如果使用SHOPT_CMDLIB_DIR 构建,则默认情况下会包含并启用所有 cmdlib 内置函数。我无法给出避免使用 subshel​​l 的位置的完整列表,但通常是在仅使用内置函数且没有重定向的情况下。

    #!/usr/bin/env ksh
    
    # doCompat arr
    # "arr" is an indexed array name to be assigned an index corresponding to the detected shell.
    # 0 = Bash, 1 = Ksh93, 2 = mksh
    function doCompat {
        ${1:+:} return 1
        if [[ ${BASH_VERSION+_} ]]; then
            shopt -s lastpipe extglob
            eval "${1}[0]="
        else
            case "${BASH_VERSINFO[*]-${!KSH_VERSION}}" in
                .sh.version)
                    nameref v=$1
                    v[1]=
                    if builtin pids; then
                        function BASHPID.get { .sh.value=$(pids -f '%(pid)d'); }
                    elif [[ -r /proc/self/stat ]]; then
                        function BASHPID.get { read -r .sh.value _ </proc/self/stat; }
                    else
                        function BASHPID.get { .sh.value=$(exec sh -c 'echo $PPID'); }
                    fi 2>/dev/null
                    ;;
                KSH_VERSION)
                    nameref "_${1}=$1"
                    eval "_${1}[2]="
                    ;&
                *)
                    if [[ ! ${BASHPID+_} ]]; then
                        echo 'BASHPID requires Bash, ksh93, or mksh >= R41' >&2
                        return 1
                    fi
            esac
        fi
    }
    
    function main {
        typeset -a myShell
        doCompat myShell || exit 1 # stripped-down compat function.
        typeset x
    
        print -v .sh.version
        x=$(print -nv BASHPID; print -nr " $$"); print -r "$x" # comsubs are free for builtins with no redirections 
        _=$({ print -nv BASHPID; print -r " $$"; } >&2)        # but not with a redirect
        _=$({ printf '%s ' "$BASHPID" $$; } >&2); echo         # nor for expansions with a redirect
        _=$(printf '%s ' "$BASHPID" $$ >&2); echo # but if expansions aren't redirected, they occur in the same process.
        _=${ { print -nv BASHPID; print -r " $$"; } >&2; }     # However, ${ ;} is always subshell-free (obviously).
        ( printf '%s ' "$BASHPID" $$ ); echo                   # Basically the same rules apply to ( )
        read -r x _ <<<$(</proc/self/stat); print -r "$x $$"   # These are free in {{m,}k,z}sh. Only Bash forks for this.
        printf '%s ' "$BASHPID" $$ | cat # Sadly, pipes always fork. It isn't possible to precisely mimic "printf -v".
        echo
    } 2>&1
    
    main "$@"
    

    出来:

    Version AJM 93v- 2013-02-22
    31732 31732
    31735 31732
    31736 31732 
    31732 31732 
    31732 31732
    31732 31732 
    31732 31732
    31738 31732
    

    所有这些内部 I/O 处理的另一个巧妙结果是一些缓冲问题就会消失。这是一个使用 teehead 内置函数读取行的有趣示例(不要在任何其他 shell 中尝试此操作)。

     $ ksh -s <<\EOF
    integer -a x
    builtin head tee
    printf %s\\n {1..10} |
        while head -n 1 | [[ ${ { x+=("$(tee /dev/fd/{3,4})"); } 3>&1; } ]] 4>&1; do
            print -r -- "${x[@]}"
        done
    EOF
    1
    0 1
    2
    0 1 2
    3
    0 1 2 3
    4
    0 1 2 3 4
    5
    0 1 2 3 4 5
    6
    0 1 2 3 4 5 6
    7
    0 1 2 3 4 5 6 7
    8
    0 1 2 3 4 5 6 7 8
    9
    0 1 2 3 4 5 6 7 8 9
    10
    0 1 2 3 4 5 6 7 8 9 10
    

    【讨论】:

    • 非常感谢您的全面回答。很遗憾,我只能将一个答案“勾选”为已接受,并且只能为您投票一次。
    • ehe,我知道信誉系统搞砸了...感谢您提出一个有趣的问题:P
    • @MarkReed:完成。很抱歉你最终投了反对票。我真的很感谢你们俩。
    【解决方案4】:

    Korn shell 不一定使用子shell 进行命令替换。它们通常在同一过程中处理。例外包括 I/O 操作

    为了更进一步,我有一个命令,在 ksh93 中,从一个非常旧的脚本中给出了一个看起来像这样的变量值:

    my_variable=(`cat ./my_file`)
    

    换句话说,反引号命令替换的括号。 "my_file" 是 4 位八进制数的列表,一行一行。

    当在 ksh93t 及更高版本中以这种方式提供时,会保留换行符,您可以使用计数器逐步遍历变量中的数字。例如,下面的代码将从上面讨论的列表中给出一个 4 位八进制数,之后,您将递增计数器:

    data_I_want=$(echo "${my_variable[$my_counter]}")
    

    在 ksh93 中,变量的命令也可以这样完成:

    my_variable=($(cat ./my_file))
    

    最后,为了消除“无用的猫”,

    my_variable=($(<./my_file))
    

    如果命令的结构没有外圆括号,则删除换行符(POSIX 标准),并且变量的第一次使用包括文件中的所有数字。使用计数器对变量的后续调用返回空值。

    将命令放在括号内会强制在新进程中使用子 shell,并避免使用 IFS="" 重置默认字段分隔符的必要性。

    很抱歉碰到了这么旧的东西,但似乎值得加入,因为我没有看到其他地方讨论过这种特殊行为。

    【讨论】:

    • 感谢罗伯特清理我的代码块。
    猜你喜欢
    • 2014-06-26
    • 1970-01-01
    • 2012-02-20
    • 1970-01-01
    • 1970-01-01
    • 2011-09-03
    • 2011-12-13
    • 2022-06-28
    相关资源
    最近更新 更多