linux signal 处理

信号分红两种: 
regular signal( 非实时信号 ), 对应的编码值为 [1,31]
real time signal 对应的编码值为 [32,64]linux

 

编码为 0 的信号 不是有效信号,只用于检查是当前进程否有发送信号的 权限 ,并不真正发送。程序员

 

线程会有本身的悬挂信号队列 , 而且线程组也有一个信号悬挂队列 .数组

信号悬挂队列保存 task 实例接收到的信号 , 只有当该信号被处理后它才会从悬挂队列中卸下 .缓存

 

信号悬挂队列还有一个对应的阻塞信号集合 , 当一个信号在阻塞信号集合中时 ,task 不会处理该被阻塞的信号 ( 可是该信号依旧在悬挂队列中 ). 当阻塞取消时 , 它会被处理 .session

 

对一个信号 , 要三种处理方式 :数据结构

忽略该信号 ;多线程

采用默认方式处理 ( 调用系统指定的信号处理函数 );app

使用用户指定的方式处理 ( 调用用户指定的信号处理函数 ).socket

对于某些信号只能采用默认的方式处理 (eg:SIGKILL,SIGSTOP).xss

 

信号处理能够分红两个阶段 : 信号产生并通知到接收方 (generation), 接收方进行处理 (deliver)

.........

 

 

简介

Unix 为了容许用户态进程之间的通讯而引入signal. 此外, 内核使用signal 给进程通知系统事件. 近30 年来, signal 只有很小的变化 .

如下咱们先介绍linux kernel 如何处理signal, 而后讨论容许进程间 exchange 信号的系统调用 .

 

The Role of Signals

signal 是一种能够发送给一个进程或一组进程的短消息( 或者说是信号 , 可是这么容易和信号量混淆). 这种消息一般只是一个整数 , 而不包含额外的参数 .

linux 提供了不少种signal, 这些signal 经过宏来标识( 这个宏做为这个信号的名字). 而且这些宏的名字的开头是SIG.eg: 
宏SIGCHLD , 它对应的整数值为17, 用来表示子进程结束时给父进程发送的消息 ( 即当子进程结束时应该向父进程发送标识符为17 的signal/ 消息/ 信号)
. 宏SIGSEGV, 它对应的整数值为11, 当进程引用一个无效的物理地址时( 内核) 会向进程发送标识符为11 的signal/ 消息/ 信号 ( 参考linux 内存管理的页错误异常处理程序, 以及linux 中断处理).

信号有两个目的:
1. 使一个进程意识到一个特殊事件发生了( 不一样的事件用不一样的signal 标识) 
2. 并使目标进程进行相应处理(eg: 执行的信号处理函数 , signal handler). 相应的处理也能够是忽略它 .

固然 , 这两个目的不是互斥的 , 由于一般一个进程意识到一个事件发生后就会执行该事件相应的处理函数 .

下表是linux2.6 在80x86 上的前31 个signals 及其相关说明 . 这些信号中有些是体系结构相关的(eg:SIGCHLD,SIGSTOP), 有些则专门了某些体系结构才存在的(eg:SIGSTKFLT) ( 能够参考中断处理 , 里面也列出了一些异常对应的signal).

The first 31 signals in Linux/i386

#

Signal name

Default action

Comment

POSIX

1

SIGHUP

Terminate

Hang up controlling terminal or process

Yes

2

SIGINT

Terminate

Interrupt from keyboard

Yes

3

SIGQUIT

Dump

Quit from keyboard

Yes

4

SIGILL

Dump

Illegal instruction

Yes

5

SIGTRAP

Dump

Breakpoint for debugging

No

6

SIGABRT

Dump

Abnormal termination

Yes

6

SIGIOT

Dump

Equivalent to SIGABRT

No

7

SIGBUS

Dump

Bus error

No

8

SIGFPE

Dump

Floating-point exception

Yes

9

SIGKILL

Terminate

Forced-process termination

Yes

10

SIGUSR1

Terminate

Available to processes

Yes

11

SIGSEGV

Dump

Invalid memory reference

Yes

12

SIGUSR2

Terminate

Available to processes

Yes

13

SIGPIPE

Terminate

Write to pipe with no readers

Yes

14

SIGALRM

Terminate

Real-timerclock

Yes

15

SIGTERM

Terminate

Process termination

Yes

16

SIGSTKFLT

Terminate

Coprocessor stack error

No

17

SIGCHLD

Ignore

Child process stopped or terminated, or got signal if traced

Yes

18

SIGCONT

Continue

Resume execution, if stopped

Yes

19

SIGSTOP

Stop

Stop process execution

Yes

20

SIGTSTP

Stop

Stop process issued from tty

Yes

21

SIGTTIN

Stop

Background process requires input

Yes

22

SIGTTOU

Stop

Background process requires output

Yes

23

SIGURG

Ignore

Urgent condition on socket

No

24

SIGXCPU

Dump

CPU time limit exceeded

No

25

SIGXFSZ

Dump

File size limit exceeded

No

26

SIGVTALRM

Terminate

Virtual timer clock

No

27

SIGPROF

Terminate

Profile timer clock

No

28

SIGWINCH

Ignore

Window resizing

No

29

SIGIO

Terminate

I/O now possible

No

29

SIGPOLL

Terminate

Equivalent to SIGIO

No

30

SIGPWR

Terminate

Power supply failure

No

31

SIGSYS

Dump

Bad system call

No

31

SIGUNUSED

Dump

Equivalent to SIGSYS

No

 

上述signal 称为regular signal . 除此以外, POSIX 还引入了另一类singal 即real-time signal
. real time signal 的标识符的值从32 到64. 它们与reagular signal 的区别在于每一次发送的real time signal 都会被加入悬挂信号队列,因此屡次发送的real time signal 会被缓存起来( 而不会致使后面的被忽略掉) . 而同一种( 即标识符同样) regular signal 不会被缓存, 即若是同一个signal 被发送屡次 , 它们只有一个会被放入接受进程的悬挂队列 .

 

虽然linux kernel 并无使用real time signal. 可是它也( 经过特殊的系统调用) 支持posix 定义的real time signal.

 

有不少系统调用能够给进程发送singal, 也有不少系统调能够指定进程在接收某一个signal 时应该如何响应( 即实行哪个函数). 下表给出了这类系统调用: ( 关于这些系统调用的更多信息参考下文)

System call

Description

kill( )

Send a signal to a thread group

tkill( )

Send a signal to a process

tgkill( )

Send a signal to a process in a specific thread group

sigaction( )

Change the action associated with a signal

signal( )

Similar to sigaction( )

sigpending( )

Check whether there are pending signals

sigprocmask( )

Modify the set of blocked signals

sigsuspend( )

Wait for a signal

rt_sigaction( )

Change the action associated with a real-time signal

rt_sigpending( )

Check whether there are pending real-time signals

rt_sigprocmask( )

Modify the set of blocked real-time signals

rt_sigqueueinfo( )

Send a real-time signal to a thread group

rt_sigsuspend( )

Wait for a real-time signal

rt_sigtimedwait( )

Similar to rt_sigsuspend( )

 

signal 可能在任意时候被发送给一个状态未知的进程 . 当信号被发送给一个当前并不正在执行的进程时, 内核必须把先把该信号保存直到该进程恢复执行. (to do ???????)

被阻塞的信号尽管会被加入进程的悬挂信号队列 , 可是在其被解除阻塞以前不会被处理(deliver),Blocking a signal (described later) requires that delivery of the signal be held off until it is later unblocked, which
acer s the problem of signals being raised before they can be delivered.

 

内核把信号传送分红两个阶段: 
signal generation: 内核更新信号的目的进程的相关数据结构 , 这样该进程就能知道它接收到了一个信号. 以为称为收到信号阶段更恰当. 这个generation 翻译成目的进程接收也不错 .

 

signal delivery(): 内核强制目的进程处理接收到的信号,这主要是经过修改进程的执行状态或者在目的进程中执行信号处理函数来实现的 . 以为称为处理收到的信号阶段更恰当 . diliver 这里翻译成处理更恰当 .

deliver 的翻译: 有不少个 , 估计翻译成in computing 比较合理

 

一个genearated signal 最多只能deliver 一次( 即一个信号最多只会被处理一次) . signal 是可消耗资源 , 一旦一个signal 被deliver, 那么全部进程对它的引用都会被取消 .

已经产生可是还未被处理(deliver) 的信号称为pending signal ( 悬挂信号). 对于regular signal, 在某一个时刻 , 一种signal 在一个进程中只能有一个实例( 由于进程没有用队列缓存其收到的signal) . 由于有31 种regualar signal , 因此一个进程某一个时刻能够有31 个各种signal 的实例. 此外由于linux 进程对real time signal 采用不一样的处理方式, 它会保存接收到的real
time signal 的实例 , 因此能够同时有不少同种signal 的实例 .

