Skip to content

L15 Exceptional Control Flow: Signals and Nonlocal Jumps

Source: https://www.bilibili.com/video/BV1iW411d7hd?p=15

ECF Exists at All Levels of a System

​ Exceptions

​ Hardware and operating system kernel software.

​ Process Context Switch.

​ Hardware timer and kernel software.

​ Signals

​ Kernel software and application software.

​ Nonlocal Jumps

​ Application code.

其中 Exceptions 和 Process Context Switch 为 L14 内容,Signals(信号机制)为 L15 即本次课内容,Nonlocal Jumps(C 的非本地跳转)则为书和补充幻灯片所补充(textbook and supplemental slides)。

Today

​ Shells

​ Signals

​ Nonlocal


Linux 进程层次系统 Linux Process Hierarchy

QQ_1765641743216

​ 所有进程都是 init 进程的子进程(或后代进程),init 进程 启动时会创建守护进程(daemon),其一般是一个长期运行的程序,通常用来提供服务,比如说 web 服务器,或者其他你希望你只在系统上运行的服务。然后会创建登录进程,也就是登陆 shell(login shell),其为用户提供了命令行接口(command-line interface)。在其中输入并执行命令,比如说 ls 命令,实际上实在要求 shell 运行名为 ls 的可执行程序,为了运行,其就创建一个子进程,并在其中运行程序。

shell 程序 Shell Programs

​ A shell is an application program that runs programs on behalf of the user.

sh: Original Unix shell (Stephen Bourne, AT&T Bell Labs, 1977).

csh/tcsh: BSD Unix C shell.

bash: "Bourne-Again" Shell (default Linux shell).

int main() {
    char cmdline[MAXLINE]; /* command line */
    while (1) {
        /* read */
        printf("> ");
        Fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))
            exit(0);

        /* evaluate */
        eval(cmdline);
    }
}

​ Execution is a sequence of read/evaluate steps.

Simple Shell eval Function

void eval(char *cmdline) {
    char *argv[MAXARGS]; /* Argument list execve() */
    char buf[MAXLINE];   /* Holds modified command line */
    int bg;              /* Should the job run in bg or fg */
    pid_t pid;           /* Process id */

    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL)
        return;          /* Ignore empty lines */

    if (!builtin_command(argv)) {
        if ((pid = Fork()) == 0) { /* Child runs user job */
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

        /* Parent waits for foreground job to terminate */
        if (!bg) {
            int status;
            if (waitpid(pid, &status, 0) < 0)
                unix_error("waitfg: waitpid error");
        } else 
            printf("%d %s", pid, cmdline);
    }
    return;
}

​ 在 shell 中有个约定,如果命令行以一个 & 符号作为结尾,那么你可以使 shell 在后台运行此作业(job)。即 shell 在读取下一个 stub 之前,不会等待这个作业完成;反之,你就是要求 shell 在前台运行此作业,shell 会等待它完成。

bg for background(后台), fg for foreground(前台)。

​ shell 有一些内建的命令,shell 会检查 argv[0],查看其是否是一个内建 shell 命令。如果不是内建的命令,那么就是在要求 shell 运行一些程序,于是 fork 一个子进程,然后使用 execve 在其中尝试运行该程序。

​ 因为 execve() 除非有错误否则不返回,或者有错误而总是返回 -1.

​ 接下来是父进程的事情,如果不是在后台运行,也就是在前台运行,则需要使用 waitpid 等待进程结束。否则输出一条信息并继续执行。

Problem with Simple Shell Example

​ Our example shell correctly waits for and reap foreground jobs.

​ But what about background jobs?

​ Will become zombies when they terminate.

​ Will never be reaped because shell (typically) will not terminate.

​ Will create a memory leak that could run the kernel out of memory.

ECF to the Rescue!

​ Solution: Exceptional control flow.

​ The kernel will interrupt regular processing to alert us when a background process completes.

​ In Unix, the alert mechanism is called a signal.

Signals

​ A signal is a small message that notifies a process that an event of some type has occurred in the system.

​ Akin to exceptions and interrupts.

​ Sent from the kernel (sometimes at the request of another process) to a process. (内核信号总是由内核发出,但有时进程也会发出信号)

​ Signal type is identified by small integer ID's ( 1-30).

​ Only information in a signal is its ID and the fact that it arrived.

  ID      Name  Default Action  Corresponding Event
   2    SIGINT       Terminate  Used typed Ctrl-C  
   9   SIGKILL       Terminate  Kill program (cannot override or ignore)
  11   SIGSEGV       Terminate  Segmentation violation
  14   SIGALRM       Terminate  Timer signal
  17   SIGCHED          Ignore  Child stopped or terminated

