wagnerc has asked for the wisdom of the Perl Monks concerning the following question:

Greetings and felicitations. I have a subroutine that performs a mass ping by spawning off a number of ping commands and reads from them in a nonblocking loop. All of the functionality works. The problem is that the file descriptors (or file handles) are sticking around even though the processes exit and I close()'d them. The script eventually dies with a "too many open files" message. The box is Solaris 10, Perl 5.12.2, and the ulimit is 256 handles. This can be raised to 1024 but it would still crash if I need to ping more than that. I've tried kill, readline, waitpid, set sig-child/pipe to ignore but they still build up. I can watch the list grow by looking at /proc/PID/fd. So does anybody know a sure fire way to clean up old file descriptors so that they can be reused? Here is a pared down code snippet.
local $/ = undef; for ($i=0;$i<=$#hosts;$i++) { $pings[$i][0] = $hosts[$i]; my $pid; $pid = open $pings[$i][1], "-|", "$pingcmd $hosts[$i] $pingsize $p +ingnumber" or die "Error executing $pingcmd: $!\n";; my $old_flags = fcntl($pings[$i][1], F_GETFL, 0) or die "can't get flags: $!"; fcntl($pings[$i][1], F_SETFL, $old_flags | O_NONBLOCK) or die "can't set non blocking: $!"; $pings[$i][2] = $pid; print "$pingcmd, $hosts[$i], $pingsize, $pingnumber failure\n" unl +ess $pid; } READ: while (@pings) { for ($i=$#pings;$i>=0;$i--) { my $buf; my $len = sysread $pings[$i][1], $buf, 580; if (not defined $len) { # loop } elsif ($len > 0) { # loop } elsif ($len == 0) { # we're done, end loop #print "read length 0 $pings[$i][0] pid $pings[$i][2]\n"; # if (kill 0, $pings[$i][2]) { # print "process $pings[$i][2] still alive\n"; # kill 9, $pings[$i][2]; # } # readline $pings[$i][1]; close $pings[$i][1]; # waitpid $pings[$i][2], 0; $results{$pings[$i][0]}{status} = $? >> 8; splice @pings, $i, 1; } else { die; } } sleep 1; }
So the rub is something to do with the $len == 0 condition. A sysread of 0 is supposed to indicate that the handle is at EOF. Can someone elucidate the handle destruction process in Perl? After this sub call every variable goes out of scope. Thanks.

Replies are listed 'Best First'.
Re: old file descriptors not being cleaned up
by mr_mischief (Monsignor) on Dec 11, 2010 at 03:06 UTC

    It would appear that the issue is you are opening them all before closing any of them. If you want file descriptors to stay under your ulimit, you really want to close some of them before opening others.

    If you're counting on the filehandles to close from going out of scope, there's some news you might not want. They won't because you have put them in an array. Try reusing the same lexical scalar over and over instead for the file handle if you want that, or manually close your file handles.

    You can read data from the files, close the files, and still keep the data around. There's no need for all those files to be open at once.

    As examples, the first of these bombs out for too many open file handles, while the second and third will just keep running:

    my $data, $i, @file; while ( 1 ) { open $file[ $i ], '<', '/dev/zero' or die "Cannot read: $!\n"; read $file[ $i ], $data, 4; $i++; print "$data\t$i iterations...\n" unless $i % 100; }
    my $data, $i; while ( 1 ) { open my $file, '<', '/dev/zero' or die "Cannot read: $!\n"; read $file, $data, 4; close $file; $i++; print "$data\t$i iterations...\n" unless $i % 100; }
    my $data, $i, @file; while ( 1 ) { open $file[ $i ], '<', '/dev/zero' or die "Cannot read: $!\n"; read $file[ $i ], $data, 4; close $file[ $i ]; $i++; print "$data\t$i iterations...\n" unless $i % 100; }
Re: old file descriptors not being cleaned up
by Anonyrnous Monk (Hermit) on Dec 11, 2010 at 02:59 UTC
    I can watch the list grow by looking at /proc/PID/fd

    I'm not sure I understand. How can you look at /proc/PID/fd, if PID is no longer there (as you're implicitly saying elsewhere)?

    Also, what processes does lsof say those file handles belong to? What does close return?

Re: old file descriptors not being cleaned up
by Khen1950fx (Canon) on Dec 11, 2010 at 03:19 UTC
    When you alter your script, make sure to do "Update:" at the bottom. In your first script, you used seek. I think that you where on the right track there. Now, to clear things up, add a dummy seek after sleep 1;
    } sleep 1; seek(@pings, 0, 1) }
Re: old file descriptors not being cleaned up
by wagnerc (Sexton) on Dec 11, 2010 at 16:59 UTC
    Thanks for the replies guys. Here are my answers.

    @anon: I monitor the process from another terminal. I run ps to get the pid and then do ls /proc/PID/fd and watch the list grow until it crashes at 256.

    @mr_mischief: I explicitly close them after sysread returns 0. That is in the elsif block where $len == 0. The close() returns true. Also the read is from a pipe to the ping command, not a file.

    @Khen1950fx: I'm not using seek. >>

      Um, no. Apparently you're not following me here.

      You open all of the file handles (that's what they are called even if it's a pipe open) in a for loop. That loop executes before the while loop below it. That while loop contains the statement in which you attempt to close a file handle.

      They are all opened before you close the first one, as I already told you. The one loop executes before the other, you see. Explicitly closing them will not keep them from all being opened at once unless you close them in the same loop where you open them. The while loop won't go back in time to close files for you. I know the Perl 5 team is good, but they haven't mastered time travel into the past just yet. I doubt you have the hardware necessary anyway.

      The control flow in your program must follow the rules of control flow in the language you are using to implement the program.

      You have to think of your program as executing over time. Making two system calls that relate to one another does not mean they happen in the same part of the program or near the same time. You must make the calls in the part of the program they are needed. The language is only doing what you ask, not guessing what you meant. I know Perl is designed to make small choices for you based on context when things are left implicit. It won't reorder your explicit control flow over multiple lines and multiple blocks, though.

      If you want fewer files open at a time, you are going to have to close some of the files before you open others.

      That the order of a set of actions influences their outcome isn't some crackpot conspiracy theory I'm trying to sell you in a survivalist pamphlet. That's just reality. If you go into your house after work tonight and open all the windows, then all of your windows will be open. If you then start closing them, you will still have had all of your windows open at some point. In order to have only a portion of the windows in your house open at a time, you must open some portion of them then close some portion of them before opening the rest. Your actions in the present can't change the past.

      You came here asking for help. If you want to argue that you're smarter than I or more experienced, that's fine. Let's have that argument. I'm game for that, although this really isn't the venue. Don't argue with the advice I gave when you asked, though, until you try it or at least take the time to understand it. Arguing against the help you requested based on your own flawed understanding will not further anyone's understanding of the situation with your work. It may inform people about you in some ways, though. Confidence is good, but being cocksure about your understanding of some concept about which you just asked for help is just silly. Either you need help or you don't. Asking for help and dismissing unconsidered any help you get is not only rude but wasteful.

        I don't understand the tone of ur reply. I never said anything remotely smug.

        But anyway, I don't understand what ur trying to say about closing them. It's not that I want fewer handles open at one time, it's that I want them to close and completely go away between invocations of that subroutine. I don't see how operating on the handles in two loop structures makes any difference to their persistence. Let's say I got rid of the for() loop and manually opened a number of handles as variables in the @pings array. Ur saying that would make a difference? The handles are all opened in the context of a subroutine and operated on solely within that same subroutine. Asked another way, what situation would make a file handle that just had close() called on it not release back to the OS?

        Thanks.