问题: 不一样种类的信号的优先级( 从值较小的开始处理) .

通常而言 , 一个信号可能会被悬挂很长是时间( 即一个进程收到一个信号后 , 该信号有可能在该进程里好久 , 由于进程没空来处理它), 主要有以下因素:

1. 信号一般被当前进程处理 . Signals are usually delivered only to the currently running process (that is, to the current process).

2. 某种类型的信号可能被本进程阻塞. 只有当其被取消阻塞好才会被处理 .

3. 当一个进程执行某一种信号的处理函数时 , 通常会自动阻塞这种信号 , 等处理完毕后才会取消阻塞 . 这意味着一个信号处理函数不会被同种信号阻塞 .

 

尽管信号在概念上很直观 , 可是内核的实现却至关复杂. 内核必须:

1. 记录一个进程阻塞了哪些信号

2. 当从核心态切换到用户态时 , 检查进程是否接受到了signal.( 几乎每一次时钟中断都要干这样的事 , 费时吗?).

3. 检查信号是否能够被忽略. 当以下条件均知足时则可被忽略:

   1). 目标进程未被其它进程traced( 即PT_PTRACED==0). 但一个被traced 的进程收到一个信号时 , 内核中止目标线程 , 而且给tracing 进程发送信号SIGCHLD. tracing 进程可能会经过SIGCONT 来恢复traced 进程的执行

   2). 目标进程未阻塞该信号 .

   3). 信号正被目标进程忽略( 或者因为忽略是显式指定的或者因为忽略是默认操做).

4. 处理信号 . 这可能须要切换到信号处理函数

 

此外, linux 还须要处理BSD, System V 中signal 语义的差别性 . 另外 , 还须要遵照POSIX 的定义 .

 

 

 

处理信号的方式 (Actions Performed upon Delivering a Signal)

一个进程能够采用三中方式来响应它接收到的信号:

1.(ignore) 显示忽略该信号

2.(default) 调用默认的函数来响应该信号( 这些默认的函数由内核定义) , 通常这些默认的函数都分红以下几种( 采用哪种取决于信号的类型 , 参考前面的表格):

Terminate: The process is terminated (killed) 
Dump: The process is terminated (killed) and a core file containing its execution context is created, if possible; this file may be used for debug purposes.

Ignore:The signal is ignored. 
Stop:The process is stopped, i.e., put in the TASK_STOPPED state. 
Continue:If the process was stopped (TASK_STOPPED), it is put into the TASK_RUNNING state.

3.(catch) 调用相应的信号处理函数 ( 这个信号处理函数一般是程序员在运行时指定的). 这意味着进程须要在执行时显式地指明它须要catch 哪种信号. 而且指明其处理函数 . catch 是一种主动处理的措施 .

注意上述的三个处理方式被标识为:ignore, default, catch. 这三个处理方式之后会经过这三个标识符引用 .

 

注意阻塞一个信号和忽略一个信号是不一样 , 一个信号被阻塞是就当前不会被处理 , 即一个信号只有在解除阻塞后才会被处理 . 忽略一个信号是指采用忽略的方式来处理该信号( 即对该信号的处理方式就是什么也不作) .

SIGKILL 和SIGSTOP 这两个信号不能忽略 , 不能阻塞 , 不能使用用户定义的函数(caught) . 因此老是执行它们的默认行为 . 因此 , 它们容许具备恰当特权级的用户杀死别的进程, 而没必要在乎被杀进程的防御措施 ( 这样就容许高特权级用户杀死低特权级的用户占用大量cpu 的时间) .

注: 有两个特殊状况. 第一 , 任意进程都不能给进程0( 即swapper 进程) 发信号 . 第二 , 发给进程1 的信号都会被丢弃(discarded), 除非它们被catch. 因此进程 0 不会死亡, 进程1 仅在int 程序结束时死亡 .

 

一个信号对一个进程而言是致命的(fatal) , 当前仅当该信号致使内核杀死该进程 . 因此,SIGKILL 老是致命的. 此外 , 若是一个进程对一个信号的默认行为是terminate 而且该进程没有catch 该信号 , 那么该信号对这个进程而言也是致命的 . 注意 , 在catch 状况下 , 若是一个进程的信号处理函数本身杀死了该进程 , 那么该信号对这个进程而言不是致命的 , 由于不是内核杀死该进程而是进程的信号处理函数本身杀死了该进程.

 

POSIX 信号以及多线程程序

 

POSIX 1003.1 标准对多线程程序的信号处理有更加严格的要求: 
( 因为linux 采用轻量级进程来实现线程 , 因此对linux 的实现也会有影响)

1. 多线程程序的全部线程应该共享信号处理函数 , 可是每个线程必须有本身的mask of pending and blocked signals

2. POSIX 接口kill( ), sigqueue( ) 必须把信号发给线程组 , 而不是指定线程. 另外内核产生的SIGCHLD, SIGINT, or SIGQUIT 也必须发给线程组 .

3. 线程组中只有有一个线程来处理(deliver) 的共享的信号就能够了 . 下问介绍如何选择这个线程 .

4. 若是线程组收到一个致命的信号 , 内核要杀死线程组的全部线程, 而不是仅仅处理该信号的线程 .

 

为了听从POSIX 标准, linux2.6 使用轻量级进程实现线程组.

 

下文中 , 线程组表示OS 概念中的进程, 而线程表示linux 的轻量级进程. 进程也( 更多地时候) 表示linux 的轻量级进程 . 另外每个线程有一个私有的悬挂信号列表 , 线程组共享一个悬挂信号列表 .

 

与信号有关的数据结构

注:pending/ 悬挂信号, 表示进程收到信号 , 可是尚未来得及处理 , 或者正在处理可是尚未处理完成 .

对于每个进程, 内核必须知道它当前悬挂(pending) 着哪些信号或者屏蔽(mask) 着哪些信号 . 还要知道线程组如何处理信号. 为此内核使用了几个重要的数据结构( 它们可经过task 实例访问), 以下图:

The most significant data structures related to signal handling

 

( 注意task 中的一些关于signal 的成员在上图中没有表现出来)

task 中关于signal 的成员列在下表中:

 

Process descriptor fields related to signal handling

Type

Name

Description

struct signal_struct *

signal

Pointer to the process's signal descriptor( 线程组共用 的信号)

struct sighand_struct *

sighand

Pointer to the process's signal handler descriptor( 线程组共用 )

sigset_t

blocked

Mask of blocked signals( 线程私有)

sigset_t

real_blocked

Temporary mask of blocked signals (used by the rt_sigtimedwait( ) 
system call) ( 线程私有)

struct sigpending

pending

Data structure storing the private pending signal s

unsigned long

sas_ss_sp

Address of alternative signal handler stack.( 能够不提供)

size_t

sas_ss_size

Size of alternative signal handler stack( 能够不提供)

int (*) (void *)

Notifier

Pointer to a function used by a device driver to block some signals of the process

void *

notifier_data

Pointer to data that might be used by the notifier function (previous field of table)

sigset_t *

notifier_mask

Bit mask of signals blocked by a device driver through a notifier function

 

 

blocked 成员 保存进程masked out 的signal . 其类型为sigset_t 
, 定义以下:

    typedef struct {

        unsigned long sig[2];

    } sigset_t;

sizeof(long)==32, sigset_t 被当成了bit array 使用. 正如前文提到的,linux 有64 种信号 , [1,31] 为regular signal, [32,64] 为real time signal. 每一种对应sigset_t 中一个bit.

 

信号描述符& 信号处理函数描述符

task 的signal, sighand 成员分别是信号描述符与信号处理函数描述符 .

signal 成员 是一个指针 , 它指向结构体signal_struct 的实例 , 该实例保存了线程组悬挂着的信号 . 也就是说线程组中的全部进程( 这里称为task 更合理) 共用同一个signal_struct 实例. signal_struct 中的shared_pending 成员保存了全部悬挂的信号( 以双向链表组织) . 此外signal_struct 中还保存了许多其它的信息(eg: 进程资源限制信息, pgrp, session 信息) .

下表列出了signal_struct 中与信号处理有关的成员:

 

The fields of the signal descriptor related to signal handling

Type

Name

Description

atomic_t

count

Usage counter of the signal descriptor

atomic_t

live

Number of live processes in the thread group

wait_queue_head_t

wait_chldexit

Wait queue for the processes sleeping in a wait4( ) 
system call

