If given no arguments or redirection use, the cat command reads from standard input.
But when I execute it with execve()
it doesn't behave as it does in bash.
Code:
#include <unistd.h>
#include <fcntl.h>
int main(int ac, char **av, char **env)
{
char *args[] = {"/bin/cat",NULL};
int ps = fork();
if (!ps)
execve("/bin/cat",args, env);
}
Output :
cat: stdin: Input/output error
I tried running it with no arguments, but it returns an error instead.
CodePudding user response:
Assumption: you're running this program from a shell which is running in a terminal. (Otherwise you wouldn't see this behavior.)
Note: if you try to reproduce this, you may see different effects due to a race condition between the parent and the child. The behavior in the question is the most likely one, and you can force it with the child_sleeps
variant below, but you might observe different behavior if the child gets a lot of CPU time before the parent exits — I'll explain below with the parent_sleeps
variant.
cat
with no argument tries to read from its standard input (i.e. file descriptor 0). Since nothing has redirected the standard input, it's still the terminal. What could go wrong when trying to read from a terminal? Let's consult some documentation about read
, for example the OpenBSD man page1 or the Linux man page or the POSIX specification. Citing POSIX (whose wording the others more or less copy):
The process is a member of a background process group attempting to read from its controlling terminal, and either the calling thread is blocking SIGTTIN or the process is ignoring SIGTTIN or the process group of the process is orphaned.
To understand this, you need to understand the basics of process groups and how they interact with terminals.
The basic idea of a process group is that it consists of a process, its children, its grandchildren, etc., apart from the sub(sub…)processes that have moved into their own process group. When you run a command from a shell, the shell puts it in its own process group. So there is a process group which contains both the original process of your program and the child process created by your call to fork
, and no other process.
Now for the part about the terminal. The basic idea is that only one program should have access to a terminal at a time. Otherwise, which program would receive the input? Some programs use subprocesses, so the ownership of the terminal goes to a process group, not just a process. The process group that owns the terminal is called the foreground process group, and other process groups are background process groups. The shell command fg
makes a process group become the foreground process group.
When a process tries to read from a terminal, the kernel checks whether it “owns” the terminal. More precisely, the process should be in the foreground process group. Unrelated processes are also allowed to read from a terminal (as long as it has had permission to open it, that's fair game, even if it's an unusual thing to do). But a process belonging to a background process group is not allowed to read from the terminal. Normally, the kernel sends the process a SIGTTIN signal, and the default effect is to suspend the process2,3. But if the process ignores or blocks SIGTTIN, there is a further step to prevent the process from reading: the read
system call errors out with EIO
. This is to avoid a situation where an unrelated background program would accidentally “steal” some input from the foreground program.
Now we can connect this with what happens with cat
. By the time cat
runs, its parent has exited. (In principle the parent might not have exited yet, if cat
starts sufficiently fast, but it's unlikely. I'll discuss this below with the parent_sleeps
variant.) So the child cat
process is alone in its process group. When the parent process exits, the shell takes back ownership of the terminal, so the process group of cat
is a background process group, and the kernel will try to prevent it from reading from the terminal.
But we're still not quite there yet: cat
does not try to handle SIGTTIN, so why doesn't the kernel send this signal? It's the other case for EIO
: the orphaned process group. Once the parent process exits (and the shell notices), cat
's parent process no longer exits. But processes must have a parent, so the init process (PID 1) “adopts” orphan processes: if a process's original parent disappears, the process's parent is set to 1. Since cat
was alone in its process group, and the parent process is 1 which is not part of the same session, the process group is an orphan process group, and the kernel makes read
return EIO
.
By the way, the reason for the different treatment for orphan process groups is that in the normal case, a background process group might go back into the foreground if the user runs the fg
command in the shell. So if the background program tries to read from the terminal, it's suspended until hopefully it regains access to the terminal. But if the process group is orphaned, there is no longer a shell job that the user can put in the foreground, so there is no “normal” way for the process to ever get back into a state where it would be allowed to read from the terminal. So there's no point in suspending it: reading is and will remain an error.
To allow cat
to run in the background, keep its parent process running. You can run the following variation parent_waits
where the parent waits for the process to exit.
/* parent_waits.c */
#include <unistd.h>
#include <fcntl.h>
int main(int ac, char **av, char **env)
{
char *args[] = {"/bin/cat",NULL};
int ps = fork();
if (ps) {
int status;
wait(&status);
} else {
execve("/bin/cat",args, env);
}
}
I mentioned above that there is a race condition. If you can't reliably reproduce the behavior in the question, use the child_sleeps
variant below, where the child sleeps for long enough for the parent to finish exiting.
/* child_sleeps.c */
#include <unistd.h>
#include <fcntl.h>
int main(int ac, char **av, char **env)
{
char *args[] = {"/bin/cat",NULL};
int ps = fork();
if (!ps) {
usleep(100000);
execve("/bin/cat",args, env);
}
}
If the parent is slow to exit, it's possible that cat
will be able to read before the parent exits. You can force this behavior by adding a delay before starting cat
, with the following parent_sleeps
variant:
/* parent_sleeps.c */
#include <unistd.h>
#include <fcntl.h>
int main(int ac, char **av, char **env)
{
char *args[] = {"/bin/cat",NULL};
int ps = fork();
if (ps) {
sleep(1);
} else {
execve("/bin/cat",args, env);
}
}
With this variant, until the sleep
ends (1 second in the code above, adjust as desired), cat
works normally. Then the parent exits and you get a shell prompt back. After that, when cat
tries to read again, it receives EIO
.
$ ./parent_sleeps
one
one
$ two
two
/bin/cat: -: Input/output error
A final note: you might be tempted to observe what's going on by looking in a debugger, or by tracing system calls. But you need to be careful not to change the situation with respect to process groups. For example, under Linux, if you try to trace the program normally with strace, the
strace` process is also in the process group and remains in the foreground.
strace -o strace_in_foreground.strace -f ./a.out
To observe the system calls leading to the EIO case, tell strace
to detach the traced program.
strace -o program_in_background.strace -D -f ./a.out
1 Disappointingly, the FreeBSD manual omits the relevant case.
2 If this happens in a process group that is a job in a shell, the shell prints a message like “suspended (tty input)” (zsh) or “Stopped (SIGTTIN)” (ksh) or “Stopped” (bash)).
3 The same happens with an attempt to write and the SIGTTOU signal. With output however, the process can ignore SIGTTOU and the write will go through.