a problem on pipes: making the child run less and parent writes to it

48 views
Skip to first unread message

Meredith Montgomery

unread,
Nov 4, 2021, 12:00:46 PM11/4/21
to
(*) Introduction

I first wrote a program to fork and make parent read from the child.
The child would invoke cat to write a file to stdout and the parent
would invoke less to let me read the file on the screen. Everything
worked as expected and I had no questions about it.

(*) New experiment

So I decided to reverse their roles --- make the child run less and the
parent cat the file. But before getting there, let's try something
easier --- instead of cat'ting the file, the parent just invokes write()
on the write-end of the pipe. That's a little simpler because we don't
call execl() and don't need to dup2-file-descriptors, say.

(*) Unexpected result

This first attempt at reversing these roles produces no effect.

--8<---------------cut here---------------start------------->8---
%./parent-is-proc-and-child-is-less.exe
%
--8<---------------cut here---------------end--------------->8---

What is my misunderstanding here? The full program is below with a lot
of commentary so you can see what's on my mind. Thank you!

(*) The program

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

//--8<---------------cut here---------------start------------->8---
// helpers
void syserr_die(char* msg) { perror(msg); exit(1); }

void pipe_die(int *fd) {
int r; r = pipe(fd); if (r < 0) { syserr_die("pipe"); }
}

pid_t fork_die() {
int p; p = fork(); if (p < 0) { syserr_die("fork"); }
return p;
}

void dup2_die(int from, int to, char *msg) {
int r;
r = dup2(from, to); if (r < 0) syserr_die(msg);
}
//--8<---------------cut here---------------end--------------->8---

int main() {
int fd[2]; pid_t pid;

pipe_die(&fd[0]);

/*
At this point I have a reading file descriptor at fd[0] and a
writing one at fd[1].
*/

pid = fork();
if (pid < 0) {
syserr_die("fork");
}

if (pid == 0) {
/*
I run /less/, so I must read from the pipe and not write to it.
So I close fd[1] right away because I don't need it.
*/
close(fd[1]);

/*
It is /less/ that will do the reading and it does it from STDIN,
so I must ask system to make this replacement for me. Let's
create the following vocabulary: move fd[0] to STDIN, that is,
to 0.
*/

dup2_die(fd[0], 0, "child dup2"); /* Let stdin be fd[0]. */

/*
When /less/ starts reading, it will be reading bytes written to
the pipe. But it's important to notice that the pipe is an
object in the kernel, not in our program. So fd[0] is not the
pipe; it's just that number that refers to that pipe. The
number 0 (which refers to STDIN) is already referring to the
pipe, so we don't need a second reference --- so we destroy
fd[0].
*/
close(fd[0]);

/*
We are ready to run less now. The binary /less/ will be loaded in
memory and what it finds as its kernel-context is everything we
prepared above. We are done with the parent.
*/

execl("/usr/bin/less", "less", (char*) 0);
syserr_die("execl of less");

/*
I keep running until /less/ is dead.
*/

}

/* I'm the writer. My job is to just write data to fd[1] ---
which is the writing end of the pipe. I don't need to read
anything from the STDIN, so I begin by closing it. I will
write to fd[1], not to STDOUT or STDERR. So close them too.

*/
close(0); close(1); close(2);

/*
I'm going to write to fd[1]. I have no business with fd[0].
*/
close(fd[0]);

for (int i = 1; i <= 200; ++i) {
char s[1000];
sprintf(s, "i = %d\n", i);
write(fd[1], s, strlen(s));
}

/*
My job is done. I'm dead by now.
*/
}

Siri Cruise

unread,
Nov 4, 2021, 2:02:47 PM11/4/21
to
In article <86a6ijv...@levado.to>,
Meredith Montgomery <mmont...@levado.to> wrote:

> execl("/usr/bin/less", "less", (char*) 0);

less has two inputs, stdin and the controlling terminal. stdin is
copied to stdout, but it interrupts the copy from time to time
until you give a go ahead from the terminal.

--
:-<> Siri Seal of Disavowal #000-001. Disavowed. Denied. Deleted. @
'I desire mercy, not sacrifice.' /|\
Discordia: not just a religion but also a parody. This post / \
I am an Andrea Doria sockpuppet. insults Islam. Mohammed

Lew Pitcher

unread,
Nov 4, 2021, 2:07:16 PM11/4/21
to
On Thu, 04 Nov 2021 12:58:35 -0300, Meredith Montgomery wrote:

Your program mostly works, but you need one small change. See below
IMHO, these "helper" functions are less than helpfull, in this program.
When debugging a program that you are using to learn a new technique,
it helps to have all the relevant logic in plain sight; helper functions
hide logic from sight, making it harder to determine if a flaw is in
your primary logic, or in the "helper" functions. Helper functions,
if inexpertly used, also obfuscate the logical flow of the program,
hiding more logic errors. Just a thought from a guy that spent 30 years
designing, writing, debugging, and modifying complex programs for a
living.
There's no need to close() stderr, and if you don't, then you gain the
(dubious, to some) ability to insert debugging fprintf(stderr,...)
statements into the code to trace execution or track down bugs.