struct task_struct *

curr_target

Descriptor of the last process in the thread group that received a signal

struct sigpending

shared_pending

Data structure storing the shared pending signals

int

group_exit_code

Process termination code for the thread group

struct task_struct *

group_exit_task

Used when killing a whole thread group

int

notify_count

Used when killing a whole thread group

int

group_stop_count

Used when stopping a whole thread group

unsigned int

flags

Flags used when delivering signals that modify the status of the process

 

 

除了signal 成员外 , 还有一个sighand 成员 用来指明相应的信号处理函数.

sighand 成员是一个指针 , 指向一个sighand_struct 变量 , 该变量为线程组共享 . 它描述了一个信号对应的信号处理函数.

 

sighand_struct 成员以下:

The fields of the signal handler descriptor

Type

Name

Description

atomic_t

count

Usage counter of the signal handler descriptor

struct k_sigaction [64]

action

Array of structures specifying the actions to be performed upon delivering the signals

spinlock_t

siglock

Spin lock protecting both the signal descriptor and the signal handler descriptor

sighand_struct 中的重要成员是action, 它是一个数组 , 描述了每一种信号对应的信号处理函数 .

sigaction 数据结构

某一些平台上, 会赋予一个signal 一些只能内核才可见的属性. 这些属性与sigaction( 它在用户态也可见) 构成告终构体k_sigaction. 在x86 上,k_sigaction 就是sigaction. 

注: 用户使用的sigaction 和内核使用的sigaction 结构体有些不一样可是 , 它们存储了相同的信息( 本身参考一下用户态使用的sigaction 结构体吧).

内核的sigaction 的结构体的成员以下:

1)sa_handler: 类型为 void (*)(int):

    这个字段指示如何处理信号 . 它能够是指向处理函数的指针 , 也能够是SIG_DFL(==0) 表示使用默认的处理函数 , 还能够是SIG_IGN(==1) 表示忽略该信号

2)sa_flags: 类型为unsigned long:

   指定信号如何被处理的标志 , 参考下表 ( 指定信号如何处理的标志) .

3)sa_mask: 类型为sigset_t:

   指定当该信号处理函数执行时,sa_mask 中指定的信号必须屏蔽 .

 

指定信号如何处理的标志

注: 因为历史的缘由 , 这些标志的前缀为SA_, 这和irqaction 的flag 相似 , 但其实它们没有关系 .

Flags specifying how to handle a signal

Flag Name

Description

SA_NOCLDSTOP

Applies only to SIGCHLD ; do not send SIGCHLD to the parent when the process is stopped

SA_NOCLDWAIT

Applies only to SIGCHLD ; do not create a zombie when the process terminates

SA_SIGINFO

Provide additional information to the signal handler

SA_ONSTACK

Use an alternative stack for the signal handler

SA_RESTART

Interrupted system calls are automatically restarted

SA_NODEFER, SA_NOMASK

Do not mask the signal while executing the signal handler

SA_RESETHAND,

SA_ONESHOT

Reset to default action after executing the signal handler

 

悬挂的信号队列 (sigpending)

 

经过前文咱们知道有些系统调用可以给线程组发信号(eg:kill, rt_sigqueueinfo), 有些操做给指定的进程发信号(eg:tkill, tgkill) .

 

为了区分这两类, task 中其实有两种悬挂信号列表: 
1.task 的 pending 字段表示了本task 上私有的悬挂信号( 列表) 
2.task 的signal 字段中的shared_pending 字段则保存了线程组共享的悬挂信号( 列表).

悬挂信号 列表用数据结构sigpending 表示 , 其定义以下: 
     struct sigpending { 
        struct list_head list; 
        sigset_t signal; 
    }

其signal 成员指明当前悬挂队列悬挂了哪些信号 .

其list 字段实际上是一个双向链表的头 , 链表的元素的类型是sigqueue. sigqueue 的成员以下:

The fields of the sigqueue data structure

Type

Name

Description

struct list_head

list

Links for the pending signal queue's list

spinlock_t *

lock

Pointer to the siglock field in the signal handler descriptor corresponding to the pending signal

Int

flags

Flags of the sigqueue data structure

siginfo_t

info

Describes the event that raised the signal

struct

user_struct *

user

Pointer to the per-user data structure of the process's owner

( 注:sigqueue 的名字有queue, 但它其实只是悬挂队列的一个元素 . 它会记录一个被悬挂的信号的信息)

 

siginfo_t 是一个包含128 byte 的数据结构 , 用来描述一个指定信号的发生,其成员以下: 
si_signo: 信号ID

si_errno: 致使这个信号被发出的错误码. 0 表示不是由于错误才发出信号的 .

si_code: 标识谁发出了这个信号 . 参考下表 :

The most significant signal sender codes

Code Name

Sender

SI_USER

kill( ) and raise( )

SI_KERNEL

Generic kernel function

SI_QUEUE

sigqueue( )

SI_TIMER

Timer expiration

SI_ASYNCIO

Asynchronous I/O completion

SI_TKILL

tkill() and tgkill()

 

_sifields: 这个字段是一个union, 它有很多成员 , 哪个成员有效取决于信号 . 好比对于SIGKILL, 则它会记录信号发送者的PID,UID; 对于SIGSEGV, 它会存储致使访问出错的内存地址 .

 

操做信号数据结构的函数

一些宏和函数会使用信号数据结构 . 在下文的解说中, set 表示指向sigset_t 变量的指针, nsig 表示信号的标识符( 信号的整数值).mask 是一个unsign long bit mask.

sigemptyset (set) and sigfillset (set)

把set 全部bit 设置为 0 或者1 .

sigaddset (set,nsig) and sigdelset (set,nsig)

把set 中对应与nsig 的bit 设置为1 或者 0. In practice, sigaddset( ) reduces to: 
    set->sig[(nsig - 1) / 32] |= 1UL << ((nsig - 1) % 32);

and sigdelset( ) to: 
    set->sig[(nsig - 1) / 32] &= ~(1UL << ((nsig - 1) % 32));

sigaddsetmask (set,mask) and sigdelsetmask (set,mask)

根据mask 的值设置set. 仅能设置1-32 个signal. The corresponding functions reduce to:    
      set->sig[0] |= mask;

and to: 
set->sig[0] &= ~mask;

 

sigismember (set,nsig)

返回set 中对应nsig 的bit 的值. In practice, this function reduces to:

    return 1 & (set->sig[(nsig-1) / 32] >> ((nsig-1) % 32));

 

sigmask (nsig)

根据信号标志码nsig 等到它的在sigset_t 中的bit 位的index.

sigandsets (d,s1,s2), sigorsets (d,s1,s2), and
signandsets (d,s1,s2)

      伪代码以下:d=s1 & s2; d=s1|s2, d=s1 & (~s2)

sigtestsetmask (set,mask)

若是mask 中的为1 的位在set 中的相应位也为1, 那么返回1. 不然返回0. 只适用于1-32 个信号.

siginitset (set,mask)

用mask 设置set 的1-32 个信号, 并把set 的33-63 个信号清空.

siginitsetinv (set,mask)

用(!mask) 设置set 的1-32 个信号, 并把set 的33-63 个信号设置为1.

signal_pending (p)

检查p 的 t->thread_info->flags 是否为 TIF_SIGPENDING. 即检查p 是否有 悬挂的非阻塞信号.

recalc_sigpending_tsk (t) and recalc_sigpending ( )

第一个函数检查 t->pending->signal 或者 t->signal->shared_pending->signal 上是否有悬挂的非阻塞信号. 如有设置 t->thread_info->flags 为 TIF_SIGPENDING.

recalc_sigpending( ) 等价于 recalc_sigpending_tsk(current) .

rm_from_queue (mask,q)

清掉悬挂信号队列q 中的由mask 指定的信号.

flush_sigqueue (q)

清掉悬挂信号队列q 中的信号.

flush_signals (t)

删除t 收到的全部信号. 它会清掉 t->thread_info->flags 中的TIF_SIGPENDING 标志, 而且调用flush_sigqueue 把t->pending 和 t->signal->shared_pending 清掉 .

 

Generating a Signal

不少内核函数会产生signal, 它完成处理处理的第一个阶段(generate a signal) , 即更新信号的目标进程的相应字段 . 可是它们并不直接完成信号处理的第二阶段(deliver the signal), 可是它们会根据目标进程的状态或者唤醒目标进程或者强制目标进程receive the signal .

注:generating a signal 这个阶段是从源进程发起一个信号 , 而后源进程在内核态下修改目标进程的相应状态, 而后可能源进程还会唤醒目的进程 .

