Linux深入理解内核 - 信号

信号的作用

信号(signal)是很短的消息,可以被发送到一个进程或一组进程。使用信号的两个主要目的是:

  1. 让进程知道已经发生了一个特定的事件。

  2. 强迫进程执行它自己代码中的信号处理程序。

取值名称解释默认动作
1SIGHUP挂起(在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联)
2SIGINT中断(程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程)
3SIGQUIT退出(和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号)
4SIGILL非法指令(执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号)
5SIGTRAP断点或陷阱指令(由断点指令或其它trap指令产生. 由debugger使用)
6SIGABRTabort发出的信号(调用abort函数生成的信号)
7SIGBUS非法内存访问(非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间))
8SIGFPE浮点异常(在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误)
9SIGKILLkill信号(用来立即结束程序的运行)不能被忽略、处理和阻塞
10SIGUSR1用户信号1(留给用户使用)
11SIGSEGV无效内存访问(试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据)
12SIGUSR2用户信号2(留给用户使用)
13SIGPIPE管道破损,没有读端的管道写数据(这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止还往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止,也会产生这个信号)
14SIGALRMalarm发出的信号(时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号)
15SIGTERM终止信号(程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号)
16SIGSTKFLT栈溢出
17SIGCHLD子进程退出(子进程结束时, 父进程会收到这个信号)默认忽略
18SIGCONT进程继续不能被阻塞
19SIGSTOP进程停止(停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行)不能被忽略、处理和阻塞
20SIGTSTP进程停止(停止进程的运行, 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号)该信号可以被处理和忽略
21SIGTTIN进程停止,后台进程从终端读数据时
22SIGTTOU进程停止,后台进程想终端写数据时
23SIGURGI/O有紧急数据到达当前进程默认忽略
24SIGXCPU进程的CPU时间片到期
25SIGXFSZ文件大小的超出上限
26SIGVTALRM虚拟时钟超时
27SIGPROFprofile时钟超时
28SIGWINCH窗口大小改变默认忽略
29SIGIOI/O相关
30SIGPWR关机默认忽略
31SIGSYS系统调用异常

这个可以在signal(7) - Linux manual page (man7.org)中查看!

POSIX标准还引入了一类新的信号,叫做实时信号(real-time signal);在Linux中它们的编码范围为32~64。它们与常规信号有很大的不同,因为它们必须排队以便发送的多个信号能被接收到。另一方面,同种类型的常规信号并不排队:如果一个常规信号被连续发送多次,那么,只有其中的一个发送到接收进程。尽管Linux内核并不使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。

信号的一个重要特点是它们可以随时被发送给状态经常不可预知的进程。 发送给非运行进程的信号必须由内核保存,直到进程恢复执行。阻塞一个信号(后面描述),要求信号的传递拖延,直到随后解除阻塞,这使得信号产生一段时间之后才能对其传递这一问题变得更加严重。因此,内核区分信号传递的两个不同阶段:

  • 信号产生,内核更新目标进程的数据结构以表示一个信号已经被发送.

  • 信号传递,内核强迫目标进程通过以下方式对信号做出反应: 或改变目标进程的执行状态,或开始执行一个特定的信号处理程序,或两者都是

信号一旦已传递出去,进程描述符中有关这个信号的所有信息都被取消。已经产生但还没有传递的信号称为挂起信号(pending signal)。

任何时候,一个进程仅存在给定类型的一个挂起信号,同一进程同种类型的其他信号不被排队,只被简单地丢弃。

但是,实时信号是不同的:同种类型的挂起信号可以有好几个。 一般来说,信号可以保留不可预知的挂起时间。必须考虑下列因素:

  • 信号通常只被当前正运行的进程传递(即由current进程传递)。

  • 给定类型的信号可以由进程选择性地阻塞(blocked).

  • 当进程执行一个信号处理程序的函数时,通常“屏蔽”相应的信号,即自动阻塞这个信号直到处理程序结束。因此,所处理的信号的另一次出现不能中断信号处理程序,所以,信号处理函数不必是可重入的。

所以,内核必须要做出以下工作!

  • 记住每个进程阻塞哪些信号。

  • 当从内核态切换到用户态时,对任何一个进程都要检查是否有一个信号已到达。

  • 确定是否可以忽略信号。这发生在下列所有的条件都满足时:

    • 目标进程没有被另一个进程跟踪(进程描述符中ptrace字段的PT_PTRACED标志等于0)。

    • 信号没有被目标进程阻塞。

    • 信号被目标进程忽略

  • 处理这样的信号,即信号可能在进程运行期间的任一时刻请求把进程切换到一个信号处理函数,并在这个函数返回以后恢复原来执行的上下文。

传递信号之前所执行的操作

进程以三种方式对一个信号做出应答:

  1. 显式地忽略信号。

  2. 执行与信号相关的缺省操作. a. Terminate 进程被终止。 b. Dump 进程被终止,并且,如果可能,创建包含进程执行上下文的核心转储文件; c. lgnore 信号被忽略。 d. Stop 进程被停止,即把进程置为TASK_STOPPED状态. e. Continue 如果进程被停止(TASK_STOPPED),continue处理中就把它置为TASK_RUNNING状态。

  3. 通过调用相应的信号处理函数捕获信号。