In fact, given that your logic write()'s to the write end of the
pipe, you don't really have to close() stdin or stdout either.

It's sometimes helpful to have these around.

>
> /*
> I'm going to write to fd[1]. I have no business with fd[0].
> */
> close(fd[0]);
>
> for (int i = 1; i <= 200; ++i) {
> char s[1000];
> sprintf(s, "i = %d\n", i);
> write(fd[1], s, strlen(s));

A (bit) better approach here might have been to use the length returned
by sprintf(3) as the write() count parameter.

However, if, instead of closing stdout, you had
dup2(STDOUT_FILENO,fd[1])
then you could have
printf("i = %d\n",i);
directly to your pipe output.

> }

Now, if your parent process terminates now, it closes the write end of the
pipe before the child has read it to the end. From the pipe(7) manual page:
"If all file descriptors referring to the write end of a pipe have been
closed, then an attempt to read(2) from the pipe will see end-of-file
(read(2) will return 0)"

You need to wait(2) here for the child to complete it's processing.

>
> /*
> My job is done. I'm dead by now.
> */
> }

--
Lew Pitcher
"In Skills, We Trust"

Rainer Weikusat

unread,
Nov 4, 2021, 3:06:35 PM11/4/21
to
Meredith Montgomery <mmont...@levado.to> writes:

> (*) Introduction
>
> I first wrote a program to fork and make parent read from the child.
> The child would invoke cat to write a file to stdout and the parent
> would invoke less to let me read the file on the screen. Everything
> worked as expected and I had no questions about it.
>
> (*) New experiment
>
> So I decided to reverse their roles --- make the child run less and the
> parent cat the file. But before getting there, let's try something
> easier --- instead of cat'ting the file, the parent just invokes write()
> on the write-end of the pipe. That's a little simpler because we don't
> call execl() and don't need to dup2-file-descriptors, say.
>
> (*) Unexpected result
>
> This first attempt at reversing these roles produces no effect.

Working program (with the noise comments removed):

----
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>

//--8<---------------cut here---------------start------------->8---
// helpers
void syserr_die(char* msg) { perror(msg); exit(1); }

void pipe_die(int *fd) {
int r; r = pipe(fd); if (r < 0) { syserr_die("pipe"); }
}

pid_t fork_die() {
int p; p = fork(); if (p < 0) { syserr_die("fork"); }
return p;
}

void dup2_die(int from, int to, char *msg) {
int r;
r = dup2(from, to); if (r < 0) syserr_die(msg);
}
//--8<---------------cut here---------------end--------------->8---

int main() {
int fd[2]; pid_t pid;

pipe_die(&fd[0]);

pid = fork();
if (pid < 0) {
syserr_die("fork");
}

if (pid == 0) {
close(fd[1]);
dup2_die(fd[0], 0, "child dup2"); /* Let stdin be fd[0]. */
close(fd[0]);
execl("/usr/bin/less", "less", (char*) 0);
syserr_die("execl of less");
}

close(fd[0]);

for (int i = 1; i <= 200; ++i) {
char s[1000];
sprintf(s, "i = %d\n", i);
write(fd[1], s, strlen(s));
}

close(fd[1]);
wait(NULL);
}
----------

fd[1] needs to be closed here because otherwise, less never sees an EOF
and thus, blocks forever expecting input which won't ever
arrive. Furher, the parent needs to wait because otherwise, the
process running less becomes orphaned and will be killed (via SIGTTOU)
when it tries to access the terminal.

Rainer Weikusat

unread,
Nov 4, 2021, 4:15:11 PM11/4/21
to
Rainer Weikusat <rwei...@talktalk.net> writes:

[...]

> Furher, the parent needs to wait because otherwise, the
> process running less becomes orphaned and will be killed (via SIGTTOU)
> when it tries to access the terminal.

A bit more precise: The shell runs each command in its own process
group. The parent process of the less is the process group leader of
this process group and the one the controlling terminal is associated
with. As soon as it exits, this controlling terminal becomes free for
reuse. The less will then be a background process running in an orphaned
process group (as the process group leader has exited) and thus, the
kernel will prevent it from using the terminal by making the system call
fail with EIO (Linux, different on other systems).

Geoff Clare

unread,
Nov 5, 2021, 11:11:06 AM11/5/21
to
Rainer Weikusat wrote:

> The shell runs each command in its own process
> group. The parent process of the less is the process group leader of
> this process group and the one the controlling terminal is associated
> with. As soon as it exits, this controlling terminal becomes free for
> reuse.

No, the controlling terminal doesn't become free until the shell (as
session leader) exits.

> The less will then be a background process running in an orphaned
> process group (as the process group leader has exited) and thus, the
> kernel will prevent it from using the terminal by making the system call
> fail with EIO

Correct.

> (Linux, different on other systems).

How do other systems differ? The behaviour you described is what POSIX
requires. XBD 11.1.4 Terminal Access Control:

If TOSTOP is set, the process group of the writing process is
orphaned, the writing process is not ignoring the SIGTTOU signal,
and the writing thread is not blocking the SIGTTOU signal, the
write() shall return -1, with errno set to [EIO] and no signal
shall be sent.