不管一个信号从内核仍是从另一个进程被发送给另外一个线程( 目标进程) , 内核都会执行以下的函数之一来发送信号:

Kernel functions that generate a signal for a process

Name

Description

send_sig( )

Sends a signal to a single process

send_sig_info( )

Like send_sig( ) , with extended information in a siginfo_t structure

force_sig( )

Sends a signal that cannot be explicitly ignored or blocked by the process

force_sig_info( )

Like force_sig( ) , with extended information in a siginfo_t structure

force_sig_specific( )

Like force_sig( ) , but optimized for SIGSTOP and SIGKILL signals

sys_tkill( )

System call handler of tkill( )

sys_tgkill( )

System call handler of tgkill( )

全部这些函数最终都会调用 specific_send_sig_info ( ) .

不管一个信号从内核仍是从另一个进程被发送给另外一个线程组( 目标进程), 内核都会执行以下的函数之一来发送信号:

Kernel functions that generate a signal for a thread group

Name

Description

send_group_sig_info( )

Sends a signal to a single thread group identified by the process descriptor of one of its members

kill_pg( )

Sends a signal to all thread groups in a process group

kill_pg_info( )

Like kill_pg( ) , with extended information in a siginfo_t structure

kill_proc( )

Sends a signal to a single thread group identified by the PID of one of its members

kill_proc_info( )

Like kill_proc( ) , with extended information in a siginfo_t structure

sys_kill( )

System call handler of kill( )

sys_rt_sigqueueinfo( )

System call handler of rt_sigqueueinfo( )

这些函数最终都调用 group_send_sig_info ( ) .

 

specific_send_sig_info 函数说明

 

这个函数给指定的目标线程( 目标进程) 发送一个信号 . 它有三个参数:

参数sig: 信号( 即某一个信号) .

参数info: 或者是 siginfo_t 变量地址或者以下三个特殊值: 
0 : 表示信号由用户态进程发送; 
1 : 表示信号由核心态( 进程) 发送;
2 : 表示信号由核心态( 进程) 发送, 而且信号是SIGKILL 或者SIGSTOP.

参数t: 目标进程的task 实例指针

specific_send_sig_info 调用时必须禁止本cpu 的中断 , 而且得到t->sighand->siglock spin lock. 它会执行以下操做:

1. 检查目标线程是否忽略该信号, 如果返回0. 当以下三个条件均知足时则可认为忽略该信号:
   1). 目标线程未被traced( 即t->ptrace 不含PT_PTRACED 标志).
   2). 该信号未被目标线程阻塞( 即sigismember(&t->blocked, sig) == 0).
   3). 该信号被目标线程显式地忽略( 即t->sighand->action[sig-1].sa_handler == SIG_IGN) 或者隐式忽略( 即handler==SIG_DFT 而且信号为SIGCONT, SIGCHLD, SIGWINCH, or SIGURG.).

2. 检查信号是不是非实时信号(sig<32) 而且一样的信号是否已经在线程的私有悬挂信号队列中了, 如果则返回0.

3. 调用send_signal(sig, info, t, &t->pending) 把信号加入目标线程的私有悬挂信号队列中. 下文会详述.

4. 若是send_signal 成功而且信号未被目标线程阻塞, 则调用signal_wake_up ( ) 来通知目标进程有新的信号达到. 这个函数执行以下步骤:
   1). 把标志TIF_SIGPENDING 加到t->tHRead_info->flags 中
   2). 调用try_to_wake_up(). 若是目标线程处于TASK_INTERRUPTIBLE 或者TASK_STOPPED 而且信号是SIGKILL 则唤醒目标线程.
   3). 若是try_to_wake_up 返回0, 则目标线程处于runnable 状态, 以后检查目标线程是否在别的CPU 上执行, 若是是则向该CPU 发送处理器中断以强制该cpu 重调度目标线程( 注: 目前咱们并未考虑多处理器的状况). 由于每个线程在从schedule() 返回时都会检查是否存在悬挂的信号, 因此这个处理器中断将会使目标线程很快就看到这个新的悬挂信号.

5. 返回1( 表示信号已经成功generated.)

 

send_signal 函数

这个函数接受四个参数:sig, info, t, signals. 其中sig, info,t 在specific_send_sig_info
中已经介绍过了. signals 则是t 的pending queue 的首地址 . 它的执行流程如:

1. 若info==2, 那么这个信号是SIGKILL 或是SIGSTOP, 而且由kernel 经过force_sig_specific 产生. 此时直接跳到9. 由于这种状况下, 内核会当即执行信号处理, 因此不用把该信号加入信号悬挂队列中.

2. 若是目标进程的用户当前的悬挂信号数目(t->user->sigpending) 小于目标进程的最大悬挂信号数目(t->signal->rlim[RLIMIT_SIGPENDING].rlim_cur), 则为当前信号分配一个sigqueue 变量, 标识为q

3. 若是目标进程的用户当前的悬挂信号数目太大, 或者上一步中分配sigqueue 变量失败, 则跳到9.

4. 增长目标进程的用户当前的悬挂信号数目(t->user->sigpending) 以及t-user 的引用数.

5. 把信号q 加入目标线程的悬挂队列:
    list_add_tail(&q->list, &signals->list);

6. 填充q, 以下

    if ((unsigned long)info == 0) {
        q->info.si_signo = sig;
        q->info.si_errno = 0;
        q->info.si_code = SI_USER;
        q->info._sifields._kill._pid = current->pid;
        q->info._sifields._kill._uid = current->uid;
    } else if ((unsigned long)info == 1) {
        q->info.si_signo = sig;
        q->info.si_errno = 0;
        q->info.si_code = SI_KERNEL;
        q->info._sifields._kill._pid = 0;
        q->info._sifields._kill._uid = 0;
    } else
        copy_siginfo(&q->info, info);

函数copy_siginfo 用caller 传进来的info 填充q->info

7. 设置悬挂信号队列中的mask 成员的与sig 相应的位( 以表示该信号在悬挂信号队列中)
    sigaddset(&signals->signal, sig);

7. 返回0 以表示信号被成功加入悬挂信号队列.

9. 若是执行这一步, 则该信号不会被加入信号悬挂队列, 缘由有以下三个:1) 有太多的悬挂信号了, 或者2) 没有空闲的空间来分配sigqueue 变量了, 或者3) 该信号的处理由内核当即执行. 若是信号是实时信号而且经过内核函数发送而且显式要求加入队列, 那么返回错误代码-EAGAIN( 代码相似以下):
    if (sig>=32 && info && (unsigned long) info != 1 &&
                   info->si_code != SI_USER)
        return -EAGAIN;

10. 设置悬挂信号队列中的mask 成员的与sig 相应的位( 以表示该信号在悬挂信号队列中)
    sigaddset(&signals->signal, sig);

11. 返回0. 尽管该信号没有放到悬挂信号队列中, 可是相应的signals->signal 中已经设置了

 

即便没有空间为信号分配sigqueue 变量,也应该让目标信号知道相应的信号已经发生, 这一点很重要. 考虑以下情形: 目标进程使用了不少内存以至于没法再分配sigqueue 变量了, 可是内核必须保证对目标进程依的kill 依然可以成功, 不然管理员就没有机会杀死目标进程了.

 

group_send_sig_info 函数

函数 group_send_sig_info 把一个信号发给一个线程组 . 这个函数有三个参数:sig, info, p . ( 和specific_send_sig_info 相似).

这个函数的执行流程以下 :

1. 检查参数sig 的正确性:

  if (sig < 0 || sig > 64)

     return -EINVAL;

2. 若是信号的发送进程处于用户态, 则检查这个发送操做是否容许. 仅当知足以下条件之一( 才视为容许):

  1). 发送者进程有恰当的权限( 一般发送者进程应该是system administrator).

  2). 信号为SIGCONT, 而且目标进程和发送者进程在同一个login session.

  3). 目标进程和发送者进程属于同一个用户

3. 若是用户态的进程不能发送此信号, 则返回-EPERM. 若是sig==0, 则当即返回.( 由于0 是无效的信号). 若是sighand==0, 也当即返回, 由于此时目标进程正在被杀死, 从而sighand 被释放.

    if (!sig || !p->sighand)

        return 0;

4. 得到锁 p->sighand->siglock, 而且关闭本cpu 中断.

