linux signal 处理

 

说明:

本文主要翻译自ULK 3rd chapter 11.

主要受 http://blog.csdn.net/yunsongice 影响,故发表在csdn.

另外,本文是最初版本,估计以后会有一个改进版本. 文中还有很多todo的地方.

另外,如果有版权问题,通知我,我马上删掉.


总结

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

 

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

 

线程会有自己的悬挂信号队列 , 并且线程组也有一个信号悬挂队列 .

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

 

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

 

对一个信号 , 要三种处理方式 :

忽略该信号 ;

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

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

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

 

信号处理可以分成两个阶段 : 信号产生并通知到接收方 (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 thert_sigtimedwait( ) system call) ( 线程私有)

structsigpending

pending

Data structure storing the private pending signals

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 await4( ) system call

struct task_struct *

curr_target

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

structsigpending

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_tstructure

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_tstructure

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_tstructure

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_tstructure

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 通过这个系统调用实现.

 

 

todo

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