--
Geoff Clare <net...@gclare.org.uk>

Rainer Weikusat

unread,
Nov 5, 2021, 6:16:02 PM11/5/21
to
Geoff Clare <ge...@clare.See-My-Signature.invalid> writes:
> Rainer Weikusat wrote:
>> The shell runs each command in its own process
>> group. The parent process of the less is the process group leader of
>> this process group and the one the controlling terminal is associated
>> with. As soon as it exits, this controlling terminal becomes free for
>> reuse.
>
> No, the controlling terminal doesn't become free until the shell (as
> session leader) exits.

It becomes sort-of free because it's now the controlling terminal of a
process unrelated to the still running background process. This could be
the shell. Or the next command executed by it.

Nevertheless, that was both sloppy use of language and lack of knowledge
on my part.

>> The less will then be a background process running in an orphaned
>> process group (as the process group leader has exited) and thus, the
>> kernel will prevent it from using the terminal by making the system call
>> fail with EIO
>
> Correct.
>
>> (Linux, different on other systems).
>
> How do other systems differ? The behaviour you described is what POSIX
> requires. XBD 11.1.4 Terminal Access Control:
>
> If TOSTOP is set, the process group of the writing process is
> orphaned, the writing process is not ignoring the SIGTTOU signal,
> and the writing thread is not blocking the SIGTTOU signal, the
> write() shall return -1, with errno set to [EIO] and no signal
> shall be sent.

Glibc documentation claims they do:

When a process in an orphaned process group (*note Orphaned
Process Groups::) receives a 'SIGTSTP', 'SIGTTIN', or 'SIGTTOU'
signal and does not handle it, the process does not stop.
Stopping the process would probably not be very useful, since
there is no shell program that will notice it stop and allow the
user to continue it. What happens instead depends on the
operating system you are using. Some systems may do nothing;
others may deliver another signal instead, such as 'SIGKILL' or
'SIGHUP'.

[at the and of 24.2.5 Job Control Signals]

Wayne Harris

unread,
Nov 6, 2021, 9:52:48 PM11/6/21
to
That explains everything I was having trouble with. It's not easy to
infer this from the documentation --- that the orphaned process would be
killed via SIGTTOU. The default action --- from signal(3) --- is that
it would stop the process, not kill it. I read the rest of thread ---
thanks very much for your analysis here (very helpful) --- and saw your
comment about the Glibc documentation and that's probably what's
happening here.

I looked up the section ``Orphaned Process Groups'' and, for the record,
here's what it says.

--8<---------------cut here---------------start------------->8---
(*) Orphaned Process Groups

When a controlling process terminates, its terminal becomes free and a
new session can be established on it. (In fact, another user could log
in on the terminal.) This could cause a problem if any processes from
the old session are still trying to use that terminal.

To prevent problems, process groups that continue running even after the
session leader has terminated are marked as orphaned process groups.

When a process group becomes an orphan, its processes are sent a SIGHUP
signal. Ordinarily, this causes the processes to terminate. However, if
a program ignores this signal or establishes a handler for it (see
section Signal Handling), it can continue running as in the orphan
process group even after its controlling process terminates; but it
still cannot access the terminal any more.
--8<---------------cut here---------------end--------------->8---

So I infer that if the parent dies before /less/ reaches for the
terminal, /less/ will then be a process in an orphaned process group and
will, therefore, get a SIGHUP. That's probably what's killing /less/ in
my case here.

Trying to see it being killed, I added a sleep(1) before the death of
the parent just to slow it down and that always makes /less/ leave the
terminal in a bad state, which suggests it is being killed indeed.

But I couldn't find a way to see which signals it was getting. I tried
running my own program in place of /less/, but I believe I did not know
how to imitate /less/ enough to simulate the behavior. My program never
seem to get any signal and didn't seem to be killed either. I wonder if
you have any suggestions here. Thank you!

Wayne Harris

unread,
Nov 6, 2021, 9:55:34 PM11/6/21
to
Point taken!
That makes sense. Thanks.

>>
>> /*
>> I'm going to write to fd[1]. I have no business with fd[0].
>> */
>> close(fd[0]);
>>
>> for (int i = 1; i <= 200; ++i) {
>> char s[1000];
>> sprintf(s, "i = %d\n", i);
>> write(fd[1], s, strlen(s));
>
> A (bit) better approach here might have been to use the length returned
> by sprintf(3) as the write() count parameter.
>
> However, if, instead of closing stdout, you had
> dup2(STDOUT_FILENO,fd[1])
> then you could have
> printf("i = %d\n",i);
> directly to your pipe output.

Oh, true. Nice.

>> }
>
> Now, if your parent process terminates now, it closes the write end of the
> pipe before the child has read it to the end. From the pipe(7) manual page:
> "If all file descriptors referring to the write end of a pipe have been
> closed, then an attempt to read(2) from the pipe will see end-of-file
> (read(2) will return 0)"
>
> You need to wait(2) here for the child to complete it's processing.

But I think I must also close fd[1] like Rainer Weikusat said.
(Otherwise /less/ blocks on a read(2) call.)

Lew Pitcher

