Signals

30 Jan 2014

Time for another live rewrite - or at least a restructuring.

Signals in newlib

Newlib has a kind of signals built in without any help from the kernel. Those signals can't be sent between processes, though, so I'd like to implement my own.

I'll just look at signal and kill and ignore things like sigaction for now.

First of all, in order to compile newlib with kernel supported signals, you need the line

newlib_cflags="${newlib_cflags} -DSIGNAL_PROVIDED"

in your entry in newlib/configure.host as described in the osdev wiki.

Then you need the syscalls:

sig_t signal(int signum, sig_t handler);
int kill(int pid, int sig);

Raising signals

The posix specification contains a lot of rules about how signals should behave in a number of situations. I probably got a lot of them wrong, but I'll try to fix them as I go along later.

The way I chose to implement signals was through a list for each process. When a signal is sent using kill or similar, a signal_t

typedef struct
{
    uint32_t sig;
    uint32_t sender;
    list_head_t queue;
} signal_t;

is created and added to the processes list:

int signal_process(int pid, int signum)
{
    process_t *p = get_process(pid);
    ...
    signal_t *signal = calloc(1, sizeof(signal_t));
    signal->sig = signum;
    signal->sender = current->proc->pid;
    init_list(signal->queue);
    append_to_list(p->signal_queue, signal->queue);

    if(p == current->proc)
    {
        handle_signals(current);
    }
    return 0;
}

If the currently running thread is in the process being signalled, we handle the signals immediately. Otherwise it can wait for a bit.

signal_process is called by the kill syscall.

The signal syscall

The signal syscall lets the process select how to handle a certain signal. Each process also contains a table of sig_t and the signal syscall calls the following function:

sig_t switch_handler(int signum, sig_t handler)
{
    ...
    sig_t old = current->proc->signal_handler[signum];
    current->proc->signal_handler[signum] = handler;
    return old;
}

The cut out part of this function contains code to make sure the handler of signal 9 (SIGKILL) is never changed from SIG_DFL.

Handling signals

Signals should be handled as soon as possible, though there's no reason to let the receiving process skip the line in the scheduler. So as soon as the process continues running should be soon enough.

I chose to hook into the my interrupt handler:

registers_t *idt_handler(registers_t *r)
{
    ...
    if(int_handlers[r->int_no)
    {
        ...
        registers_t *ret = int_handlers[r->int_no](r);

        if((ret->cs & 0x3) == 0x3)
        {
            ret = (registers_t *)handle_signals((thread_t *)ret);
            ...
        }
        ...
        return ret
    }
    ...
}

So, what does handle_signals actually do?

It start by finding the signal queue of the process to which the passed thread belongs and steps through it one entry at a time. If a signal is queued and not blocked (actual blocking is not implemented yet) it checks what the handler for that signal number is. The handler may be SIG_IGN, SIG_DFL or a pointer to a function in userspace.

If the handler is set to SIG_DFL the function looks up the correct default signal handler in a table and calls it. It may be one of the following:

void sighandler_ignore(int num)
{
    (void)num;
}
void sighandler_terminate(int num)
{
    fprintf(stderr,, "Process %x terminated by signal %x\n", \
        current->proc->pid, num);
    _exit(num);
}
void sighandler_coredump(int num)
{ /* Same as above */ }
void sighandler_stop(int num)
{ /* Not implemented yet */ }
void sighandler_continue(int num)
{ /* Not implemented yet */ }

If the handler is a function a new thread is created to handle it:

    ...
    sig_t handler = th->proc->signal_handler[signal->sig];
    thread_t *h = new_thread((void (*)(void))handler, 1);

    append_to_list(th->proc->threads, h->process_threads);
    h->proc = th->proc;
    uint32_t *stack = ((uint32_t *)th->r.useresp;
    *--stack = signal->sig;
    *--stack = SIGNAL_RETURN_ADDRESS;
    h->r.useresp = h->r.ebp = (uint32_t)stack;
    remove_from_list(signal->queue);
    free(signal);

    scheduler_remove(h);
    scheduler_sleep(th, &h->waiting);
    scheduler_cheat(h);
    schedule();
}

This creates a new thread and pushes the signal number (as an argument) and a return address to the threads stack. It then places the new thread at the beginning of the schedulers queue and switches to it.

SIGNAL_RETURN_ADDRESS is chosen to cause a page fault when the signal handler returns. The page fault handler catches this and recognizes the address so that the thread can be safely destroyed.

Now the page fault handler can also be setup to send a SIGSEGV signal when a userspace thread page faults.

Code

Git commit 1ec132c

Comments

comments powered by Disqus
© 2012 Thomas Lovén - @thomasloven - GitHub