;;; Unix wait & process objects for scsh ;;; Copyright (c) 1993, 1994, 1995 by Olin Shivers. ;;; This is a GC'd abstraction for Unix process id's. ; ;; The problem with Unix pids is (a) they clutter up the kernel ;;; process table until you wait(2) them, and (b) you can only ;;; wait(2) them once. Scsh's process objects are similar, but ;;; allow the storage to be allocated in the scsh address space, ;;; and out of the kernel process table, and they can be waited on ;;; multiple times. ;;; Process objects ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-record proc ; A process object pid ; Proc's pid. (finished? #f) ; Running, stopped, done (status (make-placeholder)) ; The cached exit status of the process (zombie #t) ; Misnomer. Whether or not the process has ; (not) been waited on. ;; Make proc objects print like #{proc 2318}. ((disclose p) (list "proc" (proc:pid p) (proc:finished? p)))) ;; Unfortunately there is no way to specify the name of the constructor- ;; function in Olins define-record macro, so I had to do this... (define (make-procobj pid) (let ((procobj (make-proc pid))) (add-finalizer! procobj procobj-finalizer) procobj)) ;; Weak pointer tables. Much more efficient than populations. ;; Maps pids to processes. Unexited processes are strong pointers, exited ;; procs are weak pointers (to allow gc'ing). ;; ;; JMG: whyever unexited processes were strong pointer, this won't work ;; with (autoreap-policy 'late), since then gc waits for the strong pointer ;; until it wait(2)s and the strong pointer waits for wait(2) which is ;; nothing but a deadlock (define process-table (make-integer-table)) (define (weak-table-ref t n) (let ((r (table-ref t n))) (if (weak-pointer? r) (weak-pointer-ref r) (error "there was a non-weak-pointer" r)))) (define (weak-table-set! t n s) (table-set! t n (make-weak-pointer s))) ;(define (weaken-table-ref! t n) ; (weak-table-set! t n (weak-table-ref t n))) (define (maybe-pid->proc pid) (weak-table-ref process-table pid)) (define (pid->proc pid . maybe-probe?) (let ((probe? (:optional maybe-probe? #f))) (or (maybe-pid->proc pid) (case probe? ((#f) (error "Pid has no corresponding process object" pid)) ((create) (let ((p (make-procobj pid))) ; Install a new one. ;; Weak because we don't know what's up with this thing. (weak-table-set! process-table pid p) p)) (else #f))))) ;;; Coerce pids and procs to procs. (define (->proc proc/pid) (cond ((proc? proc/pid) proc/pid) ((and (integer? proc/pid) (>= proc/pid 0)) (pid->proc proc/pid)) (else (error "Illegal parameter" ->proc proc/pid)))) ;;; Is X a pid or a proc? (define (pid/proc? x) (or (proc? x) (and (integer? x) (>= x 0)))) ;;; Process reaping ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; "Reaping" a process means using wait(2) to move its exit status from the ;;; kernel's process table into scsh, thus cleaning up the kernel's process ;;; table and saving the value in a gc'd data structure, where it can be ;;; referenced multiple times. ;;; ;;; - Stopped processes are never reaped, only dead ones. (May change -df) ;;; ;;; - Stopped process status codes are never cached in proc objects, ;;; only status codes for dead processes. So you can wait for a ;;; dead process multiple times, but only once per process-stop. ;;; (May change -df) ;;; ;;; - Unfortunately, reaping a process loses the information specifying its ;;; process group, so if a process is reaped into scsh, it cannot be ;;; waited for by WAIT-PROCESS-GROUP. Notice that only dead processes are ;;; reaped, not suspended ones. Programs almost never use WAIT-PROCESS-GROUP ;;; to wait for dead processes, so this is not likely to be a problem. If ;;; it is, turn autoreaping off with (autoreap-policy #f). ;;; (This never worked right, and it might be wiped out completely -fd) ;;; ;;; - Reaping can be encouraged by calling (REAP-ZOMBIES). ;;; (autoreap-policy [new-policy]) ;;; Watch this area (define *autoreap-policy* #f) ; Not exported from this module. (define (autoreap-policy . maybe-policy) (let ((old-policy *autoreap-policy*)) (if (pair? maybe-policy) (let ((new-policy (car maybe-policy))) (cond ((pair? (cdr maybe-policy)) (error "Too many args to autoreap-policy" maybe-policy)) ((not (memq new-policy '(early late #f))) (error "Illegal autoreap policy." new-policy)) (else (set! *autoreap-policy* new-policy) (cond ((eq? new-policy 'early) (sigchld-setter early-sigchld-handler) (post/gc-setter (lambda a (reap-need-reaping)))) ((eq? new-policy 'late) (sigchld-setter late-sigchld-handler) (post/gc-setter (lambda a (reap-need-reaping)))) (else (sigchld-setter noauto-sigchld-handler) (post/gc-setter (lambda (enabled-ints) (display "won't reap on gc since #f"))))))))) old-policy)) (define sigchld-setter (lambda a (error "procobj setter was not defined"))) (define (post/gc-setter handler) (set! post/gc-setter (low-interrupt-register interrupt/post-gc handler))) ;;; New (scsh 0.6) ;;; we register the post/gc-handler not until the first police change (define (install-autoreaping) (let ((setter (low-interrupt-register interrupt/chld early-sigchld-handler))) (set! *autoreap-policy* 'early) (set! sigchld-setter setter))) ;;; This list contains pids whose proc-obj were gc'd before they died ;;; We try to reap them after every gc and maybe on every SIGCHLD (define need-reaping '()) (define (need-reaping-add! pid) (set! need-reaping (cons pid need-reaping))) (define (need-reaping-remove! pid) (set! need-reaping (del pid need-reaping))) (define (reap-need-reaping) (set! need-reaping (filter (lambda (pid) (not (reap-pid pid))) need-reaping))) ;;; reap this special pid ;;; return status or #f (define (reap-pid pid) (let ((status (really-wait pid wait/poll))) (if status (dead_by_reap pid status)) status)) ;;; Handler for SIGCHLD according policy (define (late-sigchld-handler enabled-ints) (display "late sigchld handler")) (define (early-sigchld-handler enabled-ints) (reap-zombies)) (define (noauto-sigchld-handler enabled-ints) (display "won't on chld reap since #f") #f) ;;; Finalizer for procobjs ;;; (define (procobj-finalizer procobj) (display "procobj finalizer called") (if (not (proc:finished? procobj)) (need-reaping-add! (proc:pid procobj)) (display "but was already finished"))) ;;; (reap-zombies) => bool ;;; Move any zombies from the kernel process table into scsh. ;;; Return true if no more outstanding children; #f if some still live. (define (reap-zombies) (display "reap-zombies was called" (current-error-port)) (newline) (let lp () (receive (pid status) (%wait-any (bitwise-ior wait/poll wait/stopped-children)) (if pid (begin (dead_by_reap pid status) (format (current-error-port) "Reaping ~d[~d]~%" pid status) (lp)) status)))) (define (new-child-proc pid) (let ((proc (make-procobj pid))) (weak-table-set! process-table pid proc) proc)) ;;; (WAIT proc/pid [flags]) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; (wait proc/pid [flags]) => status or #f ;;; ;;; FLAGS (default 0) is the exclusive or of the following: ;;; wait/poll ;;; Return #f immediately if there are no ;;; unwaited children available. ;;; wait/stopped-children ;;; Report on suspended children as well. ;;; ;;; If the process hasn't terminated (or suspended, if wait/stopped ;;; is set) and wait/poll is set, return #f. ;;; (I'm working on the flags -df) ;;; JMG: We have to be careful about wait/poll and autoreap-policy: ;;; If it was 'late to anytime, we may missed the exit of pid/proc ;;; So we cannot just block and hope reap-zombies will give us the status ;;; TODO: add a jetzt-wart-i lock (define (wait pid/proc . maybe-flags) ;;; I'm really tired of opening everything in scsh-level-0 (let ((wait-interrupt (structure-ref events wait-interrupt)) (most-recent-event (structure-ref events most-recent-event))) (let* ((flags (:optional maybe-flags 0)) (proc (->proc pid/proc)) (win (lambda (status) (dead_by_wait proc status) status))) (cond ((proc:finished? proc) (win (placeholder-value (proc:status proc)))) ((zero? (bitwise-and flags wait/poll)) (let lp () ; we have to block and hence use the event system (wait-interrupt interrupt/chld (most-recent-event)) (cond ((wait proc (bitwise-ior flags wait/poll)) => win) (else (lp))))) ((eq? wait/poll (bitwise-and flags wait/poll)) (cond ((really-wait (proc:pid proc) flags) => win) (else #f))))))) ;;; This one is used, to wait on a positive pid ;;; We NEVER do a blocking wait syscall (define (really-wait pid flags) (if (zero? (bitwise-and flags wait/poll)) (error "really-wait without wait/poll")) (if (< pid 1) (error "really-wait on nonpos pid" pid)) (receive (err return_pid status) (%wait-pid/errno pid flags) (cond ((not err) (cond ((zero? return_pid) #f) ; failed wait/poll ((= pid return_pid) status) ; made it (else (error "mismatch in really-wait" return_pid pid)))) ((= err errno/intr) (really-wait pid flags)) (else (errno-error err %wait-pid pid flags))))) ;;; Another way to do it: ;;; Every time we reap a process, we pop out of our SIGCHLD ;;; block so that we can service an interrupt if the system ;;; so wishes. ;(define (wait/pid pid) ; ((let lp () ; (blocking signal/chld ; (or (waited pid) ; Previously waited or reaped ; (receive (next-dead status) (reap-a-pid) ; (if (= pid next-dead) (lambda () status) ; lp))))))) ;The above seems to use Olin's event model. Even so, I'm not ;sure that's the best way to do that. ;Generally correct idea, tho ;;; TODO: These 2 have to be renamed !!! ;;; All you have to do, if pid was reaped ;;; proc_obj is maybe no longer alive (define (dead_by_reap pid status) (cond ((maybe-pid->proc pid) => (lambda (proc) (obituary proc status) (push-reaped-proc proc))) (else (warn "reaped pid had no procobj" pid)))) ;;; All you have to do, if a wait on proc was successful (define (dead_by_wait proc status) (obituary proc status) (mark-proc-waited! proc)) ;;; we know from somewhere that proc is dead (define (obituary proc status) (if (not (proc? proc)) (error "proc was not a proc" proc)) (need-reaping-remove! (proc:pid proc)) ; in case it started during 'late (placeholder-set! (proc:status proc) status) (set-proc:finished? proc #t)) ;;; (wait-any [flags]) => [proc status] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; [#f #f] => non-blocking, none ready. ;;; [#f #t] => no more. (define (wait-any . maybe-flags) (let ((flags (:optional maybe-flags 0))) (if (zero? (bitwise-and flags wait/poll)) (begin (receive (pid status) ;;; before we maybe block via placeholder-value ;;; do a really-wait-any for the ones, missed by 'late (really-wait-any (bitwise-ior flags wait/poll)) (if (not pid) (let ((win (get-reaped-proc!))) (values win (placeholder-value (proc:status win)))) (values pid status)))) ;The rest of this is quite crude and can be safely ignored. -df ;;; JMG: wait-any is crude and so it's implementation ;;; It got even worse, now that we have this fu*$#%g 'late (if (maybe-obtain-lock reaped-proc-pop-lock) (if (eq? reaped-proc-head reaped-proc-tail) ;;; due to 'late we cannot be sure, that they all have been ;;; reaped (begin (release-lock reaped-proc-pop-lock) (really-wait-any flags)) (let* ((retnode (placeholder-value reaped-proc-head)) (retval (weak-pointer-ref (reaped-proc:proc retnode)))) (set! reaped-proc-head (reaped-proc:next retnode)) (release-lock reaped-proc-pop-lock) (if retval (values retval (placeholder-value (proc:status retval))) (values #f #f)))) (values #f #f))))) (define (really-wait-any flags) (if (zero? (bitwise-and flags wait/poll)) (error "real-wait-any without wait/poll" flags)) (receive (pid status) (%wait-any flags) (if pid (let ((proc (new-child-proc pid))) (dead_by_wait proc status) (values proc status)) (values #f #f)))) ;;; (wait-process-group [proc-group flags]) => [proc status] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; [#f #f] => non-blocking, none ready. ;;; [#f #t] => no more. ;;; ;;; ;;; If you are doing process-group waits, you do *not* want to use ;;; early autoreaping, since the reaper loses process-group information. ;;; (I'm working on it -df) (define (wait-process-group . args) (let-optionals args ((proc-group 0) (flags 0)) (let ((proc-group (cond ((integer? proc-group) proc-group) ((proc? proc-group) (proc:pid proc-group)) (else (error "Illegal argument" wait-process-group proc-group))))) (receive (pid status) (%wait-process-group proc-group flags) (if pid (let ((proc (pid->proc pid))) (dead_by_wait proc status) (values proc status)) (values pid status)))))) ; pid = #f -- Empty poll. ;;; (%wait-any flags) (%wait-pid pid flags) (%wait-process-group pgrp flags) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Direct interfaces to waitpid(2) call. ;;; [#f #f] means no processes ready on a non-blocking wait. ;;; [#f #t] means no waitable process on wait-any. (define (%wait-pid pid flags) (let lp () (receive (err pid status) (%wait-pid/errno pid flags) (cond ((not err) (and (not (zero? pid)) status)) ; pid=0 => none ready. ((= err errno/intr) (lp)) (else (errno-error err %wait-pid pid flags)))))) (define (%wait-any flags) (let lp () (receive (err pid status) (%wait-pid/errno -1 flags) (cond (err (cond ((= err errno/child) (values #f #t)) ; No more. ((= err errno/intr) (lp)) (else (errno-error err %wait-any flags)))) ((zero? pid) (values #f #f)) ; None ready. (else (values pid status)))))) (define (%wait-process-group pgrp flags) (let lp () (receive (err pid status) (%wait-pid/errno (- pgrp) flags) (cond (err (cond ((= err errno/child) (values #f #t)) ; No more. ((= err errno/intr) (lp)) (else (errno-error err %wait-process-group pgrp flags)))) ((zero? pid) (values #f #f)) ; None ready. (else (values pid status)))))) ;;; Reaped process table ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; We keep track of procs that have been reaped but not yet waited on by ;;; the user's code. These proces are eligible for return by WAIT-ANY. ;;; We keep track of these so that WAIT-ANY will hand them out exactly once. ;;; What this code needs is traditional condition variables. ;;; This is (so far) reliable in the following ways: ;;; 1. No process will be returned twice by wait-any, ever. Even two different ;;; wait-anys. ;;; 2. Being un-reaped will not prevent garbage collection. ;;; (actually, there seems to be a problem with this -df) ;;; 3. If a process is waited on, or is gc'ed, wait-any will do the Right ;;; Thing. ;;; And UNreliable in the following ways: ;;; 1. If a wait and a wait-any are blocking simultaneously, the wait will ;;; always return the object. However, whether the wait-any will or not ;;; is based on racing semaphores. ;;; 2. While processes can still be garbage collected, the nodes on the ;;; wait-any list will not, and if the program never wait-any's, the queue ;;; will snake around, eating up memory like pac-man with the munchies. ;;; 3. The process may be garbage collected before wait-any gets to it, and ;;; that's just tough. ;;; -df (define-record reaped-proc proc (next (make-placeholder)) prev) (define reaped-proc-tail (make-placeholder)) (define reaped-proc-head reaped-proc-tail) (define reaped-proc-push-lock (make-lock)) (define reaped-proc-pop-lock (make-lock)) ;;; Zippy sez: pop lock! (define (push-reaped-proc proc) (obtain-lock reaped-proc-push-lock) (let ((push-me (make-reaped-proc (make-weak-pointer proc) reaped-proc-tail))) (placeholder-set! reaped-proc-tail push-me) ; (add-finalizer! proc (lambda ignore (remove-reaped-proc push-me))) (set! reaped-proc-tail (reaped-proc:next push-me))) (release-lock reaped-proc-push-lock)) (define (remove-reaped-proc reaped-proc) (spawn (lambda () ;This is blocking, so should run by itself (placeholder-set! (reaped-proc:prev reaped-proc) (placeholder-value (reaped-proc:next reaped-proc)))) "reaped-proc-removing-thread")) (define (pop-reaped-proc) (obtain-lock reaped-proc-pop-lock) ;;; pop lock pop lock pop lock! (let ((pop-me (placeholder-value reaped-proc-head))) (set! reaped-proc-head (reaped-proc:next pop-me)) (release-lock reaped-proc-pop-lock) (weak-pointer-ref (reaped-proc:proc pop-me)))) ;;; Pop one off the list. (define (get-reaped-proc!) (let loop ((try (pop-reaped-proc))) (if (and try (proc:zombie try)) try (loop (pop-reaped-proc))))) ;;; PROC no longer eligible to be in the list. Delete it. (define (mark-proc-waited! proc) (set-proc:zombie proc #f))