Unix信号之sigaction函数

sigaction函数是除signal函数之外的另一个可以设置信号处理的函数。sigaction用法比signal函数复杂,但是可以对信号处理进行更加精准更灵活的控制。sigaction是POSIX的信号接口,而signal函数是标准C的信号接口,所以sigaction函数的可移植性更强。

sigaction函数说明

函数功能

检查或修改与指定信号相关联的处理动作,可以同时检查和修改。

函数原型

1
2
3
4
5
6
7
8
struct sigaction{
void (*sa_handler)(int); //信号处理函数地址
sigset_t sa_mask; //信号掩码集,当调用信号处理函数时,程序将阻塞sa_mask中的信号
int sa_flag; //位掩码,指定用于控制信号处理过程中的各种选项。
void (*sa_sigaction)(int,siginfo_t*,void*); //暂不用
};
int sigaction(int signo,const struct sigaction*restrict act,struct sigaction*restrict oact);

函数参数

  • signo : 指定操作的信号

  • act : 新修改的sigaction

  • oldact : 保存该函数原有的sigaction。

上述struct sigaction中sa_flag取值说明(常用的有以下两个):

  • SA_NODEFER:当信号处理函数正在进行时,不堵塞对于信号处理函数自身信号功能。
  • SA_RESETHAND:当用户注册的信号处理函数被执行过一次后,该信号的处理函数被设为系统默认的处理函数

函数返回值

  • 0 : 返回0表示设置成功
  • -1 : 返回-1表示设置失败

函数示例

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
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
void sighandler(int sig) {
printf("this is in the sig handler\n");
for (int i = 0; i < 5; i++) {
printf("num:%d\n", i);
}
}
int main() {
struct sigaction act;
struct sigaction oldact;
act.sa_handler = sighandler;
act.sa_flags = SA_NODEFER ;//| SA_RESETHAND;
sigaction(SIGINT , &act,&oldact);
printf ("this is the main function\n");
pid_t pid = getpid();
printf("%d", pid);
kill(pid, SIGINT);
}

Comment and share

Unix高级编程之signal

signal函数相关的细节描述详见另外两篇篇博客,这里不详细赘述:
https://langzi989.github.io/2017/09/08/C++%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88%E5%AD%A6%E4%B9%A0/
https://langzi989.github.io/2017/05/04/Wait%E5%87%BD%E6%95%B0%E8%AF%A6%E8%A7%A3/

显示信号的描述

信号的个数可以用宏NSIG获取。

显示信号的描述有三种方法:

1
2
3
4
5
6
7
8
9
#include <string.h>
//first method
char* strsignal(int sig);
//second method,
void psignal(int sig, char* msg);
//third memthod
sys_siglist[sig];

上述三种方法的区别

sys_siglist是直接存储信号描述的数组,一般情况下,推荐使用strsignal。

strsignal和psignal函数对locale敏感,会打印出当地的语言。
调用psignal会在本地的错误出输出流输出,msg:strsignalmsg;

如:

1
2
//此时错误数据流将会打印出:SIGINT:Interrupt
psignal(SIGINT, "SIGINT");

信号集

许多相关的系统调用涉及到一组不同的信号,这时候需要信号集。linux中使用sigset_t结构体来表示信号集。一般情况,信号集是使用掩码实现的,但是可能有一些是其他实现方式。
信号集结构体相关的函数.

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
#include <signal.h>
//初始化空信号集。注意不可使用memset或者静态变量全局变量初始化信号集为空,这样会出问题。因为不是所有的信号集都是通过掩码实现的
//0出错,1成功
int sigemptyset(sigset_t* set);
//初始化信号集包括所有的信号
//0出错,1成功
int sigfillset(sigset_t* set);
//向信号集中添加信号
//0出错,1成功
int sigaddset(sigset_t* set, int sig);
//从信号集中去除信号
//0出错,1成功
int sigdelset(sigset_t* set, int sig);
//检查某一信号是不是在当前信号集中。返回1在,0不在
int sigismember(const sigset_t* set, int sig);
//以下三个为GNU C中的非标准函数,
#define _GNU_SOURCE
//对两个信号集作交集存储于dest中
int sigandset(sigset_t* dest, sigset_t* left, sigset_t* right);
//对两个信号集做并集存储于dest中
int sigorset(sigset_t* dest, sigset_t* left, sigset_t* right);
//判断信号集是否为空
int sigisemptyset(const sigset_t* set);

信号掩码(进程中阻塞信号传递)

内核会为每个进程维护一个信号掩码(标识一组信号),当一个信号被传递到该进程的时候,若该信号在信号掩码中,进程会阻塞该信号的传递,直到将该信号从信号掩码中剔除。

