信号的基本概念
信号是Linux系统为了响应某些状况而产生的事件。进程收到信号后应采取相应的动作。
会引发信号的几种情况:
- 键盘事件
- 非法内存
- 硬件故障
- 环境切换
系统的信号列表
kill -l:
不可靠信号:1-31号信号—-非实时信号 :会出现信号丢失
可靠信号:34-64号信号—-实时信号:不会出现信号丢失
信号处理常见方式
- 忽略此信号
- 执行该信号的默认处理动作
- 捕获并处理,当信号来了,执行我们自己写的代码。
注意:SIGKILL和SIGSTOP信号不能忽略,也不能捕获。
每个信号的缺省处理:
man 7 signal
注册信号
或者
handler :
1. 自己写函数
2. SIG_IGN 忽略
3. SIG_DFL 缺省
6 void handler(int s)
7 {
8 printf("recv %d\n",s);
9 }
10 int main()
11 {
12 __sighandler_t old;
13 old=signal(SIGINT,handler);//按ctrl+c 发送一个信号
14 if(old==SIG_ERR)
15 perror("signal"),exit(1);
16 for( ; ;)
17 {
18 printf("hehe\n");
19 sleep(1);
20 }
21 }
按ctrl+c就会发送一个2号信号。
10 int main()
11 {
12 __sighandler_t old;
13 old=signal(SIGINT,handler);//按ctrl+c 发送一个信号
14 if(old==SIG_ERR)
15 perror("signal"),exit(1);
16 printf("before while\n");
17 while(getchar()!='\n')
18 ;
19 printf("after while\n");
20
21 signal(SIGINT,old);//恢复到旧的状态,即按ctrl+c就会退出。在这之前,ctrl+c都只是发送一个信号,不退出。
22
23 printf("before for\n");
24 for( ; ;)
25 ;
26
27 printf("after for\n");
28 }
发送信号:
kill -信号 pid
ctrl + c
ctrl + \
给pid进程发送signum信号
int kill(pid_t pid , int signum);//可以给任意进程发送任意信号
//给任何进程或进程组发任何信号,成功返回0,失败返回-1设errno
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
1 //kill 实现给进程发送信号
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5 #include<signal.h>
6
7 int main()
8 {
9 int pid;//接受信号的进程
10 int num;//发送的信号
11 while(1)
12 {
13 printf("pid:");
14 scanf("%d",&pid);
15 printf("signum:");
16 scanf("%d",&num);
17
18 kill(pid,num);
19 }
20
21 }
raise(int signum); // 给本进程发送信号
//给调用的线程或进程发信号,如果发送的信号被handler处理了,会在handler返回后再返回,成功返回0,失败返回非0
#include<signal.h>
int raise(int sig);
在单线程程序中等价于 kill(getpid,sig)
在多线程程序中等价于pthread_kill(pthread_self(), sig);
alarm //定时
setitimer()
//现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,
//用getitimer来得到定时器的状态,这两个调用的声明格式如下:
int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。
三个计时器由参数which指定,如下所示:
TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。
ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。
ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。
定时器中的参数value用来指明定时器的时间,其结构如下:
struct itimerval {
struct timeval it_interval; /* 下一次的取值 */
struct timeval it_value; /* 本次的设定值 */
};
该结构中timeval结构定义如下:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒,1秒 = 1000000 微秒*/
};
sigqueue()
//将信号和数据排好发给指定进程,成功返回0,失败返回-1设errno
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
int sival_int;
void *sival_ptr;
};
pid //进程的编号(给谁发信号)
sig //信号值/信号的名称(发送什么样的信号)
value //联合类型的附加数据(伴随什么样的数据)
信号在内核中的表示
信号其他相关常见概念:
- 信号抵达:进程执行信号处理函数的动作被称为信号抵达(delivery)。
- 信号未决:从信号产生到信号抵达之间的状态称之为信号未决(pending)。
- 信号阻塞:内核可以阻塞(block)信号,当一个被阻塞的信号产生时,就是出于未决状态。
注意:阻塞和忽略不同:只要信号被阻塞就不会抵达,忽略是在抵达后,可选的一种处理动作。
信号集
未决和阻塞标志可以用相同的数据类型sigset_t 来存储,sigset_t称为信号集,这个类型可以表示 每个信号的“有效”和“无效”状态。
信号集操作函数
//这些都是信号集操作函数
//sigemptyset()/sigfillset()/ sigaddset()/ sigdelset() 成功返回0,失败返回-1设errno
//如果signum是指定信号集的一员,sigismember()返回1,不是返回0,失败返回-1设errno。
#include <signal.h>
int sigemptyset(sigset_t *set); //清空信号集
int sigfillset(sigset_t *set); //填满信号集
int sigaddset(sigset_t *set, int signum); //添加信号到信号集
int sigdelset(sigset_t *set, int signum); //删除信号集的信号
int sigismember(const sigset_t *set, int signum); //判断信号是否属于信号集
sigprocmask()
//sigprocmask()只适用与单线程进程,多线程的参考pthread_sigmask()
//在某些特殊程序的执行过程中, 是不能被信号打断的, 此时需要信号的屏蔽技术来解决该问题
//获取并修改调用线程的屏蔽信号集
//成功返回0,失败返回-1设errno
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:how
SIG_BLOCK //新的屏蔽信号集是原有集合(不是oldset,oldset是用来存储的)和set的合集
SIG_UNBLOCK //set内的信号被从屏蔽集合oldset中移除
SIG_SETMASK //屏蔽信号集就是setThe set of blocked signals is set to the argument set.
oldset
如果oldset 不是NULL,则之前的信号集被存储在其中
如果set是NULL,屏蔽信号集不会变量,但现存的屏蔽信号集还是会被存储在oldset中
sigpending()
//检查mask期间捕捉到但是没有处理的信号, 通过形参带出结果,成功返回0,失败返回-1设errno
#include <signal.h>
int sigpending(sigset_t *set);
代码:
1 //3号信号对2号信号的阻塞进行解除,对2号信号解除阻塞后,继续按ctrl+c还处于未决状态
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<signal.h>
5 #include<unistd.h>
6 void handler(int s)
7 {
8 printf("recv %d\n",s);
9 }
10 void handler_quit(int s)
11 {
12 sigset_t set;
13 sigemptyset(&set);//清空信号集
14 sigaddset(&set,SIGINT);//添加2号信号
15 sigprocmask(SIG_UNBLOCK,&set,NULL);//解除对二号信号的阻塞
16
17 }
18 int main()
19 {
20 signal(SIGINT,handler);//2号信号
21 signal(SIGQUIT,handler_quit);//3号信号
22
23 sigset_t set;
24 sigemptyset(&set);
25 sigaddset(&set,SIGINT);
26 sigprocmask(SIG_BLOCK,&set,NULL);//阻塞SIGINT信号
27
28 for(;;)
29 {
30 int i=0;
31 sigset_t pset;
32 sigemptyset(&pset);
33 sigpending(&pset);//查看未决状态各进程情况
34 for(i=1;i<_NSIG;i++)//_NSIG为64,表示最大的信号个数
35 {
36 if(sigismember(&pset,i))
37 printf("1");
38
39 else
40 printf("0");
41 }
42 printf("\n");
43 sleep(1);
44 }
45 }
可以看到按ctrl+c时,2(SIGINT)号信号被阻塞,但按ctrl+\(即发送SIGQUIT信号)后,2号信号就已解除阻塞,但是再按键ctrl+c时,SIGINT信号仍处于未决状态。
sigaction()
//是前面的信号处理的集大成者且有很多扩展,是一个非常健壮的signal处理接口,建议使用,成功返回0,失败返回-1设errno
//可以读取和修改与指定信号相关联的处理动作。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void); //obsolete, should not be used.
};
代码:
5 void handler(int s)
6 {
7 printf("recv %d\n",s);
8 }
9 int main()
10 {
11 struct sigaction act;
12 act.sa_handler=handler;
13 sigemptyset(&act.sa_mask);
14 act.sa_flags=0;
15
16 sigaction(SIGINT,&act,NULL);
17 for(;;)
18 pause();
19 }
pause()
//让调用的线程一直睡直到接收到了一个终止进程或者引发捕获信号函数的信号,当接受到这样的信号时,返回-1设errno
int pause(void);
sigsuspend
//pause+信号屏蔽
//高级版pause,调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,
//可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待,
//当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。
#include <signal.h>
int sigsuspend(const sigset_t *mask);
alarm
//seconds秒之后给调用进程发送SOGALRM信号,返回距离下次闹钟响起剩余的描述,没有闹钟返回0
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
用sigsuspeng和alarm实现mysleep:
7 void handler(int s)
8 {
9
10 }
11
12 int mysleep(int sec)
13 {
14 signal(SIGALRM,handler);
15 //先将alarm信号阻塞
16 sigset_t set;
17 sigemptyset(&set);
18 sigaddset(&set,SIGALRM);
19 sigprocmask(SIG_BLOCK,&set,NULL);
20 alarm(sec);
21
22 //增强版pause sigsuspend();
23 sigset_t myset;
24 sigemptyset(&myset);
25 sigsuspend(&myset);
26 sigprocmask(SIG_UNBLOCK,&set,NULL);
27 //pause();
28 return alarm(0);
29 }
30 int main()
31 {
32 int n=1;
33 for(; ;)
34 {
35 printf("mmc %d\n",n++);
36 mysleep(1);
37 }
38 }
函数的可重入和不可重入
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
满足下列条件的函数多数是不可重入的:
(1)函数体内使用了静态的数据结构;
(2)函数体内调用了malloc()或者free()函数;
(3)函数体内调用了标准I/O函数。
SIGCHLD信号
一般子进程在终止时,会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需处理自己的工作,不必关心子进程,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
在Linux中,父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。这样可以避免产生僵尸进程。
测试代码:
1 //练习SIGCHLD信号,子进程和父进程同步执行,当子进程执行完,父进程依然执行,但没有僵尸子进程
2 #include<stdio.h>
3 #include<stdlib.h>
4 #include<signal.h>
5 #include<unistd.h>
6 #include<sys/wait.h>
7 void handler(int s)
8 {
9 wait(NULL);
10 }
11 int main()
12 {
13 int i=0;
14 signal(SIGCHLD,handler);
15 if(fork()==0)//子进程
16 for( i=0;i<20;i++)
17 {
18 printf("c");
19 fflush(stdout);
20 sleep(1);
21 }
22 else
23 {
24 for( i=0;i<30;i++)
25 {
26 printf("p");
27 fflush(stdout);
28 sleep(1);
29 }
30 }
31 }
可以看到当子进程退出后,没有产生僵尸进程。