5. 调用handle_stop_signal 函数, 这个函数检查sig 是否会和现有的悬挂的信号冲突, 会的话解决冲突. 这个函数的步骤以下:

  1). 若是线程组正在被杀死(SIGNAL_GROUP_EXIT) ,则返回.

  2). 若是sig 是IGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 中的一种, 则调用rm_from_queue, 把线程组中全部悬挂的SIGCONT 删除. 注意: 包含线程组共享的悬挂信号队列中的(p->signal->shared_pending) 以及每个线程私有悬挂队列中的.

  3). 若是sig 是SIGCONT, 则调用rm_from_queue, 把线程组中全部悬挂的SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 删除. 注意: 包含线程组共享的悬挂信号队列中的(p->signal->shared_pending) 以及每个线程私有悬挂队列中的. 以后为每个线程调用try_to_wake_up.

6. 检查线程组是否忽略该信号, 若是忽略返回0.

7. 若是是非实时信号, 而且该线程组已经有这种悬挂的信号了, 那么返回0:

    if (sig<32 && sigismember(&p->signal->shared_pending.signal,sig))

        return 0;

8. 调用send_signal( ) 把信号加到线程组的共享悬挂信号队列中, 若是send_signal 返回非0 值, 则group_send_sig_info 退出并把该非零值返回.

9. 调用_ _group_complete_signal( ) 来唤醒线程组中的一个轻量级进程. 参考下文.

10. 释放p->sighand->siglock 而且打开本地中断.

11. 返回 0 (success).

 

函数 _ _group_complete_signal ( ) 扫描目标线程组 , 而且返回一个可以处理 (receive) 该新信号的进程 . 这样的进程必须同时具有以下的条件 :

1) 该进程不阻塞新信号.

2) 进程的状态不是EXIT_ZOMBIE, EXIT_DEAD, TASK_TRACED, or TASK_STOPPED. 可是当信号是SIGKILL 是, 进程的状态容许是TASK_TRACED or TASK_STOPPED.

3) 进程不处于正在被杀死的状态, 即状态不是PF_EXITING.

4) 或者进程正在某一个cpu 上执行, 或者进程的TIF_SIGPENDING 的标志未被设置.

 

一个线程组中知足上诉条件的线程( 进程) 可能不少, 根据以下原则选择一个:

1) 若是group_send_sig_info 中的参数p 指定的进程知足上述条件, 则选择p.

2) 不然从最后一个接收线程组信号的线程(p->signal->curr_target) 开始查找知足上述条件的线程, 找到为止.

 

( 若是线程组中没有一个线程知足上述条件怎么办?)

 

如_ _group_complete_signal( ) 成功找到一个进程( 表示为selected_p), 那么:

1. 检查该信号是不是致命的, 如果, 经过给线程组中的每个线程发送SIGKILL 来杀死线程组

2. 若不是, 调用signal_wake_up 来唤醒selected_p 并告知它有新的悬挂信号,

 

Delivering a Signal

经过上面的介绍, 内核经过修改目标进程的状态, 告知目标进程有新的信号到达. 可是目标进程对到达的新信号的处理(deliver signal) 咱们尚未介绍. 下面介绍目标进程如何在内核的帮助下处理达到的新信号.

注意当内核( 代码) 要把进程从核心态恢复成用户态时( 当进程从异常/ 中断处理返回时), 内核会检查该进程的 TIF_SIGPENDING 标识 , 若是存在悬挂的信号 , 那么将先处理该信号 .

这里须要介绍一下背景: 当进程在用户态( 用U1 表示) 下因为中断/ 异常而进入核心态, 那么须要把U1 的上下文记录到该进程的内核堆栈中.

为了处理非阻塞的信号 , 内核调用do_signal 函数 . 这个函数接受两个参数:

regs: 指向U1 上下文在内核堆栈的首地址 ( 参考进程管理).

oldest:  保存了一个变量的地址, 该变量保存了被阻塞的信号的信息( 集合). 若是该参数为NULL, 那么这个地址就是&current->blocked ( 以下文). 注意当自定义信号处理函数结束后, 会把oldest 设置为当前task 的阻塞信号集合.( 参考源代码, 以及rt_frame 函数).

咱们这里描述的do_signal 流程将会关注信号delivery( 处理), 而忽略不少细节, eg: 竞争条件 , 产生core dump, 中止和杀死线程组等等 .

通常,do_signal 通常仅在进程即将返回用户态时执行 . 所以 , 若是一个中断处理函数调用do_signal, 那么do_signal 只要按以下方式放回:
    if ((regs->xcs & 3) != 3)
        return 1;

若是oldest 为NULL, 那么 do_signal 会把它设置为当前进程阻塞的信号:

    if (!oldset)
        oldset = &current->blocked;

 

do_signal 的核心是一个循环 , 该循环调用dequeue_signal 从进程的私有悬挂信号队列和共享悬挂队列获取未被阻塞的信号. 若是成功得到这样的信号, 则经过handle_signal 调用相应的信号处理函数, 不然退出do_signal .

( 这个循环不是用C 的循环语句来实现, 而是经过修改核心栈的regs 来实现. 大概的流程能够认为以下: 当由核心态时切换向用户态时, 检查是否有非阻塞的悬挂信号, 有则处理( 包含: 准备信号处理函数的帧, 切换到用户态以执行信号处理函数, 信号处理函数返回又进入核心态), 无则返回原始的用户态上下文)

 

dequeue_signal 先从私有悬挂信号列表中按照信号值从小到大取信号,取完后再从共享悬挂信号列表中取 . ( 注意取后要更新相应的信息)

 

接着咱们考虑, do_signal 如何处理得到的信号( 假设用signr 表示) .

首先 , 它会检查是否有别的进程在监控(monitoring) 本进程 , 若是有 , 调用do_notify_parent_cldstop 和schedule 来让监控进程意识到本进程开始信号处理了.

接着,do_signal 得到相应的信号处理描述符( 经过current->sig->action[signr-1]) , 从而得到信号处理方式的信息 . 总共有三种处理方式: 忽略 , 默认处理 , 使用用户定义的处理函数
.

 

若是是忽略 , 那么什么也不作 :

if (ka->sa.sa_handler == SIG_IGN)

        continue;

 

执行默认的信号处理函数

若是指定的是默认的处理方式. 那么do_signal 使用默认的处理方式来处理信号 . ( 进程 0 不会涉及 , 参考前文)

对于init 进程除外 , 则它要丢弃信号:
    if (current->pid == 1)
        continue;

对于其它进程, 默认的处理方式取决于信号 .

第一类: 这类信号的默认处理方式就是不处理

    if (signr==SIGCONT || signr==SIGCHLD ||

            signr==SIGWINCH || signr==SIGURG)

        continue;//

第二类: 这类信号的默认处理方式以下:

    if (signr==SIGSTOP || signr==SIGTSTP ||

            signr==SIGTTIN || signr==SIGTTOU) {

        if (signr != SIGSTOP &&

               is_orphaned_pgrp(current->signal->pgrp))

            continue;

        do_signal_stop(signr);

    }

这里, SIGSTOP 与其余的信号有些微的区别.

SIGSTOP 中止整个线程组. 而其它信号只会中止不在孤儿进程组中的进程( 线程组).

孤儿进程组(orphand process group).

非孤儿进程组 指若是进程组A 中有一个进程有父亲, 而且该父进程在另一个进程组B 中, 而且这两个进程组A,B 都在用一个会话(session) 中, 那么进程组A 就是非孤儿进程组. 所以若是父进程死了, 可是启动在进程的session 依旧在, 那么进程组A 都不是孤儿.

注: 这两个概念让我迷糊.

do_signal_stop 检查当前进程是不是线程组中的第一个正在被中止的进程, 若是是, 它就激活一个组停(group stop) 。本质上, 它会把信号描述符的 group_stop_count 字段设置为正值, 而且唤醒线程组中的每个进程。每个进程都会查看这个字段从而认识到正在中止整个线程组, 并把本身的状态改成 TASK_STOPPED, 而后调用schedule. do_signal_stop 也会给线程组的父进程发送SIGCHLD, 除非父进程已经被设置为SA_NOCLDSTOP
flag of SIGCHLD.

默认行为是dump 的信号处理可能会进程工做目录下建立一个core 文件. 这个文件列出了进程的地址空间和cpu 寄存器的值. do_signal 建立这个文件后, 就会杀死整个线程组. 剩下18 个信号的默认处理是terminate, 这仅仅是简单地杀死整个线程组. 为此,do_signal 调用了do_group_exit 。

 

使用指定的函数来处理信号(catching the signal)

若是程序为信号设置了处理函数 , 那么do_signal 将会经过调用handle_signal 来强制该信号函数被执行:

    handle_signal(signr, &info, &ka, oldset, regs);

    if (ka->sa.sa_flags & SA_ONESHOT)

        ka->sa.sa_handler = SIG_DFL;

return 1;

 