向信号掩码中添加一个信号的方式有以下几种:

  • 当调用信号处理器程序的时候,可将引发该调用的信号自动添加到信号掩码中,这取决于sigaction函数在安装信号时使用的标志。
  • 使用sigaction函数建立信号处理程序时,可以指定一组额外信号,当调用该处理器程序时将阻塞。
  • 使用sigprocmask函数修改进程的信号掩码。

sigprocmask函数

1
2
#include <signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* old);

参数:

  • how : 指定修改信号掩码的方式,有三种方式
    • SIG_BLOCK : 向指定信号中添加指定信号.
    • SIG_UNBLOCK: 将指定信号从原有的信号掩码中移除。若被移除的信号掩码不存在不报错
    • SIG_SETMASK: 直接设置(赋值),覆盖原有的值
  • set : 需要设置的新的信号掩码集
  • old: 旧的信号掩码集。可在设置信号掩码集之后回复原有的信号掩码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <signal.h>
int main() {
time_t start = time(NULL);
sigset_t new_set, pre;
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);
if (sigprocmask(SIG_SETMASK, &new_set, &pre) == -1) {
std::cout<< "sigprocmask set error" << std::endl;
}
while (true) {
sleep(1);
time_t end = time(NULL);
if (end - start >= 15) {
std::cout << "hahah 接触阻塞" << std::endl;
sigprocmask(SIG_SETMASK, &pre, NULL);
}
}
}

sigpending获取正在等待状态的信号

若进程接收信号被阻塞之后,我们希望获取被阻塞的信号,则可以使用sigpending函数

1
2
#include <signal.h>
int sigpending(sigset_t* set);

使用此函数的场景是:若某个进程接收到被阻塞的信号,如果希望这些信号被移出阻塞队列,此时可以通过sigpending获取被阻塞的信号,然后将此信号的处理器函数IGNORE,并将其剔除信号掩码即可。

在信号被阻塞的时候,不对信号做排队处理,即即使进程阻塞了100个SIGINT信号,此时当SIGINT从信号掩码中去除时,该进程接收的还是只是一个SIGINT信号。

sigaction函数

除了signal函数之外,sigaction系统调用是设置信号处理的另一个选择。sigaction和signal函数相比更加灵活和具有可移植性。sigaction允许在不改变信号处理器程序的情况下获取信号的默认处理方式。

Comment and share

C++函数指针学习

使用函数指针的优点

使用函数指针有助于我们设计出更优秀,更简洁更高效的程序。在下面的情景中我们常用到函数指针:

  • 使用函数指针作为参数
  • 使用函数指针作为返回值
  • 使用函数指针作为回调函数
  • 使用函数指针数组
  • 类的静态方法和非静态方法的函数指针
  • 使用函数指针实现动态绑定
  • 在结构体中定义函数

使用函数指针提高函数的效率

