一,信号的基本概念
1,什么是信号?
日常生活中,当我们走到马路上时,看到的绿灯是一种信号,它能提示我们怎样安全的过马路。又比如,新学期开始学校给每个班发的课表也是一种信号,它能提示同学们在适当的时间地点去上相应的课程而不是虚度光阴……生活中其实我们忽略了很多信号,正是由于这些信号的存在,才使得我们的生活方便而有序。
总结一下你会发现信号是什么,信号就是当你看到它是知道它是什么,并且知道看到信号之后应该做什么,至于你遵不遵守就是你自己的事了, 计算机中的信号也不例外。
2,计算机中的信号
同日常生活中的信号一样,计算机在收到信号之后,并不一定会立即处理它,它会将收到的信号记录在其相应进程的PCB中的信号部分,等待合适的时间再去处理它。换句话说,一个进程是否收到信号,需要查看其进程PCB中的信号信息,给进程发信号实则是向进程PCB中写入信号信息。同时,我们的操作系统是很智能的,当任何一个进程接收到任何一个信号时,操作系统会自动地知道各信号应作何处理。
查看系统定义的信号列表:kill -l
注意列表中不是64个信号而是62个信号。
1-31:普通信号
34-64:实时信号
———-以下内容只针对普通信号进行讲解———-
每个信号都有一个宏名称和一个信号编号,这些宏定义可以在signal.h中找到,这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:
查看命令:man 7 signal
各种信号产生的原因:
[http://blog.sina.com.cn/s/blog_7ee076050101bz5v.html]
3,普通信号的存储:
试想一下,如果你是操作系统的设计人员,你会用什么来存储这31个信号呢?当然,设计人员都很聪明,他们采用的是位图存储,每一个bit位存储一个相应信号,31个信号只需4个字节(仅一个整形的大小)即可存储,bit位的位置表示信号的编号,bit位的值则用来表示是否收到该信号(0—-未收到该信号,1—-收到该信号)。
信号量的本质就是修改PCB中管理信号变量中的某个bit位。
4,信号产生的主要条件
背景知识:
前台进程:基本不用和用户交互,优先级稍微低一些(运行前台进程: ./test.c)
后台进程:需要和用户进行交互,需要较高的相应速度,优先级别高(运行后台进程: ./test.c &)
1〉用户在终端按下某些组合键时,终端驱动程序会发送信号给前台进程。例如:
Ctrl-C产生SIGINT信号(2号信号,终止前台进程),
Ctrl-\产生SIGQUIT信号(3号信号,捕捉信号),
Ctrl-Z产生SIGTSTP信号(20号信号,可使前台进程停止)。
2〉硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
例如:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU(内存管理单元)会产生异常,内核将这个异常解释SIGSEGV信号发送给进程。
3〉一个进程调用kill(2)函数可以发送信号给另一个进程。
可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。 如果不想按默认动作处理信号,用户程序可以调用sigaction(2)函数告诉内核如何处理某种信号。
5,处理信号的方法
1〉忽略此信号(大多数信号都可以使用该方法处理)。
特例:SIGKILL , SIGSTOP
原因:它们向超级用户提供一种使进程终止或停止的方法,同时,如果忽略某些由硬件异常产生的信号,则进程的行为是未定义的。
2〉执行该信浩的默认处理动作(与信号的种类有关,大多数信号会直接终止该进程)。
3〉用信号捕捉函数为该信号指定自定义动作。
特例:不能捕捉SIGKILL , SIGSTOP信号
原因:为了防止非法用户的恶意入侵使得进程永远杀不掉。
信号捕捉函数:修改信号的默认动作,有些信号是不能被捕捉的,如9号信号SIGKILL等。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:signum:信号编号 handler:指向怎样捕捉该信号的函数
返回值:signal函数的返回值是一个函数指针,成功返回返回以前的处理 配置,失败返回错误码对应的错误提示。
makefile:
signal:signal.c
2 gcc -o signal signal.c
3 .PHONY:clean
4 clean:
5 rm -f signal
signal.c:
#include<unistd.h>
#include<stdio.h>
#include<signal.h>
void myhandler(int signo)
{
printf("got SIGINT\n");
}
int main()
{
signal(2,myhandler);//捕捉2号信号SIGINT:Ctrl C
while(1)
{
sleep(1);
}
return 0;
}
运行结果:
捕捉2号信号:
杀死进程(此时ctrl C不再能够结束进程):
进程结束:
改进:signal.c
#include<unistd.h>
#include<stdio.h>
#include<signal.h>
typedef void (*sighandler_t)(int);//函数指针
sighandler_t old_handler = NULL;
void myhandler(int signo)
{
printf("got SIGINT\n");//第一次按ctrlC捕捉2号信号SIGINT
signal(2, old_handler);//恢复默认的处理,再按ctrl C的话,就会终止程序
}
int main()
{
old_handler = signal(2,myhandler);//捕捉2号信号SIGINT:Ctrl C
while(1)
{
sleep(1);
}
return 0;
}
运行结果:
二,产生信号
1,通过终端按键产生信号
上面说过,SIGINT(ctrl C)的默认处理动作是终止进程,SIGQUIT(ctrl )的默认处理动作是终止进程并且Core Dump,那么什么是Core Dump呢?
Core Dump:核心转储。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。
代码验证:
1>首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:ulimit -c 1024
2>然后写一个死循环程序:
makefile:
.PHONY: all
all:signal sig
signal:signal.c
gcc -o $@ $^
sig:sig.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f signal sig
sig.c:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("pid is %d\n",getpid());
while(1);
return 0;
}
运行结果:
ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具 有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。
2,软硬件异常产生信号
软硬件异常信号 其他信号 SIGCHLD or SIGCLD 子进程结束时, 父进程会收到这个信号。 如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。
3,调用系统函数想进程发信号
1〉首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号。
3603是sig进程的id。之所以要再次回车才显示“段错误”是因为在3603程终止掉 之前已经回到了Shell提示符等待用户输入下一条令,Shell不希望“段错误”信息和用户的输入交错在一起,所以等用户输入命令之后才显示。
指定某种信号的kill命令可以有多种写 法,上面的命令还可以写成:
kill -SIGSEGV 3603或kill -11 3603, 11是信号SIGSEGV的编号。
以往遇 到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误。
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
#include <signal.h>
int kill(pid_t pid, int signo);//给任意进程发信号
int raise(int signo);//自己给自己发任意信号
这两个函数都是成功返回0,错误返回-1。
kill函数实例:
makefile:
1 .PHONY: all
2 all: signal sig kill
3 signal:signal.c
4 gcc -o $@ $^
5 sig:sig.c
6 gcc -o $@ $^
7 kill:kill.c
8 gcc -o $@ $^
9 .PHONY:clean
10 clean:
11 rm -f signal sig kill
kill.c:
7 #include<stdio.h>
8 #include<signal.h>
9 #include<stdlib.h>
10
11 static void usage(const char* proc)//.kill用法
12 {
13 printf("usage:%s sig pid\n",proc);
14 }
15 int main(int argc,char* argv[])
16 {
17 if(argc != 3)
18 {
19 usage(argv[0]);
20 return 1;
21 }
22 int pid = atoi(argv[2]);//进程号
23 int sig = atoi(argv[1]);//信号编号
24 kill(pid,sig);//用当前进程给pid号进程发送sig号信号
25 return 0;
26 }
1〉首先在前台运行上面的死循环程序
2〉使用kill函数向死循环进程发送2号信号终止该进程
raise函数的实例:
makefile:
1 .PHONY: all
2 all: signal sig kill raise
3 signal:signal.c
4 gcc -o $@ $^
5 sig:sig.c
6 gcc -o $@ $^
7 kill:kill.c
8 gcc -o $@ $^
9 raise:raise.c
10 gcc -o $@ $^
11 .PHONY:clean
12 clean:
13 rm -f signal sig kill raise
raise.c:
7 #include<stdio.h>
8 #include<signal.h>
9 #include<stdlib.h>
10 #include<unistd.h>
11
12 void myhandler()
13 {
14 printf("this is a raise test:pid is %d\n",getpid());
15 }
16 int main()
17 {
18 signal(2,myhandler);
19 while(1)
20 {
21 raise(2);//给自己发2号信号
22 sleep(1);
23 }
24 return 0;
25 }
运行中用kill命令杀死该进程:kill -9 4009
abort函数使当前进程接收到信号而异常终止。
#include <stdlib.h>
void abort(void);//自己给自己发终止信号
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
makefile:
1 .PHONY: all
2 all: signal sig kill raise abort
3 signal:signal.c
4 gcc -o $@ $^
5 sig:sig.c
6 gcc -o $@ $^
7 kill:kill.c
8 gcc -o $@ $^
9 raise:raise.c
10 gcc -o $@ $^
11 abort:abort.c
12 gcc -o $@ $^
13 .PHONY:clean
14 clean:
15 rm -f signal sig kill raise abort
abort.c:
7 #include<stdio.h>
8 #include<unistd.h>
9 #include<stdlib.h>
10 #include<signal.h>
11
12 int count = 0;
13 void myhandler(int sig)
14 {
15 printf("count is %d,sig is:%d\n",count++,sig);
16 }
17 int main()
18 {
19 for(int i = 1; i <= 31; ++i)
20 {
21 signal(i,myhandler);
22 }
23 while(1)
24 {
25 sleep(1);
26 abort();//使当前进程接收到信号而异常止
27 }
28 return 0;
29 }
4,由软件条件产生信号
由软件条件产生的信号:SIGPIPE,SIGALRM(主要介绍)
alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
返回值:是0或者是以前设定的闹钟时间还余下 的秒数
如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
makefile:
1 .PHONY: all
2 all: signal sig kill raise abort alarm
3 signal:signal.c
4 gcc -o $@ $^
5 sig:sig.c
6 gcc -o $@ $^
7 kill:kill.c
8 gcc -o $@ $^
9 raise:raise.c
10 gcc -o $@ $^
11 abort:abort.c
12 gcc -o $@ $^
13 alarm:alarm.c
14 gcc -o $@ $^
15 .PHONY:clean
16 clean:
17 rm -f signal sig kill raise abort alarm
alarm.c:
7 #include<stdio.h>
8 #include<signal.h>
9 #include<stdlib.h>
10 #include<unistd.h>
11
12 int count = 0;
13 void myhandler()
14 {
15 printf("count is %d\n",count);
16 exit(1);
17 }
18 int main()
19 {
20 signal(SIGALRM,myhandler);
21 alarm(1);//设置闹钟
22 while(1)
23 {
24 count++;
25 }
26 return 0;
27 }
这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。