Signal Concepts: Sending a Signal

​ Kernel sends (delivers) a signal to a destination process by updating some state in the context of the destination process.

​ Kernel sends a signal for one of following reasons:

​ Kernel has detected a system event such as divide-by-zero (SIGFPE) or the termination of a child process (SIGCHLD)

​ Another process has invoked kill system call to explicitly request the kernel to send a signal to the destination process. (注意,kill 只是一种发信号的方式,并不一定是杀进程,起这个名字也是因为一些神秘原因)

Signal Concepts: Receiving a Signal

​ A destination process receives a signal when it is forced by the kernel to react in some way to the delivery of the signal.

​ Some possible ways to react:

Ignore the signal (do nothing).

Terminate the process (with optional core dump).

Catch the signal by executing a user-level function called signal handler(信号处理程序).

​ Akin to a hardware exception handler being called in response to an asynchronous interrupt:

(1) Signal received  |
    by process       |  (2) Control passes
                     v   to signal handler
               I_curr---------------------->|
               I_next|\                     | (3) Signal 
                     | \                    |  handler runs
                     |  \                   v
                     |   <-------------------
                     v   (4) Signal handler
                           returns to next instruction

Signal Concepts: Pending and Blocked Signals

​ 如果一个信号被发出但尚未被接收,则称其待处理。A signal is pending if sent but not yet received.

​ There can be at most one pending signal of any particular type.

​ Important: Signals are not queued.

​ If a process has a pending signal of type k, then subsequent signals of type k that are sent to that process are discarded. (在某时刻,只能有一个相同类型的待处理信号。)

​ 一个进程可以阻塞某些信号的接收。A process can block the receipt of certain signals.

​ 被阻塞的信号可以被传递,但在阻塞解除前不能被接收。Blocked signals can be delivered, but will not be received until the signal is unblocked.

​ 信号传递无法被阻止,但可以阻止进程在收到信号后的响应。

​ 待处理信号最多被接收一次。A pending signal is received at most once.

Signal Concepts: Pending/Blocked Bits

​ Kernel maintains pending and blocked bit vectors(位向量) in the context each process.

pending: represents the set of pending signals.

​ Kernel sets bit k in pending when a signal of type k is delivered.

​ Kernel clears bit k in pending when a signal of type k is received.

blocked: represents the set of blocked signals.

​ Can be set and cleared by using the sigprocmask function.

​ Also referred to as the signal mask(信号掩码).

Sending Signals: Process Groups(进程组)

QQ_1765907847857

Sending Signals with /bin/kill Program

/bin/kill program sends arbitrary signal to a process or process group.

​ Examples:

/bin/kill -9 24818 Send SIGKILL to process 24818.

kill -9 很常用,使用这个来杀掉某个进程。

/bin/kill -9 -24817 Send SIGKILL to every process in process group 24817.

linux> ./forks 16
Child1: pid=24818 pgrp=24817
Child2: pid=24819 pgrp=24817

linux> ps
  PID TTY          TIME CMD
24788 pts/2    00:00:00 tcsh
24818 pts/2    00:00:02 forks
24819 pts/2    00:00:02 forks
24820 pts/2    00:00:00 ps
linux> /bin/kill -9 -24817
linux> ps
  PID TTY          TIME CMD
24788 pts/2    00:00:00 tcsh
24820 pts/2    00:00:00 ps
linux> 

Sending Signals from the Keyboard

QQ_1765954400861

Example of ctrl-c and ctrl-z

bluefish> ./forks 17
Child: pid=28108 pgrp=28107
Parent: pid=28107 pgrp=28107
<types ctrl-z>
Suspended
bluefish> ps w
  PID TTY      STAT   TIME COMMAND
27699 pts/8    Ss     0:00 -tcsh
28107 pts/8    T      0:01 ./forks 17
28108 pts/8    T      0:01 ./forks 17
28109 pts/8    R+     0:00 ps w
bluefish> fg
./forks 17
<types ctrl-c>
bluefish> ps w
  PID TTY      STAT   TIME COMMAND
27699 pts/8    Ss     0:00 -tcsh
28110 pts/8    R+     0:00 ps w

​ STAT (process state) Legend:

​ First letter: S: Sleeping; T: Stopped; R: Running.

​ Second letter: s: session leader; +: foreground proc group.

