[Oberon] Is it really necessary to hide the list of open file descriptors from the GC?

Andreas Pirklbauer andreas_pirklbauer at yahoo.com
Mon Feb 14 01:19:39 CET 2022

Question: Is it really necessary to hide the list of open file descriptors from the GC?

In the Project Oberon 2013 system, the list of open files (file descriptors) is hidden from the garbage collector (GC) by representing it by integers (addresses) instead of pointers:

  MODULE Files;
        FileDesc =
          RECORD next: INTEGER; (*list of files invisible to the GC*)
          END ;
     VAR root: INTEGER (*File*); (*list of open files*)
  END Files.

The rationale for this decision is provided in chapter 7.3 on p.92 of the book Project Oberon 2013 Edition:

“All [file ] descriptors are linked together in a list anchored by the global variable root and linked by the descriptor field next. This measure may seem to solve the problem of avoiding inconsistencies smoothly. However, there exists a pitfall that is easily overlooked: All opened files would permanently remain accessible via root, and the garbage collector could never remove a file descriptor nor its associated buffers. This would be unacceptable. In order to hide this list from the garbage collector, it is represented by integers (addresses) instead of pointers”.

In the original implementation of the Oberon system on Ceres computers, the garbage collector (implemented as one monolithic assembler procedure Kernel.GC) was written in such a way that the list of file descriptors (rooted in the global variable Files.root) was excluded from its mark phase.

In the Project Oberon 2013 implementation, the list of files is also hidden from the garbage collector (as outlined above - by representing it by integers), but the step to rebuild the list of file descriptors such that only still referenced ones remain in the list is now *separated out* into a separate procedure RestoreList exported by module Files:

  MODULE Oberon;
    PROCEDURE GC;  (*garbage collector*)
      mod := Modules.root;
      WHILE mod # NIL DO
        IF mod.name[0] # 0X THEN Kernel.Mark(mod.ptr) END ;    (*mark phase of the garbage collector*)
        mod := mod.next
      END ;
      Files.RestoreList;  (*<------- rebuild the list of file descriptors such that it only contains still referenced (=marked) ones*)
      Kernel.Scan;  (*scan phase of the garbage collector*)

This raises the question whether it is actually necessary to *hide* the list of of file descriptors from the garbage collector.

It seems that *so long as* it is guaranteed that procedure Files.RestoreList is called immediately after the mark phase but before the scan phase (which is the case in Oberon.GC) and that these steps are atomic (i.e. the system does not use a pre-emptive scheduler) (which is the case in the Oberon system, where tasks and procedures run to completion), there seems to be no need to do so.

I have tested this by making the necessary (minor) modifications to module Files (see code excerpt below) and the system works just as fine.

Any comments? Any caveats?

Modified module Files:

MODULE Files;  (*list of files no longer hidden from the garbage collector*)
        FileDesc =
          RECORD next: File; (*instead of INTEGER*)
          END ;

     VAR root: File; (*instead of INTEGER*)    (*list of open files*)

     PROCEDURE Old*(name: ARRAY OF CHAR): File;
       IF header # 0 THEN
         f := root;
         WHILE (f # NIL) & (f.sec[0] # header) DO f := f.next END ;   (*no longer need calls to SYSTEM.VAL(..) here*)

      … several other minor changes in Old and New, eliminating type casts between type File and INTEGER

      PROCEDURE Init*;
      BEGIN root := NIL; (*instead of 0*) ...
      END Init;

      PROCEDURE RestoreList*; (*after mark phase of garbage collection*)
         VAR f, f0: File;  (*instead of type INTEGER*)

          PROCEDURE mark(f: File): INTEGER;   (*instead of mark(f: INTEGER)*)
            VAR m: INTEGER;
            IF f = NIL THEN m := -1 ELSE m := SYSTEM.VAL(INTEGER, f); SYSTEM.GET(m-4, m) END ;
            RETURN m
          END mark;

        BEGIN  (* RestoreList *)
          WHILE mark(root) = 0 DO root := root.next END ;
          f := root;
          WHILE f # NIL DO
            f0 := f;
            REPEAT f0 := f0.next UNTIL mark(f0) # 0;   (*no more calls to GET, PUT here...*)
            f.next := f0; f := f0
      END RestoreList;

  END Files.

More information about the Oberon mailing list