Receiving SIGINT and exception Handles in Linux

2019-01-27 06:01发布

Let's say we have a program in C that uses the sleep() function

The program executes and goes to sleep. Then we type ctrl-c to send a SIGINT signal to the process.

We know that the default action upon receipt of a SIGINT is to terminate the process, we also know that the sleep() function resume the process whenever the sleeping process receives a signal.

And my textbook says in order to allow sleep() function to return, we must install a SIGINT handler like this:

void handler(int sig){
    return; /* Catch the signal and return */
}
...
int main(int argc, char **argv) {
   ...
   if (signal(SIGINT, handler) == SIG_ERR) /* Install SIGINT handler */
      unix_error("signal error\n");
   ...
   sleep(1000)
}

Althouth the code seems to be straightforward, I still have questions if I want to dig deeper:

Background: When the process is sleeping and we type ctrl-c to send SIGINT

Q1-My understanding is, Kernel sends SIGINT to the process by updating the SIGINT's corresponging pending bit in the pend bit vector, is my understanding correct?

Q2-The processor detects the existance of SIGINT, but since we overwrite the handler to make it return in stead of terminating the process, so our handler get executed, and then Kernel clears SIGINT's corresponging pending bit, is my understanding correct?

Q3- Since SIGINT's corresponging pending bit is cleared, then how can sleep() function gets return? I think it should be in sleep still because in theory, sleep() function has no way of knowing the existance of SIGINT(has been cleared)

3条回答
混吃等死
2楼-- · 2019-01-27 06:30

Your understanding is correct.

Think about it. The process is blocked in the kernel. We need to return to user space to run the handler. How can we do that without interrupting whatever blocking kernel call was running? We only have one process/thread context to work with here. The process can't be both sleeping and running a signal handler.

The sequence is:

  1. Process blocks in some blocking kernel call.
  2. Signal is sent to it.
  3. Bit is set, process is made ready-to-run.
  4. Process resumes running in kernel mode, checks for pending non-blocked signals.
  5. Signal dispatcher is invoked.
  6. Process context is modified to execute signal handler upon resumption.
  7. Process is resumed in user space
  8. Signal handler runs.
  9. Signal handler returns.
  10. Kernel is invoked by end of signal handler.
  11. Kernel makes decision whether to resume system call or return interruption error.
查看更多
对你真心纯属浪费
3楼-- · 2019-01-27 06:32
  • Q1: the kernel checks if the process has blocked the received signal, if so, it updates the pending signal bit (unreliable, on systems with relable signals, this should be a counter) in the process entry, for the signal handler to be called when signals are unblocked again (see below). If not blocked, the system call prepares the return value and errno value and returns to user mode with a special code installed in the program's virtual stack that makes it to call the signal handler (already in user mode) before returning from the generic syscall code. The return from the system call gives -1 to the caller code, and the errno variable is set to EINTR. This requires the process to have installed a signal handler, because by default the action is to abort the process, so it will not return from the system call it is waiting on. Think that when one says the kernel the actual code executed is in the system call being awaken and notified of the special condition (a signal received) The interrupted call, detects that a signal handler is to be called, and prepares the user stack to jump to the proper place (the interrupt handler in user code) before returning from the syscall() wrapper.

  • Q2: pending bit is only used to save that a pending signal handler is to be called, so this is not the case. In the execution part of the process, the unix program loader installs some basic code to jump to the signal handler before returning from the system call. This is because the signal handler has to execute in user mode (not in kernel mode) so everything happens upon termination of system call. The signal handler executed is the SIGINT, but the code interrupted is a system call, and nothing happens until the system call returns (with the return code and the errno variable already fixed)

  • Q3: well, your reasoning was based on a wrong premise, that is, the interrupt pending flag is indicating that an interrupt has been received. This bit only signals that an unprocessed interrupt has been marked for delivery as soon as you unblock it, and this only happens in another system call (to unblock a signal). As soon as the signal is unblocked, the return code of the sigsetmask(2) syscall will execute the signal handler. In this case, the signal will be delivered to the process as soon as the timer elapses, the system call will be interrupted and, if you have not installed a signal handler for the SIGALRM signal (but sleep(2) implementation does this ---at least, old implementations did) the program will be aborted.

NOTE

When I say that the program is aborted by the kernel but in both cases, the signals involved (SIGINT and SIGALRM) don't make it to dump a core file. The program is aborted without generating core. This is different to the behaviour of the abort() routine, which sends a SIGABRT and so, it makes de kernel to dump a core file of the process.

查看更多
兄弟一词,经得起流年.
4楼-- · 2019-01-27 06:54

Q3- Since SIGINT's corresponging pending bit is cleared, then how can sleep() function gets return?

Imagine the sleep() function in the kernel as a function that:

  • allocates and sets fields in some kind of "timer event" structure
  • adds the "timer event" to a list of timer events for the timer's IRQ handler to worry about later (when the expiry time has elapsed)
  • moves the task from the "RUNNING" state to the "SLEEPING" state (so the scheduler knows not to give the task CPU time), causing scheduler to do a task switch to some other task
  • configures return parameters for user-space (the amount of time remaining or 0 if the time expired)
  • figures out why the scheduler gave it CPU time again (did the time expire or was the sleep interrupted by a signal?)
  • potentially mangles the stack a bit (so that the kernel returns to the signal handler if the sleep() was interrupted by a signal instead of returning to the code that called sleep())
  • returns to user-space

Also imagine that there's a second function (that I'm going to call wake() for no particular reason) that:

  • removes the "timer event" from the list of timer events (for the timer's IRQ handler to worry)
  • moves the task from the "SLEEPING" state to the "READY TO RUN" state (so the scheduler knows that the task can be given CPU time again)

Naturally, if the timer's IRQ handler notices that the "timer event" has expired then the timer's IRQ handler would call the wake() function to wake the task up again.

Now imagine there's a third function (that I'm going to call send_signal()) which might be called by other functions (e.g. called by kill()). This function might set a "pending signal" flag for the task that's supposed to receive the signal, then check what state the receiving task is in; and if the receiving task is in the "SLEEPING" state it calls the wake() function to wake it up (and then lets the latter part of the sleep() function worry about delivering the signal back to user-space whenever the scheduler feels like giving the task CPU time later).

查看更多
登录 后发表回答