1

I have a really small question. Let's take this command as an example cat < file.txt.

When the shell sees the <, it redirects stdin (0 file descriptor) by forking a new process and only changing stdin to that file for that specific process environment (since it's not a builtin).

Nevertheless when we have a builtin command, the shell doesn't fork a new process, but just changes stdin to the specified file in the whole terminal environment (don't know if it's for the whole terminal environment but since the shell doesn't fork a new process I don't see how it can be done just for the program (the builtin)) and changes it back to e.g /dev/pts/0 (the normal stdin) when the program is finished

But here is where it gets fuzzy for me. When running two builtin commands of which one is in the background and both have their stdout or stdin redirected to two different files, this would mean that they'd both change the stdin (or stdout) to their own files but that means that one of the programs would use the stdin of the other program since they can't have both the stdin redirected at the same time because they aren't forked.

This is only the case though if what I mentioned above is true that stdin gets changed for the entire terminal environment and not only for the program in case of a builtin, because of not being forked

If I'm not clear, it's really hard to explain but I'll try to formulate it another way: with builtin commands, the shell doesn't fork new processes, and if one is run in the bg and one in the fg, both of which have redirected stdin or stdin to different files, how can they both at the same time have different stdins since the shell for built-in programs changes stdin in the entire terminal until the program has finished executing?

1 Answer 1

1

The shell does fork builtin commands under various conditions. One of the conditions is to run background jobs. Here, print and zselect are both builtins in the backgrounded function inbg:

#!/usr/bin/env zsh
zmodload zsh/system
zmodload zsh/zselect
function inbg { print BG PID $sysparams[pid] > pid.bg ; zselect -t 333 }
function infg { print FG PID $sysparams[pid] > pid.fg }
inbg &
infg
wait

When run the inbg function runs under a different process ID as can be observed via inspecting the process tree or the correct logging of the child process ID (via $sysparams[pid] in ZSH; shells vary here).

% zsh builtins & sleep 1; pgrep -lf builtins; wait
[1] 97170
97170 zsh builtins
97172 zsh builtins
[1]  + done       zsh builtins
% cat pid*
BG PID 97172
FG PID 97170

This allows standard output to be rewired to pid.bg in one process and pid.fg in the other process easily enough.

Otherwise, the "whole terminal environment" is in no way affected; a shell (or any process on unix) can rewire file descriptors so that output to standard output can be changed from one destination to another temporarily. This could allow a builtin to send standard out elsewhere for a while, or so forth:

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int nullfd, savefd;

    puts("out1");
    savefd = dup(STDOUT_FILENO);

    nullfd = open("/dev/null", O_WRONLY);
    close(STDOUT_FILENO);
    dup2(nullfd, STDOUT_FILENO);
    puts("nothing");
    close(nullfd);

    close(STDOUT_FILENO);
    dup2(savefd, STDOUT_FILENO);
    puts("out2");

    return 0;
}

Via the above rewirings nothing is printed to /dev/null between the puts to standard out (unless one or more of the system calls fail, which the above code, for clarity, does not check):

% make redirect-stdout
cc     redirect-stdout.c   -o redirect-stdout
% ./redirect-stdout
out1
out2

For more reading about such "rewirings", Advanced Programming in the Unix Environment (APUE) covers pipe(2) and dup(2) in more detail.

Process State

Proof that redirection of standard I/O is per-process can be obtained by creating two threads and having them redirect standard output to two different files:

#include <err.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_cond_t th_cnd  = PTHREAD_COND_INITIALIZER;
pthread_mutex_t th_mtx = PTHREAD_MUTEX_INITIALIZER;
void *threader(void *);

int main(void) {
    pthread_t xxx, yyy;
    pthread_create(&xxx, NULL, threader, (void *) "thread.x");
    pthread_create(&yyy, NULL, threader, (void *) "thread.y");
    pthread_cond_wait(&th_cnd, &th_mtx);
    return 0;
}

void *threader(void *ptr) {
    char *label = (char *) ptr;
    int fd, i;
    fd = open(label, O_APPEND | O_CREAT | O_WRONLY, 0666);
    if (fd < 0) err(1, "open failed");
    close(STDOUT_FILENO);
    dup2(fd, STDOUT_FILENO);
    for (i = 0; i < 5; i++) { puts(label); usleep(100000); }
    pthread_cond_broadcast(&th_cnd);
    return (void *) 0;
}

This code results, maybe, in

% CFLAGS=-lpthread make thread-io-redirect
cc -lpthread    thread-io-redirect.c   -o thread-io-redirect
% rm thread.*
zsh: no matches found: thread.*
% ./thread-io-redirect
% stat -f '%N %z' thread.*
thread.x 0
thread.y 90

all the output placed into thread.y (or it could all end up in thread.x, or maybe some other edge cases depending on how busy the system is). This shows that the standard output redirection is global to the process. (A simpler argument is that there is only one STDOUT_FILENO descriptor per process, and functions like puts(3) will only use that one number.)

2
  • Thank you so much for the reply! So when putting a process in bg, it will fork a new process (although it's a built-in), otherwise the whole terminal environment (the currently opened terminal) would be affected temporarily when I redirect stdout and stdin for two built-in commands (both in bg) and assuming that it didn't fork new processes there would be a conflict (in the sense of both commands get the same stdin or stdout) as I described because both built-in commands changed the stdin or stdout to different files and were not forked. Jul 25, 2022 at 17:01
  • It may help to drop the "whole terminal environment" notion; a shell process can run without a terminal, for example as a cron job, or a script run as part of the system boot process. Redirections are internal to a process, and in no way involve the parent process, regardless of whether the parent happens to be a terminal (e.g. xterm) or not (e.g. crond, the system init, ...).
    – thrig
    Jul 25, 2022 at 20:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .