Looks like cron spawns a shell which in turn spawns a script; how can I get rid of the intermediate shell?
What am I doing wrong?
Nothing. What you are doing is not wrong, it is at most sub-optimal. The described situation is normal. It can be optimized though. Keep reading.
Preliminary note
There are different implementations of cron and different implementations of sh
. I could run a scratch CentOS 7 and pretty accurately deduce your cron and your sh
from it, I think. But I won't. This answer is going to be more generic. Therefore don't be surprised if I write something like "sh
could do this and this, but apparently your sh
doesn't".
Analysis
When the time comes your cron runs /bin/sh
(or another shell) and passes the /root/script.sh > /dev/null 2>&1
string as an option-argument for -c
. So it's like
/bin/sh -c '/root/script.sh > /dev/null 2>&1'
except the above line is what you would type in a shell, while cron builds (or at least should build) the argument array directly, where /bin/sh
, -c
and /root/script.sh > /dev/null 2>&1
are separate elements and there is no need for additional single-quotes (so you can use single-quotes in the command and they will not break anything).
There is at least one thing cron does to the specified command prior to passing it to sh
: it interprets %
(see man 5 crontab
). Your command does not contain %
, so this mechanism is not relevant. Cron could do more:
- It could detect
$
,|
,&&
,;
and such and decide it should certainly run a shell that will handle these. - In your case it could detect the shell would not be needed, if only cron itself handled the redirections; so it would handle the redirections and run
/root/script.sh
directly. - It could detect really simple commands (e.g.
/usr/bin/beep
) and run them directly.
It could but it shouldn't. It's not its job to optimize and act like a shell. Its job is to run a command interpreter (shell) which takes it from here.
In your case sh
is spawned. It interprets /root/script.sh > /dev/null 2>&1
, handles the redirections and runs /root/script.sh
as a child process.
In general a shell can optimize this step, your sh
apparently doesn't. Some shells do. It's possible for a shell to reliably tell if it can "disappear". I will elaborate below.
Solutions
-
(inferior) Run the command asynchronously. If your command is
/root/script.sh > /dev/null 2>&1 &
then cron will run
sh
and thesh
will exit without waiting forscript.sh
to finish. This has side effects though:- cron will see its child process (i.e.
sh
) exited. There will be no direct connection between cron and thescript.sh
process (unless cron sets itself up as a "subreaper", your cron probably doesn't). - cron will see its child process (i.e.
sh
) exited with exit status0
. Normally (without&
) the exit status ofsh
would depend on the exit status ofscript.sh
; but for this to happensh
must wait forscript.sh
to finish.
By manually killing
sh
you inadvertently introduced these side effects. In particular cron doesn't care much, I think (it can be configured to log failed jobs though). But in general the parent process may care, so users should not "randomly" kill intermediate shells; and shells should not arbitrarily decide they can exit without waiting for the last command. As far as I know no shell does this kind of "optimization". - cron will see its child process (i.e.
-
(suprior) Explicitly
exec
toscript.sh
:exec /root/script.sh > /dev/null 2>&1
exec
makes the shell replace itself with the command without creating a new process. In this casescript.sh
will replacesh
under the same process ID. In the context of the side effects mentioned above, this is very nice:- The parent process won't notice when
sh
"disappears". It will seescript.sh
instead ofsh
; and it will considerscript.sh
the child. Withoutexec
the parent process would be notified when its child (sh
) exits just afterscript.sh
exits. Withexec
the parent process will be notified whenscript.sh
exits, because this is the child now. Effectively in both cases it's whenscript.sh
exits. - The parent process will get the exit status directly from
script.sh
when it terminates. The factsh
no longer exists is irrelevant. Withoutexec
the parent process would get the exit status relayed bysh
from the last command it runs, i.e. fromscript.sh
. Effectively in both cases it's the exit status fromscript.sh
.
This method is almost transparent, even if the parent process cares about when the child exits or what exit status it returns. This is the optimization shells can do. Bash does it. In some cases it may be quite confusing if you don't know about it; compare this answer of mine where there is
sshd
instead of cron.If you tell cron to use
bash
and you don't useexec
then the optimization may kick in, as if you usedexec
; or it may not, it depends on the command. So if you want the shell toexec
for sure then explicitly tell it toexec
; do not rely on optimization capabilities of the shell.Now your cron uses
sh
which most likely never optimizes like this, so useexec
explicitly in the command.Side note:
exec command1; command2
orexec command1 && command2
will never get tocommand2
. Keep this in mind, especially if you happen to haveexec command1
and decide to addcommand2
to the job. These should work:command1; exec command2
orcommand1 && exec command2
. But then expectsh
process to exist whencommand1
runs; you need it to runcommand2
later. - The parent process won't notice when