ECF中的信号、并发与跳转详解

本节开始我们将针对信号展开讨论

在之前的学习中,我们已经看到了硬件与软件通过合作来提供基本的异常机制,也看到了操作系统通过利用异常来支持进程的上下文切换的异常控制流形式。本节我们将讨论一种软件形式的高层异常,即linux 信号

每个信号其实都可以理解为一条消息,用于通知进程系统中发生了什么事。

底层的硬件异常是由内核异常处理程序去处理的,正常情况下这种处理对于用户进程来讲是不可见的。但信号会直接通知用户进程发生了某些异常。

如进程试图除以零,内核将发送给它一个SIGFPE信号;如果执行非法指令,将收到SIGILL信号;非法内存引用,将收到SIGSEGV信号。

还有部分较高层软件事件。Ctrl-c:SIGINT,子进程终止:SIGCHLD。进程也可以通过向另一个进程发送SIGKILL信号去强制终止它。

传送一个信号到目标进程主要分为两个过程,即发送信号与接收信号。 内核可能因为系统事件的发生或者进程中kill函数的调用发送信号给目的进程。当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略信号,终止或者通过信号处理程序去捕捉这个信号(用户层函数)。

发出而没有被接收的信号被称为待处理信号(pending signal)。 一个进程只会有一种类型的待处理信号,这是因为内核为每个进程在pending位向量中维护着待处理信号的集合,若传输了一个类型为k的信号,则内核会设置pending中的第k位,剩余发送的同类型信号只会被简单地直接丢弃。

进程可以有选择的阻塞接受某种信号。 内核同样会在blocked位向量中维护着被阻塞的信号集合。当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。

下面我们着重讲一下第一个步骤,发送信号。

所有向进程发送信号的机制都是基于一个非常重要的概念:进程组(process group)。

每个进程都属于且只属于一个特定的进程组。 进程组可以通过一个正整数ID去标识。

可以通过getpgrp函数返回当前进程的进程组ID:

#include 

pid_t getpgrp(void);

默认情况下,子进程与父进程同属一个进程组。

一个进程可以通过使用set-pgid来改变自己或其他进程的进程组:

#include 

int set(pid_t pid,pid_t pgid);

该函数可以将pid进程的进程组改为pgid。如果pid是0,则默认使用当前进程的pid;如果pgid为0,则默认使用进程的ID号作为进程组ID。

Unix shell往往有自己内置的kill命令,此处我们以完整路径/bin/kill来讲解通过kill程序发送信号。

格式如下:

linux> /bin/kill -9 15213

该命令将发送信号9给进程15213。如果进程pid为负,信号9将被发送给进程组15213中的每个进程。

linux> /bin/kill -9 -15213

Unix Shell使用作业(job)这个抽象概念来表示为对一条命令行求值而创建的进程。

在任意时刻,至多只有一个前台作业和0个或多个后台作业。

shell会为每个作业创建一个独立的进程组,进程组ID通常取自作业中父进程中的一个。

进程还可以直接通过kill函数去发送信号。

#include 
#include 

int kill(pid_t pid,int sig);

sig非常好理解,即发送信号号码。

对于pid,我们有如下情况:

如果pid>0,那么该信号会被发送给进程pid;
如果pid=0,那么该信号会被发送给调用进程所在进程组的每个进程;
如果pid<0,那么该信号会被发给-pid进程组的所有进程。

也可以使用alarm函数发送信号:

#include 

unsigned int alarm(unsigned int secs);

alarm函数的作用是安排内核在secs秒后发送一个SIGALRM信号给调用进程。可以用alarm实现server与client的定时通信,如puase()函数可以因为接受到信号14而中断(即SIGALRM)。

当内核把某个进程从用户态切换到内核态时(系统调用返回或上下文切换),处理逻辑应该是先检查该进程的未被阻塞的待处理信号的集合(pending & ~blocked)。

此时如果该集合为空,则内核将控制传递到该进程逻辑控制流中的下一条指令。

此时如果集合非空,内核从pending中从小到大选择某个信号k,并且强制该进程接受信号k。这个信号将会触发进程采取某种行为。当该行为被完成了过后,控制就会被传递回该进程逻辑控制流中的下一条指令。

信号类型的默认行为大致有以下几种:

进程终止

进程终止并转储内存

进程停止(挂起)直到被SIGCONT信号重启

进程忽略该信号

除了SIGSTOP与SIGKILL两个信号的默认行为不能修改外,进程可以通过signal函数修改和信号相关联的默认行为。

#include 
typedef void(* sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

此处signum是信号k,handler如果是SIG_IGN,则默认忽略类型为signum的信号;handler如果是SIG_DFL,那么该类型信号的处理将恢复默认行为。

如果上述两者都不是,那么handler就是用户定义的函数地址。

这个函数被称为信号处理程序,通过把处理程序地址传入signal函数从而改变接收信号默认行为的过程叫做设置信号处理程序。

调用信号处理程序被称为捕获信号,执行信号处理程序被称为处理信号。

信号处理程序可以被别的信号处理程序中断。

练习题 8.7

#include "all.h"
void handler(int sig) return;

unsigned int snooze(unsigned int secs){
	unsigned int re = sleep(secs);
	printf("Slept for %d of %d secs.\n",secs-re,secs);
	return re;
}

int main(int argc,char** argv){
	if(argc!=2){
		exit(0);
	}
	if(signal(SIGINT,handler)==SIG_ERR) unix_error("signal error\n");
	(void)snooze(atoi(argv[1]));
	exit(0);
}

请读者自行领悟。

你可能感兴趣的:(深入理解计算机系统)