0%

unix环境高级编程 - 信号

最近在学习 APUE,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习APUE的童鞋们提供一份参考。

本系列博文均根据学习《UNIX环境高级编程》一书总结而来;

运行环境:

  • 操作系统: ubutnu 16.04
  • 编译器:QtCreator CLion 2020.3

信号

信号是软件中断。很多重要的应用程序都需要处理信号,信号提供了一种处理异步事件的方法;

信号机制

信号的状态

信号有三种状态:产生、未决和递达。

  • 信号的产生

    ​ Ø 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\

    ​ Ø 系统调用产生,如:kill、raise、abort

    ​ Ø 软件条件产生,如:定时器alarm

    ​ Ø 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)

    ​ Ø 命令产生,如:kill命令

  • 未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。

  • 递达:递送并且到达进程。

信号的处理

  • 执行默认动作

  • 忽略信号(丢弃不处理)

  • 捕捉信号(调用用户的自定义的处理函数)

信号的特质

信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。

Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集

信号四要素

通过man 7 signal可以查看信号相关信息

  • 1 信号的编号

    ​ Ø 使用kill -l命令可以查看当前系统有哪些信号,不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。

  • 信号的名称

  • 产生信号的事件

  • 信号的默认处理动作

    ​ Ø Term:终止进程

    ​ Ø Ign:忽略信号 (默认即时对该种信号忽略操作)

    ​ Ø Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)

    ​ Ø Stop:停止(暂停)进程

    ​ Ø Cont:继续运行进程

  • 特别需要注意的是:The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

  • 几个常用到的信号

SIGINT、SIGQUIT、SIGKILL、SIGSEGV、SIGUSR1、SIGUSR2、SIGPIPE、SIGALRM、SIGTERM、SIGCHLD、SIGSTOP、SIGCONT

信号函数

signal

函数作用:注册信号捕捉函数;

函数原型

1
2
3
4
5
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

函数参数:

signum:信号编号

handler:信号处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* @file signal信号实例
*
* 示例程序 - 01_signal.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
//信号处理函数
void sighandler(int signo)
{
printf("signo = %d\n",signo);
}


int main()
{
//1.创建管道
//int pipe(int pipefd[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0)
{
perror("pipe error");
return -1;
}

signal(SIGPIPE,sighandler);

close(fd[0]); //关闭读端
write(fd[1],"Hello,world",sizeof("Hello,world")); //关闭读端写

return 0;
}

kill

  • 给指定进程发送指定信号

  • kill命令:kill -SIGKILL 进程PID

  • kill函数原型:int kill(pid_t pid, int sig);

  • 函数返回值:

    ​ Ø 成功:0;

    ​ Ø 失败:-1,设置errno

  • 函数参数:

    ​ Ø sig信号参数:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。

    ​ Ø pid参数:

pid > 0: 发送信号给指定的进程。

pid = 0: 发送信号给与调用kill函数进程属于同一进程组的所有进程。

pid < -1: 取|pid|发给对应进程组。

pid = -1:发送给进程有权限发送的系统中所有进程。

进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* @file 父进程接收子进程信号
*
* 示例程序 - 03_kill_parent.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <errno.h>
int main(int argc,char *argv[])
{
int i = 0;
for (; i < 3; i++) {
pid_t pid = fork();
if(pid < 0) //fork失败
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
printf("Father Process PID == %d Process PPID = %d\n",getpid(),getppid());
/*if(i == 0) //父进程杀死第一个子进程
{
kill(pid,SIGKILL);
}
sleep(1);*/
}
else if(pid == 0) //子进程
{
printf("child Process PID == %d Process PPID = %d\n",getpid(),getppid());
break;
}
}

if(i == 0) //FirstProcess
{
printf("[%d]:first child---pid:%d\n",i,getpid());
//kill(getppid(),SIGKILL); //子进程杀死父进程
sleep(1);
kill(-1,SIGKILL);
}
if(i == 1) //FirstProcess
{
printf("[%d]:second child---pid:%d\n",i,getpid());
sleep(10);
}
if(i == 2) //FirstProcess
{
printf("[%d]:third child---pid:%d\n",i,getpid());
sleep(10);
}
if(i == 3) //FirstProcess
{
printf("[%d]:Father child---pid:%d\n",i,getpid());
sleep(10);
}
sleep(10);
return 0;
}

raise

  • raise函数

    • Ø 函说描述:给当前进程发送指定信号(自己给自己发)

    • 函数原型:int raise(int sig);

    • 函数返回值:成功:0,失败非0值

    • 函数拓展:raise(signo) == kill(getpid(), signo);

abort

  • 函数描述:给自己发送异常终止信号 6) SIGABRT,并产生core文件

  • 函数原型:void abort(void);

  • 函数拓展:abort() == kill(getpid(), SIGABRT);

alarm

1
unsigned int alarm(unsigned int seconds);

设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14**)**SIGALRM信号。进程收到该信号,默认动作终止每个进程都有且只有唯一的一个定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @file 测试你的电脑1s执行多少
*
* 示例程序 - 06_alarm.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <errno.h>
#include <time.h>


int main(int argc,char *argv[])
{
alarm(1);
int i = 0;
while (1)
{
printf("[%d]\n",i++);
}
return 0;
}

setitimer

1
2
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);

设置定时器(闹钟),可代替alarm函数,精度微秒us,可以实现周期定时。

  • which:指定定时方式

    ​ 自然定时:ITIMER_REAL → 14**)**SIGALRM计算自然时间

    ​ 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26**)**SIGVTALRM 只计算进程占用cpu的时间

    ​ 运行时计时(用户+内核):ITIMER_PROF → 27**)**SIGPROF计算占用cpu及执行系统调用的时间

  • new_value:struct itimerval, 负责设定timeout时间。

    ​ itimerval.it_value: 设定第一次执行function所延迟的秒数 itimerval.it_interval: 设定以后每几秒执行function

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct itimerval { 

    struct timerval it_interval; // 闹钟触发周期

    struct timerval it_value; // 闹钟触发时间

    };

    struct timeval {

    long tv_sec; // 秒

    long tv_usec; // 微秒

    }
  • old_value: 存放旧的timeout值,一般指定为NULL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>