​ See man ps for more details.

注:Suspended 被挂起

Sending Signals with kill Function

void fork12() {
    pid_t pid[N];
    int i;
    int child_status;

    for (i = 0; i < N; i++)
        if ((pid[i] = fork()) == 0) {
            /* Child: Infinite Loop */
            while (1)
                ;
        }

    for (i = 0; i < N; i++) {
        printf("Killing process %d\n", pid[i]);
        kill(pid[i], SIGINT);
    }

    for (i = 0; i < N; i++) {
        pid_t wpid = wait(&child_status);
        if (WIFEXITED(child_status))
            printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
        else 
            printf("Child %d terminated abnormally\n", wpid);
    }
}

Receiving Signals

​ Suppose kernel is returning from an exception handler and is ready to pass control to process p.

|Time   Process A       process B
|          |                         user code
|          |
|          -------\  
|                  \                 kernel code \
|                   \------*                      context switch
|                          |         user code   /
|                          | 
|                   /-------
|                  /                 kernel code \
|          -------/                               context switch
|          |                         user code   /
|          |
|          v 
v

​ 在 * 处就是发送接收检查信号,以确保交接控制权的一切都无误。

​ Kernel computes pnb = pending & ~blocked 内核通过计算位向量 pnb 来做到这一点

​ The set of pending nonblocked signals for process p.

​ If pnb == 0

​ Pass control to next instruction in the logical flow for p.

​ Else

​ Choose least nonzero bit k in pnb and force process p to receive signal k.

​ The receipt of the signal triggers some action by p.

​ Repeat for all nonzero k in pnb.

​ Pass control to next instruction in logical flow for p.

Default Actions

​ Each signal type has a predefined default action, which is one of:

​ The process terminates. 进程终止并转储内存。

​ The process stops until restarted by a SIGCONT signal.

​ The process ignore the signal.

Installing Signal Handlers

​ The signal function modifies the default action associated with the receipt of signal signum:

handler_t *signal(int signum, handler_t *handler)

注:和 kill 一样,这也是个有误导性的起名, 调用kill 不一定杀死进程,而只是向进程发信号;调用 signal 并不是向进程发信号,而是修改信号的默认操作。

signal 函数指定信号的信号号(signal number,此处的 signum)和一个对信号默认行为的修改(此处的 handler)。

​ Different values for handler:

​ SIG_IGN: ignore signals of type signum.

​ SIG_DFL: revert to the default action on receipt of signals of type signum.

​ Otherwise, handler is the address of a user-level signal handler(用户级信号处理函数的地址).

​ Called when process receives signal of type signum.

​ Referred to as "installing" the handler.

​ Executing handler is called "catching" or "handling" the signal.

​ When the handler executes its return statement, control passes to instruction in the control flow of the process that was in?? receipt of the signal.

Signal Handling Example

void sigint_handler(int sig) { /* SIGINT handler */
    printf("So you think you can stop the bomb with ctrl-c, do you?\n");
    sleep(2);
    printf("Well...");
    fflush(stdout);
    sleep(1);
    printf("OK. :-)\n");
    exit(0);
}
int main() {
    /* Install the SIGINT handler */
    if (signal(SIGINT, sigint_handler) == SIG_ERR)
        unix_error("signal error");

    /* Wait for the receipt of a signal */
    pause();

    return 0;
}

Signals Handler as Concurrent Flows

​ A signal handler is a separate logical flow (not process) that runs concurrently with the main program.

|Time    Process A      Process A      Process B
|          while(1);    handler(){}
|           o
|                                          o
|                          o
|           o
|                                          o
|
v

​ 信号是并发的一种形式(Signals are another form of concurrency),信号是一个并发流。

Another View of Signal Handlers as Concurrent Flows

|Time   Process A       process B
|          |                         user code (main)
|          | I_curr <- Signal delivered to process A
|          -------\  
|                  \                 kernel code 
|                   \------*                      context switch
|                          |         user code (main)
|                          | 
|                   /-------
|                  /                 kernel code 
|          -------/                               context switch
|          |                         user code (handler)
|          |
|          |                         kernel code
|          | I_next <- Signal received by process A
|          |                         user code (main)
|          |
|          v
|
v

QQ_1765981185878

Blocking and Unblocking Signals

​ Implicit blocking mechanism

​ Kernel blocks any pending signals of type currently being handled.

​ e.g. A SIGINT handler can't be interrupted by another SIGINT.