如果一个进程正在被跟踪时接收到一个信号,内核就停止这个进程,并向跟踪进程发送一个SIGCHLD信号以通知它一下。跟踪进程又可以使用SIGCOUNT信号重新恢复被跟踪进程的执行。 SIGKILLSIGSTOP信号不可以被显式地忽略、捕获或阻塞, 因此,通常必须执行它们的缺省操作。

POSIX信号和多线程应用

POSIX 1003.1标准对多线程应用的信号处理有一些严格的要求:

  1. 信号处理程序必须在多线程应用的所有线程之间共享;不过,每个线程必须有自己的挂起信号掩码和阻塞信号掩码。挂起是信号已经产生,等待处理中。阻塞是即使信号已经产生,也不会进行实际处理。

  2. POSIX库函数kill()sigqueue()必须向所有的多线程应用而不是某个特殊的线程发送信号。 所有由内核产生的信号同样如此(如:SIGCHLD、SIGINTSIGQUIT)。

  3. 每个发送给多线程应用的信号仅传送给一个线程,这个线程是由内核在从不会阻塞该信号的线程中随意选择出来的。

  4. 如果向多线程应用发送了一个致命的信号,那么内核将杀死该应用的所有线程,而不仅仅是杀死接收信号的那个线程。

有两个例外:不可能给进程0(swapper)发送信号,而发送给进程1(init)的信号在捕获到它们之前也总被丢弃。因此,进程0永不死亡,而进程1只有当init程序终止时才死亡。如果一个挂起信号被发送给了某个特定线程,那么这个信号是私有的;如果被发送给了整个线程组,它就是共享的。

结构

类型名称说明
struct signal_struct*signal
struct sighand_struct*sighand
sigset_tblocked
sigset_treal_blocked
struct sigpendingpending
unsigned longsas_ss_sp
size_tsas_ss_size
int(*)(void*)notifier
void*notifier_data
sigset_t*notifier_mask

blocked字段存放进程当前所屏蔽的信号。它是一个sigset_t位数组,每种信号类型对应一个元素: 信号的编号对应于sigset_t类型变量中的相应位下标加1

信号描述符和信号处理程序描述符

信号描述符被属于同一线程组的所有进程共享,信号描述符中与信号处理有关的字段如表11-4所示:

sigaction数据结构

(1). sa_handler:指向信号处理程序的一个指针/SIG_DFL/SIG_IGN. (2). sa_flags:这是一个标志集. (3). sa_mask:当运行信号处理程序时要屏蔽的信号。

挂起信号队列

有几个系统调用能产生发送给整个线程组的信号,如kill()rt_sigqueueinfo(),而其他的一些则产生发送给特定进程的信号,如tkill()tgkill()。内核把两个挂起信号队列与每个进程相关联: (1). 共享挂起信号队列,存放整个线程组的挂起信号。 (2). 私有挂起信号队列,存放特定进程(轻量级进程)的挂起信号。

挂起信号队列由sigpending数据结构组成,它的定义如下:

struct siigpending{
    struct list_head list;
    sigset_t    signal;
};
类型名称说明
struct list_headlist链接挂起信号对应的链表
spinlock_t*lock指向与挂起信号相应的信号处理程序描述符中siglock字段的指针
intflagssigqueue数据结构的标志
siginfo_tinfo描述产生信号的事件
struct user_structuser指向进程拥有者的每用户数据结构的指针

siginfo_t是一个128字节的数据结构,其中存放有关出现特定信号的信息。它包含下列字段: a. si_signo 信号编号。 b. si_errno 引起信号产生的指令的出错码,或者如果没有错误则为0。 c. si_code 发送信号者的代码(参见表11-8)。

代码名发送者
SI_USERkill, raise
SI_KERNEL一般内核函数
SI_QUEUEsigqueue
SI_TIMER定时器到期
SI_ASYNC异步IO完成
SI_TKILLtkill和tgkill

产生信号

很多内核函数都会产生信号:即根据需要更新一个或多个进程的描述符。它们不直接执行第二步的信号传递操作,而是可能根据信号的类型和目标进程的状态唤醒一些进程,并促使这些进程接收信号。

函数名称说明
send_sig向单一进程发送信号
send_sig_info与上类似,还使用siginfo_t扩展信息
force_sig发送不能被进程显示忽略,也不能被进程阻塞的信号
force_sig_info与上类似,还使用siginfo_t扩展信息
force_sig_specific与上类似,优化了SIGSTOP和SIGKILL信号的处理
sys_tkilltkill系统调用的处理函数
sys_tgkilltgkill系统调用的处理函数

所有函数在结束时都调用specific_send_sig_info()函数.

函数名称说明
send_group_sig_info向某一个进线程组发送信号,该线程组由它的一个成员进程的描述符来标识。
kill_pg向一个进程组中所有的进程组发送信号!
kill_pg_info与上一个类似只是,还使用siginfo_t扩展信息
kill_proc向某一个线程组发送信号该线程组,有它的一个成员进程的pid来标识
kill_proc_info与上一个类似只是,还使用siginfo_t扩展信息
sys_killkill系统调用处理函数
sus_rt_sigqueueinfort_sigqueueinfo系统调用处理函数

所有函数在结束时都调用group_send_sig_info()函数.

specific_send_sig_info()函数

specific_send_sig_info()函数向指定进程发送信号,它作用于三个参数:

sig:信号编号

