[net.unix-wizards] Undocumented behavior of select

abs@nbc1.UUCP (07/16/86)

I've encountered some undocumented behavior of select(2) that I
find useful.  I am afraid to depend on it if it is not
documented, so I wonder if anyone out in netland has encountered
this behavior and could confirm my suspicions about it.  DEC
software support has not been very helpful.

I have a server serving multiple clients, using stream sockets in
the UNIX domain.  The server waits for data to come from any of
its currently connected clients by creating a mask from the file
descriptors for these connected sockets, and calling select so
that it blocks indefinitely, thus:

		select(20,&readfds,0,0,0)

So here's the problem:  when one of these clients dies or shuts
down its end of the socket, the select in the server returns with
the bit in readfds set for the descriptor for the server's end of
that socket.  Doing an ioctl(fd,FIONREAD,&n), where fd is that
file descriptor, yields zero bytes pending on that socket!  So
there is a conflict:  select says there are bytes pending, and
ioctl says there are none.  If I do a read on that fildes, the
read returns 0 (EOF), conforming to the documented behavior of
read(2).

Since this select/ioctl conflict seems to occur only when the
socket has been closed on the other end of the connection, it
turns out to be a handy way of finding out if a socket has shut
down without having to read from, or write to, the socket.  My
hypothesis is that this is the correct (but undocumented)
behavior of select under these circumstances;  now I just need
some kind wizard(s) in netland to confirm this for me (I don't
have access to source code).

Any advice would be appreciated.  Thanks in advance.
-- 
Andrew Siegel, N2CN		NBC Computer Imaging, New York, NY
philabs!nbc1!abs		(212)664-5776

chris@umcp-cs.UUCP (07/17/86)

In article <154@nbc1.UUCP> abs@nbc1.UUCP (Andrew Siegel) writes:
>I've encountered some undocumented behavior of select(2) that I
>find useful. ...  calling select so that it blocks indefinitely, thus:
>
>		select(20,&readfds,0,0,0)

Actually, you should at least insert a few casts:

	int cc, nfds;
	fd_set readfds;

	...
	cc = select(nfds, &readfds, (fd_set *) 0, (fd_set *) 0,
		(struct timeval *) 0);

4.2BSD defines `fd_set', but leaves it undocumented.  (In 4.3, an
fd_set contains more than 32 bits: no longer is one limited to 30
descriptors.)  Incidentally, the following macros are useful to
obtain `forwards compatibility' with 4.3 on a 4.2 system:

	/*
	 * 4.3 and V8 style file descriptor mask macros.
	 * Should be in <sys/types.h> but are missing in 4.2.
	 */
	#ifndef FD_SET
	#ifdef notdef			/* this already exists in 4.2 */
	typedef struct fd_set {
		int	fds_bits[1];
	} fd_set;
	#endif
	#define	FD_ZERO(p)	((p)->fds_bits[0] = 0)
	#define	FD_SET(n, p)	((p)->fds_bits[0] |= (1 << (n)))
	#define	FD_CLR(n, p)	((p)->fds_bits[0] &= ~(1 << (n)))
	#define	FD_ISSET(n, p)	((p)->fds_bits[0] & (1 << (n)))
	#endif

FD_ZERO clears an entire fd_set; FD_SET, FD_CLR, and FD_ISSET set,
clear, and test the appropriate bit in an fd_set given a file
descriptor and a pointer to the fd_set.

>... when one of these clients dies or shuts down its end of the
>socket, the select in the server returns with the bit in readfds
>set for the descriptor for the server's end of that socket.  Doing
>an ioctl(fd,FIONREAD,&n), where fd is that file descriptor, yields
>zero bytes pending on that socket!  So there is a conflict:  select
>says there are bytes pending, and ioctl says there are none.  If
>I do a read on that fildes, the read returns 0 (EOF), conforming
>to the documented behavior of read(2).

The real bug is in the select manual.  It should mention that in
fact selection for reading or writing holds true if the read or
write will not block due to EOF or error.  (I guess that errors
might someday move to `exceptional condition' status, and I it is
conceivable that even EOF might move there, but this select `feature'
still works in 4.3.  I think at this point it would be too painful
to change.)

>My hypothesis is that this is the correct (but undocumented)
>behavior of select under these circumstances;  now I just need
>some kind wizard(s) in netland to confirm this for me (I don't
>have access to source code).

It takes no extra code in the tty-select routines, but it does
take an extra test on sockets.  That makes it fairly obviously
intentional.
-- 
In-Real-Life: Chris Torek, Univ of MD Comp Sci Dept (+1 301 454 1516)
UUCP:	seismo!umcp-cs!chris
CSNet:	chris@umcp-cs		ARPA:	chris@mimsy.umd.edu

stuart@rochester.UUCP (07/17/86)

