linux 进程学习笔记-进程信号sigal

信号(或软中断)是在软件层次上对中断的一个模拟,其运行在“用户空间”,一个进程对另外一个或几个进程通过发送信号来实现异步通信。当接收进程接收到信号后,其可以注册一下处理函数来说对这些信号进行处理(也可以选择忽略该信号或者采用系统默认的处理方式)。 

我看可以通过“kill -l”命令来查看系统支持的信号,比如SIGKILL它表示需要终止一个进程,它有一个系统特定的信号值9。这些值都定义在signal.h中 

在signal.h中有个叫做_NSIG(一般为64)的宏其表示该系统支持的最多信号数,而SIGRTMAX (_NSIG-1)则表示信号的最大值,而与之相对应的SIGRTMIN却不是表示信号的最小值,其表示可靠信号的最小值。按照信号的可靠性,可将信号分为“可靠信号(实时信号)”和“不可靠信号(非实时信号)”,以SIGRTMIN为界限,值小于SIGRTMIN的信号为不可靠信号,其继承于早期的UNIX系统,SIGRTMIN到SIGRTMAX之间的为可靠信号。 

不可靠信号有两点需要注意:一是其在执行完自定义信号处理函数后会将信号处理方式重置为系统默认方式(如果要多次处理同一个信号,则要多次按照信号处理函数,不过好像后期的Linux对这点做了改进而无需重新安装)。二是不可靠信号不支持排队,其有可能会出现信号丢失的情况。 

  

假设进程A向进程B发送信号,那么一个很简单的信号流程是:进程A调用某个函数产生某个信号,信号被发送到进程B,然后进程B对该信号进行处理。 

  

信号的产生和发送: 

我们用结构 


typedef struct siginfo {
//… some info of a signal
} signifo_t;
来表示一个信号的相关信息(比如信号值,由谁发送的等等)
用结构
struct sigqueue {
         struct sigqueue *next;
         siginfo_t info;
};
来表示有n个siginfo_t结构构成的队列。
再假设有这样一个数据结构:
typedef struct {
         unsigned long sig[_NSIG_WORDS];
} sigset_t;

其中_NSIG_WORDS表示的值一般为2。我们知道long为32位,那么sig[_NSIG_WORDS]则为64位,其可以表示64个位的集合,很容易地就可以将集合中的元素其映射到_NSIG个信号上:如果该位为1则表示有着对应值的信号在该集合中。(实际上,因为没有0号信号,所以信号值和位值刚好错开一位) 

OK,我们如何用这些数据结构来表示哪些信号被发送到了某个进程呢,很简单地,如果进程描述符(PCB)中有sigset_t类型的字段的话,将该字段的对应位置1就可以了,而这些信号的详细信息如果能被保存在sigqueue类型的字段中的话就更完美了。实际上PCB就是这么做的,只不过其将sigset_t和sigqueue合并成了一个称为sigpending的结构: 


struct sigpending {
         struct sigqueue *head, **tail;
         sigset_t signal;
};

所以PCB的sigpending字段表示被发送到了该进程但还没有来得及处理的信号(被挂起的)。 

从这里我们可以看出,所谓的“信号的产生”实际上就是某个进程请求内核去将另外一个进程的sigpending字段设置成相应的值罢了,并将相应的其他信息插入到sigqueue队列中。 

设置sigpending时有一个比较有意思的地方:我们知道,可以用sigset_t中的某一个位表示对应的信号是否被发送到了该进程,其只能简单地表示“有(1)”或“无(0)”,如果某个信号被发送了多次的话,则无法在sigset_t中体现,但可以通过sigqueue队列来体现,实际上,对应不可靠信号(值小于SIGRTMIN的),当设置sigpending时,如果内核发现signal_t相应位已经被置1的话,则内核会丢弃该信号,这也就是为什么说该信号是“不可靠的”;而对应可靠信号,即便signal_t相应位已经被置1,此次信号的相关数据(siginfo_t)仍然会被包装成一个队列节点而被插入到队列中去。 

另外从编程角度,要发送一个信号,一般调用下面这些函数: 


int kill(pid_t pid, int sig); 
向进程或进程组发送某个信号
int raise(int sig); 
向调用进程自身发信号
int sigqueue(pid_t pid, int signo, const union sigval value); 
与kill类似,但多了一些附加信息
unsigned alarm(unsigned seconds);
指定的时间后向调用进程发送一个SIGALRM信号(闹钟信号)
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
与alarm类似同样发送SIGALRM信号,但支持更详细的设定,更多参考这里
void abort(void);
向进程发送SIGABRT信号,让进程中止。



信号的接收和处理 

进程从内核态返回用户态时,内核会去检查是否有信号被发送到了该进程,如果有,则让该进程该处理该信号。进程对于一个信号可以有三种处理方式: 

1,显示地忽略该信号: 

   其中SIGKILL和SIGSTOP是不能被忽略的,其必须采用下面的第2种方式。 

2,采用系统默认的处理方式进行处理: 

   默认处理可以有这么几种方式:abort; abort并dump; ignore; stop(暂停进程,置为 TASK_STOPPED状态); continue(继续执行,与stop向对应,置为TASK_RUNNING状态) 

3,调用进程注册的信号处理函数进行自定义处理。  

当处理完毕后,内核会改变sigpending中的相关值以表示处理完毕了(比如将sigset_t相应位置0,从sigqueue中删除相关元素等) 

  

至于如何注册信号处理函数,Linux有下面两种方式: 

typedef void (*sighandler_t)(int); 

sighandler_t signal(int signum, sighandler_t handler); 

调用signal()函数,其表示从现在开始我关心那些信号值为signum的信号,如果该信号发送到笨进程的话,请调用handler去处理它。其主要用于非可靠信号。 

比如下面这个DEMO,去自定义处理SIGINT (按ctrl-c终止进程时会发送该信号) 


#include <stdio.h>
#include <signal.h>
#include <unistd.h>
 
#define SIG2CATCH 2
 
void handler(int signum)
{
if(signum == SIG2CATCH)
{
printf("you wanna kill me with ctrl-c ?! no way\n");
}
}
 
int main()
{
printf("app start ...\n");
 
signal(SIG2CATCH, handler);
 
while(1)
{
sleep(1);
}
 
printf("app end ...\n");
 
return 0;
}

运行程序,并试图用ctrl-c结束程序后,SIGINT(值为2)信号会发送到该进程,默认情况下其会终止程序,但这里采用了自定义处理函数,其仅仅打印出一段文字(可以用ctrl-z结束) 

  

另一个注册方法是: 

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

其主要用于可靠信号,并且可以在sigaction中包含附件信息。详细的参考这里。 

 

posted on 2015-11-23 14:29  zyz913614263  阅读(1022)  评论(0编辑  收藏  举报

导航