若是用户在为信号设置信号处理函数时指定了 SA_ONESHOT , 那么当该信号处理函数第一次执行后 , 其将会被reset. 即之后来的这样的信号将会使用默认的处理函数 .

Notice how do_signal( ) returns after having handled a single signal. Other pending signals won't be considered until the next invocation of do_signal( ) . This approach ensures that real-time signals will be dealt with in the proper order.

执行一个信号处理函数至关复杂 , 由于须要内核当心处理用户信号处理函数的调用栈, 而后把控制权交给用户处理函数( 注意这里涉及内核态到用户态的转换) .

用户的信号处理函数定义在用户态中而且包含在用户代码段中,它须要在用户态(U2) 下执行. hande_signal 函数在核心态下执行. 此外, 因为当前的核心态是在前一个用户态(U1) 转过来, 这意味着当信号处理函数(U2) 结束, 回到内核态, 而后内核态还须要回到U1, 而当从U2 进入核心态后, 内核栈存放的已经再也不是U1 的上下文了( 而是U2), 此外通常信号处理函数中还会发生系统调用( 用户态到核心态的转换), 而系统调用结束后要回到信号处理函数.

注意: 每个内核态切换到用户态, 进程的内核堆栈都会被清空.

那么handle_signal 如何调用信号处理函数呢??

Linux 采用的方法以下: 每次调用信号处理函数以前, 把U1 的上下文拷贝到信号处理函数的栈中( 通常信号处理函数的栈也是当前进程的用户态的栈, 可是程序员也能够在设置信号处理函数时指定一个本身定义的栈, 可是这里不影响这个方法, 因此咱们只描述信号处理函数使用进程用户态的栈的状况). 而后再执行信号处理函数. 而当信号处理函数结束以后, 会调用sigreturn() 从U2 的栈中把U1 的上下文拷贝到内核栈中.

 

下图描述了信号处理函数的执行流程. 一个非阻塞的信号发给目标进程. 当一个中断或异常发生后, 目标进程从用户态(U1) 进入核心态. 在它切换回用户态(U1) 以前, 内核调用do_signal. 这个函数逐一处理悬挂的非阻塞信号. 而若是目标进程设置了对信号的处理函数, 那么它会调用handle_signal 来调用自定义的信号处理函数( 这期间须要使用 setup_frame 或setup_rt_frame 来为信号处理函数设置栈 ), 此时当切换到用户态时, 目标进程执行的是信号处理函数而不是U1.
当信号处理函数结束后, 位于 setup_frame 或setup_rt_frame 栈之上的返回代码 ( return code) 被执行, 这返回代码会执行sigreturn 或者rt_sigreturn 从而把U1 的上下文从setup_frame 或setup_rt_frame 栈中拷贝到核心栈. 而这结束后, 内核能够切换回U1.

注意: 信号有三种处理方式, 只有使用自定义处理函数才须要这样麻烦啊.

 

接下来咱们须要仔细瞧瞧这一切怎么发生的.

 

Setting up the frame

为了能恰当地为信号处理函数设置栈,handle_signal 调用setup_frame( 当信号没有相应的siginfo_t 时) 或者setup_rt_frame(
当信号有相应的siginfo_t 时). 为了判断采用哪种, 须要参考 sigaction 中的sa_flag 是否包含SA_SIGINO.

setup_frame 接受四个参数, 以下:

sig: 信号标识

ka: 与信号相关的 k_sigaction 实例

oldest: 进程阻塞的信号

regs: U1 上下为在核心栈的地址.

 

setup_frame 函数会在用户栈中分配一个sigframe 变量, 该变量包含了可以正确调用信号处理函数的信息( 这些信息会被 sys_sigreturn 使用 ). sigframe 的成员以下( 其示意图以下):

pretcode : 信号处理函数的返回地址. 其指向标记为 kernel_sigreturn 的代码

sig : 信号标识.

sc : sigcontext 变量. 它包含了U1 的上下文信息, 以及被进程阻塞的非实时信号的信息.

fpstate : _fpstate 实例, 用来存放U1 的浮点运算有关的寄存器.

extramask : 被进程阻塞的实时信号的信息 .

retcode :8 字节的返回代码, 用于发射 sigreturn 系统调用. 早期版本的linux 用于信号处理函数返回后的善后处理.linux2.6 则用于特征标志, 因此调试器可以知道这是一个信号处理函数的栈.

Frame on the User Mode stack

 

setup_frame 函数首先得到sigframe 变量的地址, 以下:

frame =(regs->esp - sizeof(struct sigframe)) & 0xfffffff8

注意: 默认地信号处理函数使用获得栈是进程在用户态下的栈, 可是用户在设置信号处理函数时能够指定. 这里只讨论默认状况. 对于用户指定其实也同样.

另外因为栈从大地址到小地址增加, 因此上面的代码要看明白了. 此外还须要8 字节对齐.

以后使用 access_ok 来验证 frame 是否可用, 以后用__put_user 来填充frame 各个成员. 填充好以后, 须要修改核心栈, 这样从核心态切换到用户态时就能执行信号处理函数了, 以下:

    regs->esp = (unsigned long) frame;

    regs->eip = (unsigned long) ka->sa.sa_handler;

    regs->eax = (unsigned long) sig;

    regs->edx = regs->ecx = 0;

    regs->xds = regs->xes = regs->xss = _ _USER_DS;

    regs->xcs = _ _USER_CS;

 

setup_rt_frame 和setup_frame 相似, 可是它在用户栈房的是一个rt_sigframe 的实例, rt_sigframe 除了sigframe 外还包含了siginfo_t( 它描述了信号的信息). 另外它使用 _ _kernel_rt_sigreturn.

 

Evaluating the signal flags

设置好栈后,handle_signal 检查和信号有关的flags. 若是没有设置 SA_NODEFER , 那么在执行信号处理函数时, 就要阻塞sigaction.sa_mask
中指定的全部信号以及sig 自己. 以下:

    if (!(ka->sa.sa_flags & SA_NODEFER)) {

        spin_lock_irq(&current->sighand->siglock);

       sigorsets(&current->blocked, &current->blocked, &ka->sa.sa_mask);

        sigaddset(&current->blocked, sig);

        recalc_sigpending(current);

        spin_unlock_irq(&current->sighand->siglock);

}

如前文所述,recalc_sigpending 会从新检查进程是否还有未被阻塞的悬挂信号, 并依此设置进程的 TIF_SIGPENDING 标志.

 

注意: sigorsets(&current->blocked, &current->blocked, &ka->sa.sa_mask) 等价于current->blocked |= ka->sa.sa_mask. 而current->blocked 原来的值已经存放在frame 中了.

 

handle_signal 返回到do_signal 后,do_signal 也当即返回.

 

Starting the signal handler

do_signal 返回后, 进程由核心态切换到用户态, 因而执行了信号处理函数.

 

Terminating the signal handler

信号处理函数结束后, 由于其返回值的地址( pretcode 指定的 ) 是_ _kernel_sigreturn 指向的代码段, 因此就会执行_ _kernel_sigreturn 指向的代码. 以下:

    _ _kernel_sigreturn:

      popl %eax

      movl $_ _NR_sigreturn, %eax

      int $0x80

这会致使 sigreturn 被执行 ( 会致使从用户态切换到核心态).