info:或者是siginfo_t表的地址,或者是三个特殊值中的一个。零意味着信号是由用户态进程发送的,一意味着是由内核发送的,二意味着是由内核发送的信号

t:指向目标进程描述符的指针

必须在关本地中断和已经获得t->sighand->siglock自旋锁的情况下调用specific_send_sig_info()函数。函数执行下面的步骤:

  1. 检查进程是否忽略信号,如果是就返回0(不产生信号)。当下面的三个忽略信号的条件全部满足时,信号就被忽略:

    1. 进程没有被跟踪(t->ptrace中的PT_PTRACED标志被清0).

    2. 信号没有被阻塞(sigismember(&t->blocked,sig)返回0).

    3. 或者显式地忽略信号(t->sighand->action[sig-1]sa_handler字段等于SIG_IGN),或者隐含地忽略信号(sa_handler字段等于SIG_DFL而且信号是SIGCONT、SIGCHLD、SIGWINCHSIGURG).

  2. 检查信号是否是非实时的(sig<32),而且是否在进程的私有挂起信号队列上已经有另外一个相同的挂起信号。如果是,就什么都不需要做,因此返回0

  3. 调用send_signal( sig,info,t,&t->pending),把信号添加到进程的挂起信号集合中.

  4. 如果send_signal()成功地结束,而且信号不被阻塞(sigismember(&t->blocked,sig)返回0),就调用signal_wake_up()函数通知进程有新的挂起信号。

signal_wake_up

a. 把t->thread_info->flags中的TIF_SIGPENDING标志置位。 b. 如果进程处于TASK_INTERRUPTIBLETASK_STOPPED状态,而且信号是SIGKILL,就调用try_to_wake_up()唤醒进程。 c. 如果try_to_wake_up()返回0,那么说明进程已经是可运行的:这种情况下,它检查进程是否已经在另外一个CPU上运行,如果是就向那个CPU发送一个处理器间中断,以强制当前进程的重新调度。因为在从调度函数返回时,每个进程都检查是否存在挂起信号,因此,处理器间中断保证了目标进程能很快注意到新的挂起信号。 d. 返回1(已经成功地产生信号)。

send_signal()函数

send_sigmal()函数在挂起信号队列中插入一个新元素,

static int send_signal(int sig, struct siginfo *info, struct task_struct *t, struct sigpending *signals)

将信号加入到进程挂起信号掩码。需要时,分配并构造sigqueue,加入sigqueue链表。sigqueue链表使得掩码中指定的一个信号,可以存在多个链表节。sigqueue可以使得进一步存储信号关联的数据信息。

group_send_sig_info()函数

group_send_sig_info()函数向整个线程组发送信号。它作用于三个参数:信号编号sigsiginfo_t表的地址info(可选的值为0、12,如前面“specific_send_sig_info()函数“一节中所描述的)以及进程描述符的地址p

int group_send_sig_info(int sig, struct siginfo *info, struct task_struct *p)

该函数主要执行下面的步骤:

  1. 检查参数sig是否正确:

if(sig<0 Il sig>64)
    return -EINVAL;
  1. 如果信号是由用户态进程发送的,则该函数确定是否允许这个操作。下列条件中至少有一个成立时信号才能被传递:

    1. 发送进程的拥有者具有适当的权能(这通常意味着通过系统管理员发布信号)。

    2. 信号为SIGCONT且目标进程与发送进程处于同一个注册会话中。

    3. 两个进程属于同一个用户。如果不允许用户态进程发送信号,函数就返回值-EPERM

    4. 如果参数sig的值为0,则函数不产生任何信号,立即返回:

if(!sig ll !p->sighand)
    return 0;
  1. 因为0是无效的信号编码,用于让发送进程检查它是否有向目标线程组发送信号所必需的特权。如果目标进程正在被杀死(通过检查它的信号处理程序描述符是否已经被释放来获知),那么函数也返回。

  2. 获取p->sighand->siglock自旋锁并关闭本地中断。

  3. 调用handle_stop_signal()函数,该函数检查信号的某些类型,这些类型可能使目标线程组的其他挂起信号无效。

handle_stop_signal()函数执行下面的步骤:
  1. 如果线程组正在被杀死(信号描述符的flags字段的SIGNAL_GROUP_EXIT标志被设置),则函数返回。

  2. 如果sigSIGSTOP、SIGTSTP、SIGTTINSIGTTOU信号,就调用rm_from_queue()函数从共享挂起信号队列p->signal->shared_pending和线程组所有成员的私有信号队列中删除SIGCONT信号。

  3. 如果sigSIGCONT信号,就调用rm_from_queue()函数从共享挂起信号队列p->signal->shared_pending中删除所有的SIGSTOP、 SIGTSTP、SIGTTIN和SIGTTOU信号,然后从属于线程组的进程的私有挂起信号队列中删除上述信号,并唤醒进程:

rm_from_queue(0x003c0000,&p->signal->shared_pending);
t = p;
do {
    rm_from_queue(0x003c0000,&t->pending);
    try_to_wake_up(t,TASK_STOPPED,0);
    t = next_thread(t);
} while(t != p);

