Linux Kernel Hacking
Textbooks
Links
Signals
The Role of Signals
Actions Performed upon Reeiving a Signal
Data Structures Associated with Signals
Operations on Signal Data Structures
Sending a Signal
Receiving a Signal
Ignoring the Signal
Executing the Default Action for the Signal
Catching the Signal
System Calls Related to Signal Handling
- Originated 30 or so years ago in the original Unix systems to simplify IPC.
- Most signals are visible in userland.
- They're small messages that are sent to 1 or more processes in a group or on their own. Only the signal number is given to the process.
- All "signals" are represented by macros that expand to a number in the kernel.
- Examples
- SIGCHLD - macro expands to 17 on Intel Linux
- Sent to a parent process when a child finishes by normal execution or termination.
- SIGSEGV - macro expands to 11 on intel Linux
- Sent to a process when it makes an invalid memory reference.
Signals serve two main purposes
- Make processes aware that specific events have occurred.
- Force processes to execute signal handlers.
Most signals are architecture specific
New "Real-Time Signals" are a POSIX standard and are defined in include/linux/signal.h
Signals may be sent to processes at anytime, and when they're in any state
Signals can be sent to non-running processes and must be saved up in the kernel until execution of the processes resumes.
The kernel distinguishes between two signal transmissions
- Signal sending
- The kernel updates the process descriptor to represent the fact that a signal was sent.
- Signal Receiving
- The kernel forces the receiving process to react to the signal by changing its execution state, running a signal processor, or both.
Signals are considered to be consumable resources
Pending signals are signals that have been sent but not received
Only one of each signal type may be associated with a process at one time; any additional sends of signals already in the processes' queue will be thrown away by the kernel until those signals have been dealt with.
Signals can be pending for any amount of time (in general) with some conditions
- Signals are usually received only by the currently running process
-
- Some signals may be blocked by a process so that they will not be received until that process removes the block
-
- When signal handlers run, they usually mask the signal that they are currently processing, so it automatically blocks that signal until the handler has finished processing
The idea of signals is straight forward, but the implementation is complex, the kernel must
- keep track of which signals are blocked on which process
-
- when switching from kernel mode to user mode, it must check to see if any new signals have been sent, this happens around every 10ms or roughly on each timer interrupt
-
- determine if the signal can be ignore, which depends on
- The destination process is not being traced by another process.
-
- The signal is not blocked by the receiving process
-
- The signal is being ignored by the receiving process, this can be done by changing the default signal handler to ignore, or the default signal handler was already set to ignore
- handle the signal, which might include switching the context of the process to its handler and then resuming the context when the handler finishes
- Processes can handle signals in three different ways
- explicitly ignore the signal
- execute the default handler, which is predefined by the kernel, which may be one of the following actions
- Abort - the process is destroyed
- Dump - the process is destroyed and a core file is created
- Ignore - the signal is ignored
- Stop - the process execution is stopped and put into the TASK_STOPPED state
- Continue - if the process is currently stopped, it is put into the TASK_RUNNING state
- catch the signal by calling the installed signal handler
- blocking a signal is different from ignoring it
- when a signal is blocked, the signal is not received by the process
- when a signal is ignored, the signal is received but no action is taken
- SIGKILL and SIGSTOP cannot be caught or ignored. Their default actions are always executed. This allows users with the correct permissions to stop and kill different processes with two exceptions
- process 0 (the swapper process) ignores all signals and cannot be stopped or killed
- process 1 (the init process) only acknowledges certain signals and therefore only dies when the init executable dies
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
where _NSIG_WORDS is defined in include/asm-i386/signal.h
#define _NSIG 64
#define _NSIG_BPW 32
#define _NSIG_WORDS (_NSIG / NSIG_BPW)
so sig is currently an array of size 2, and the maximum signal number is 63
signal number 0 does not exist, so only 63 signals are available
the first 32 (31 actually) available signals are held in sig[0] and are the standard system signals
the second 32 available signals are held in sig[1] and are for real time signals
the task_struct structure which is used to represent a process includes a number of signal related members. The book states:
- signal - A sigset_t variable that denotes the signals sent to the process
- blocked - A sigset_t variable that denotes the blocked signals
- sigpending - A flag set if one or more nonblocked signals are pending
- gsig - A pointer to a signal_struct data structure that describes how each signal must be handled
If you look in the 2.4.9 version of include/linux/sched.h, we see that task_struct now contains:
int sigpending; /* Is there a signal pending? */
int pdeath_signal; /* The signal sent when the parent dies */
/* signal handlers */
spinlock_t sigmask_lock; /* Protects signal and blocked */
struct signal_struct *sig;
sigset_t blocked;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
TODO - sas_ss_sp, sas_ss_size, notifer and notifier_data, do they really apply to signals?????
the signal_struct structure is defined in include/linux/sched.h
struct signal_struct {
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
}
on Intel architectures an atomic_t is defined in include/asm-i386/atomic.h
typedef struct { volatile int counter; } atomict_t;
on Intel architectures the k_sigaction structure is defined in include/asm-i386/signal.h
struct k_sigaction {
struct sigaction sa;
}
the spinlock_t structure is defined in include/linux/spinlock.h
/*
* Your basic spinlocks, allowing only a single CPU anywhere
*
* Most gcc versions have a nasty bug with empty initializers.
*/
#if (__GNUC__ > 2)
typedef struct { } spinlock_t;
#define SPIN_LOCK_UNLOCKED (spinlock_t) { }
#else
typedef struct { int gcc_is_buggy; } spinlock_t;
#define SPIN_LOCK_UNLOCKED (spinlock_t) { 0 }
#endif
it appears as though the gsig member from the 2.2.x kernels has been replaced by the sig member
- the count field contains the number of processes sharing this signal_struct structure
- the siglock field is used for synchronization on this structure between those processes
- the action field is an array of structures that represents how each signal should be handled
- some architectures assign properties to signals that are only visible to the kernel, so these properties are contained in the k_sigaction structure, but on the Intel architectures these properties are visible to both the kernel and userland apps
the sigaction structure is defined in include/asm-i386/signal.h
struct sigaction {
__sighandler_t _sa_handler;
unsigned long sa_flags;
void (*sa_restorer)(void);
sigset_t sa_mask; /* mask last for extensibility */
}
_sa_handler is a pointer to the actual signal handler as installed by a user, SIG_DFL which is the default signal handler and has a value of 0, or SIG_IGN which has a value of 1 and means ignore the signal
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
sa_flags is a set of flags that specify how the signal should be handled, a table of some of the values are on page 255
sa_mask is a flag which specifies which signals the kernel should mask out
TODO - sa_restorer ???
the sigpending structure can be found in include/linux/signal.h and looks like
struct sigpending {
struct sigqueue *head, **tail;
sigset_t signal;
};
the sigqueue structure can be found in include/linux/signal.h and looks like
struct sigqueue {
struct sigqueue *next;
siginfo_t info;
};
see include/asm-i386/siginfo.h for the siginfo_t structure and more information
- static __inline__ void sigaddset(sigset_t *set, int _sig); - sets a bit in set->sig to 1 and can be found in include/asm-i386/signal.h
- static __inline__ void sigdelset(sigset_t *set, int _sig) - sets a bit in set->sig to 0 and can be found in include/asm-i386/signal.h
- #define sigismember(set,sig) \
(__builtin_constant_p(sig) ? \
__const_sigismember((set),(sig)) : \
__gen_sigismember((set),(sig)))
can be found in include/asm-i386/signal.h - returns the value of the particular
bit
- static __inline__ int __const_sigismember(sigset_t *set, int _sig) - can be found in include/asm-i386/signal.h and does ???????? TODO
- static __inline__ int __gen_sigismember(sigset_t *set, int _sig) - can be found in include/asm-i386/signal.h and does ????????? TODO
- static __inline__ int sigfindinword(unsigned long word) can be found in include/asm-i386/signal.h and does ??????? TODO
- #define sigmask(sig) (1UL << ((sig) - 1)) can be found in include/asm-i386/signal.h and just determines which bit a given signal is
- static inline int signal_pending(struct task_struct *p) can be found in include/linux/sched.h and just checks to see if a process has nonblocking signals
waiting to be handled
-
/* Reevaluate whether the task has signals pending delivery.
This is required every time the blocked sigset_t changes.
All callers should have t->sigmask_lock. */
static inline void recalc_sigpending(struct task_struct *t)
can be found in include/linux/sched.h and calls
/*
* Re-calculate pending state from the set of locally pending
* signals, globally pending signals, and blocked signals.
*/
static inline int has_pending_signals(sigset_t *signal, sigset_t *blocked)
passing it &t->pending.signal and &t->blocked
- sigandsets, sigorsets and signansets perform the logical operation on 2 sigset_t structures and store the results in the third passed in sigset_t structure
-
/*
* Dequeue a signal and return the element to the caller, which is
* expected to free it.
*
* All callers must be holding current->sigmask_lock.
*/
int dequeue_signal(sigset_t *mask, siginfo_t *info)
can be found in kernel/signal.c. It checks to see if the current process has any nonblocking pending signals. If it does, it returns the lowest numbered or highest priority signal, and updates any data structs to show that the signal is no longer pending.
-
/*
* Flush all pending signals for a task.
*/
void flush_signals(struct task_struct *t)
can be found in kernel/signal.c and just clears out any pending signals by removing them from the queues without processing them.
------------------------------------------------------------------------------------------------
- static inline void sigaddsetmask(sigset_t *set, unsigned long mask) - can be found in include/linux/signal.h and sets all bits in mask to 1
- static inline void sigdelsetmask(sigset_t *set, unsigned long mask) - can be found in include/linux/signal.h and sets all bits in mask to 0
- static inline int sigtestsetmask(sigset_t *set, unsigned long mask) - can be found in include/linux/signal.h and just tests to see if the given bits are set to 1
- static inline void siginitset(sigset_t *set, unsigned long mask) - can be found in include/linux/signal.h and sets the lower 32 bits to mask, and the upper 32 bits to 0
- static inline void siginitsetinv(sigset_t *set, unsigned long mask) - can be found in include/linux/signal.h and sets the lower 32 bits to the compliment of mask, and the upper 32 bits to -1
When a signal is sent to a process, the kernel calls one of four functions to perform the first phase. These four functions are
int send_sig_info(int sig, struct siginfo *info, struct task_struct *t)
/*
* Force a signal that the process can't ignore: if necessary
* we unblock the signal and change any SIG_IGN to SIG_DFL.
*/
int force_sig_info(int sig, struct siginfo *info, struct task_struct *t)
void force_sig(int sig, struct task_struct *p)
int send_sig(int sig, struct task_struct *p, int priv)
the last two are for backwards compatibility and mearly call the first two.
the book has a pretty good explaination of send_sig_info on page 258
- first check to see if the signal is in the right range 0 < x < 64 - then check to see if the signal was sent by a user mode process
- if it was, check to see if sending a signal is allowed between the two processes
- if the signal is 0, bail out. 0 is not a valid signal, but it is a valid way of checking to see if one process can send a signal to another process
- if the receiving process is in the zombie state, bail out
- call handle_stop_signal kernel/signal.c
- if the signal is SIGKILL or SIGCONT
- if the receiving process is in the TASK_STOPPED state
- wake up the process, so that it can execute the do_exit function
- - remove SIGSTOP, SIGTSTP, SIGTTOU, SIGTTIN signals from the process
- if the signal is SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU
- remove SIGCONT from the process
- call ignored_signal
- if the process is being traced or the requested signal is on the process' blocked list return 0
- else call signal_type
- depending on the return type of signal_type, we wake up, kill, or ignore the signal
- if ignored_signal returns 1
- if the process is in the TASK_INTERRUPTIBLE state and if it has other pending signals that are nonblocking, the process is woken up
- if ignored_signal returns 0
- if the signal is not a real time signal and the process already has that signal pending
- if the process is in the TASK_INTERRUPTIBLE state and if it has other pending signals that are nonblocking, the process is woken up
- else call deliver_signal
- call send_signal
- adjust the process data structure as necessary
- wake up the process as above
the book's explanation is limited on force_sig_info but thats because it ends up calling send_sig_info eventually
- if the handler is set to ignore, change it to the default handler (this is what keeps signals like SIGKILL from being ignored by processes
- adjust some data structures
- call send_sig_info
- the kernel always checks to see if a process has pending non-blocking signals in ret_from_intr
- when a process does have pending non-blocking signals, the kernel will call do_signal
- do_signal repeatedly calls dequeue_signal until there are no more pending non-blocking signals left
- do_signal receives two params - regs and oldset
- regs - a pt_regs pointer that points to where the current process' registers have been saved
- oldset - sigset_t pointer that points to where the old bitmask of blocked signals should be saved
- if current is being monitored by some other process (i.e. gdb) it will call notify_parent and schedule so that current can actually process that signal
- the process' k_sigaction structure is then examined to see what the handler pointer is. One of three actions then takes place
- Ignore the signal.
- Execute the default signal handler.
- Execute the user installed signal handler.
- do_signal will normally skip over a signal if it's handler is set to SIG_IGN, but there is an exception to this
- if the signal to be processed is SIGCHLD, it calls sys_wait4, which causes the process to read any information from its child processes to clean up any memory etc if they have finished execution. This is where resources from zombie processes get cleaned up.
- do_signal will call the default handler for a signal
- If the receiving process is init then the signal is ignored
- otherwise
- if the default action is ignore, then it skips this signal and gets the next pending one
- if the signal is SIGTSTP, SIGTTIN, or SIGTTOU, then it looks to see if the process is in an orphaned process group. If it is, we skip the signal. If it isn't, then we fall through to the SIGSTOP case below.
- if the signal is SIGSTOP
- set the process' state to TASK_STOPPED
- set the exit code for the stopped process
- check to see if the process set the SA_NOCLDSTOP flag, if it did not, then we notify the parent
- call schedule
- if the default action is to "dump", then a core dump is created, and the process is killed.
- for the remaining signals, the process is killed
- do_signal will call handle_signal if the user insalled a signal handler. When this is the case, only one pending signal is handled per call to do_signal. Right after the invocation of handle_signal, do_signal returns with a value of 1. This allows real time signals to be handled in the proper order.
- handle_signal does the following
- Setup the user mode stack to handle the calling of the handler and the return to kernel mode properly. This is done via setup_frame, or setup_rt_frame.
- Evaluate the signal flags
- handle_signal then returns to do_signal, which also returns immediately. The next time current is executed, it will be executing the signal handler because of the work done be setup_frame. During execution of the signal handler, all other signals will be blocked.
- sys_sigreturn or sys_rt_sigreturn are called upon finishing execution of the signal handler. This function sets up the user mode stack to return execution of current to where it was when it first received the signal.
GCC INLINE ASSEMBLY OPERAND CONSTRAINTS
`r'
A register operand is allowed provided that it is in a general register.
`I', `J', `K', ... `P'
Other letters in the range `I' through `P' may be defined in a machine-dependent fashion to permit
immediate integer operands with explicit integer values in specified ranges. For example, on the 68000, `I'
is defined to stand for the range of values 1 to 8. This is the range permitted as a shift count in the shift
instructions.
`m'
A memory operand is allowed, with any kind of address that the machine supports in general.
`='
Means that this operand is write-only for this instruction: the previous value is discarded and replaced by
output data.