当通过switch case多用多个相同类型的函数的时候,这个时候使用函数指针可以大大简化函数代码并可以明显的提高程序的执行效率。例:

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
#include <iostream>
#include <time.h>
#include <string>
#include <sys/time.h>
using namespace std;
#define StartTime(id) struct timeval __now1105##id;\
gettimeofday(&__now1105##id, 0);
#define EndTime(id) struct timeval __now21105##id; \
gettimeofday(&__now21105##id, 0); \
printf("timer_%s spend time:%d us\n",#id,(__now21105##id.tv_sec-__now1105##id.tv_sec)* 1000000 + (__now21105##id.tv_usec-__now1105##id.tv_usec));
double add(double a, double b) {
return a + b;
}
double sub(double a, double b) {
return a - b;
}
double multi(double a, double b) {
return a * b;
}
double div(double a, double b) {
return a/b;
}
typedef double (*op)(double, double);
void func1(double a, double b, int flag) {
switch(flag) {
case 0:
add(a, b);
break;
case 1:
sub(a, b);
break;
case 2:
multi(a, b);
break;
case 3:
div(a, b);
break;
default:
break;
}
}
//使用函数指针调用函数
void func2(double a, double b, op cb) {
cb(a, b);
}
int main() {
StartTime(func1);
for (int i = 0; i < 100000; i++) {
func1(0.2, 0.034, 3);
}
EndTime(func1);
StartTime(func2);
for (int i = 0; i < 100000; i++) {
func2(0.2, 0.034, multi);
}
EndTime(func2);
}

由上面可以看出,由于上述switch,case中调用的函数类型(返回值类型,参数个数以及对应的类型)完全一致,我们将函数指针以参数的形式传到处理函数中。
运行上述函数的结果如下:

1
2
timer_func1 spend time:2861 us
timer_func2 spend time:2178 us

可以看出使用函数指针的效率远远高于switch case

函数指针用做回调函数

来源于wiki
在计算机程序设计中,回调函数是指通过函数参数传递到其他代码的,某一块可执行代码的引用。这一设计允许底层代码调用在高层定义的子程序。如Linux C中的signal函数就是这样一个例子。

signal底层的其中一个实现版本如下:由其实现可以看出信号处理的回调函数的主要功能是将处理信号的函数指针替换为用户高层自定义的函数地址fun,从而达到当接收到该信号时底层代码调用高层定义代码的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Sigfunc *
signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
} else {
act.sa_flags |= SA_RESTART;
}
if (sigaction(signo, &act, &oact) < 0)
return(SIG_ERR);
return(oact.sa_handler);
}

当我们使用signal函数的时候,如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include <signal.h>
#include <stdio.h>
void signalcb(int signum) {
printf("this is the signal:%d", signum);
}
int main() {
signal(SIGINT, signalcb);
pause();
return 0;
}

函数指针的本质及声明方法以及赋值

函数指针的本质

函数指针类似于数据指针,其本质实质上是一类函数(返回值类型相同,参数个数以及对应的参数类型相同)的入口地址,即该可执行代码在内存中的起始地址。函数指针可以让我们通过函数地址去调用该函数。有利于实现函数的动态绑定,即在运行时才决定去调用哪个函数。

函数指针的声明方法

函数指针的声明方法有两种,包括:

  • 普通函数的函数指针和类静态成员函数的函数指针
  • 类非静态成员函数的函数指针

普通函数的函数指针和类静态成员函数的函数指针:
int (funptr)(int qa, int qb);
类非静态成员函数的函数指针:
int MyClass::(
funptr)(int qa, int qb);

注意上述函数指针声明时(*funptr)中的括号不能省略,若省略,有可能会产生歧义,其意义就变成了返回值为int*的函数定义了

上述两种函数指针声明不兼容的原因如下:(《深度探索C++对象模型》)
获取类的静态和非静态函数的函数指针的地址都是其在内存中实际的地址。那为什么非静态成员的指针需要绑定(指明类)?原因是类的非静态成员需要操作类的数据成员,所以类的非静态成员需要绑定this指针找到类的数据成员。故对nonstatic函数取地址是不完整的。

函数指针的赋值与使用

函数指针赋值

对于普通指针和类的静态成员指针,有两种赋值方式:

1
2
funptr = fnc1
funptr = &fun1

对于类的非静态成员指针,只能用上面第二种形式赋值。为了保证形式的一致性和避免二义性,一般统一使用取地址符号进行赋值可避免错误出现。

1
funptr = &classInstance.func();

函数指针使用

函数指针的使用类似于赋值。
对于普通指针和类的静态成员函数指针的调用,有两种调用方式:

1
2
3
funptr(1,2);
(*funptr)(1,2);

对于类的静态成员函数,只能用上述第二种方式进行调用,为了保持一致性和避免二义性,一般统一使用使用*来解引用函数指针进行调用.

函数指针作为参数

函数指针是一个类型,将函数指针作为参数传入函数中与其他参数类似。使用方式与上述函数指针用做回调函数中signal接收参数的方式相同。这里不再详解

函数指针作为返回值

既然函数指针是函数的入口地址,所以函数指针也可以作为函数的返回值返回。不过函数指针作为函数的返回值的写反比较复杂。
若一个函数func只有一个参数int, 其返回值类型是float (*) (float, float);则其函数原型如下:

1
float (*func(int op)) (float, float);

函数指针作为函数返回值的原则是,将函数名以及参数写到*后,函数指针的返回值放在最前面,函数指针的参数放在最后面。

由上面的知识我们可以分析一下signal函数原型。

1
void (*signal)(int signo,void (*func)(int)))(int);

其定义了一个有两个参数分别为int和void (func)(int),返回值为void (func)(int)的函数。

其本质上是将函数处理指针替换为用户自定义的函数指针。那为什么需要返回值为void (func)(int)呢?原因是*signal函数的返回值是旧的信号处理函数的指针,我们可以通过这个指针暂时改变signal函数处理信号的方式。之后可以通过返回的指针恢复该信号默认的处理方式。

注意:signal的信号处理其函数中的int参数的含义是:当信号到达的时候,内核将该信号以整形的方式传给处理器函数,即为void (*func)(int sig)中的sig.

使用typedef定义函数指针

一般情况下,如果通过普通的方式定义函数指针,在使用的很不方便。这个时候我们可以通过typedef定义函数指针的新类型。通过typedef定义新类型时与普通类型定义新类型方式不同。对于普通类型,定义方式如下:

1
typedef int64_t int64;

但是对于函数指针,定义方式如下:

1
2
3
4
5
6
7
//此处定义个名字为func的类型,它表示函数指针float (*func)(float, float)类型
typedef float (*func)(float, float);
float function(float a, floatb);
//使用func定义变量
func test = &function;

Comment and share

  • page 1 of 1

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China