掩码0x003c0000选择以上四种停止信号。宏next_thread每次循环都返回线程组中不同轻量级进程的描述符地址.

  1. 检查线程组是否忽略信号,如果是就返回0值(成功)。如果在前面“信号的作用”一节中所提到的忽略信号的三个条件都满足(也可参见前面“specific-send-sig.info()函数”一节中的第1步),就忽略信号。

  2. 检查信号是否是非实时的,并且在线程组的共享挂起信号队列中已经有另外一个相同的信号,如果是,就什么都不需要做,因此返回0值(成功)。

if(sig<32 && sigismember(&p->signal->shared_pending.signal,sig))
    return 0;
12

调用send_signal()函数把信号添加到共享挂起信号队列中。如果send_signal()返回非0的错误代码,则函数终止并返回相同的值。

  1. 调用__group_complete_signal()函数唤醒线程组中的一个轻量级进程.

  2. 释放p->sighand->siglock自旋锁并打开本地中断。

  3. 返回0(成功)。

__group_complete_sigmal

函数__group_complete_sigmal()扫描线程组中的进程,查找能接收新信号的进程。满足下述所有条件的进程可能被选中:

  • 进程不阻塞信号。

  • 进程的状态不是EXIT_ZOMBIE、EXIT_DEAD、TASK_TRACEDTASK_STOPPED(作为一种异常情况,如果信号是SIGKILL,那么进程可能处于TASK_TRACED或者TASK_STOPPED状态)。

  • 进程没有正在被杀死,即它的PF_EXITING标志没有置位。

  • 进程或者当前正在CPU上运行,或者它的TIF_SIGPENDING标志还没有设置。

  • (实际上,唤醒一个有挂起信号的进程是毫无意义的:通常,唤醒操作已经由设置了TIF_SIGPENDING标志的内核控制路径执行;另一方面,如果进程正在执行,则应该向它通报有新的挂起信号。)

一个线程组可能有很多满足上述条件的进程,函数按照下面的规则选择其中的一个进程:

  1. 如果p标识的进程(由group_send_sig_info()的参数传递的描述符地址)满足所有的优先准则, 并因此而能接收信号,函数就选择该进程。

  2. 否则,函数通过扫描线程组的成员搜索一个适当的进程,搜索从接收线程组最后一个信号的进程(p->signal->curr_target)开始。

  3. 如果函数__group_complete_signal()成功地找到一个适当的进程,就开始向被选中的进程传递信号。

    首先,函数检查信号是否是致命的:

    如果是,通过向线程组中的所有轻量级进程发送SIGKILL信号杀死整个线程组。

    否则,函数调用signal_wake_up()函数通知被选中的进程:有新的挂起信号到来。

传递信号

确保进程的挂起信号得到处理内核所执行的操作。内核在允许进程恢复用户态下的执行之前,检查进程TIF_SIGPENDING标志的值。每当内核处理完一个中断或异常时,就检查是否存在挂起信号。

do_signal

为了处理非阻塞的挂起信号,内核调用do_signal()函数,它接收两个参数:

regs: 栈区的地址,当前进程在用户态下的寄存器内容存放在这个栈中

oldset:变量的地址,假设函数把阻塞信号的位掩码数组存放在这个变量中!如果没有必要保存为掩码数组,则把它设为空 如果中断处理程序调用do_signal(),则该函数立刻返回:

if((regs->xcs & 3)  != 3)
    return l;

如果oldset参数为NULL,函数就用current->blocked字段的地址对它初始化:

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

do_signal()函数的核心由重复调用dequeue_signal()函数的循环组成,直到在私有挂起信号队列和共享挂起信号队列中都没有非阻塞的挂起信号时,循环才结束。dequeue_signal()的返回码存放在signr局部变量中。如果值为0,意味着所有挂起的信号已全部被处理,并且do_signal()可以结束。只要返回一个非0值,就意味着挂起的信号正等待被处理,并且do_signal()处理了当前信号后又调用了dequeue_sigmal()

dequeue_signal

dequeue_signal()函数首先考虑私有挂起信号队列中的所有信号,并从最低编号的挂起信号开始。然后考虑共享队列中的信号。它更新数据结构以表示信号不再是挂起的,并返回它的编号。看do_signal()函数如何处理每一个挂起的信号,其编号由dequeue_signal()返回。首先,它检查current接收进程是否正受到其他一些进程的监控;在肯定的情况下,do_signal()调用do_notify_parent_cldstop()schedule()让监控进程知道进程的信号处理。然后,do_signal()把要处理信号的k_sigaction数据结构的地址赋给局部变量kaka =¤t->sig->action[signr-1];.根据ka的内容可以执行三种操作:忽略信号、执行缺省操作或执行信号处理程序。如果显式忽略被传递的信号,那么do_signal()函数仅仅继续执行循环,并由此考虑另一个挂起信号.

执行信号的缺省操作

如果ka->sa.sa_handler等于SIG_DFLdo_signal()就必须执行信号的缺省操作。唯一的例外是当接收进程是init时,在这种情况下,这个信号被丢弃:

if(current->pid == 1)
    continue;

如果接收进程是其他进程,对缺省操作是Ignore的信号进行处理也很简单:

if(signr==SIGCONT ll signr==SIGCHLD II signr==SIGWINCH || signr==SIGURG)
    continue;

缺省操作是Stop的信号可能停止线程组中的所有进程。为此,do_signal()把进程的状态都置为TASK_STOPPED,并在随后调用schedule()函数

