Contents

Plan 9 I/O

Introduction

This page gives some information about the differences in the Plan 9 I/O facilities, compared to the regular I/O package.

Non-blocking I/O

One feature which is notably absent from Plan 9 is the poll system call (and its cousin, select). In fact, there are no non-blocking I/O facilities at all. This is a rather painful omission, and the only way around it is to fork a different process for the potentially blocking I/O calls. The Plan 9 version of Object Icon provides some classes which assist with this, namely FileWorker and NonBlockStream.

FileWorker

FileWorker takes an ordinary FileStream instance, dups its file descriptor, forks a background process, and performs I/O operations on the file in that process as instructed by the client. A buffer is allocated for read and write data. At any time, the foreground (client) process can query the FileWorker to check whether it is busy carrying out an operation. For example, the client may wish to read some data from the file. The op_read method is called; firstly, this waits until the worker is no longer busy. Then it causes the background process to start a read() call; the worker is now busy, and the op_read method returns. The client can check at intervals to see if the operation is complete, and then read the error status and/or data.

Other than reading, FileWorker can carry out write and close operations. It can also open and create a file, in which case an initial file is not passed to the constructor. The resulting file descriptor can be duped and used elsewhere by the client, or used in further operations in the FileWorker.

An important method in FileWorker is await(). This waits for the background operation (if any) to finish. Then, if the result code of the last operation indicates an error, it fails, setting &why. Otherwise it succeeds, returning the result code.

Here is an example program which opens a file and performs five background read operations, outputting the results.

Download testfileworker.icn

import io

procedure main(a)
   local w, n

   # Create a worker
   w := FileWorker()

   # Start an open operation in the background
   w.op_open(a[1], FileOpt.RDONLY)
   # Wait for it to finish, and stop if there was an error
   w.await() | stop(&why)

   every 1 to 5 do {
      # Start a read into the buffer
      w.op_read()
      # Print some dots whilst we're waiting
      while w.is_running() do {
         writes(".")
         delay(100)
      }
      # Get the result (await won't block since the worker isn't
      # running now).
      if n := w.await() then {
         if n = 0 then
            write(" EOF")
         else
            write(" -> ", w.get_buff(n))
      } else
         write(" failed ", &why)
   }

   w.close()
end

An example result of running it on a file from the tickfs example :-

% testfileworker /n/tick/4
...... -> *
...... -> *
...... -> *
...... -> *
...... -> *

NonBlockStream

This class builds upon FileWorker. It too wraps another file, and utilizes two separate FileWorkers to operate on it - one for reading and one for writing. (In fact, if the file is read-only or write-only, then only one FileWorker is used). It also has a read buffer so that one background read result can be consumed by several calls to in().

Here is the above example re-written to use a NonBlockStream.

Download testnonblockstream.icn

import io

procedure main(a)
   local f, nb, s

   # Open a file for reading.
   f := FileStream(a[1]) | stop(&why)

   # Create a read-only NonBlockStream operating on f
   nb := NonBlockStream(f)
   every 1 to 5 do {
      # Print some dots whilst we're waiting
      while nb.get_in_status() == NonBlockStream.READING do {
         writes(".")
         delay(100)
      }
      # Get the result
      if s := nb.in(32) then {
         if /s then
            write(" EOF")
         else
            write(" -> ", s)
      } else
         write(" failed ", &why)
   }

   # This will close f too.
   nb.close()
end

The output is just the same as testfileworker above.

Note how it is not necessary to start a read operation explicitly; this is done automatically at startup and whenever the read buffer is emptied by in().

Poll

Both FileWorker and NonBlockStream can be used with the DescStream.poll method, which in Plan 9 is implemented as a simple loop instead of the usual native method. The test program can be altered to use poll as follows :-

Download testpoll.icn

import io

procedure main(a)
   local f, nb, s, l

   # Open a file for reading.
   f := FileStream(a[1]) | stop(&why)

   # Create a read-only NonBlockStream operating on f
   nb := NonBlockStream(f)
   every 1 to 5 do {
      repeat {
         # Poll with a zero timeout (ie return immediately).
         l := DescStream.poll([nb, Poll.IN], 0) | stop(&why)
         if /l then {
            # A null result means timeout, so no data
            writes(".")
            delay(100)
         } else {
            # Otherwise print the result value and break
            writes(" ", l[1])
            break
         }
      }

      # Get the result
      if s := nb.in(32) then {
         if /s then
            write(" EOF")
         else
            write(" -> ", s)
      } else
         write(" failed ", &why)
   }

   # This will close f too.
   nb.close()
end

The output will again be the same as above, except that this program also prints out the value returned by poll. But the main point is that this program is almost identical to a non-Plan 9 program; the only Plan 9 specific part is the wrapping of the FileStream with a NonBlockStream. This makes is quite easy to get non-Plan 9 programs which use poll to work.

NetStream

Plan 9 doesn’t have sockets in the Unix sense, so there is no SocketStream implementation. Instead, there is a NetStream class which provides similar functionality. To illustrate this, here is the Plan 9 version of the simple server program shown on the co-expressions page.

Download server9.icn

import io

global sched, x_flag

procedure handle(a)
   local t, s
   t := Task(sched, create {
      # Service the client with a 30s timeout
      a := TaskStream(NonBlockStream(a), t, 30000) 
      write("\tHandling...")
      repeat {
         s := a.read() | {
            write("\tRead failed ", &why)
            break
         }
         # An input of "x" shuts down the server
         if s == "x" then {
            x_flag := 1
            break
         }
         # An input of "q" ends this client's session
         if s == "q" then 
            break
         a.write(map(s))
      } 
      write("\tEnd of handling.")
      a.close()
      t.revert()
   })
   t.start()
end

procedure main()
   local f, x, lf

   # Use a 50ms delay in the Scheduler's call to poll.
   sched := Scheduler(50)

   x := Task(sched, create {
      f := NetStream.announce("tcp!*!7000") | stop(&why)
      write("Listening...")
      repeat {
         lf := f.listen(, x) | stop(&why)
         lf.accept()
         handle(lf)
      }
   })
   x.start()

   until \x_flag do {
      sched.work()
      # Could do some extra work between polls.
   }

   f.close()
   write("Exiting")
end

Other additions

The io.Files class provides some additional methods which are Plan 9 specific. Files.unmount_windows unmounts unmounts any Object Icon window mount directories in /n; this is useful to avoid inadvertently creating a reference to the mount on a fork; such a reference will keep the window open after it is closed by the program.

The Files class also includes bind, mount and unmount methods which do what you would expect. Mount option flags can be found in io.MountOpt.

The Files.wstat method (and DescStream.wstat too) is slightly different in Plan 9, allowing the length and name to be set too. The Stat class has some Plan 9 additions, including the file name.

FileStream has two static methods, open9 and create9, which match more exactly the Plan 9 open and create system calls. Option flags can be found in io.FileOpt9. The pipe() call is also extended to take option flags for the pipe files.

Contents