In a typical Bash script, each command is executed by calling fork and exec.
Here’s what that looks like:
# We'll use strace to follow the fork+exec syscalls
# Here, we create an alias with some useful flags to make the output less verbose
$ alias STRACE = 'strace -fqq -e signal=none -e status=successful -e trace=execve'
# Typical program - two commands, each run in a separate process
# You can tell that fork() was called because of the "[pid XXX]" sections
$ STRACE bash -c 'sleep 1 ; hostname'
execve( "/usr/bin/bash" , [ "bash" , "-c" , "sleep 1 ; hostname" ] , 0x7ffdc84f5770 /* 42 vars * /) = 0
[ pid 139443] execve( "/usr/bin/sleep" , [ "sleep" , "1" ] , 0x55e3139282f0 /* 42 vars * /) = 0
[ pid 139444] execve( "/usr/bin/hostname" , [ "hostname" ] , 0x55e313928c70 /* 42 vars * /) = 0
lyeager-dt
In your Bash script, you can choose to “exec” a command, which skips the fork and executes the program directly, without returning control back to Bash afterwards.
# Notice how the second command is missing the [pid] section
# This means that we skipped the fork and ran exec directly
$ STRACE bash -c 'sleep 1 ; exec hostname'
execve( "/usr/bin/bash" , [ "bash" , "-c" , "sleep 1 ; exec hostname" ] , 0x7fff0a83ca08 /* 42 vars * /) = 0
[ pid 141756] execve( "/usr/bin/sleep" , [ "sleep" , "1" ] , 0x560642aba330 /* 42 vars * /) = 0
execve( "/usr/bin/hostname" , [ "hostname" ] , 0x560642abacb0 /* 41 vars * /) = 0
lyeager-dt
Today, I noticed that when you give a single command to the “-c” Bash flag (e.g. bash -c hostname), there is an implicit “exec” inserted before your command.
# Notice how the hostname command is not run in a separate process
$ STRACE bash -c 'hostname'
execve( "/usr/bin/bash" , [ "bash" , "-c" , "hostname" ] , 0x7ffe4265fd18 /* 42 vars * /) = 0
execve( "/usr/bin/hostname" , [ "hostname" ] , 0x557d7410a330 /* 42 vars * /) = 0
lyeager-dt
# Here, the exec is explicit. The output is the same.
$ STRACE bash -c 'exec hostname'
execve( "/usr/bin/bash" , [ "bash" , "-c" , "exec hostname" ] , 0x7fff2499fa28 /* 42 vars * /) = 0
execve( "/usr/bin/hostname" , [ "hostname" ] , 0x55e4f1424330 /* 42 vars * /) = 0
lyeager-dt
As far as I can tell, this is entirely undocumented in bash(1).
But it actually enables something pretty cool.
This chain of bash+numactl+bash+numactl programs all completes without ever needing to fork.
# Notice how all of the subcommands are exec'd in the same process
$ STRACE bash -c 'numactl --membind=0 -- "$@"' -- bash -c 'numactl --show'
execve( "/usr/bin/bash" , [ "bash" , "-c" , "numactl --membind=0 -- \" $@ \" " , "--" , "bash" , "-c" , "numactl --show" ] , 0x7ffca329ed70 /* 42 vars * /) = 0
execve( "/usr/bin/numactl" , [ "numactl" , "--membind=0" , "--" , "bash" , "-c" , "numactl --show" ] , 0x564ce6e6c300 /* 42 vars * /) = 0
execve( "/usr/bin/bash" , [ "bash" , "-c" , "numactl --show" ] , 0x7fff37bc6230 /* 42 vars * /) = 0
execve( "/usr/bin/numactl" , [ "numactl" , "--show" ] , 0x55e870a982f0 /* 42 vars * /) = 0
policy: bind
preferred node: 0
physcpubind: 0 1 2 3 4 5 6 7 8 9 10 11
cpubind: 0
nodebind: 0
membind: 0