bash 和 ksh 之间的子 shell 差异

问题描述 投票:0回答:4

我一直认为子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 之间的这种差异是预期的,还是我错误地理解了症状?

编辑:附加信息: 在 Linux 上的 bash 和 ksh 上运行

strace -f
显示 bash 对示例命令调用
clone
两次(它不调用
fork
)。所以 bash 可能正在使用线程(我尝试过
ltrace
但它核心转储了!)。 KornShell 既不调用
fork
vfork
,也不调用
clone

linux bash shell scripting ksh
4个回答
12
投票

在 ksh 中,子 shell 可能会也可能不会产生新进程。 我不知道条件是什么,但 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

10
投票

ksh93 异常努力地避免子 shell。部分原因是避免使用 stdio 并广泛使用 sfio,它允许内置函数直接通信。另一个原因是 ksh 理论上可以有如此多的内置函数。如果使用

SHOPT_CMDLIB_DIR
构建,则默认情况下包含并启用所有 cmdlib 内置函数。我无法给出避免使用子 shell 的地方的完整列表,但通常是在仅使用内置函数且没有重定向的情况下。

#!/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 处理的另一个巧妙结果是一些缓冲问题消失了。这是一个使用

tee
head
内置函数读取行的有趣示例(不要在任何其他 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

2
投票

bash 手册页内容如下:

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

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

维基百科的消歧义页面也用子进程术语描述了子shell。子进程本身当然也是一个进程。

ksh 手册页(乍一看)并不直接说明其自己的子 shell 定义,因此它并不暗示子 shell 是一个不同的进程。

学习 Korn Shell 说它们是不同的过程。

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


1
投票

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

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

my_variable=(`cat ./my_file`)

换句话说,用括号括住反引号命令替换。 “my_file”是 4 位八进制数字的列表,一位到一行。

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

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

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

my_variable=($(cat ./my_file))

最后,消除“无用的猫的使用”,

my_variable=($(<./my_file))

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

将命令放在括号内会强制在新进程中使用子 shell,并避免使用

IFS=""
重置默认字段分隔符的必要性。

很抱歉撞到了这么旧的东西,但似乎值得将其包括在内,因为我还没有看到其他地方讨论过这种特殊行为。

© www.soinside.com 2019 - 2024. All rights reserved.