unread,
Nov 7, 2021, 2:48:59 PM11/7/21
to
On Sat, 06 Nov 2021 22:53:24 -0300, Wayne Harris wrote:

> Lew Pitcher <lew.p...@digitalfreehold.ca> writes:
[snip]
>> You need to wait(2) here for the child to complete it's processing.
>
> But I think I must also close fd[1] like Rainer Weikusat said.
> (Otherwise /less/ blocks on a read(2) call.)

Yah. I was wrong there. You need to close() first. Sorry; I didn't
think it all the way through.

>>>
>>> /*
>>> My job is done. I'm dead by now.
>>> */
>>> }




Lew Pitcher

unread,
Nov 7, 2021, 2:53:51 PM11/7/21
to
I spent a bit of time to write a one-off that both performs the
requisite task correctly, /and/ embodies the stylistic points that
I made in the prior post. This is what I came up with...

/******************* Code begins **********************************/

/*
** This program demonstrates a solution to a problem posted by
** Meredith Montgomery in the comp.unix.programmer usenet group.
**
** The programmer wants a program that creates a pipe between
** itself and the stdin of a child process. The child process
** runs less(1) on it's stdin (the read end of the pipe) while
** the parent process generates print data to the write end of
** the pipe.
**
** The development of this program followed "Occam's razor",
** in that it subscribed to the principle that "entities
** should not be multiplied beyond necessity". In this case,
** I endeavoured to avoid needless encapsulation and abstraction
** of logic, in order to create a simple working version of the
** solution. Embellishments, such as logic encapsulation and
** isolation, are left to others to implement.
**
** == Evolution ==
** 0: basic framework
** 1: create pipe
** 2: fork
** 3: child process reads input from pipe
** 4: parent process output to pipe
** 5: document finer points in comments
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
int status = EXIT_FAILURE;
int pfd[2];

/*
** Step 1, make our pipe
*/
if (pipe(pfd) == 0) /* On error, pipe(2) will return -1 and set errno */
{
/*
** Step 2: we got a valid pipe pair, now launch child and perform all processing
*/

switch (fork())
{
/*
** On error, fork(2) will return -1, and set errno appropriately
*/
case -1: /* fork failed */
fprintf(stderr,"%s: fork() failed - %s\n",argv[0],strerror(errno));
break;

/*
** In the child process, fork(2) returns 0
*/
case 0: /* CHILD process */
/*
** Step 3: prepare the child process to read the pipe through stdin
** and run "less" on the input
*/
close(pfd[1]); /* don't hold pipe write end open */
if (dup2(pfd[0],STDIN_FILENO) != -1)
{
close(pfd[0]); /* close the now useless fd */
execl("/usr/bin/less","less",NULL); /* use "less" to process stdin */
/* we only get here if execl() failed */
fprintf(stderr,"%s: execl() failed - %s\n",argv[0],strerror(errno));
}
else fprintf(stderr,"%s: child dup2() failed - %s\n",argv[0],strerror(errno));
break;

/*
** In the parent process, fork(2) returns the non-zero, non -1 PID
** of the child process
*/
default: /* PARENT process */
/*
** Step 4: prepare the parent process to write the pipe through stdout
** write some data so that the child can process it, wait for
** the child to process the data and then terminate the parent.
*/
close(pfd[0]); /* dont hold pipe read end open */
if (dup2(pfd[1],STDOUT_FILENO) != -1) /* use the write end of pipe as stdout */
{
close(pfd[1]); /* close the now useless fd */

/* generate some output to be piped to the child */
for (int count = 0; count < 100; ++count)
printf("This is generated line number %d\n",count);
fclose(stdout); /* signals EOF to child process stdin */

/*
** The parent needs to wait because otherwise, the process running
** less(1) becomes orphaned and will be killed (via SIGTTOU) when
** it tries to access the terminal.
** (Rainer Weikusat <rwei...@talktalk.net>)
*/
wait(NULL); /* wait for the child to finish */

status = EXIT_SUCCESS; /* Mission Accomplished! */
}
else fprintf(stderr,"%s: parent dup2() failed - %s\n",argv[0],strerror(errno));
break;
}
}
else fprintf(stderr,"%s: pipe() failed - %s\n",argv[0],strerror(errno));

return status;
}
/******************* Code ends **********************************/

Rainer Weikusat

unread,
Nov 7, 2021, 2:54:54 PM11/7/21
to
Wayne Harris <whar...@protonmail.com> writes:
> Rainer Weikusat <rwei...@talktalk.net> writes:

[...]


> I looked up the section ``Orphaned Process Groups'' and, for the record,
> here's what it says.
>
>
> (*) Orphaned Process Groups
>
> When a controlling process terminates, its terminal becomes free and a
> new session can be established on it. (In fact, another user could log
> in on the terminal.) This could cause a problem if any processes from
> the old session are still trying to use that terminal.
>
> To prevent problems, process groups that continue running even after the
> session leader has terminated are marked as orphaned process groups.
>
> When a process group becomes an orphan, its processes are sent a SIGHUP
> signal.

That's not applicable because the session leader hasn't terminated. For
an interactive terminal session, this will usually be the login shell.

Geoff Clare