void signalHandler(int signo)
{
switch (signo){
case SIGALRM:
printf("Caught the SIGALRM signal!\n");
break;
}
}

int main(int argc, char *argv[])
{
signal(SIGALRM, signalHandler);

struct itimerval new_value, old_value;
new_value.it_value.tv_sec = 0;
new_value.it_value.tv_usec = 1;
new_value.it_interval.tv_sec = 0;
new_value.it_interval.tv_usec = 200000;
setitimer(ITIMER_REAL, &new_value, &old_value);

for(;;);

return 0;
}

信号集相关

未决信号集和阻塞信号集的关系

阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还处于未决状态的信号的集合,这两个集合存储在内核的PCB中。

  • 下面以SIGINT为例说明信号未决信号集和阻塞信号集的关系:

当进程收到一个SIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1:

  • 如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集上该位置上的值保持为1,表示该信号处于未决状态;

  • 如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。

信号集相关函数

由于信号集属于内核的一块区域,用户不能直接操作内核空间,为此,内核提供了一些信号集相关的接口函数,使用这些函数用户就可以完成对信号集的相关操作。

信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。既然是一个集合,就需要对集进行添加、删除等操作。

  • int sigemptyset(sigset_t *set);

函数说明:将某个信号集清0

函数返回值:成功:0;失败:-1,设置errno

  • int sigfillset(sigset_t *set);

函数说明:将某个信号集置1

函数返回值:成功:0;失败:-1,设置errno

  • int sigaddset(sigset_t *set, int signum);

函数说明:将某个信号加入信号集合中

函数返回值:成功:0;失败:-1,设置errno

  • int sigdelset(sigset_t *set, int signum);

函数说明:将某信号从信号清出信号集

函数返回值:成功:0;失败:-1,设置errno

  • int sigismember(const sigset_t *set, int signum);

函数说明:判断某个信号是否在信号集中

函数返回值:在:1;不在:0;出错:-1,设置errno

  • sigprocmask函数

函数说明:用来屏蔽信号、解除屏蔽也使用该函数。其本质,读

取或修改进程控制块中的信号屏蔽字(阻塞信号集)。

特别注意,屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。

函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

函数返回值:成功:0;失败:-1,设置errno

函数参数:

u how参数取值:假设当前的信号屏蔽字为mask

l SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set

l SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set

l SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

u set:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字。

u oldset:传出参数,保存旧的信号屏蔽字。

  • sigpending函数

² 函数原型:int sigpending(sigset_t *set);

² 函数说明:读取当前进程的未决信号集

² 函数参数:set传出参数

² 函数返回值:成功:0;失败:-1,设置errno

练习:编写程序,设置阻塞信号集并把所有常规信号的未决状态打印至屏幕。

信号捕捉

sigaction

1
2
3
4
5
6
7
8
9
10
11
12
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);
}

sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
//int sigaction(int signum, const struct sigaction *act,
// struct sigaction *oldact);

//信号处理函数
//信号处理函数
void sighandler(int signo)
{
printf("signo==[%d]\n", signo);
sleep(4);
}

int main()
{
//注册信号处理函数
struct sigaction act;
act.sa_handler = sighandler;
sigemptyset(&act.sa_mask); //在信号处理函数执行期间, 不阻塞任何信号
//sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);


signal(SIGQUIT, sighandler);

while(1)
{
sleep(10);
}

return 0;
}

sigchld

使用信号父进程回收子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* @file 父进程使用SICCHLD信号完成对子进程的回收
*
* 示例程序 - 10_sigchld.c
*
* @author Steve & sYstemk1t
*
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void waitchild(int signo)
{
pid_t wpid;

//回收子进程
while(1)
{
wpid = waitpid(-1, NULL, WNOHANG);
if(wpid>0)
{
printf("child is quit, wpid==[%d]\n", wpid);
}
else if(wpid==0)
{
printf("child is living, wpid==[%d]\n", wpid);
break;
}
else if(wpid==-1)
{
printf("no child is living, wpid==[%d]\n", wpid);
break;
}
}
}

int main()
{
int i = 0;
int n = 3;

//将SIGCHLD信号阻塞
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);

for(i=0; i<n; i++)
{
//fork子进程
pid_t pid = fork();
if(pid<0) //fork失败的情况
{
perror("fork error");
return -1;
}
else if(pid>0) //父进程
{
printf("father: fpid==[%d], cpid==[%d]\n", getpid(), pid);
sleep(1);
}
else if(pid==0) //子进程
{
printf("child: fpid==[%d], cpid==[%d]\n", getppid(), getpid());
break;
}
}

//父进程
if(i==3)
{
printf("[%d]:father: fpid==[%d]\n", i, getpid());
//signal(SIGCHLD, waitchild);
//注册信号处理函数
struct sigaction act;
act.sa_handler = waitchild;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sleep(5);
sigaction(SIGCHLD, &act, NULL);

//解除对SIGCHLD信号的阻塞
sigprocmask(SIG_UNBLOCK, &mask, NULL);

while(1)
{
sleep(1);
}
}

//第1个子进程
if(i==0)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
//sleep(1);
}

//第2个子进程
if(i==1)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
sleep(1);
}

//第3个子进程
if(i==2)
{
printf("[%d]:child: cpid==[%d]\n", i, getpid());
sleep(1);
}

return 0;
}