​ Explicit blocking and unblocking mechanism.

sigprocmask function.

​ Supporting functions

sigemptyset: Create empty set.

sigfillset: Add every signal number to set.

sigaddset: Add signal number to set.

sigdelset: Delete signal number from set.

Temporarily Blocking Signals

    sigset_t mask, prev_mask;

    Sigemptyset(&mask);
    Sigaddset(&mask, SIGINT);

    /* Block SIGINT and save previous blocked set */
    Sigprocmask(SIG_BLOCK, &mask, &prev_mask);

    ... /* Code region that will not be interrupted by SIGINT */

    /* Restore previous blocked set, unblocking SIGINT */
    Sigprocmask(SIG_SETMASK, &prev_mask, NULL);

Safe Signal Handling

​ Handlers are tricky because they are concurrent with main program and share the same global data structures.

​ Shared data structures can become corrupted.

​ We'll explore concurrency issues later in the term.

​ For now here are some guidelines to help you avoid trouble.

Guidelines for Writing Safe Handlers

烦请参考《Unix 环境高级编程》信号一章。

​ G0: Keep your handlers as simple as possible.

​ e.g., Set a global flag and return.

​ G1: 在你的处理程序中只调用异步信号安全函数。Call only async-signal-safe functions in your handlers.

printf, sprintf, malloc, and exit are not safe!

​ G2: Save and restore errno on entry and exit.

​ So that other handlers don't overwrite your value of errno.

​ G3: Protect accesses to shared data structures by temporarily blocking all signals.

​ To prevent possible corruption.

​ G4: Declare global variables as volatile.

​ To prevent compiler from storing them in a register.

​ G5: Declare global flags as volatile sig_atomic_t

flag: variable that is only read or written (e.g. flag = 1, not flag).

​ Flag declared this way does not need to be protected like others.

异步信号安全 Async-Signal-Safety

​ Function is async-signal-safe if either reentrant (e.g., all variables stored on stack frame, CS:APP3e 12.7.2) or non-interruptible by signals.

​ Posix 标准确保了 117 个函数是同步信号安全的。Posix guarantees 117 functions to be async-signal-safe.

​ Source: man 7 signal.

​ Popular functions on the list:

_exit, write, wait, waitpid, sleep, kill.

​ Unfortunate fact: write is the only async-signal-safe output function.

Safely Generating Formatted Output

​ Use the reentrant SIO (Safe I/O library) from csapp.c in your handlers.

ssize_t sio_puts(char s[]) /* Put string */

ssize_t sio_putl(long v) /* Put long */

void sio_error(char s[]) /* Put msg & exit */

void sigint_handler(int sig) { /* Safe SIGINT handler */
    Sio_puts("So you think you can stop the bomb with ctrl-c, do you?\n");
    sleep(2);
    Sio_puts("Well...");
    sleep(1);
    Sio_puts("OK. :-)\n");
    _exit(0);
}

Correct Signal Handling

int ccount = 0;
void child_handler(int sig) {
    int olderrno = errno;
    pid_t pid;
    if ((pid = wait(NULL)) < 0)
        Sio_error("wait error");
    ccount--;
    Sio_puts("Handler reaped child ");
    Sio_putl((long)pid);
    Sio_puts(" \n");
    sleep(1);
    errno = olderrno;
}

void fork14() {
    pid_t pid[N];
    int i;
    ccount = N;
    Signal(SIGCHLD, child_handler);

    for (i = 0; i < N; i++) {
        if ((pid[i] = Fork()) == 0) {
            Sleep(1);
            exit(0); /* Child exits */
        }
    }
    while (ccount > 0) /* Parent spins */
        ;
}

​ Pending signals are not queued.

​ For each signal type, one bit indicates whether or not signal is pending...

​ ... thus at most one pending signal of any particular type.

​ You can't use signals to count events, such as children terminating.

​ Must wait for all terminated child processes.

​ Put wait in a loop to reap all terminated children.

void child_handler2(int sig) {
    int olderrno = errno;
    pid_t pid;
    while ((pid = wait(NULL)) > 0) {
        ccount--;
        Sio_puts("Handler reaped child ");
        Sio_putl((long)pid);
        Sio_puts(" \n");
    }
    if (errno != ECHILD) 
        Sio_error("wait error");
    errno = olderrno;
}

Portable Signal Handling

​ Ugh! Different versions of Unix can have different signals handling semantics.

​ Some older systems restore action to default after catching signal.

​ Some interrupted system calls can return with errno == EINTR.

