next up previous
Next: 2.18 Summary Up: 2. Environmentally Friendly I/O Previous: 2.16 Time


2.17 Low-Level System Interface

For most purposes, the high-level SETL model embodied in filters, pump streams, and so on, with its automatic buffering and process management, will be the most direct and convenient. Occasionally, however, access to certain of the Unix 98 [154] mechanisms underlying this model will be desired. The routines described in this section aim to be a supporting cast of utilities in this spirit, and have names that should be mnemonic to programmers familiar with Unix.

2.17.1 I/O and File Descriptors

The lowest-level Unix facilities for creating ``pipes'' and file descriptor aliases directly are now available in SETL. It should seldom, if ever, be necessary to use these primitives, but an example in Section 2.17.2 [Processes] shows how these traditional tools could be used with fork, exec, and wait to implement piping from a child process in the manner of a SETL `pipe-from' stream or a C stream obtained from popen in `r' mode.

A Unix-level pipe is created with *

[rfdwfd] := pipe();      -- trailing ``()'' optional but recommended
which leaves a readable file descriptor in rfd and a writable file descriptor in wfd. These file descriptors are not open at the SETL level, but are open at the operating system level.

It is still easy to open a SETL stream over such a file descriptor, as shown in Section 2.11 [Opening Streams Over File Descriptors], and then all the appropriate SETL operations become available on it. Also, close can always be called on any file descriptor, and will close all levels that are open. See Section 2.13 [Normal and Abnormal Endings] for more information on close.

To create a new file descriptor that refers to the same kernel object as an existing file descriptor, one of the following two calls may be used: *

fd2 := dup (fd1);   -- system picks fd2
dup2 (fd1fd2);    -- we demand fd2
In the first case, dup, the system chooses the lowest-numbered free file descriptor. In the second case, dup2, the caller chooses the desired file descriptor fd2, and the system executes close on fd2 if necessary before reopening it as an alias of fd1.

For example, if a process wished to ``redirect'' the input from an inherited file descriptor fd1 to its own stdin channel for convenience, it could execute *

dup2 (fd1stdin);
There is in fact a precise correspondence between dup2 and the standard Unix 98 shell syntax for file descriptor redirection. For example, dup2 (6, 7) is equivalent to the shell's ``7<&6'' if file descriptor 6 is read-only, or ``7>&6'' if it is write-only.

Very occasionally, in contexts where such low-level operations are already being used, it is useful to bypass the SETL stream buffering and make direct calls to the following interfaces to the fundamental read and write Unix system primitives: *

s := sys_read (fdn);
n := sys_write (fds);
The sys_read procedure returns a string of up to n characters. Sys_write tries to write s to fd and returns an integer indicating how many characters of s it wrote. The call to sys_write should therefore normally be wrapped something like this: *
procedure my_write (fds);
  while #s > 0 loop
    n := sys_write (fds);
    s(1..n) := `';
  end loop;
end my_write;

2.17.2 Processes

The fundamental process creation primitive in SETL is fork, which returns a new process id in the parent process and 0 in the child. The child is otherwise essentially a clone of the parent, and inherits its open file descriptors and signal and timer dispositions: *

process_id := fork();            -- trailing ``()'' connotes action
See the vendor-specific fork Unix manual pages for more details. A feature of all fork implementations now in popular use is that they take advantage of the page modification flags supported by virtual memory hardware in order to defer actually copying pages of cloned data space until one process or another writes into them.

If there are insufficient resources available for the spawning of a child process, fork returns om.

Direct use of fork is rarely necessary, because one of system, filter, pump, or an I/O mode that starts a child process (see Sections 2.2.2 [Pipes] and 2.2.3 [Pumps]) will provide a more convenient fit to most data processing needs. Still, it is helpful to have some appreciation of the Unix process model upon which the SETL model is built.

If a process which calls fork is still active when the child completes, it should clear the system's record of the child's process id and exit status by calling wait: *

id := wait (block);          -- block is boolean
If the flag block is false, wait will return immediately with either the process id of a child process that has exited, or 0 if none has yet. If block is true (the default), wait earns its name by waiting until a child process does exit, and then returns its process id. An important side-effect of a wait call that succeeds in ``reaping'' a process id in this way is that the record of the child process is cleared from the kernel's process table.

The high-level SETL operations that invoke child processes all effectively call wait at the appropriate time, namely when close is called on a pipe or pump stream (normally by the SETL programmer, but otherwise upon program exit), or during the last stage of a system or filter call.

After a successful wait, implicit or explicit, the exit status of the child process is available as the integer-valued *

which is 0 if no child has yet exited. Note that a SETL program can exit with a particular status using the optional integer argument of the stop statement, e.g.: *
stop 12;       -- a multiple of 4 for IBM 360 nostalgia :-)

In the Unix realm, one of the first things a child process that is working at the level of fork will usually do, after some file descriptor rearrangement using dup2, is to replace its entire memory image with a new program, using exec: *

exec (pathnameargvenvt);
The envt parameter is optional, and if it is absent, the argv parameter is also optional. The pathname is a string, and the optional parameters are tuples of strings if supplied. By default, argv will be taken to be [pathname].

Exec does not return, but constructs a call to the Unix routine execve if envt was supplied, or execv if it was not. See the manual pages for those routines for more information on how the pathname is used to find the executable file, and how the argument lists (argv and envt in SETL, mapped in the obvious way to null-terminated arrays of NUL-terminated C strings) are seen by the program when it is newly launched.

Here is a sketch of how fork, pipe, dup2, exec, wait, and close can be used in classical Unix style to provide a near-equivalent of the SETL `pipe-from' input mode: *

[rfdwfd] := pipe();      -- create pipe
process_id := fork();
if process_id = 0 then
  -- Child, redirects stdout into pipe
  close (rfd);             -- close readable side of pipe
  if wfd /= stdout then
    dup2 (wfdstdout);    -- redirect stdout
    close (wfd);           -- now retire this alias
  end if;
  -- String name identifies an executable file:
  exec (nameargs ...);   -- replace process image
  assert false;            -- exec should not return
end if;
-- Parent or only process continues here
close (wfd);               -- close writable side of pipe
if process_id /= om then
    ...                -- read from child via rfd, until EOF
  wait;                    -- clear child record from kernel
  -- No child was spawned
  printa (stderr, `fork() failed');
end if;
close (rfd);               -- done with readable side of pipe

See also the pid (process id), pexists (process existence), and kill (send signal) operations described in Section 2.8 [Processes and Process Groups].

next up previous
Next: 2.18 Summary Up: 2. Environmentally Friendly I/O Previous: 2.16 Time
David Bacon