unread,
Nov 8, 2021, 8:41:06 AM11/8/21
to
Rainer Weikusat wrote:

> Geoff Clare <ge...@clare.See-My-Signature.invalid> writes:
>>
>> How do other systems differ? The behaviour you described is what POSIX
>> requires. XBD 11.1.4 Terminal Access Control:
>>
>> If TOSTOP is set, the process group of the writing process is
>> orphaned, the writing process is not ignoring the SIGTTOU signal,
>> and the writing thread is not blocking the SIGTTOU signal, the
>> write() shall return -1, with errno set to [EIO] and no signal
>> shall be sent.
>
> Glibc documentation claims they do:
>
> When a process in an orphaned process group (*note Orphaned
> Process Groups::) receives a 'SIGTSTP', 'SIGTTIN', or 'SIGTTOU'
> signal and does not handle it, the process does not stop.
> Stopping the process would probably not be very useful, since
> there is no shell program that will notice it stop and allow the
> user to continue it. What happens instead depends on the
> operating system you are using. Some systems may do nothing;
> others may deliver another signal instead, such as 'SIGKILL' or
> 'SIGHUP'.
>
> [at the and of 24.2.5 Job Control Signals]

My guess is that this text is very old and is referring to the
behaviour of various systems pre-POSIX.

--
Geoff Clare <net...@gclare.org.uk>

Rainer Weikusat

unread,
Nov 9, 2021, 12:25:27 PM11/9/21
to
Lew Pitcher <lew.p...@digitalfreehold.ca> writes:

[...]

> /*
> ** The parent needs to wait because otherwise, the process running
> ** less(1) becomes orphaned and will be killed (via SIGTTOU) when
> ** it tries to access the terminal.
> ** (Rainer Weikusat <rwei...@talktalk.net>)
> */

This is unfortunately almost completely wrong: The process becomes
orphaned but this only means that it becomes a child of init (whatever
that's called in the FatRat Lunix version du jour). The important bit
here is that the process group becomes orphaned. When processes
belonging to it which don't block or ignore SIGTTOU try to write to
terminal, the system call will fail with EIO. That's presumably what
terminates the less here.

Lew Pitcher

unread,
Nov 9, 2021, 12:34:02 PM11/9/21
to
Noted.
Corrected.

Meredith Montgomery

unread,
Mar 2, 2022, 11:33:41 AMMar 2
to
I have a question about Lew Pitcher's program. I still don't know what
else must be done if we replace the printf() calls with an execution of
cat. I seem to understand the original program completely, but my
understanding must be wrong somewhere because I can't make a simple
modification and predict the output. Please see the details below.
(Notice I paste the full program at the end of the message just in case
it helps anyone to copy it and compile it.)
What happens if I replace this for-loop with an execution of cat? For
example, what happens if we place this entire for-loop with

execl("/usr/bin/cat", "cat", "lew.c", NULL)?

Assume we remove the fclose and the wait below, although I think that
has no effect if execl succeeds.

I expected it to work, but it doesn't. The less process gets killed and
leaves the terminal in a inconsistent state.

> /*
> ** The parent needs to wait because otherwise, the process running
> ** less(1) becomes orphaned and will be killed (via SIGTTOU) when
> ** it tries to access the terminal.
> ** (Rainer Weikusat <rwei...@talktalk.net>)
> */
> wait(NULL); /* wait for the child to finish */

Wouldn't cat wait for the reader to close its reading end? Notice that
Rainer Weikusat and Lew Pitcher discussed this comment above. I'll show
it again here just in case it helps to answer my question.

--8<---------------cut here---------------start------------->8---
Path: aioe.org!news.swapon.de!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail
From: Rainer Weikusat <rwei...@talktalk.net>
Newsgroups: comp.unix.programmer
Subject: Re: a problem on pipes: making the child run less and parent writes to it
Date: Tue, 09 Nov 2021 17:25:22 +0000
Lines: 19
Message-ID: <87ilx1s...@doppelsaurus.mobileactivedefense.com>
References: <86a6ijv...@levado.to> <sm17gg$tp4$1...@dont-email.me>
<sm9asb$g1o$2...@dont-email.me>
Mime-Version: 1.0
Content-Type: text/plain
X-Trace: individual.net sxs5sVhUULAg0ZwigtwFiABaDVfBheWEJi/dzPUCrDgnmybJc=
Cancel-Lock: sha1:J8Xuih6gHa9fFrX1f3ROY2bpM6U= sha1:C4vMs1rkbtNeEEqaS2XCfuavJgo=
User-Agent: Gnus/5.13 (Gnus v5.13) Emacs/24.5 (gnu/linux)
Xref: aioe.org comp.unix.programmer:23115

Lew Pitcher <lew.p...@digitalfreehold.ca> writes:

[...]

> /*
> ** The parent needs to wait because otherwise, the process running
> ** less(1) becomes orphaned and will be killed (via SIGTTOU) when
> ** it tries to access the terminal.
> ** (Rainer Weikusat <rwei...@talktalk.net>)
> */

This is unfortunately almost completely wrong: The process becomes
orphaned but this only means that it becomes a child of init (whatever
that's called in the FatRat Lunix version du jour). The important bit
here is that the process group becomes orphaned. When processes
belonging to it which don't block or ignore SIGTTOU try to write to
terminal, the system call will fail with EIO. That's presumably what
terminates the less here.
--8<---------------cut here---------------end--------------->8---

So it does seem that somehow cat is not waiting for less? How is this
properly done? I still haven't understood this. Thanks for your patience.

>
> status = EXIT_SUCCESS; /* Mission Accomplished! */
> }
> else fprintf(stderr,"%s: parent dup2() failed - %s\n",argv[0],strerror(errno));
> break;
> }
> }
> else fprintf(stderr,"%s: pipe() failed - %s\n",argv[0],strerror(errno));
>
> return status;
> }
> /******************* Code ends **********************************/

