select() vs. pipes (was [Python-Dev] Re: PEP 324 (process module))

Chris McDonough chrism at plope.com
Wed Aug 4 22:15:50 CEST 2004


On Wed, 2004-08-04 at 16:03, Chris McDonough wrote:
> I was all set to try to refute this, but after writing a minimal test
> program to do what I want to do, I find that you're right.  That's good
> news!  I'll need to revisit my workaroudns in the program that caused me
> to need to do this.  Thanks for the schooling.

Ugh.  I spoke a bit too soon..

The following program demonstrates that a particular usage of select
(under Linux at least) always returns the output side of a pipe
connected to a child process' stdout as "ready" after it gets any output
from that child process, even if the child process has no further data
to provide after it has provided a bit of data to the parent.  This is
what causes the "busywait" behavior I've experienced in the past (note
that as this program runs, your CPU utilization will likely be near
100%).

Or am I doing something silly?

import select
import errno
import fcntl
import os
import stat
import sys
from fcntl import F_SETFL, F_GETFL

def get_path():
    """Return a list corresponding to $PATH, or a default."""
    path = ["/bin", "/usr/bin", "/usr/local/bin"]
    if os.environ.has_key("PATH"):
        p = os.environ["PATH"]
        if p:
            path = p.split(os.pathsep)
    return path

def get_execv_args(command):
    """Internal: turn a program name into a file name, using 
    $PATH."""
    commandargs = command.split()
    program = commandargs[0]
    if "/" in program:
        filename = program
        try:
            st = os.stat(filename)
        except os.error:
            return None, None
    else:
        path = get_path()
        for dir in path:
            filename = os.path.join(dir, program)
            try:
                st = os.stat(filename)
            except os.error:
                continue
            mode = st[stat.ST_MODE]
            if mode & 0111:
                break
        else:
            return None, None
    if not os.access(filename, os.X_OK):
        return None, None
    return filename, commandargs

def spawn(command):
    """Start the subprocess."""

    filename, argv = get_execv_args(command)
    if filename is None:
        raise RuntimeError, '%s is an invalid command' % command

    child_stdin, stdin = os.pipe()
    stdout, child_stdout = os.pipe()
    stderr, child_stderr = os.pipe()

    # open stderr, stdout in nonblocking mode so we can tail them
    # in the mainloop without blocking
    for fd in stdout, stderr:
        flags = fcntl.fcntl(fd, F_GETFL)
        fcntl.fcntl(fd, F_SETFL, flags | os.O_NDELAY)

    pid = os.fork()

    if pid != 0:
        # Parent
        os.close(child_stdin)
        os.close(child_stdout)
        os.close(child_stderr)
        return stdin, stdout, stderr

    else:
        # Child
        try:
            os.dup2(child_stdin, 0)
            os.dup2(child_stdout, 1)
            os.dup2(child_stderr, 2)
            for i in range(3, 256):
                try:
                    os.close(i)
                except:
                    pass
            os.execv(filename, argv)
        finally:
            os._exit(127)

def go(out_fds):
    while 1:
        try:
            r, w, x = select.select(out_fds, [], [], 1)
            if not r:
                print "timed out"
        except select.error, err:
            if err[0] != errno.EINTR:
                raise
        for fd in r:
            sys.stdout.write(os.read(fd, 1024))

stdin, stderr, stdout = spawn('echo "foo"')

go([stderr, stdout])





More information about the Python-Dev mailing list