bash和ksh之间的子shell差异

时间:2022-02-13 00:30:15

I always believed that a sub-shell was not a child process, but another shell environment in the same process.

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

I use a basic set of built-ins:

我使用一组基本的内置函数:

(echo "Hello";read)

On another terminal:

在另一个终端:

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

So, no child process in kornShell (ksh).

所以,kornShell(ksh)中没有子进程。

Enter bash, it appears to behave differently, given the same command:

输入bash,看起来行为不同,给出相同的命令:

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

So, a child process in bash.
From reading the man pages for bash, it is obvious that another process is created for a sub-shell, however it fakes $$, which is sneeky.

所以,bash中的子进程。从阅读bash的手册页来看,显然是为子shell创建了另一个进程,但是它伪造了$$,这是一个偷偷摸摸的。

Is this difference between bash and ksh expected, or am I reading the symptoms incorrectly?

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

Edit: additional information: Running strace -f on bash and ksh on Linux shows that bash calls clone twice for the sample command (it does not call fork). So bash might be using threads (I tried ltrace but it core dumped!). KornShell calls neither fork, vfork, nor clone.

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

3 个解决方案

#1


9  

ksh93 works unusually hard to avoid subshells. Part of the reason is the avoidance of stdio and extensive use of sfio which allows builtins to communicate directly. Another reason is ksh can in theory have so many builtins. If built with SHOPT_CMDLIB_DIR, all of the cmdlib builtins are included and enabled by default. I can't give a comprehensive list of places where subshells are avoided, but it's typically in situations where only builtins are used, and where there are no redirects.

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

#!/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 "$@"

out:

出:

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

Another neat consequence of all this internal I/O handling is some buffering issues just go away. Here's a funny example of reading lines with tee and head builtins (don't try this in any other shell).

所有这些内部I / O处理的另一个巧妙结果是一些缓冲问题就会消失。这是一个有趣的例子,用tee和head builtins读取线条(不要在任何其他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


11  

In ksh, a subshell might or might not result in a new process. I don't know what the conditions are, but the shell was optimized for performance on systems where fork() was more expensive than it typically is on Linux, so it avoids creating a new process whenever it can. The specification says a "new environment", but that environmental separation may be done in-process.

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

Another vaguely-related difference is the use of new processes for pipes. In ksh and zsh, if the last command in a pipeline is a builtin, it runs in the current shell process, so this works:

另一个模糊相关的区别是管道的新工艺的使用。在ksh和zsh中,如果管道中的最后一个命令是内置命令,它将在当前shell进程中运行,因此这有效:

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

In bash, all pipeline commands are subshells, so it doesn't:

在bash中,所有管道命令都是子shell,因此它不会:

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

$

#3


1  

The bash manpage reads:

bash手册内容如下:

Each command in a pipeline is executed as a separate process (i.e., in a subshell).

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

While this sentence is about pipes, it strongly implies a subshell is a separate process.

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

Wikipedia's disambiguation page also describes a subshell in child-process terms. A child process is certainly itself a process.

*的消歧页面还描述了子进程术语中的子shell。子进程当然是一个过程。

The ksh manpage (at a glance) isn't direct about its own definition of a subshell, so it does not imply one way or the other that a subshell is a different process.

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

Learning the Korn Shell says that they are different processes.

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

I'd say you're missing something (or the book is wrong or out of date).

我会说你遗失了一些东西(或者这本书错了或已经过时了)。

#1


9  

ksh93 works unusually hard to avoid subshells. Part of the reason is the avoidance of stdio and extensive use of sfio which allows builtins to communicate directly. Another reason is ksh can in theory have so many builtins. If built with SHOPT_CMDLIB_DIR, all of the cmdlib builtins are included and enabled by default. I can't give a comprehensive list of places where subshells are avoided, but it's typically in situations where only builtins are used, and where there are no redirects.

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

#!/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 "$@"

out:

出:

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

Another neat consequence of all this internal I/O handling is some buffering issues just go away. Here's a funny example of reading lines with tee and head builtins (don't try this in any other shell).

所有这些内部I / O处理的另一个巧妙结果是一些缓冲问题就会消失。这是一个有趣的例子,用tee和head builtins读取线条(不要在任何其他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


11  

In ksh, a subshell might or might not result in a new process. I don't know what the conditions are, but the shell was optimized for performance on systems where fork() was more expensive than it typically is on Linux, so it avoids creating a new process whenever it can. The specification says a "new environment", but that environmental separation may be done in-process.

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

Another vaguely-related difference is the use of new processes for pipes. In ksh and zsh, if the last command in a pipeline is a builtin, it runs in the current shell process, so this works:

另一个模糊相关的区别是管道的新工艺的使用。在ksh和zsh中,如果管道中的最后一个命令是内置命令,它将在当前shell进程中运行,因此这有效:

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

In bash, all pipeline commands are subshells, so it doesn't:

在bash中,所有管道命令都是子shell,因此它不会:

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

$

#3


1  

The bash manpage reads:

bash手册内容如下:

Each command in a pipeline is executed as a separate process (i.e., in a subshell).

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

While this sentence is about pipes, it strongly implies a subshell is a separate process.

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

Wikipedia's disambiguation page also describes a subshell in child-process terms. A child process is certainly itself a process.

*的消歧页面还描述了子进程术语中的子shell。子进程当然是一个过程。

The ksh manpage (at a glance) isn't direct about its own definition of a subshell, so it does not imply one way or the other that a subshell is a different process.

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

Learning the Korn Shell says that they are different processes.

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

I'd say you're missing something (or the book is wrong or out of date).

我会说你遗失了一些东西(或者这本书错了或已经过时了)。