(*) Full program

Scott Lurndal

unread,
Mar 2, 2022, 1:27:03 PMMar 2
to
Meredith Montgomery <mmont...@levado.to> writes:
>I have a question about Lew Pitcher's program. I still don't know what
>else must be done if we replace the printf() calls with an execution of
>cat. I seem to understand the original program completely, but my
>understanding must be wrong somewhere because I can't make a simple
>modification and predict the output. Please see the details below.
>(Notice I paste the full program at the end of the message just in case
>it helps anyone to copy it and compile it.)

>> /* generate some output to be piped to the child */
>> for (int count = 0; count < 100; ++count)
>> printf("This is generated line number %d\n",count);
>> fclose(stdout); /* signals EOF to child process stdin */
>
>What happens if I replace this for-loop with an execution of cat? For
>example, what happens if we place this entire for-loop with
>
> execl("/usr/bin/cat", "cat", "lew.c", NULL)?
>
>Assume we remove the fclose and the wait below, although I think that
>has no effect if execl succeeds.

That execl(2) system call is executed in the context of the parent.

What does exec(2) do? It completely replaces the current process
with a new process. So, you're replacing the parent process with
a shell executing the cat(1) command. The wait for the child, being part of the
former parent process, can never be executed as that process no longer
exists.

Lew Pitcher

unread,
Mar 2, 2022, 4:20:27 PMMar 2
to
On Wed, 02 Mar 2022 13:33:33 -0300, Meredith Montgomery wrote:

Scott Lurndal's reply is spot on.

> I have a question about Lew Pitcher's program. I still don't know what
> else must be done if we replace the printf() calls with an execution of
> cat. I seem to understand the original program completely, but my
> understanding must be wrong somewhere because I can't make a simple
> modification and predict the output. Please see the details below.
> (Notice I paste the full program at the end of the message just in case
> it helps anyone to copy it and compile it.)

In my local copy of the original code, I have this:
default: /* PARENT process */
/*
** Step 4: prepare the parent process to write the pipe through stdout
** write some data so that the child can process it, wait for
** the child to process the data and then terminate the parent.
*/
close(pfd[0]); /* dont hold pipe read end open [Note 1] */
if (dup2(pfd[1],STDOUT_FILENO) != -1) /* use the write end of pipe as our stdout */
{
close(pfd[1]); /* close the now useless write fd [Note 1] */

/* generate some output to be piped to the child */
for (int count = 0; count < 100; ++count)
printf("This is generated line number %d\n",count);
fclose(stdout); /* signals EOF to child process stdin [Note 1] */

wait(NULL); /* wait for the child to finish [Note 2] */

status = EXIT_SUCCESS; /* Mission Accomplished! */
}
else fprintf(stderr,"%s: parent dup2() failed - %s\n",argv[0],strerror(errno));
break;

and "Note 2" (comment beside the wait() call) later...
== Note 2 ==
Conceptually, the parent process, once it has written all its lines and
closed the write end of the pipe, /should/ be able to just exit(3) and
terminate. In this case, the only side-effect is that the child process
becomes an orphan process until it terminates.

However, when the child process runs less(2), other things happen. /If/
we had forked off /usr/bin/cat instead of /usr/bin/less, we could have
forgone the wait(2). But, /usr/bin/less expects to have control of the
controlling terminal (where /usr/bin/cat does not), and once our parent
process terminates, control of the controlling terminal is given back
to the shell.

Additionally, less(1) maintains an internal buffer of it's input data
to permit backwards scrolling, etc.

The combination of no terminal control, and an internal buffer causes
less(1) to abort all output.

So, instead of terminating the parent process immediately (and relinquishing
the terminal), we wait(2) for less(1) to terminate before terminating
the parent process. This guarantees that less(1) can access the terminal
as it needs, and display its buffered data.

While this comment talks about the difference in behaviour between less(1)
and cat(1) with respect to the parent process, the observation also applies
to how the parent process works. Specifically, if you substitute
execl("/usr/bin/cat","cat","some.text",NULL);
for the printf() loop in the parent process, you again do not wait() for the
child process to terminate. And, as we saw with the original code, you need
to wait() for this specific child process ("/usr/bin/less") to complete
in order to retain a coherent terminal control state.

I don't believe that I posted the original code with these comments.
I apologize; I should have.


[snip]

HTH

Ralf Fassel