In article <154@nbc1.UUCP>, abs@nbc1.UUCP (Andrew Siegel) writes:
> I've encountered some undocumented behavior of select(2) [...]
> So here's the problem:  when one of these clients dies or shuts
> down its end of the socket, the select in the server returns with
> the bit in readfds set for the descriptor for the server's end of
> that socket.  Doing an ioctl(fd,FIONREAD,&n), where fd is that
> file descriptor, yields zero bytes pending on that socket!  So
> there is a conflict:  select says there are bytes pending, and
> ioctl says there are none.  If I do a read on that fildes, the
> read returns 0 (EOF), conforming to the documented behavior of
> read(2).
> Andrew Siegel, N2CN		NBC Computer Imaging, New York, NY
> philabs!nbc1!abs		(212)664-5776

Mini Answer: 

1)  Whenever a selected file descriptor changes state in any way,
select will wake up.  If the state change was an error condition or
anything related to the status of a device or connection, both the
input AND the output masks will be filled in.

2)  If a device or connection has been closed (on the other end) the
appropriate thing to do is close it on your end.  Unless you do,
select will continue to tell you about it. (And you will continue to
tie up resources)

3)  There is no good way to find out what exactly happened to the
file descriptor in general.  Although the FIONREAD ioctl gives useful
information, you can't find out exactly what the new condition is
unless you try to read or write from it.  If it's an error condition,
the read or write will return -1, and errno will tell you why.  But
you can't find out without trying to do the IO.  This is awkward.

Main Answer:
    (80 more lines, stop now if the mini answer satisfied you)

It is my belief that the original designers of select did not intend
this to happen but that *every* implementation of 4.2-style select
behaves in this way.  At a minimum, BSD 4.2 and 4.3 do, as do the
Sun 2.0 and 3.0 releases (based on BSD 4.2)

For programs with simple control structure this "feature" of select
is not too bad a problem.  For complicated programs that are trying
to be robust against failure (that means we don't just die when we
get an error, we identify it and try to do something about it, like
maybe go out and locate a secondary server), it becomes a pain in the
neck.

While I'm showing my irritation in public, I also wish that stat
reported something useful (ie, not a zeroed buffer) for sockets.
After two major releases with sockets (4.2, 4.3), why doesn't it?  

But even stat doesn't tell you everything you'd like to know.  I'd
like to be able to get at the connection status (which is protocol/
device dependent) and the error status (which generally isn't)
and I can't get at either of those without trying to do IO.

Let me quote two paragraphs from a report Derek Pitcher and I
wrote recently:

   "The situation is slightly more complicated than just
    described for three reasons.  First, the designers of select
    made provision for a third bit mask.  This mask was intended
    for "exceptional conditions" on a file descriptor or a
    socket.  This mask has never been implemented.  Instead,
    when an exceptional condition occurs, it matches either the
    input or output masks, if they happen to be set.  This is
    annoying, because it means we can not completely trust
    select when it tells us that input is available.  When the
    router finally does the select on the appropriate socket, it
    has to be prepared for an error condition instead of input.
    Worse, there is no way to test a socket for an error
    condition other than trying to read or write on it.

    [...]

   "In programming the router we had to detect and handle enough
    exceptional conditions to fervently wish that select had
    been fully implemented.  We would like to treat events like
    "new connection available", "pending connection
    established", and "existing connection dropped or refused"
    differently from the routine operations of forwarding data
    between users.  [...]"

      S.A. Friedberg & D.H. Pitcher
      Hierarchical Process Composition Project Report 3
      "HPC IPC Implementation -- Unmodified UNIX Host Version"
      Computer Science Department
      University of Rochester
      Rochester, New York, 14627, USA

This "feature" affects more than sockets.  Unfortunately, I no
longer have a copy of the first article, but Rich Burridge
pointed out recently (7 July 86) that pseudo-terminals are also
affected.  Here is a heavily cut version of his comments:

   "Say I used 'cat' to redirect a small file to /dev/ttyq5
    [...] nfds returned a 1 to indicate that there was a file
    descriptor with outstanding data to be read, and the
    readmask had the appropriate bit set.  [...] when I come to
    do the select call again, it should return 0 and the
    readmask should also be zero. But no, it tells me that there
    is data to be read, and when I use the read call as above,
    it returns -1 in nread with errno set to 5 (EIO I/O error).
    [...] Steve Schoch at the NASA Ames Research Center provides
    the solution:  [...] after the 'cat' is done writing the
    file to /dev/ttyq5, it exits, which closes that tty.  When
    you close a tty file for the last time it causes it to hang
    up.  If this was a real tty, it would drop DTR, but on a
    pseudo tty, it show that it is hung up by having the read
    fail on the master side."

      Rich Burridge, <3@yarra.OZ>, <6@yarra.OZ>

Stuart Friedberg  {seismo, allegra}!rochester!stuart  stuart@rochester