此为牛客Linux C++和黑马Linux系统编程课程笔记。
文章目录
- 0. 信号的概念
- 1. Linux信号一览表
- 2. 信号相关函数
- 3. kill函数
- 4. raise函数
- 5. abort函数
- 6. alarm函数
- 7. setitimer函数
- 8. signal函数
- 9. 信号集
- 10. 自定义信号集相关函数
- 11. sigprocmask函数
- 12. sigpending函数
- 13. sigaction函数
- 14. 内核实现信号捕捉过程
0. 信号的概念
A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
1. Linux信号一览表
红色为重点掌握的信号
2. 信号相关函数
3. kill函数
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
功能:给任何的进程或者进程组pid, 发送任何的信号 sig
参数:
- pid :
< 0 : 将信号发送给指定的进程
= 0 : 将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 这个pid=某个进程组的ID取反 (-12345)
- sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
如kill(getppid(), 9);
能够杀死父进程;kill(getpid(), 9);
能够杀死当前进程。
4. raise函数
#include <sys/types.h>
#include <signal.h>int raise(int sig);
功能:给当前进程发送信号;
参数:sig : 要发送的信号;
返回值:成功 0, 失败 非0。
相当于kill(getpid(), sig);
5. abort函数
#include <sys/types.h>
#include <signal.h>void abort(void);
功能: 发送SIGABRT(编号为6)信号给当前的进程,杀死当前进程;
相当于kill(getpid(), SIGABRT);
或raise(SIGBRT);
。
6. alarm函数
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM。
参数:seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
返回值:
- 之前没有定时器,返回0
- 之前有定时器,返回之前的定时器剩余的时间
常用:使用alarm(0)取消定时器,返回旧闹钟余下秒数。
每个进程都有且只有唯一个定时器。 比如:进程先执行了alarm(10),2秒后又执行了一个alarm(5),alarm(5)的返回值是8,因为之前有定时器,返回的是之前定时器的剩余时间。然后从现在起该进程还是只有一个定时器,定时5秒,因为后来的定时器会刷新之前的定时器。
注意,alarm定时是与与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸…无论进程处于何种状态,alarm都计时。
看以下示例程序:
#include <unistd.h>
#include <stdio.h>
int main()
{int i;alarm(1);for(i = 0; ; i++) {printf("%d\n", i);}return 0;
}
用定时器让程序执行1s后停止。
我们用time ./alarm
来查看该程序的运行时间:
可以看到实际运行时间几乎是1秒,但是发现用户时间和系统时间加起来与总的运行时间不同,这是为什么呢。
实际执行时间 = 系统时间 + 用户时间 + 等待时间。程序的很多时间浪费在printf上了。
7. setitimer函数
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时。
参数:
- which : 定时器以什么时间计时,有以下三种参数,一般用第一种自然定时。
ITIMER_REAL: 真实时间(自然定时),时间到达发送 SIGALRM 常用
ITIMER_VIRTUAL: 用户时间,时间到达发送 SIGVTALRM
ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达发送 SIGPROF - new_value: 设置定时器的属性
struct itimerval { // 定时器的结构体struct timeval it_interval; // 每个阶段的时间,间隔时间struct timeval it_value; // 延迟多长时间执行定时器
};struct timeval { // 时间的结构体time_t tv_sec; // 秒数 suseconds_t tv_usec; // 微秒
};
- old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
如以下示例程序能够实现延迟3秒,每2秒发送一次信号。
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>// 过3秒以后,每隔2秒钟定时一次
int main() {struct itimerval new_value;// 设置间隔的时间new_value.it_interval.tv_sec = 2;new_value.it_interval.tv_usec = 0;// 设置延迟的时间,3秒之后开始第一次定时new_value.it_value.tv_sec = 3;new_value.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的printf("定时器开始了...\n");if(ret == -1) {perror("setitimer");exit(0);}getchar();return 0;
}
由于还没有介绍signal信号捕捉函数,setitimer发出的信号让程序终止,所以无法演示其周期性发送信号的功能,接下来介绍signal函数。
8. signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:设置某个信号的捕捉行为
注意:并不是该函数来捕捉信号,该函数只是向内核注册对某个信号的捕捉行为。
参数:
- signum: 要捕捉的信号
- handler: 捕捉到信号要如何处理,可以有以下三种参数:
- SIG_IGN : 忽略信号
- SIG_DFL : 使用信号默认的行为
- 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
返回值:
- 成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
- 失败,返回SIG_ERR,设置错误号
注意:SIGKILL 和 SIGSTOP不能被捕捉,不能被忽略。
在setitimer的示例代码中加入signal后,示例代码如下:
void myfunc(int num) {printf("捕捉到了信号的编号是:%d\n", num);printf("xxxxxxx\n");
}// 过3秒以后,每隔2秒钟定时一次
int main() {// 注册信号捕捉// signal(SIGALRM, SIG_IGN);// signal(SIGALRM, SIG_DFL);// void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。signal(SIGALRM, myfunc);struct itimerval new_value;// 设置间隔的时间new_value.it_interval.tv_sec = 2;new_value.it_interval.tv_usec = 0;// 设置延迟的时间,3秒之后开始第一次定时new_value.it_value.tv_sec = 3;new_value.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的printf("定时器开始了...\n");if(ret == -1) {perror("setitimer");exit(0);}getchar();return 0;
}
signal的第二个参数传入函数地址,当当前进程捕捉到SIGALRM信号时,将执行程序员自定义的myfunc函数,myfun函数的int类型参数是捕捉到的信号的值(编号)。
程序运行结果如下:
程序运行3秒后第一次发出信号,程序输出一次,然后每隔2秒发出一次信号。
9. 信号集
一个进程的PCB中除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
阻塞信号集(信号屏蔽字): 将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
未决信号集:
- 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
- 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
信号集本质上是一个64位的二进制数,其每一位的0或1代表着该位序号对应的信号的状态。
当信号产生时,PCB中未决信号集中的该位立即置为1,然后去阻塞信号集的同样位置查看是否为1,如果阻塞信号集的对应位置也为1,说明该信号要阻塞,未决信号集的该位置保持1不变;直到阻塞解除,这个信号就被处理。
10. 自定义信号集相关函数
以下信号集相关的函数都是对自定义的信号集进行操作。
int sigemptyset(sigset_t *set);
- 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
- 参数:set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set);
- 功能:将信号集中的所有的标志位置为1
- 参数:set:传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigaddset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号 - 返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置不阻塞的那个信号 - 返回值:成功返回0, 失败返回-1
int sigismember(const sigset_t *set, int signum);
- 功能:判断某个信号是否阻塞
- 参数:
- set:需要操作的信号集
- signum:需要判断的那个信号 - 返回值:
1 : signum被阻塞
0 : signum不阻塞
-1 : 失败
一个用到以上函数的示例程序如下:
#include <signal.h>
#include <stdio.h>int main() {// 创建一个信号集sigset_t set;// 清空信号集的内容sigemptyset(&set);// 判断 SIGINT 是否在信号集 set 里int ret = sigismember(&set, SIGINT);if(ret == 0) {printf("SIGINT 不阻塞\n");} else if(ret == 1) {printf("SIGINT 阻塞\n");}// 添加几个信号到信号集中sigaddset(&set, SIGINT);sigaddset(&set, SIGQUIT);// 判断SIGINT是否在信号集中ret = sigismember(&set, SIGINT);if(ret == 0) {printf("SIGINT 不阻塞\n");} else if(ret == 1) {printf("SIGINT 阻塞\n");}// 判断SIGQUIT是否在信号集中ret = sigismember(&set, SIGQUIT);if(ret == 0) {printf("SIGQUIT 不阻塞\n");} else if(ret == 1) {printf("SIGQUIT 阻塞\n");}// 从信号集中删除一个信号sigdelset(&set, SIGQUIT);// 判断SIGQUIT是否在信号集中ret = sigismember(&set, SIGQUIT);if(ret == 0) {printf("SIGQUIT 不阻塞\n");} else if(ret == 1) {printf("SIGQUIT 阻塞\n");}return 0;
}
11. sigprocmask函数
之前的信号集函数均是对自定义的信号集进行操作,那如何修改内核中的阻塞信号集呢?可以使用sigprocmask函数,用自定义的信号集设置内核阻塞信号集。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)。
参数:
- how : 如何对内核阻塞信号集进行处理,有以下可选参数:
SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变(假设内核中默认的阻塞信号集是mask, mask | set)。
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞(相当于 mask = mask & ~set)。
SIG_SETMASK: 用set覆盖内核中原来的值。
- set :已经初始化好的用户自定义的信号集
- oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL。
返回值: 成功:0 ;失败:-1,并设置错误号。
12. sigpending函数
#include <signal.h>
int sigpending(sigset_t *set);
功能:获取内核中的未决信号集。
参数:set,传出参数,保存的是内核中的未决信号集中的信息。
13. sigaction函数
sigaction函数通常用于替代signal函数,用来捕捉信号,同时自定义信号的处理动作。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:检查或者改变信号的处理。信号捕捉。
参数:
- signum : 需要捕捉的信号的编号或者宏值(信号的名称)
- act :捕捉到信号之后的处理动作
- oldact : 上一次对信号捕捉相关的设置,一般不使用,传NULL即可
返回值: 成功 0 失败 -1
其中参数act的类型sigaction结构体定义如下:
struct sigaction {// 函数指针,指向的函数就是信号捕捉到之后的处理函数void (*sa_handler)(int);// 不常用void (*sa_sigaction)(int, siginfo_t *, void *);// 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。sigset_t sa_mask;// 使用哪一个信号处理对捕捉到的信号进行处理// 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigactionint sa_flags;// 被废弃掉了void (*sa_restorer)(void);};
其中sa_sigaction和sa_restorer我们基本用不到,所以掌握以下三个即可:
① sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作。
② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
③ sa_flags:通常设置为0,表使用默认属性。
该函数与signal函数最大的区别就在于sa_mask上,sa_mask是程序员自定义的一个信号集,该信号集充当调用信号处理函数时的一个临时的阻塞信号集,也就是说:
进程正常运行时,默认PCB中有一个信号屏蔽字(阻塞信号集),假定为☆,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由☆来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为☆。
示例程序如下:
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>void catchFunc(int signo) {printf("捕捉到了信号:%d \n", signo);
}int main() {struct sigaction act;act.sa_handler = catchFunc;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, SIGQUIT); // 想要在捕捉函数中屏蔽SIGQUIT信号int res = sigaction(SIGINT, &act, NULL);if(res == -1) {perror("sigaction error");exit(1);}while(1);return 0;
}
执行结果如下:
每次在终端输入ctrl+c(产生SIGINT信号)时,输出:捕捉到了信号2。
当在键盘中输入ctrl+\(产生SIGQUIT)时, 程序退出。那么有个问题,程序中不是已经设置了sigaddset(&act.sa_mask, SIGQUIT);
来屏蔽信号了吗?为什么输入ctrl+\时, 程序依然会退出?是因为sigaction函数设置的sa_mask只在信号处理函数执行中生效,输出语句后信号处理函数以及执行完毕。
再看下面示例程序:该程序让信号处理函数睡眠10秒。
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>void catchFunc(int signo) {printf("捕捉到了信号:%d \n", signo);sleep(10);printf("-----finish-----");
}int main() {struct sigaction act;act.sa_handler = catchFunc;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask, SIGQUIT); // 想要在捕捉函数中屏蔽SIGQUIT信号int res = sigaction(SIGINT, &act, NULL);if(res == -1) {perror("sigaction error");exit(1);}return 0;
}
运行程序后,输入ctrl+c,终端输出如下:
此时10秒以内,依然在执行信号捕捉函数catchFunc,也就是说当前sa_mask是生效的。此时我们输入crtl+\:
程序并没有退出,因为此时sa_mask中屏蔽了SIGQUIT信号。等待10秒过后:
发现程序自动退出,这是因为10秒过后信号捕捉函数catchFunc执行完毕,临时的阻塞信号集(sa_mask)失效,此时生效的是原PCB中的阻塞信号集,未决信号集(SIGQUIT处的值为1)查询到后阻塞信号集中SIGQUIT处的值是0后,SIGQUIT信号递达,程序退出。
这里还有一个值得注意的细节:
当信号捕捉函数catchFunc执行时,我输入了多个ctrl+c后,信号捕捉函数执行完毕后,只输出了一个“捕捉到了信号:2”,这是因为我们无论向当前进程发出多少个相同信号,未决信号集的对应位都是1,无法记录相同信号的数量,所以当临时阻塞信号集被取消后,只输出了一个“捕捉到了信号:2”。有以下结论:
阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)。