if(signr==SIGSTOP ll signr==SIGTSTP || signr==SIGTTIN II signr==SIGTTOU) {
    if(signr != SIGSTOP && is_orphaned_pgrp(current->signal->pgrp))
        continue;
    do_signal_stop(signr);
}
12345

SIGSTOP与其他信号的差异比较微妙:SIGSTOP总是停止线程组,而其他信号只停止不在“孤儿进程组”中的线程组。

do_signal_stop

do_signal_stop()函数检查current是否是线程组中第一个被停止的进程,如果是,它激活"组停止":本质上,该函数把一个正数值赋给信号描述符中的group_stop_count字段,并唤醒线程组中的所有进程。所有这样的进程都检查该字段以确认正在进行“组停止”操作,然后把进程的状态置为TASK_STOPPED,并调用schedule()。如果线程组领头进程的父进程没有设置SIGCHLDSA_NOCLDSTOP标志,那么do_signal_stop()函数还要向它发送SIGCHLD信号。

缺省操作为Dump的信号可以在进程的工作目录中创建一个“转储”文件,这个文件列出进程地址空间和CPU寄存器的全部内容。do_signal()创建了转储文件后,就杀死这个线程组。

剩余18个信号的缺省操作是Terminate, 它仅仅是杀死线程组。为了杀死整个线程组,函数调用do_group_exit()执行彻底的“组退出”过程。

捕获信号

如果信号有一个专门的处理程序,do_signal()就函数必须强迫该处理程序执行。这是通过调用handle_signal()进行的:

handle_signal(signr,&info,aka,oldset,regs);
if(ka->sa.sa_flags& SA_ONESHOT)
    ka->sa.sa_handler = SIG_DFL;
return 1;

执行一个信号处理程序是件相当复杂的任务,因为在用户态和内核态之间切换时需要谨慎地处理栈中的内容。我们将正确地解释这里所承担的任务。

信号处理程序是用户态进程所定义的函数,并包含在用户态的代码段中。handle_signal()函数运行在内核态,而信号处理程序运行在用户态,这就意味着在当前进程恢复“正常”执行之前,它必须首先执行用户态的信号处理程序。此外,当内核打算恢复进程的正常执行时,内核态堆栈不再包含被中断程序的硬件上下文,因为每当从内核态向用户态转换时,内核态堆栈都被清空。而另外一个复杂性是因为信号处理程序可以调用系统调用,在这种情况下,执行了系统调用的服务例程以后,控制权必须返回到信号处理程序而不是到被中断程序的正常代码流。

Linux所采用的解决方法是把保存在内核态堆栈中的硬件上下文拷贝到当前进程的用户态堆栈中。用户态堆栈也以这样的方式被修改,即当信号处理程序终止时,自动调用sigreturn()系统调用把这个硬件上下文拷贝回到内核态堆栈中,并恢复用户态堆栈中原来的内容。

11-2说明了有关捕获一个信号的函数的执行流。

  1. 一个非阻塞的信号发送给一个进程。

  2. 当中断或异常发生时,进程切换到内核态。正要返回到用户态前,内核执行do_signal()函数,

  3. 这个函数又依次处理信号(通过调用handle_signal())和建立用户态堆栈(通过调用setup_frame()setup_rt_frame())。当进程又切换到用户态时,因为信号处理程序的起始地址被强制放进程序计数器中,因此开始执行信号处理程序。

当处理程序终止时,setup_frame()setup_rt_frame()函数放在用户态堆栈中的返回代码就被执行。这个代码调用sigreturn()rt_sigreturn()系统调用,相应的服务例程把正常程序的用户态堆栈硬件上下文拷贝到内核态堆栈,并把用户态堆栈恢复到它原来的状态(通过调用restore_sigcontext())。当这个系统调用结束时,普通进程就因此能恢复自己的执行。

setup_frame