sys_sigreturn 函数能够计算获得sigframe 的地址. 以下:

    frame = (struct sigframe *)(regs.esp - 8);

    if (verify_area(VERIFY_READ, frame, sizeof(*frame)) {

        force_sig(SIGSEGV, current);

         return 0;

    }

接着, 它要从frame 中把进程真正阻塞的信号信息拷贝到current->blocked 中. 结果那些在sigaction 中悬挂的信号解除了阻塞. 以后调用 recalc_sigpending.

接着 sys_sigreturn 须要调用restore_sigcontext 把frame 的sc( 即U1 的上下文) 拷贝到内核栈中并把frame 从用户栈中删除.

_ _kernel_sigreturn 的处理与这相似.

 

 

从新执行系统调用( 被信号处理掐断的系统调用 )

注: 当用核心态转向用户态时, 该核心态多是系统调用的核心态.

小小总结 : 当内核使用用户指定的处理方式时 , 由于是从用户态转向内核态再转向用户态 , 因此其处理比较复杂 . 以下描述 : 当从用户态 (U1) 转入内核态后 , 在内核态试图回到 U1 时 , 会先判断是否有非阻塞的悬挂信号 , 若是有就会先调用用户的处理函数 ( 即进入用户态 , 这里是用户态 2), 处理完后 , 再回到内核态 , 而后再回到 U1.  注意在 U2 中也有可能发生系统调用从而再次进入内核态 . ( 注意在 U2 过程当中 , 系统处于关中断状态 , 因此信号处理应该尽量地快
), 咱们知道当用户态进入核心态时会把用户态的信息保存在核心态的栈中 ,  为了不在从 U2 因系统调用再进入核心态是破坏 U1 在核心态中的信息 , 在进入 U2 以前 , 要不 U1 在核心栈中的信息拷贝到 U1 的栈中 , 并在 U2 返回后 , 再把 U2 栈中保存 U1 的信息拷贝会核心栈 .

注 :U2 使用的栈能够和 U1 是同一个栈 , 也能够是用户在设置信号处理函数时指定的一段内存 .

当一个进程调用某些并不能立刻知足的系统调用(eg: 写文件) 时, 内核会把该进程的状态设置为 TASK_INTERRUPTIBLE 或者TASK_UNINTERRUPTIBLE.

当一个进程( 表示为wp) 处于TASK_INTERRUPTIBLE 状态, 而另一个进程又给它发信号, 那么内核会把wp 的状态的进程设置为TASK_RUNNING( 可是此时wp 的系统调用仍未完成). 而当wp 切换会用户态时, 这个信号会被deliver. 若是这种状况真的发生了, 则系统调用服务例程并无成功完成任务, 可是会返回错误码EINTR , ERESTARTNOHAND , ERESTART_RESTARTBLOCK , ERESTARTSYS , 或 ERESTARTNOINTR.
( 参考中断处理的从中断返回部分).

从实践上看, 用户得到的错误代码是是EINTR, 这意味着系统调用没有成功完成. 程序员能够决定是否再次发起该系统调用. 其他的错误代码由内核使用来判断是否在信号处理以后自动从新执行该系统调用.

下表列出了这些错误代码在每一种可能的中断行为下对未完成系统调用的影响. 表中用的词定义以下: 

Terminate: 该系统调用不会被内核自动从新执行. 而用户获得的该系统调用的返回值是-EINTER. 对程序员而言该系统调用失败.

Reexecute: 内核会强制进程在用户态下自动从新执行该系统调用( 经过把中断号放到eax, 执行int 0x80 或者sysenter 指令). 可是这对程序员透明.

Depends: 若是当被deliver 的信号设置了 SA_RESTART 标志, 那么自动从新执行该系统调用. 不然停止系统调用并返回-EINTER.

 

Reexecution of system calls

Error codes and their impact on system call execution

Signal

Action

EINTR

ERESTARTSYS

ERESTARTNOHAND

ERESTART_RESTARTBLOCK

ERESTARTNOINTR

Default

Terminate

Reexecute

Reexecute

Reexecute

Ignore

Terminate

Reexecute

Reexecute

Reexecute

Catch

Terminate

Depends

Terminate

Reexecute

 

注: ERESTARTNOHAND , ERESTART_RESTARTBLOCK 使用不一样的机制来从新自动执行系统调用( 参下文 ).

 

当 delivering 一个信号时, 内核必须确信进程正在执行系统调用中,这样它才能reexecute 该系统调用, 而 regs 中的成员orig_eax 就是干这个事情的. 回想一下这个成员在中断/ 异常时如何被初始化的:

Interrupt: 它等于 IRQ 数值 - 256.

0x80 exception ( 或者 sysenter): 它等于系统调用的编号.

Other exceptions: 它等于-1.

因此若是该值>=0, 那么可肯定进程是在处于系统调用中被信号处理唤醒的( 即信号处理唤醒一个等待系统调用完成( 状态为 TASK_INTERRUPTIBLE ) 的进程). 因此内核在delivering 信号时, 可以返回上述的错误代码, 并做出恰当的挽救.

 

重启被非自定义信号处理函数中断的系统调用

注:上面语句的中断不是OS 中的中断, 而是平常生活中的中断的含义.

若是系统调用由于信号的默认处理函数或者信号的忽略处理而中断( 即由系统调用把task 的状态改成可中断状态, 可是却被信号的默认处理函数或者忽略信号操做把该task 的状态改成running, 如前文所述), 那么do_signal 函数须要分析系统调用的错误码来决定是否自动从新执行被中止的系统调用. 若是须要重启该系统调用, 那么必须修改regs 中的内容, 从而在切换到用户态后, 在用户态下再次执行该系统调用( 即再次在用户态下让eax 存放系统调用的编号, 而后执行int 0x80 或者sysenter).
以下代码:

    if (regs->orig_eax >= 0) {

        if (regs->eax == -ERESTARTNOHAND || regs->eax == -ERESTARTSYS ||

              regs->eax == -ERESTARTNOINTR) {

            regs->eax = regs->orig_eax;

            regs->eip -= 2;

        }

        if (regs->eax == -ERESTART_RESTARTBLOCK) {

            regs->eax = __NR_restart_syscall;

            regs->eip -= 2;

        }

    }

regs->eax 存放系统调用的编号 . 此外,int 0x80 或者sysreturn 均为2 字节. 因此regs->eip -=2 等价于切换到用户态后从新执行int 0x80 或者sysretrun 指令.

对于错误码 ERESTART_RESTARTBLOCK, 它须要使用restart_syscall 系统调用, 而不是使用原来的系统调用. 这个错误码只用在与时间有关的系统调用. 一个典型的例子是 nanosleep( )
: 想象一下, 一个进程调用这个函数来暂停20ms, 10ms 后因为一个信号处理发生( 从而激活这个进程), 若是这信号处理后从新启动这个系统调用, 那么它在重启的时候不能直接再次调用nanosleep, 不然将会致使该进程睡觉30ms. 事实上, nanosleep 会在当前进程的thread_info 的restart_block 中填写下若是须要重启nanosleep, 那么须要调用哪个函数, 而若是其被信号处理中断, 那么它会返回-ERESTART_RESTARTBLOCK,
而在重启该系统调用时,sys_restart_syscall 会根据restart_block 中的信息调用相应的函数. 一般这个函数会计算出首次调用与再次调用的时间间距, 而后再次暂停剩余的时间段.

 

重启由自定义信号处理函数中断的系统调用

在这种状况下,handle_signal 会分析错误码以及 sigaction 中的标志是否包含了SA_RESTART, 从而决定是否重启未完成的系统调用. 代码以下:

    if (regs->orig_eax >= 0) {

        switch (regs->eax) {

            case -ERESTART_RESTARTBLOCK:

             case -ERESTARTNOHAND:

                regs->eax = -EINTR;

                break;

            case -ERESTARTSYS:

                if (!(ka->sa.sa_flags & SA_RESTART)) {

                    regs->eax = -EINTR;

                    break;

                 }

            /* fallthrough */

            case -ERESTARTNOINTR:

                regs->eax = regs->orig_eax;

                regs->eip -= 2;

        }

}

 

若是须要重启系统调用, 其处理与do_signal 相似. 不然向用户态返回 -EINTR.

 

 

问题 :

在信号处理函数中能够发生中断吗 , 能够再 发出系统调用吗,能够发出异常吗 ?

若是不行 会有什么影响 ??

 

 

与信号处理相关的系统调用

由于当进程在用户态时, 容许发送和接受信号. 这意味着必须定义一些系统调用来容许这类操做.
不幸的是, 因为历史的缘由这些操做的语义有可能会重合, 也意味着某些系统调用可能不多被用到. 好比,sys_sigaction, sys_rt_sigaction 几乎相同, 因此C 的接口sigaction 只调用了sys_rt_siaction. 咱们将会描述一些重要的系统调用.

 

进程组 : Shell 上的一条命令行造成一个进程组 . 注意一条命令其实能够启动多个程序 . 进程组的 ID 为其领头进程的 ID.

 

kill( ) 系统调用

原型为: int kill(pid_t pid, int sig)

其用来给一个线程组( 传统意义上的进程) 发信息. 其对应的系统服务例程(service routine) 是sys_kill. sig 参数表示待发送的信号,pid 根据其值有不一样的含义, 以下:

pid > 0: 表示信号sig 发送到由pid 标识的线程组( 即线程组的PID==pid).

pid = 0: 表示信号sig 发送到发送进程所在的进程组中的全部线程组.

pid = -1: 表示信号sig 发送到除进程0, 进程1, 当前进程外的全部进程

pid < -1: 表示信号sig 发送到进程组-pid 中的全部线程组.

服务例程sys_kill 会初始化一个siginfo_t 变量, 而后调用kill_something_info. 以下:

    info.si_signo = sig;

    info.si_errno = 0;

    info.si_code = SI_USER;

    info._sifields._kill._pid = current->tgid;

    info._sifields._kill._uid = current->uid;

    return kill_something_info(sig, &info, pid);

 

kill_something_info 会调用kill_proc_info( 这个函数调用 group_send_sig_info 把信号发给线程组 ) 或者 kill_pg_info( 这个会扫描目标进程组而后逐一调用send_sig_info ) 或者为系统中的每个进程调用group_send_sig_info( 当pid=-1 时).

系统调用kill 能够发送任意信号, 然而它不保证该信号被加到目标进程的悬挂信号队列中. ( 这个是指对于非实时信号 它也有可能会丢弃该信号吗????
) 对于实时信号, 可使用 rt_sigqueueinfo.

System V and BSD Unix 还有killpg 系统调用, 它能够给一组进程发信号. 在linux 中, 它经过kill 来实现. 另外还有一个raise 系统调用, 它能够给当前进程发信号. 在linux 中,killpg, raise 均以库函数提供.

tkill( ) & tgkill( ) 系统调用

这两个函数给指定线程发信号.
pthread_kill 使用它们之一来实现. 函数原型为:

int tkill(int tid, int sig);

long sys_tgkill (int tgid, int pid, int sig);

tkill 对应的服务例程是sys_tkill, 它也会填充一个siginfo_t 变量, 进程权限检查, 而后掉用specific_send_sig_info.

tgkill 与tkill 的差异在于它多了一个tgid 的参数, 它要求pid 必须是tgid 中的线程. 其对应的服务例程是sys_tgkill, 它作的事情和sys_tkill 相似, 但它还检查了pid 是否在tgid 中. 这种检查在某些状况下能够避免 race condition. 好比: 一个信号被发给了线程组A 中的一个正在被杀死的线程(killing_id), 若是另一个线程组B 很快地建立一个新的线程而且其PID= killing_id, 那么信号有可能会发送到线程组B 中的新建的线程.
tgkill 能够避免这种状况, 由于线程组A,B 的ID 不同.

 

设置信号处理函数

程序员能够经过系统调用sigaction (sig,act,oact) 来为信号sig 设置用户本身的信号处理函数act. 固然若是用户没有设置, 那么系统会使用默认的信号处理函数. 其函数原型为:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

oldact 用来保存信号signum 的旧的信号处理函数( 由于signum 的新的信号处理函数是act, 保存旧的是但愿可以恢复使用旧的信号处理函数).

其对应的服务例程是sys_sigaction, 它首先检查act 地址的有效性, 而后act 的内容拷贝到一个类型为 k_sigaction 的 本地变量new_ka ,以下:

    _ _get_user(new_ka.sa.sa_handler, &act->sa_handler);

    _ _get_user(new_ka.sa.sa_flags, &act->sa_flags);

    _ _get_user(mask, &act->sa_mask);

    siginitset(&new_ka.sa.sa_mask, mask);

 

接着调用 do_sigaction 把new_ka 拷贝到current->sig->action[sig-1] 中的. 相似以下:

    k = &current->sig->action[sig-1];

    if (act) {

        *k = *act;

        sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));

        if (k->sa.sa_handler == SIG_IGN || (k->sa.sa_handler == SIG_DFL &&

         (sig==SIGCONT || sig==SIGCHLD || sig==SIGWINCH || sig==SIGURG))) {

        rm_from_queue(sigmask(sig), &current->signal->shared_pending);

            t = current;

            do {

                rm_from_queue(sigmask(sig), &current->pending);

                recalc_sigpending_tsk(t);

                t = next_thread(t);

            } while (t != current);

        }

    }

 

