das@eplunix.UUCP (David Steffens) (01/11/90)
Late last summer (August?) there was a discussion in this group about how csh/tcsh handles process groups. The problem as I remember was that under SunOS on a SUN4, you don't want to use vfork(). But vfork() enforces a certain processing order which is absent otherwise. So if you don't use vfork(), you end up with the following annoyance: % egrep foo bar.c | less Stopped (tty output) % The discussion seemed to revolve around how process groups are set up for pipelines. Chris Torek (I think) said that there was a race. I don't remember whether it was ever explained why this problem doesn't occur with the SunOS4.0 version of csh. At this point I lost track of the discussion -- too much work to keep up with the volume in this group! -- so I never did hear if there ever was a fix posted or suggested. Anyone know what I'm talking about? Can you fill me in? Tell me where I can get a fix? advTHANKSance -- {harvard,mit-eddie,think}!eplunix!das David Allan Steffens 243 Charles St., Boston, MA 02114 Eaton-Peabody Laboratory (617) 573-3748 Mass. Eye & Ear Infirmary
lm@snafu.Sun.COM (Larry McVoy) (01/14/90)
In article <829@eplunix.UUCP> das@eplunix.UUCP (David Steffens) writes: >Late last summer (August?) there was a discussion in this group >about how csh/tcsh handles process groups. The problem as I remember >was that under SunOS on a SUN4, you don't want to use vfork(). >But vfork() enforces a certain processing order which is absent otherwise. >So if you don't use vfork(), you end up with the following annoyance: > % egrep foo bar.c | less > Stopped (tty output) > % > >The discussion seemed to revolve around how process groups are set up >for pipelines. Chris Torek (I think) said that there was a race. >I don't remember whether it was ever explained why this problem >doesn't occur with the SunOS4.0 version of csh. This is a nifty little problem. There are actually several potential problems here (and they get worse under posix). They are not specific to csh, any job control cognizant shell can have these problems. Here's the deal. There are several terms: controlling terminal (tty): This is an open tty device that is distinguished by its association to a particular set of processes (session in posix). In posix, there is the concept of a session leader; that is the process that established the association (usually via open(2)). An easy way to see if you have a controlling terminal is to open /dev/tty; the open will fail if you don't. controlling terminal's process group (tty's pgrp): If a tty is a controlling terminal then it has an associated process group. When the session leader gets the tty the tty's pgrp is set to the session leaders pgrp. session: A group of process groups. Usually associated with a tty but not necessarily so. Never associated with more than one tty. foreground process group: The process group who's value matches the tty's pgrp. (This is how tty signals work; the tty driver sees the control char in the input stream, recognizes it as one of the specials, and sends off a signal to its pgrp.) tuple [pid, pgid, sid]: This is a notation I use when describing this sort of thing. pid == process id, pgid = process group id, sid == session id. A session leader looks like [100, 100, 100]. (Note: I probably did a so-so job on these terms; the POSIX 1003.1 standard defines these and more in a very nice way. They also do a nice job of explaining job control). Now that that's out of the way, we can get down to business. When a shell forks a pipeline, what happens? It usually looks something like: signal(SIGCHLD, reaper); pgrp = 0; for i in <pipe components> { if (tmp = [v]fork()) { if (!pgrp) { pgrp = tmp; ioctl(0, TIOCSPGRP, &pgrp); } setpgrp(tmp, pgrp); /* changing child's */ } else { /* set up pipe file desc */ /* set up signals */ /* whatever */ exec(i); } } /* shell waits for all kids here */ There are several problems with this: (1) ioctl+setpgrp/exec race. In order for things to be "right", the child process should have the tty (ie, the tty's pgrp should == child's pgrp) and the child should be in its own pgrp (child's pgrp != shell's pgrp) before the exec. Since not all systems have vfork(), some shells use fork(). There is no guarantee which half of the fork starts first. If the parent keeps going all is well; if the child goes first there is trouble (if it produces output or trys an ioctl() on the tty, it will be stopped if it is not the foreground process group [well, in many cases it will be stopped; there are exceptions]). (2) Suppose we did the following: $ echo foo | cat And we started from a shell that was [100, 100, 100]; the echo is process 200, and the cat is 201. We want things to look like [100, 100, 100] tty's pgrp: [200] [200, 200, 100] | [201, 200, 100] Suppose that we fork the echo, it runs, exits, and is reaped (SIGCHLD), all before we fork the cat. (This can happen.) POSIX has a restriction that says when you create a process group it has to be the same value as the processes pid. Other than that, the only way to change your process group is to join an existing process group (white lie: calling setsid() also changes your pgrp if it succeeds). If the echo is gone, the setpgrp() for the cat will fail because pgrp 200 doesn't exist anymore. (3) It is also possible that a stupid shell would fork all the processes and then set up the tty and pgrp's. Suppose that we have the same echo cat example. Suppose that the shell starts working backwards; it will try to do setpgrp(201, 200) before setpgrp(200, 200). Fixes: (1) For the race, the easiest thing to do is to duplicate the code in the child so: signal(SIGCHLD, reaper); pgrp = 0; first = 1; for i in <pipe components> { if (tmp = fork()) { if (first) { first = 0; pgrp = tmp; ioctl(0, TIOCSPGRP, &pgrp); } setpgrp(tmp, pgrp); /* changing child's */ } else { if (first) { pgrp = getpid(); setpgrp(pgrp, pgrp); ioctl(0, TIOCSPGRP, &pgrp); } else { setpgrp(0, pgrp); } /* set up pipe file desc */ exec(i); } } Then it doesn't matter who gets there first. The vfork() people should do this too - depending on vfork() semantics is a kludge (in spite of the fact that I tried to convince Guy otherwise years ago. Sigh.) (2) The easy fix to this is to have the shell block SIGCHLD while forking off the kids. If the reap doesn't happen until you are done, all is well (well, it should be OK. This assumes that process groups live after the process exits until it is reaped. This is not the case in current SunOS and probably BSD implementations; I had to fix it for SunOS 4.1). (3) In the immortal words of Rob Gingell: "Don't do that." That's way too long to wait to set things up and I can't think of a reason why you would need to set them up in reverse order (there may be one though: the shell may choose a different method of pipelining where the first process forked forks the next and so on. There are several variations on this and it's not impossible that someone can come up with a legitimate example that won't work.) --- What I say is my opinion. I am not paid to speak for Sun, I'm paid to hack. Larry McVoy, Sun Microsystems (415) 336-7627 ...!sun!lm or lm@sun.com