为了适当地建立进程的用户态堆栈,handle_signal()函数或者调用setup_frame()(对不需要siginfo_t表的信号;或者调用setup_rt_frame()。为了在这两个函数之间进行选择,内核检查与信号相关的sigactionsa_flags字段的SA_SIGINFO标志值。setup_frame()函数接收四个参数,它们具有下列含义:

sig: 信号编号

ka: 与信号相关的k_sigaction表地址

oldset:掩码数组

regs: 栈区的地址

oldset:变量的地址,假设函数把阻塞信号的位掩码数组存放在这个变量中!如果没有必要保存为掩码数组,则把它设为空 如果中断处理程序调用do_signal(),则该函数立刻返回: setup_frame()函数把一个叫做帧(frame)的数据结构推进用户态堆栈中,这个帧含有处理信号所需要的信息,并确保正确返回到handle_signal()函数。一个帧就是包含下列字段的sigframe表(见图11-3):

pretcode:信号处理函数的返回地址

sig: 信号编号

sc:包含正好切换到内核态前用户态进程的硬件上下文

fpstate:可以用来存放用户态进程的浮点寄存器内容

被阻塞的实时信号的位数组

retcode:发出sig return系统调用的八字节代码

setup_frame()函数首先调用get_sigframe()计算帧的第一个内存单元,这个内存单元通常是在用户态堆栈中,因此函数返回值:(regs->esp - sizeof(struct sigframe))& 0xfffffff8。因为栈朝低地址方向延伸,所以通过把当前栈顶的地址减去它的大小,使其结果与8的倍数对齐,就获得了帧的起始地址。然后用access_ok宏对返回地址进行验证。如果地址有效,setup_frame()就反复调用__put_user()填充帧的所有字段。帧的pretcode字段初始化&__kernel_sigreturn,一些粘合代码的地址放在vsyscall页中。一旦完成了这个操作,就修改内核态堆栈的regs区,这就保证了当current恢复它在用户态的执行时,控制权将传递给信号处理程序。

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_frame()函数把保存在内核态堆栈的段寄存器内容重新设置成它们的缺省值以后才结束。现在,信号处理程序所需的信息就在用户态堆栈的顶部。setup_rt_frame()函数与setup_frame()非常相似,但它把用户态堆栈存放在一个扩展的帧中(保存在rt_sigframe数据结构中),这个帧也包含了与信号相关的siginfo_t表的内容。此外,该函数设置pretcode字段以使它指向vsyscall页中的__kernel_rt_sigreturm代码。

检查信号标志

建立了用户态堆栈以后,handle_signal()函数检查与信号相关的标志值。如果信号没有设置SA_NODEFER标志,在sigaction表中sa_mask字段对应的信号就必须在信号处理程序执行期间被阻塞:

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标志。然后,handle_signal()返回到do_signal()do_signal()也立即返回。

开始执行信号处理程序

do_signal()返回时,当前进程恢复它在用户态的执行。由于如前所述setup_frame()的准备,eip寄存器指向信号处理程序的第一条指令,而esp指向已推进用户态堆栈顶的帧的第一个内存单元。因此,信号处理程序被执行。

终止信号处理程序

信号处理程序结束时,返回栈顶地址,该地址指向帧的pretcode字段所引用的vsyscall 页中的代码:

__kernel_sigreturn:
    popl %eax
    movl $__NR_sigreturn,%eax
    int $0x80

因此,信号编号(即帧的sig字段)被从栈中丢弃,然后调用sigreturn()系统调用。sys_sigreturn()函数计算类型为pt_regs的数据结构regs的地址,其中pt_regs包含用户态进程的硬件上下文。从存放在esp字段中的值,由此而导出并检查帧在用户态堆栈内的地址:

frame =(struct sigframe *)(regs.esp - 8);
if(verify_area(VERIFY_READ,frame,sizeof(*frame)){
    force_sig(SIGSEGV,current);
    return 0;
}

然后,函数把调用信号处理程序前所阻塞的信号的位数组从帧的sc字段拷贝到currentblocked字段。结果,为信号处理函数的执行而屏蔽的所有信号解除阻塞。然后调用recalc_sigpending()函数。此时,sys_sigreturn()函数必须把来自帧的sc字段的进程硬件上下文拷贝到内核态堆栈中,并从用户态堆栈中删除帧,这两个任务是通过调用restore_sigcontext()函数完成的。

rt_sigqueueinfo()这样的系统调用需要与信号相关的siginfo_t表,如果信号是这种系统调用发送的,则其实现机制非常相似。扩展帧的pretcode字段指向vsyscall页面中的__kernel_rt_sigreturn代码,它依次调用rt_sigreturn()系统调用,其相应的sys_rt_sigreturn()服务例程把来自扩展帧的进程硬件上下文拷贝到内核态堆栈,并通过从用户态堆栈删除扩展帧以恢复用户态堆栈原来的内容。

系统调用的重新执行

内核并不总是能立即满足系统调用发出的请求,在这种情况发生时,把发出系统调用的进程置为TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE状态。如果进程处于TASK_INTERRUPTIBLE状态,并且某个进程向它发送了一个信号, 那么,内核不完成系统调用就把进程置成TASK_RUNNING状态。当切换回用户态时信号被传递给进程。当这种情况发生时,系统调用服务例程没有完成它的工作,但返回EINTR、ERESTARTNOHAND、ERESTART_RESTARTBLOCK、ERESTARTSYSERESTARTNOINTR错误码。

实际上,这种情况下用户态进程获得的唯一错误码是EINTR,这个错误码表示系统调用还没有执行完(应用程序的编写者可以测试这个错误码并决定是否重新发出系统调用)。内核内部使用剩余的错误码来指定信号处理程序结束后是否自动重新执行系统调用。表11-11列出了与未完成的系统调用相关的出错码及这些出错码对信号三种可能的操作产生的影响。在表项中出现的几个术语的含义如下:

当传递信号时,内核在试图重新执行一个系统调用前必须确定进程确实发出过这个系统调用。这就是regs硬件上下文的orig_eax字段起重要作用之处。让我们回顾一下中断或异常处理程序开始时是如何初始化这个字段的:

  • 中断:这个字段包含了只与中断相关的IRQ号减去256

  • 0X80异常:这个字段包含系统调用号

  • 其他异常:这个字段包含的值为-1

因此,orig_eax字段中的非负数意味着信号已经唤醒了在系统调用上睡眠的TASK_INTERRUPTIBLE进程。服务例程认识到系统调用曾被中断,并返回前面提到的某个错误码。

重新执行被未捕获信号中断的系统调用

如果信号被显式地忽略,或者如果它的缺省操作已被强制执行,do_signal()就分析系统调用的出错码,并如表11-11中所说明的那样决定是否重新自动执行未完成的系统调用。如果必须重新开始执行系统调用,那么do_signal()就修改regs硬件上下文,以便在进程返回到用户态时,eip指向int $0x80指令或sysenter指令,且eax包含系统调用号:

if(regs->orig_eax >= 0){
    if(regs->eax ==-ERESTARTNOHAND ll regs->eax ==-ERESTARTSYS II 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 $0x80sysreturn的长度都是两个字节,因此该函数从eip 中减去2,使eip指向引起系统调用的指令。

ERESTART_RESTARTBLOCK错误代码是特殊的,因为eax寄存器中存放了restart_syscall()的系统调用号,因此,用户态进程不会重新执行被信号中断的同一个系统调用。这个错误代码仅用于与时间相关的系统调用,当重新执行这些系统调用时,应该调整它们的用户态参数。一个典型的例子是nanosleep()系统调用。假设进程为了暂停执行20ms而调用了nanosleep(),而在10ms后出现了一个信号。如果像通常那样重新执行该系统调用(不调整其用户态参数),那么总的时间延迟会超过30ms。可以采用另一种方式,nanosleep()系统调用的服务例程把重新执行时所使用的特定服务例程的地址赋给currentthread_info结构中的restart_block字段,并在被中断时返回-ERESTART_RESTARTBLOCKsys_restart_syscall()服务例程只执行特定的nanosleep()的服务例程,考虑到原始系统调用的调用到重新执行之间有时间间隔,该服务例程调整这种延迟。

为所捕获的信号重新执行系统调用

如果信号被捕获,那么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;
    }
}

如果系统调用必须被重新开始执行,handle_signal()就与do_signal()完全一样地继续执行;否则,它向用户态进程返回一个出错码-EINTR

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

kill()系统调用

一般用kill(pid,sig)系统调用向普通进程或多线程应用发送信号,其相应的服务例程是sys_kill()函数。整数参数pid的几个含义取决于它的值:

  1. pid > 0 :把sig信号发送器pid等于pid的进程所属进程组

  2. pid = 0: 把sig信号发送到与调用进程同组的进程的所有进程组

  3. pid = -1: 把信号发送到所有进程除了pid为0,1和当前进程以外的所有进程组

  4. pid < -1:把信号发送到进程组-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()系统调用能发送任何信号,即使编号在32~64之间的实时信号。然而,我们在前面“产生信号”一节已看到,kill()系统调用不能确保把一个新的元素加入到目标进程的挂起信号队列,因此,挂起信号的多个实例可能被丢失。实时信号应当通过rt_sigqueueinfo()系统调用进行发送。

tkill()和tgkill()系统调用

tkill()tgkill()系统调用向线程组中的指定进程发送信号。所有遵循POSIX标准的pthread库的pthread_kill()函数,都是调用这两个函数中的任意一个向指定的轻量级进程发送信号。

tkill()系统调用需要两个参数:信号接收进程的pid PID和信号编号sigsys_tkill()服务例程为siginfo表赋值、获取进程描述符地址、进行许可性检查并调用specific_send_sig_info()发送信号。

tgkill()系统调用和tkill()有所不同,tgkill()还需要第三个参数:信号接收进程所在线程组的线程组ID(tgid)sys_tgkill()服务例程执行的操作与sys_tkill()完全一样,不过还要检查信号接收进程是否确实属于线程组tgid。这个附加的检查解决了在向一个正在被杀死的进程发送消息时出现的竞争条件的问题:如果另外一个多线程应用正以足够快的速度创建轻量级进程,信号就可能被传递给一个错误的进程。因为线程组ID在多线程应用的整个生存期中是不会改变的,所以系统调用tgkill()解决了这个问题。

改变信号的操作

sigaction(sig,act,oact)系统调用允许用户为信号指定一个操作。当然,如果没有自定义的信号操作,那么内核执行与传递的信号相关的缺省操作。相应的sys_sigaction()服务例程作用于两个参数:sig信号编号和类型为old_sigactionact表(表示新的操作)。第三个可选的输出参数oact可以用来获得与信号相关的以前的操作。(old_sigaction数据结构包括与sigaction结构相同的字段,只是字段的顺序不同)。这个函数首先检查act地址的有效性。然后用*act相应的字段填充类型为k_sigactionnew_ka局部变量的sa_handler、sa_flagssa_mask字段:

_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);
1234

函数还调用do_sigaction()把新的new_ka表拷贝到current->sig->action的在sig-1位置的表项中(信号的编号大于在数组中的位置,因为没有0信号):

k =&current->sig->action[sig-1];
if(act){
    *k =*act;
    sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL)I sigmask(SIGSTOP));
    if(k->sa.sa_handler== SIG_IGN  II (k->sa.sa_handler == SIG_DFL && (sig==SIGCONT lI sig==SIGCHLD lI sig==SIGWINCH II 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〉;
    }
}
1234567891011121314

POSIX标准规定,当缺省操作是“Ignore”时,把信号操作设置成SIG_IGNSIG_DFL 将引起同类型的的任一挂起信号被丢弃。此外还要注意,对信号处理程序来说,不论请求屏蔽的信号是什么,SIGKILLSIGSTOP从不被屏蔽。sigaction()系统调用还允许用户初始化表sigactionsa_flags字段。在表11-6 (本章前面)中,我们列出了这个字段的可能取值及其相关的含义。

检查挂起的阻塞信号

sigpending()系统调用允许进程检查挂起的阻塞信号的集合,也就是说,检查信号被阻塞时已产生的那些信号。相应的服务例程sys_sigpending()只作用于一个参数set,即用户变量的地址,必须将位数组拷贝到这个变量中:

sigorsets(&pending, &current->pending.signal, &current->signal->shared_pending.signal);
sigandsets(&pending, &current->blocked, &pending);
copy_to_user(set, &pending, 4);
123

修改阻塞信号的集合

sigprocmask()系统调用允许进程修改阻塞信号的集合。这个系统调用只应用于常规信号(非实时信号)。相应的sys_sigprocmask()服务例程作用于三个参数:

  • oset:进程地址空间的一个指针,指向存放以前为掩码的一个位数组

  • set: 进程地址空间的一个指针指向包含新位掩码的位数组

  • how: 一个标志可以有以下列的一个值

    • SIG_BLOCK:*set位掩码数组中的信号,指定必须加到阻塞信号的位掩码

    • SIG_UNBLOCK:*set位掩码数组中的信号,指定必须从阻塞信号的位掩码数组中删除的信号

    • SIGG_SETMASK:*set位掩码数组中的信号,指定阻塞信号新的位掩码数组

sys_sigprocmask()调用copy_from_user()set参数所指向的值拷贝到局部变量new_set中,并把current标准阻塞信号的位掩码数组拷贝到old_set局部变量中。然后根据how标志来指定这两个变量的值:

if(copy_from_user(&new_set, set, sizeof(*set)))
    return -EFAULT;
new_set &=~(sigmask(SIGKILL) I 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()系统调用把进程置为TASK_INTERRUPTIBLE状态,当然这是把mask 参数指向的位掩码数组所指定的标准信号阻塞以后设置的。只有当一个非忽略、非阻塞的信号发送到进程以后,进程才被唤醒。相应的sys_sigsuspend()服务例程执行下列这些语句:

mask &=~(sigmask(SIGKILL) | sigmask(SIGSTOP));
saveset = current->blocked;
siginitset(&current->blocked, mask);
recalc_sigpending(current);
regs->eax =-EINTR;
while (1)(
    current->state = TASK_INTERRUPTIBLE;
    schedule(   );
    if(do_signal(regs, &saveset))
        return -EINTR;
}

schedule()函数选择另一个进程运行。当发出sigsuspend()系统调用的进程又开始执行时,sys_sigsuspend()调用do_signal()函数来传递唤醒了该进程的信号。如果do_signal()的返回值为1,则不忽略这个信号。因此,这个系统调用返回-EINTR出错码后终止。sigsuspend()系统调用可能看似多余,因为sigprocmask()sleep()的组合执行显然能产生同样的效果。但这并不正确:这是因为进程可能在任何时候交错执行,你必须意识到调用一个系统调用执行操作A,紧接着又调用另一个系统调用执行操作B,并不等于调用一个单独的系统调用执行操作A,然后执行操作B

在这种特殊情况中,sigprocmask()可以在调用sleep()之前解除对所传递信号的阻塞。如果这种情况发生,进程就可以一直停留在TASK_INTERRUPTIBLE状态,等待已被传递的信号。另一方面,在解除阻塞之后、schedule()调用之前,因为其他进程在这个时间间隔内无法获得CPU,因此,sigsuspend()系统调用不允许信号被发送。

实时信号的系统调用

因为前面所提到的系统调用只应用到标准信号,因此,必须引入另外的系统调用来允许用户态进程处理实时信号。实时信号的几个系统调用(rt_sigaction()、rt_sigpending()、rt_sigprocmask()rt_sigsuspend())与前面描述的类似,因此不再进一步讨论。

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
深入理解Linux内核》是一本经典的Linux内核书籍,由Daniel P. Bovet和Marco Cesati合著。该书深入剖析了Linux内核的各个方面,包括进程管理、内存管理、文件系统、网络协议栈等等,是学习Linux内核的重要参考资料。该书的目录包括: 第1部分 内核基础 第1章 操作系统概述 第2章 Linux内核概述 第3章 进程管理 第4章 进程间通信 第5章 系统调用 第6章 中断和异常 第7章 内核数据结构 第8章 内存管理 第2部分 进程管理 第9章 进程描述符 第10章 调度 第11章 进程地址空间 第12章 进程创建 第13章 进程结束 第3部分 进程间通信 第14章 信号 第15章 管道 第16章 FIFO和消息队列 第17章 信号量 第18章 共享内存 第19章 内存映射 第4部分 系统调用 第20章 系统调用实现 第21章 标准C库 第5部分 中断和异常 第22章 中断处理 第23章 硬件设备 第24章 字符驱动程序 第25章 块驱动程序 第26章 网络设备驱动程序 第6部分 内存管理 第27章 物理内存管理 第28章 虚拟内存管理 第29章 内存映射 第7部分 文件系统 第30章 文件系统概述 第31章 Linux虚拟文件系统(VFS) 第32章 文件系统实现 第33章 文件系统mount和unmount 第8部分 网络 第34章 网络概述 第35章 套接字接口 第36章 TCP/IP协议栈 第37章 名字和地址解析 第38章 套接字实现 第39章 TCP/IP实现 第40章 网络设备驱动程序 该书内容详实,通俗易懂,适合有一定编程经验的读者学习。如果您想深入了解Linux内核,这本书是必不可少的参考资料。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值