unread,
Mar 3, 2022, 4:15:33 AMMar 3
to
* Lew Pitcher <lew.p...@digitalfreehold.ca>
| However, when the child process runs less(2), other things happen. /If/
| we had forked off /usr/bin/cat instead of /usr/bin/less, we could have
| forgone the wait(2). But, /usr/bin/less expects to have control of the
| controlling terminal (where /usr/bin/cat does not), and once our parent
| process terminates, control of the controlling terminal is given back
| to the shell.

I always thought that fork() duplicates the current process with
'everything', so I would have expected the child (which eventually execs
"less") to also have "control of the controlling terminal".
But obviously there is more to that...

R'

Scott Lurndal

unread,
Mar 3, 2022, 9:41:12 AMMar 3
to
Indeed. You need to look at the controlling terminal in the context
of a process "tree", for example:

$ ps -ejH
PID PGID SID TTY TIME CMD
...
1807 1401 1401 ? 00:00:16 xterm
1811 1811 1811 pts/3 00:00:02 ksh
17690 17690 1811 pts/3 00:00:12 xpdf
3518 3518 1811 pts/3 00:00:09 xpdf
1101 1101 1811 pts/3 00:00:18 xpdf
3533 3533 1811 pts/3 00:00:01 xpdf
23125 23125 1811 pts/3 07:08:21 firefox
12781 12781 1811 pts/3 00:51:03 firefox-bin
12834 12781 1811 pts/3 00:00:00 Socket Process
12922 12781 1811 pts/3 00:37:44 Web Content
12998 12781 1811 pts/3 00:03:21 WebExtensions
13058 12781 1811 pts/3 00:10:30 Web Content
13112 12781 1811 pts/3 00:00:11 Privileged Cont
13275 12781 1811 pts/3 00:08:39 Web Content
13362 12781 1811 pts/3 00:02:07 RDD Process
13517 12781 1811 pts/3 00:07:27 Web Content
14273 12781 1811 pts/3 00:10:50 Web Content
14356 12781 1811 pts/3 00:11:43 Web Content
14393 12781 1811 pts/3 00:09:23 Web Content
14671 12781 1811 pts/3 00:05:45 Web Content
12281 12281 1811 pts/3 00:00:01 xrn
12386 12386 1811 pts/3 00:00:00 ps
...

Here, the controlling terminal is 'pts/3', which was
opened by xterm and passed to the korn shell (ksh).
ksh issued a 'setsid(2)' system call to become the session
leader, and thus "owns" the controlling terminal (by
virtual of being the session leader). Within a session
are one or more process groups (usually created with
the shell job-control facilities, e.g. '&' or control-z).

Only one process group can be the 'foreground' group, and
that group receives input from and sends output to
the terminal until it is suspended
(e.g. SIGTSTP/SIGSTOP) or the process group leader terminates.

https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html

Lew Pitcher

unread,
Mar 3, 2022, 10:04:02 AMMar 3
to
Well, /every/ process that has not divorced itself from it's controlling
terminal has control of that terminal, in that every process can read from it,
write to it, and change it's characteristics (line echo, etc.).

I still don't understand the intricacies of it myself, but in it's simplest
form, imagine if two processes both are trying to read from the controlling
terminal: which process gets the input? How about one process that turns off
echo (say, to prevent it's input from distorting the output on the terminal,
like less(1) would do), and one process that assumes that echo is turned on
(like, say, the shell). Or, if each process emits a termcap/termios control
string at the same time; how does the terminal interpret two simultaneous,
conflicting command strings?

So, usually, only one process at a time uses the controlling terminal, while
all other processes wait. I don't know the mechanism of that co-ordination;
I'm just learning this stuff myself.

Scott Lurndal

unread,
Mar 3, 2022, 11:23:58 AMMar 3
to
Lew Pitcher <lew.p...@digitalfreehold.ca> writes:
>On Thu, 03 Mar 2022 10:15:27 +0100, Ralf Fassel wrote:
>
>> * Lew Pitcher <lew.p...@digitalfreehold.ca>
>> | However, when the child process runs less(2), other things happen. /If/
>> | we had forked off /usr/bin/cat instead of /usr/bin/less, we could have
>> | forgone the wait(2). But, /usr/bin/less expects to have control of the
>> | controlling terminal (where /usr/bin/cat does not), and once our parent
>> | process terminates, control of the controlling terminal is given back
>> | to the shell.
>>
>> I always thought that fork() duplicates the current process with
>> 'everything', so I would have expected the child (which eventually execs
>> "less") to also have "control of the controlling terminal".
>> But obviously there is more to that...
>
>Well, /every/ process that has not divorced itself from it's controlling
>terminal has control of that terminal, in that every process can read from it,
>write to it, and change it's characteristics (line echo, etc.).
>
>I still don't understand the intricacies of it myself, but in it's simplest
>form, imagine if two processes both are trying to read from the controlling
>terminal: which process gets the input?

"If a process is in the foreground process group of its controlling
terminal, read operations shall be allowed, as described in Input
Processing and Reading Data. Any attempts by a process in a background
process group to read from its controlling terminal cause its process
group to be sent a SIGTTIN signal unless one of the following special
cases applies: if the reading process is ignoring the SIGTTIN signal
or the reading thread is blocking the SIGTTIN signal, or if the
process group of the reading process is orphaned, the read() shall
return -1, with errno set to [EIO] and no signal shall be sent. The
default action of the SIGTTIN signal shall be to stop the process
to which it is sent."

