Replacing lib$spawn with vfork/execv

Post Reply

Topic author
jhamby
Contributor
Posts: 22
Joined: Wed Oct 04, 2023 8:59 pm
Reputation: 0
Status: Offline

Replacing lib$spawn with vfork/execv

Post by jhamby » Mon Oct 30, 2023 10:04 pm

This is a continuation of something I'd been discussing on comp.os.vms, but the newsgroup has been taken over by spam lately, and this is probably a more appropriate venue for the discussion.

My original project was attempting to build Perl using Clang, in which I ran into several issues that prevented me from building it with either /pointer_size=32 or /pointer_size=64=argv.

I set that aside to look at how the Perl port spawns subprocesses, and it's a really large chunk of code contributed in 2000 that uses mailboxes to shuttle stdin, stdout, and stderr back and forth between the parent and the child. It then calls lib$spawn() to spawn a DCL script named VMSPIPE, which reads the real sys$input, sys$output, sys$command, and sys$error from DCL symbols which Perl has set, along with the real command to run, which may be split into up to 4 DCL symbols depending on how long it is.

Ideally, vfork() and execv() would support running arbitrary DCL commands, but so far I've only really had success running .exe images. I have an experimental branch of Perl 5 where you can see my work so far in adding a fast path for .exe images only (command starts with a "$" or "mcr ", looks like a UNIX path, or can be looked up as a foreign command, which worked quite well for finding e.g. "MMK").

https://github.com/jhamby/vms-perl5/

Long story short, I was eventually able to get the fast path to work using pipe(), vfork(), execv(), and decc$set_child_standard_streams(), as long as I didn't leave any hanging file descriptors, and I also had to add a handler just before exiting the parent to wait for any child that it happened to spawn as part of parsing its own command-line to look for UNIX-style "<", ">", and "|", which it handles itself. Otherwise, the child can be killed before it's finished writing its output.

Besides the limitations in what exec*() can run that I ran into, I had problems with DCL scripts not returning the exit status back to me properly, and I think they may need some of the extra steps from the vmspipe helper script that Perl calls.

The benefits so far of the fast path case vs. lib$spawn include that the parent is able to get the CPU usage (user, sys) of the children as part of calling the real waitpid(), instead of getting a local exit status from the pipe handling code, so "perl harness" prints the child CPU usage info out, which it didn't do before.

Also, some test cases that had been failing due to extra "\n" in the output which I couldn't figure out the source of in the lib$spawn() path now magically work because the pipe() channel is binary stream-oriented.

One drawback is now child processes can now hang in RWMBX if the pipe size/quota isn't large enough. The current implementation can allocate as many 8K buffers as it needs if the reader isn't reading. Setting decc$pipe_buffer_size to 8192 and _quota to 65536 seem more than adequate. I tried swapping out pipe() for socketpair() and that seemed promising but caused a number of Perl test cases to fail that succeeded before, so that's a non-starter.

There's one remaining issue that I think may be a VMS C RTL bug which I'll try to create a standalone C test case for. The "io/dup.t" test redirects stdout and stderr to a temp file, then calls subprocesses to add lines to that file, then calls an OS-specific command to print the file ("type" for VMS, with a ".;" appended so the command doesn't add .LIS). The output I'm seeing has lines 6 and 7 output to the parent's sys$output before the contents of the file are printed. They should be the last lines of the temp file, not printed immediately.

Based on some fprintf(stderr) debug output, it looks like if you redirect sys$output and sys$error to a file using the UNIX close()/dup2() method, and then call a subprocess with vfork()/execv(), the sys$output and sys$error of the child *both* end up being "SYS$OUTPUT:.;" instead of the file name.

I tried setting decc$set_child_standard_streams() to different values like (0, 1, 2) instead of (-1, -1, -1), and printing the getname() string from the fileno() for Perl's stdin/stdout/stderr, to make sure it was the temp file and (0, 1, 2) respectively, and everything looks good on the parent side. I wonder if this is a code path that hasn't been exercised. Usually programs don't redirect their own stdout+stderr at the file descriptor level to a temp file and then write to that file themselves and then spawn a subprocess to write to what it thinks is stdout but should be the temp file inherited as fd 1 and 2.

It seems like the level of nesting of subprocesses may be part of the issue as well. A different Perl test that strangely fails with my new pipe() path is "run/exit.t", which works properly when called directly, but when called inside "perl harness", the harness only sees the first line and none of the "ok" lines.

I thought this was worth starting a thread about here, since there are some interesting pain points with the POSIX APIs in the C RTL providing a reasonably UNIX-like experience until you run into a C RTL bug, or you want to run a DCL script or "SET", "LIBRARY", "CC", "TYPE", etc., or you want to spawn a subprocess that outlives the parent and also can write to the terminal. In that case, you're in for a lot of mess to get lib$spawn to enable you to set up the command with the desired sys$input, sys$command, sys$output, and sys$error values (hence the need for the awkward vmspipe helper script and the custom mailbox-based two-way pipe between the parent's open files and the child's).

The shortest route to being able to remove custom hackery to do UNIX-like redirection like Perl currently uses would be for vfork/exec to do the right thing for DCL commands and scripts, and to enable subprocesses that can outlive their parent and also still write to the terminal (which breaks with DECC$DETACHED_CHILD_PROCESS enabled). I realize this is a decades-old complaint about VMS. ;)