POSIX 规定当默认行为是忽略时, 把信号处理函数设置为SIG_IGN 或者SIG_DFT 会致使悬挂的信号被丢弃. 此外, SIKKILL 和SIGSTOP 永远不会被屏蔽 ( 参考上述代码).

此外, sigaction 系统调用还容许程序员初始化sigaction 中的sa_flags.

System V 也提供signal 系统调用. C 库的signal 使用rt_sigaction 来实现. 可是linux 仍然有相应的服务例程sys_signal. 以下:

    new_sa.sa.sa_handler = handler;

    new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;

    ret = do_sigaction(sig, &new_sa, &old_sa);

    return ret ? ret : (unsigned long)old_sa.sa.sa_handler;

 

得到被阻塞的悬挂信号

系统调用 sigpending () 容许 用户得到当前线程被阻塞的悬挂信号. 函数原型为:

int sigpending(sigset_t *set);

set 用来接收被阻塞的悬挂信号的信息.

其对应的服务例程是sys_sigpending, 其实现代码以下:

    sigorsets(&pending, &current->pending.signal,

                        &current->signal->shared_pending.signal);

    sigandsets(&pending, &current->blocked, &pending);

    copy_to_user(set, &pending, 4);

 

修改被阻塞的信号的集合

系统函数sigprocmask 能够用来修改当前线程的阻塞信号集合. 可是它仅适用于非实时信号. 函数原型为:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

假设在执行这个函数以前线程的阻塞信号的集合为bs. 执行这个函数以后线程的阻塞信号的集合为nbs.

 

oldsett: 用于返回( 返回) 线程当前阻塞的信号的集合(*oldest=bs)

set: 用于存储信号集合. 怎么用它还取决于how 参数.

how: 执行线程的新的阻塞信号集合若是经过set 参数得到. 其可能的值及其含义以下:

SIG_BLOCK: nbs=bs|set

SIG_UNBLOCK:nbs=bs-set

SIG_SETMASK:nbs=set

其对应的服务例程是 sys_sigprocmask( ) . 它调用copy_from_user 把set 值拷贝到本地变量new_set, 并把bs 拷贝到oldset 中. 其执行的代码相似以下:

    if (copy_from_user(&new_set, set, sizeof(*set)))

        return -EFAULT;

    new_set &= ~(sigmask(SIGKILL)|sigmask(SIGSTOP));

    old_set = current->blocked.sig[0];

    if (how == SIG_BLOCK)

        sigaddsetmask(&current->blocked, new_set);

    else if (how == SIG_UNBLOCK)

        sigdelsetmask(&current->blocked, new_set);

    else if (how == SIG_SETMASK)

        current->blocked.sig[0] = new_set;

    else

        return -EINVAL;

    recalc_sigpending(current);

    if (oset && copy_to_user(oset, &old_set, sizeof(*oset)))

            return -EFAULT;

    return 0;

 

悬挂( 暂停) 进程

系统调用 sigsuspend 的原型以下:

int sigsuspend(const sigset_t *mask);

其含义是: 把本线程的阻塞信号设置为mask 并把线程状态设置为 TASK_INTERRUPTIBLE. 而且只有当一个 nonignored, nonblocked 的信号发到本线程后才会把本线程唤醒(deliver 该信号, 系统调用返回).

其相应的服务例程为sys_sigsuspend, 执行的代码为:

    mask &= ~(sigmask(SIGKILL) | sigmask( 
SIGSTOP ));

    saveset = current->blocked;// saveset 本地局部变量

    siginitset(&current->blocked, mask);

    recalc_sigpending(current);

    regs->eax = -EINTR;

    while (1) {

        current->state = TASK_INTERRUPTIBLE;

        schedule( );

        if (do_signal(regs, &saveset))// 把阻塞信号集合恢复为saveset

            return -EINTR;

}

( 注意, 本系统调用自己指望它被信号处理函数中断.)

函数schedule 会致使执行别的进程( 线程), 而当本进程再次执行时( 即上面的schedule 返回了), 它会调用do_signal 来处理其未被阻塞的悬挂的信号, 而后恢复线程的阻塞信号集合(saveset). 若是do_signal 返回非0(do_signal 中调用用户自定义信号处理函数或者杀死本线程时返回非0), 那么该系统调用返回.

即只有当本线程处理完不被阻塞的信号( ==(!mask)| SIGKILL| SIGSTOP) 后, 它才会返回.

 

实时信号的系统调用

前面所述的系统调用仅适用于非实时信号,linux 还引入了支持实时信号的系统调用.

一些实时系统调用( 如: rt_sigaction, rt_sigpending, rt_sigprocmask, rt_sigsuspend) 与它们的非实时的版本相似( 只是在名字加了rt_). 下面仅简单描述两个实时信号的系统调用.

rt_sigqueueinfo( ): 把一个实时信号发给线程组( 放到线程组的共享悬挂信号列表中). 库函数sigqueue 利用这个系统调用来实现.

rt_sigtimedwait( ): 把阻塞的悬挂信号从悬挂信号队列中删除, 若是在调用这个系统调用时尚未相应的阻塞悬挂信号, 那么它会把本进程(task) 阻塞一段时间. 库函数sigwaitinfo,sigtimedwait 经过这个系统调用实现.

相关文章
相关标签/搜索