"If a process is in the foreground process group of its controlling
terminal, write operations shall be allowed as described in Writing
Data and Output Processing. Attempts by a process in a background
process group to write to its controlling terminal shall cause the
process group to be sent a SIGTTOU signal unless one of the following
special cases applies: if TOSTOP is not set, or if TOSTOP is set and
the process is ignoring the SIGTTOU signal or the writing thread is
blocking the SIGTTOU signal, the process is allowed to write to the
terminal and the SIGTTOU signal is not sent. If TOSTOP is set, the
process group of the writing process is orphaned, the writing process
is not ignoring the SIGTTOU signal, and the writing thread is not blocking
the SIGTTOU signal, the write() shall return -1, with errno set to [EIO]
and no signal shall be sent."


This implies that multiple processes _within_ a process group need to
coordinate both read and write operations to avoid conflicting with
each other (regardless of the type of file - block special, character
special, fifo, pipe or regular file).

Lew Pitcher

unread,
Mar 3, 2022, 11:49:35 AMMar 3
to
And, if they don't co-ordinate read and write operations, then....
the unpredictable happens.

Just like Meredith is seeing with the various iterations of her program,
where both less(1) and the shell compete for input (and output) from
the controlling terminal.


>(regardless of the type of file - block special, character
> special, fifo, pipe or regular file).

Scott Lurndal

unread,
Mar 3, 2022, 12:16:55 PMMar 3
to
It gets even worse if both processes are sharing a file descriptor
but are using different FILE objects to access that file descriptor
due to the stdio buffering (avoided by disabling buffering with setvbuf(3)).

Meredith Montgomery

unread,
Mar 12, 2022, 9:39:13 AMMar 12
to
Thank you so much for this thread. It's very clear now the sort of
stuff in the system that I must study.

My exercise, therefore, needs another fork if I intend for the parent
process to exec cat because cat won't wait for the child and so the
child might lose its parent and, therefore, losing rights to talk to the
terminal. Forking again before running cat allows the parent to wait
for both cat and less, keeping both programs with the right to talk to
the terminal. So it seems I may conclude that for a situation such as

cat --> less

we really need three processes, one for cat, one for less and one parent
for waiting for both to avoid any of them from losing the right to talk
to the terminal (assuming any of them might try to). In general, a
pipeline of n processes requires at least n+1 processes. That's what I
conclude right now. I suppose there could be exceptions there and I
might be in general wrong, but I don't see any possible exceptions right
now.

Rainer Weikusat

unread,
Mar 13, 2022, 4:47:42 PMMar 13
to
Meredith Montgomery <mmont...@levado.to> writes:

[...]

> My exercise, therefore, needs another fork if I intend for the parent
> process to exec cat because cat won't wait for the child and so the
> child might lose its parent and, therefore, losing rights to talk to the
> terminal. Forking again before running cat allows the parent to wait
> for both cat and less, keeping both programs with the right to talk to
> the terminal. So it seems I may conclude that for a situation such as
>
> cat --> less
>
> we really need three processes, one for cat, one for less and one parent
> for waiting for both to avoid any of them from losing the right to talk
> to the terminal (assuming any of them might try to). In general, a
> pipeline of n processes requires at least n+1 processes.

This happens to be true when creating pipelines from the shell as
there's always a shell process involved. But in general, it's not
true. You're just creating the pipeline in the wrong way. There are 3
kinds of processes in a pipeline

1. The first process if there's more than one. Stdin of the first
process refers to the terminal and stdout refers to a pipe.

2. n intermediate processes, n >= 0. Stdin of an intermediate process is
the most recently create pipe. Stdout is a newly created pipe.

3. The last process reads from the most recently created pipe if there's
more than one process, otherwise from the terminal. It writes to the
terminal.

Assuming there are only well-behaved commands in the pipeline, ie
commands which actually process all of their stdin input, the initial
process needs to run the last command to ensure that everythings works
as it should:

Example:

----
#include <unistd.h>

static void exec_cmd(int in_fd, char *cmd)
{
if (in_fd != -1) {
dup2(in_fd, 0);
close(in_fd);
}

execlp("sh", "/bin/sh", "-c", cmd, (void *)0);
}

static int start_cmd(int in_fd, char *cmd)
{
int fds[2];

pipe(fds);

if (fork()) {
close(in_fd);
close(fds[1]);

return *fds;
}

dup2(fds[1], 1);
close(*fds);
close(fds[1]);

exec_cmd(in_fd, cmd);
}

int main(int argc, char **argv)
{
char *cmd, *next;
int fd;

fd = -1;
++argv;

cmd = *argv;
while (next = *++argv) {
fd = start_cmd(fd, cmd);
cmd = next;
}

exec_cmd(fd, cmd);
}
----

This shell commands as arguments an runs them in a pipeline, eg

./a.out 'cat /var/log/syslog' 'sed "s/Mar/Ram/"' less
Reply all
Reply to author
Forward
0 new messages