Topic author
jhamby
Contributor
Posts: 22
Joined: Wed Oct 04, 2023 8:59 pm
Reputation: 0
Status: Offline

Re: Replacing lib$spawn with vfork/execv

Post by jhamby » Tue Oct 31, 2023 7:38 pm

Correcting what I wrote about subprocesses: the lib$spawn() that VMS Perl is using isn't creating detached processes. It's creating subprocesses that can outlive the Perl image running in the process that called lib$spawn. Combined with the redirection parsing code that the port uses to handle "<", ">", etc., you can do stuff like:

Code: Select all

$ perl -e "sleep 3; print qq{Hello};" &
0002EC9D
$
  Hello
Subprocess JHAMBY_2496 has completed
This is the desired behavior that I can't replicate with vfork()/execv(). So while it'd be nice to also be able to create detached processes, to achieve parity with lib$spawn, the ideal code path for Perl (or for any UNIX shell or scripting language port that runs commands) should be able to run arbitrary DCL commands with the same file descriptor inheritance and waitpid() return value behavior as for running C/C++ programs, and also to create subprocesses that don't get killed when the parent image exits and returns to DCL so the user (or parent script) can run more images. That concept of a process that can run multiple images in sequence doesn't exist in most other OS's, and I'd confused the two somewhat.

Added in 31 minutes 58 seconds:
Here's a test program to reproduce the file redirection C RTL bug that I discovered testing Perl:

Code: Select all

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

int main() {
    char *childname = (char *) "./testredirchild";
    char *childargv[] = { childname, NULL };
    FILE *outfile = fopen("testfile.out", "a");
    int outfd = fileno(outfile);
    fprintf(outfile, "This should be line 1.\n");
    fflush(outfile);
    close(1);
    dup2(outfd, 1);
    close(2);
    dup2(outfd, 2);
    int pid = vfork();
    if (pid == 0) {
        execv(childname, childargv);
        return EXIT_FAILURE;
    }
    waitpid(pid, NULL, 0);
    fprintf(outfile, "This should be line 4.\n");
    close(1);
    close(2);
    fclose(outfile);
}
That's the parent, and here's the child.

Code: Select all

#include <stdio.h>

int main() {
    printf("This should be line 2.\n");
    fflush(stdout);
    fprintf(stderr, "This should be line 3.\n");
}
It should append "This should be line 1." through "line 4" in testfile.out, assuming you've named the child program "testredirchild" in the current directory. That's what I see on Linux, and I presume elsewhere.

On VMS, the child isn't inheriting the file that the parent has for fd 1 and 2, so "This should be line 2" and "line 3" are both printed to sys$output, rather than to the file as expected.


pustovetov
VSI Expert
Contributor
Posts: 22
Joined: Thu Sep 14, 2023 1:26 am
Reputation: 0
Status: Offline

Re: Replacing lib$spawn with vfork/execv

Post by pustovetov » Wed Nov 01, 2023 7:42 am

jhamby wrote:
Tue Oct 31, 2023 8:10 pm
Here's a test program to reproduce the file redirection C RTL bug that I discovered testing Perl:
Welcome to our forum, Jake.
No, your example will definitely not work on VMS. Here we have to write a little more strangely. Something like this:

Code: Select all

int main(int argc, char *argv_main[]) {
    
    char *argv[] = {"child.exe", 0};

    FILE *outfile = fopen("testfile.out", "a");
    fprintf(outfile, "This should be line 1.\n");
    fclose(outfile);

    int out_fd = open("testfile.out", O_RDONLY | O_CLOEXEC, 0600);
    if (out_fd == -1) 
	printf("open error %i %i\n", errno, vaxc$errno);

    decc$set_child_standard_streams(-1, out_fd, -1);

    int pid = vfork();
    if (pid == 0) {
        int res = execv("child.exe", argv);
        printf("error %i %i\n", res, vaxc$errno);
    }
    
    close(out_fd);

    if (pid == -1) {
	printf("fork error %i %i\n", errno, vaxc$errno);
    }
    else {
        int child_status;
    	waitpid(pid, &child_status, 0);
    }

    outfile = fopen("testfile.out", "a");
    fprintf(outfile, "This should be line 4.\n");
    fclose(outfile);

    return 0;
}
But in this case, there are also two issues - 1) While the child process is running, the file is not opened for appending, so the child process overwrites it; 2) For some reason stderr is not written in stdout. Although it seems to be the default. I'll try to fix them.


Topic author
jhamby
Contributor
Posts: 22
Joined: Wed Oct 04, 2023 8:59 pm
Reputation: 0
Status: Offline

Re: Replacing lib$spawn with vfork/execv

Post by jhamby » Wed Nov 01, 2023 3:22 pm

Thanks for the quick reply!

Post Reply