The textbook describes OpenSolaris/Illumos signals in Ch. 2.11, starting with general Unix theory and then continuing to implementation. Illumos man pages are signal(3c), signal.h(3head), sigaction(2), sigsend(2), siginfo(3head). [As per standard Unix convention, the manual section name is written in ()s after the manual topic name. So, "man signal" gives a page in section 3C (libc functions), "man sigaction" in section 2 (syscalls), etc. Linux section names and numbers are slightly different.] The key points here are: 1. All signals except SIGKILL and SIGSTOP can be ignored or "caught" by a process (reflect on why these two exceptions were designed so). By default, uncaught signals that aren't explicitly ignored would cause the process to be terminated by the kernel. 2. In Illumos, a process can specify "dispositions" w.r.t. signals, such as SIG_IGN, SIG_DFL (useful for restoring the default after it was modified), or a function pointer to a custom function (handler) to call when a signal arrives. Dispositions are per-process (i.e., shared by all threads). 3. In addition to process dispositions, threads of a process can "mask" (block) signals on per-thread basis. Thus, if a thread can mask a signal, the process' disposition will not be reached. 4. There are several styles of managing both signal delivery/disposition and signal handling in a process. See "man signal" for the older one, and "man sigaction" for the newer, where richer information about a signal is available (see "man siginfo"). For example, signals/sig.c has an example of sigaction() use that allows a process to catch signals and check their origin. Suggestion: understand why signals delivered via the terminal by pressing ^C end up with the reported PID of 0. Note that a disposition can be _either_ a function pointer or a small integer constant---see macros SIG_DFL and SIG_IGN in http://src.illumos.org/source/xref/illumos-gate/usr/src/uts/common/sys/iso/signal_iso.h or do "gcc -E" on a file that includes signal.h. It gets better: the signal handler function that you are allowed to register can have different signatures depending on whether you are using signal(2) or sigaction(2). The latter allows you to use either a wimpier void (*sa_handler)() or a much richer void (*sa_sigaction)(int, siginfo_t*, void*) depending on whether you set the SA_SIGINFO flag (see sigaction(2)). The older signal(2) style allows the handler only one argument: the actual number of the signal caught (designed that way to have the same function handle several signals, by switching on this number). Sigaction(2) provides two additional arguments with all kinds of goodies, such as who sent the signal, etc. Run through the example in http://www.cs.dartmouth.edu/~sergey/cs258/signals/sig.c and try different other ways to handle a signal and different signals. Notice the simple line pid_t sender_pid = siginfo->si_pid; But, as we looked for the definition of siginfo_t, we found a scary looking union of structs in that definition, in /usr/include/sys/siginfo.h. What is si_pid and how come this access to the __pid field of siginfo looks so simple? Well, si_pid is a actually a macro, walking the right variants within the union: #define si_pid __data.__proc.__pid Now the simple line above makes sense. There are other macros besides it, for different other signals, say, si_addr for SIGSEGV, etc. Read signal.h(3head) and siginfo.h(3head) for more details. Suggestion: Write a C program that prints out its own memory map (cf. pmap) by trying to access memory pages in its virtual address space and catching signals resulting from segmentation faults. ==================[ Illumos signal internals ]================== Registering a signal handler for a process with sigaction(2): http://src.illumos.org/source/xref/illumos-gate/usr/src/uts/common/syscall/sigaction.c#43 Notice the copyin() for the userland sigaction struct (Linux has a similar copy_from_user()). This system call must pass to the kernel code a struct that resides in userland memory space. In order to consume this argument safely, the kernel must copy it into kernel memory, where userland code cannot change that data---and only then can kernel code start checking that data for being well-formed (say, the signal number being less than the maximum NSIG). Otherwise, any check performed by the kernel code would be useless, as hostile useland code could change any already checked value. Also note the SPARC pointer alignment check (all SPARC function addresses are 4-byte aligned). Notice the copyout() of the old signal-handling data (if the user supplied a non-NULL pointer to a userland buffer)---data gathered in the kernel must, of course, be copied into userland memory for the user to be able to read it! Sigaction(2) calls on setsigact(), which sets the new disposition http://src.illumos.org/source/xref/illumos-gate/usr/src/uts/common/os/sig.c#1563 1570 PTOU(curproc)->u_signal[sig - 1] = disp; and then manages various sets (bitmaps) of signals caught or ignored, using sigaddset() and sigdelset() on the proc_t's siginfo structure. This logic is non-trivial, and depends on the disposition. In last year's midterm problem 2, my first few tries to set the bits of these sets right (to make the process "invincible" to SIGINT) actually failed, despite setting the SIG_IGN disposition. Then I used mdb -k to simply dump to files all related proc_t members for a process before and after calling "sigignore(SIGINT);" in it, and diff-ed the files. Sure enough, setting them worked :) ==================[ Kernel's handling of signals ]================== Where does the kernel create the siginfo struct that a signal handler in a process receives? For example, where is the sa_pid written into it? Observe, e.g. http://src.illumos.org/source/xref/illumos-gate/usr/src/uts/common/os/sig.c#sigsendproc lines 1860--1876. Note that data structure being filled is sigqueue_t. This sigqueue_t sqp contains a pointer to the next sigqueue_t and a k_siginfo_t, where the information for the future siginfo to be delivered to sa_sigaction is stored. 442 typedef struct sigqueue { 443 struct sigqueue *sq_next; 444 k_siginfo_t sq_info; 445 void (*sq_func)(struct sigqueue *); /* destructor function */ 446 void *sq_backptr; /* pointer to the data structure */ 447 /* associated by sq_func() */ 448 int sq_external; /* comes from outside the contract */ 449 } sigqueue_t; k_siginfo_t is packed differently based on what kind of flags are set and what kind of signal is being responded to; so use "::print -ta k_siginfo_t" to see the actual offsets at which different members lie. Suggestion: See these structures via ::print -t in MDB, for a process. Suspend that process, then send it some signals---that will queue them under its proc_t's p_sigqueue. The sigqalloc() function called to create sqp makes it easy to see how the queuing is actually done: http://src.illumos.org/source/xref/illumos-gate/usr/src/uts/common/os/sig.c#sigqalloc An aside: notice kmem_malloc() in there. Kmem_malloc? Isn't KMEM supposed to be about cached objects, whereas VMEM specializes in raw memory chunks? Well, a look at the code of kmem_malloc() shows that it does indeed dispatch to the familiar kmem_cache_alloc() rather soon, after finding the KMEM cache for the chunks of the needed size---or, in case of lacking a KMEM with chunks large enough, falls through to vmem_alloc(). Suggestion: examine the caches used by kmem_alloc(). You'll find them at the top of kmem.c: http://src.illumos.org/source/xref/illumos-gate/usr/src/uts/common/os/kmem.c#kmem_alloc_table How many are there? ("kmem_alloc_table::print -t" would reveal it). Write DTrace queries to catch only those allocations that come from the signal queues. Suggestion: So sigaction or signal set custom function pointers for handlers to be called when a signal arrives. But where/how is control actually passed to them? More precisely: how does the process get its EIP/RIP pointed the address of the handler function? DTrace's proc provider (http://dtrace.org/guide/chp-proc.html) will help you puzzle this out; sched (http://dtrace.org/guide/chp-sched.html) can also help.