​ Some systems don't block signals of the type being handled.

​ Solution: sigaction

handler_t *Signal(int signum, handler_t *handler) {
    struct sigaction action, old_action;

    action.sa_handler = handler;
    sigemptyset(&action.sa_mask); /* Block sigs of type being handled */
    action.sa_flags = SA_RESTART; /* Restart syscalls if possible */

    if (sigaction(signum, &action, &old_action) < 0)
        unix_error("Signal error");
    return (old_action.sa_handler);
}

Synchronizing Flows to Avoid Races

​ Simple shell with a subtle synchronization error because it assumes parent runs before child.

int main(int argc, char **argv) {
    int pid;
    sigset_t mask_all, prev_all;

    Sigfillset(&mask_all);
    Signal(SIGCHLD, handler);
    initjobs(); /* Initialize the job list */

    while (1) {
        if ((pid = Fork()) == 0) { /* Child */
            Execve("/bin/date", argv, NULL);
        }
        Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); /* Parent */
        addjob(pid); /* Add the child to the job list */
        Sigprocmask(SIG_SETMASKJ, &prev_all, NULL);
    }
    exit(0);
}

​ SIGCHLD handler for a simple shell.

void handler(int sig) {
    int olderrno = errno;
    sigset_t mask_all, prev_all;

    Sigfillset(&mask_all);
    while ((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap child */
        Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        deletejob(pid); /* Delete the child from the job list */
        Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    }
    if (errno != ECHILD)
        Sio_error("waitpid error");
    errno = olderrno;
}

Corrected Shell Program without Race

int main(int argc, char **argv) {
    int pid;
    sigset_t mask_all, mask_one, prev_one;

    Sigfillset(&mask_all);
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one, SIGCHLD);
    Signal(SIGCHLD, handler);
    initjobs(); /* Initialize the job list */

    while (1) {
        Sigprocmask(BIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
        if ((pid = Fork()) == 0) { /* Child process */
            Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
            Execve("/bin/date", argv, NULL);
        }
        Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
        addjob(pid); /* Add the child to the job list */
        Sigprocmask(SIG_SETMASKJ, &prev_one, NULL); /* Unblock SIGCHLD */
    }
    exit(0);
}

Explicitly Waiting for Signals

​ Handlers for program explicitly waiting for SIGCHLD to arrive.

volatile sig_atomic_t pid;

void sigchld_handler(int s) {
    int olderrno = errno;
    pid = Waitpid(-1, NULL, 0); /* Main is waiting for nonzero pid */
    errno = olderno;
}
void sigint_handler(int s) {

}

​ Similar to a shell waiting for a foreground job to terminate.

int main(int argc, char **argv) {
    sigset_t mask, prev;
    Signal(SIGCHLD, sigchld_handler);
    Signal(SIGINT, sigint_handler);
    Sigemptyset(&mask);
    Sigaddset(&mask, SIGCHLD);

    while (1) {
        Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
        if (Fork() == 0) /* Child */
            exit(0);
        /* Parent */
        pid = 0;
        Sigprocmask(SIG_SETMASK, &prev, NULL); /* Unblock SIGCHLD */

        /* Wait for SIGCHLD to be received (wasteful!) */
        while (!pid)
            ;
        /* Do some work after receiving SIGCHLD */
        printf(".");
    }
    exit(0);
}

​ Program is correct, but very wasteful.

​ Other options:

while (!pid) /* Race! */
    pause();
while (!pid) /* Too slow! */
    sleep(1);

​ Solution: sigsuspend.

Waiting for Signals with sigsuspend

int sigsuspend(const sigset_t *mask)

​ Equivalent to atomic (uninterruptable) version of:

sigprocmask(SIG_BLOCK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
int main(int argc, char **argv) {
    sigset_t mask, prev;
    Signal(SIGCHLD, sigchld_handler);
    Signal(SIGINT, sigint_handler);
    Sigemptyset(&mask);
    Sigaddset(&mask, SIGCHLD);

    while (1) {
        Sigprocmask(SIG_BLOCK, &mask, &prev); /* Block SIGCHLD */
        if (Fork() == 0) /* Child */
            exit(0);

        /* Wait for SIGCHLD to be received*/
        pid = 0;
        while (!pid)
            Sigsuspsend(&prev);

        /* Optionally unblock SIGCHLD */
        Sigprocmask(SIG_SETMASK, &prev, NULL);
        /* Do some work after receiving SIGCHLD */
        printf(".");
    }
    exit(0);
}