一、信号的定义
信号是一个通讯方式,正在运行的进程可以收到各种信号,可以是终端发送的信号,可以是父子进程间发送结束信号,也可以是程序运行错误时发送的终止信号。
二、信号的种类
在linux终端下使用kill -l命令可以查看所有信号的种类,如下:
常用的信号有在中断使用ctrl+c而产生的的信号SIGINT,会使得程序终止;子进程结束回向父进程发送的信号SIGCHLD等。
三、信号的发送
发送信号一般是有用户、内核、进程和操作系统 等等来发送的:
用户可以在终端使用ctrl+c(SIGINT),ctrl+z(SIGTSTP)、kill+pid(SIGKILL)(杀死进程)等来向进程发送信号。
那么在进程中,c语言也向我们提供了可以手动向进程发送信号的函数,如下:
1.kill()
函数头文件 #include <sys/types.h>#include <signal.h>函数原型 int kill(pid_t pid, int sig);函数功能 向指定的进程发送一个信号函数参数 pid:进程的id;sig:信号的id;函数返回值 成功返回 0;失败返回 -1,并设置 errno;
2.raise()
函数头文件 #include <sys/types.h>#include <signal.h>函数原型 int raise(int sig);函数功能 向调用者发送一个信号;函数参数 sig:信号编号
四、信号的处理
1.linux中信号有三种处理方式
- 默认
- 忽略
- 自定义函数处理
进程接收到信号后,会交给内核进行处理,内核会判定这个信号将使用哪种处理方式;在内核接收信号的处理中有一个task_struct的结构体,这个结构体会去判定信号的处理方式,这个结构体属于内核源码,保存管理进程的所有信息,内容非常多,这个结构体就不过多说明,下面一些进程的默认的处理方式:
- 进程退出 SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
- 进程忽略 SIGCHLD,SIGPWR,SIGURG,SIGWINCH
- 进程暂停 SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认处理方式:就是我们不做设置的情况下,进程接收到信号的执行的操作。
忽略:这种方式需要我们通过signal()函数手动设置。
函数原型
#include <signal.h> void (*signal(int signum, void (*handler)(int)))(int);参数
- signum:要处理的信号的编号,例如
SIGINT
、SIGALRM
等。- handler:指向信号处理函数的指针。这个函数接受一个
int
参数(信号编号),并且没有返回值。返回值
- 如果成功,
signal
函数返回指向之前信号处理函数的指针。- 如果失败,返回
SIG_ERR
。可以使用perror
或strerror
函数来打印错误信息。
自定义处理函数:经常会使用,使用自定义函数,通常可以使用有两个函数来进行处理:一种是通过signal()函数来进行设置,另一种是通过sigaction()函数。
2.signal()函数处理方式
下面是signal()函数的一个示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
//也可以写成typedef void (*)(int) sighandler_t
void function(int SIG){ //当子进程结束时会发送一个SIGCHLD信号;
printf("%d:%s\n",SIG,strsignal(SIG)); //strsignal()函数是用来获取信号的信息;
}
int main()
{
sighandler_t SIG = signal(SIGCHLD,function); //设置信号处理函数;
if(SIG == SIG_ERR){
perror("signal");
exit(EXIT_FAILURE);
}
pid_t pid = fork(); //创建一个子进程;
if(pid == -1){
perror("fork");
exit(EXIT_FAILURE);
}else if(pid ==0 ){ //子进程开始;
printf("cpid=%d start\n",getpid()); //getpid是获取当前进程的ID号;
printf("cpid=%d end\n",getpid());
exit(EXIT_SUCCESS);
}else if(pid > 0){ //主进程;
printf("parent start\n");
wait(NULL); //等待子进程结束,并释放资源;
printf("parent end\n");
exit(EXIT_SUCCESS);
}
}
结果如下:
可以看到,当子进程结束时,成功发出了SIGCHLD信号,并执行了自定义信号处理函数, 打印出了信号的编号,相关信息。
3.sigaction()函数处理方式
函数原型
#include <signal.h> int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);参数
- sig:指定要操作的信号。
- act:指向
struct sigaction
结构体的指针,它指定了信号的处理方式。如果这个参数是 NULL,sigaction
将不改变当前的信号处理,而是将当前的信号处理方式填充到 oldact 指向的结构体中。- oldact:如果非 NULL,它指向一个
struct sigaction
结构体,sigaction
将把当前的信号处理方式填充到这个结构体中。返回值
- 如果成功,
sigaction
返回 0。- 如果出错,返回 -1,并设置
errno
以指示错误原因
这里面出现了参数为act和oldact两个结构体指针,这两个结构体正如其名,一个是旧的结构体,也就是旧的信号处理方式,一个是新的信号处理函数。通过提供两个结构体参数,sigaction函数允许用户同时设置新的行为并检索旧的行为。这种设计提供了灵活性,使得用户可以在更改信号处理行为之前了解当前的行为,并在需要时恢复它。当然还有其他优点,这里不过多叙述。后面会又关于这个的示例。
3.1.struct sigaction结构体
可以看到在参数中出现了结构体,那来看一下这个结构体:
struct sigaction {
void (*sa_handler)(int); /* 信号处理函数 */
void (*sa_sigaction)(int, siginfo_t *, void *); /* 另一种信号处理函数 */
sigset_t sa_mask; /* 信号集,指定在信号处理函数执行时需要被屏蔽的信号 */
int sa_flags; /* 信号处理选项 */
void (*sa_restorer)(void); /* 已废弃,保留为 NULL */
};
在这个结构体中,出现了五个变量,但因为void (*sa_restorer)(void); 已经废弃,所以这个结构体中就只有四个变量需要关心,分开来看一下。
1.void (*sa_handler)(int);
sigaction()函数也是支持使用void (*sa_handler)(int)的,只需要将函数的地址赋给这个指针就可以。没有特别的需要或者情况,sa_flags和sa_mask可以不用设置,下面是一段简单的示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void function(int SIG){ //自定义处理函数;
printf("%d:%s\n",SIG,strsignal(SIG)); //strsignal()函数是用来获取信号的信息;
}
int main()
{
struct sigaction sa; //定义一个结构体变量;
memset(&sa, 0, sizeof(sa));//初始化;
sa.sa_handler = &function;//将地址给函数指针;
int res = fork(); //创建一个子进程;
if(res == -1){
perror("fork");
exit(EXIT_FAILURE);
}else if(res == 0){ //子进程部分;
printf("child start\n");
sleep(2);
printf("chlid end\n");
exit(EXIT_SUCCESS);
}else{ //主进程部分
printf("parent start\n");
if(sigaction(SIGCHLD, &sa, NULL) == -1){ //调用自定义处理函数;
perror("sigaction");
exit(EXIT_FAILURE);
}
wait(NULL); //等待子进程,同时释放资源;
printf("parent end\n");
}
return 0;
}
~
结果如下:
可以发现运行结果相同,说明sigaction()函数确实可以代替signal()函数,但 sigaction()函数却不仅于此,继续往下看。
2.void (*sa_sigaction)(int, siginfo_t *, void *);
void (*sa_sigaction)(int, siginfo_t *, void *),这个函数指针相较于第一种增加了一些参数,可以接收额外的信息。这里函数的第二个参数是sigset_t*的一个指针,第三个参数是用来打印信息,信号处理函数中并不常用,它是一个指向上下文信息的指针,通常是一个ucontext_t结构体的指针。这个上下文结构体包含了信号发生时的进程上下文,如寄存器状态、栈信息等。
我们来主要看sigifno_t,它是一个结构体,里面的变量是下面这些:
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
union sigval si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count;
POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
void *si_lower; /* Lower bound when address violation
occurred (since Linux 3.19) */
void *si_upper; /* Upper bound when address violation
occurred (since Linux 3.19) */
int si_pkey; /* Protection key on PTE that caused
fault (since Linux 4.6) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
因为在不同的内核的版本中,对它的定义都不大相同,所以就在linux下man手册中找,毕竟是做嵌入式还是得站在linux上,可以看到信息非常多,但这些我们只需要打印对我们有帮助的信息好,下面是一个sigaction()函数的示例 :
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void function(int SIG, siginfo_t* info,void* p){ //自定义信号处理函数;第三个参数并没有使用;
if(info == NULL){
printf("info is NULL\n");
exit(EXIT_FAILURE);
}
printf("Signal sent by process %d\n", info->si_pid);//打印调用者的pid号;
printf("Signal code: %d\n", info->si_code); //打印通信编码;
}
int main()
{
struct sigaction sa; //这里定义了两个结构体变量;
struct sigaction old_sa; //一个是新的信号处理函数,一个用来保存旧的信号处理方式;
memset(&sa, 0, sizeof(sa)); //初始化;
sa.sa_sigaction = &function; //将地址赋给函数指针;
if(sigaction(SIGINT, &sa, &old_sa) == -1) {//调用信号处理函数;注意这里old_sa
perror("sigaction"); //需要用来接收旧的信号处理方式;
exit(EXIT_FAILURE);
}
pause(); // 使程序暂停,等待信号;
if(sigaction(SIGINT, &old_sa, NULL) == -1) {//将信号的处理方式恢复;
perror("sigaction");
exit(EXIT_FAILURE);
}
while(1); //死循环,可以由SIGINT信号终止,
return 0;
}
下面是运行结果:
简述一下这段代码,SIGINT信号是一个可以终止当前程序的信号,由用户在终端输入ctrl+c产生,在这段代码中我将SIGINT的处理方式更改为了自定义函数处理,所以进程接收到了SIGINT信号并会结束,而时执行对应的自定义信号处理函数,但代码中有一段whlie()死循环,所以不能将SIGINT一直设定为自定义,需要回复默认的处理方法,于是我们定义了old_sa,它可以将旧的处理方式保存下来,便于我们去恢复。
从运行结果可以看到,程序在第一次受收到SIGINT信号并没有终止,而时打印出了相关信息,但在第二次接收的SIGINT信号,程序终止了,没有问题,是我们要得到的结果。
3.sigset_t sa_mask;
结构体的第三个成员是sa_mask,sa_masksa_mask成员是一个信号集,用于指定在信号处理函数执行期间应该被临时阻塞的信号集。这是一个sigset_t类型的成员,它允许你指定一组信号。
先看一下siget_t类型:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
是一个结构体类型的数组,在使用的过程中,我们并不需要关心如何向里面存放,因为有对应的函数供我们去使用。
sa_mask简单来说,是为了防止在执行信号处理函数时,有别的信号发出,打断当前操作,所以我们可以将要发生的信号设置为阻塞,使用的时候需要先进行初始化:
sigemptyset(&sa.sa_mask);
然后进行添加操作:
sigaddset(&sa.sa_mask, SIGINT);
sigaddset(&sa.sa_mask, SIGQUIT);
这样在进行自定义处理函数,这两个信号就不会打断当前操作了。方式还时比较简单的。
需要注意的自定义处理函数花费事件尽量要短,防止错过重要的信号,或着是采用其他方式来保证信号的接收。
4.int sa_flags;
sa_flags时结构体的第四个成员,它用于指定信号处理的行为选项。以下是一些常用的 sa_flags 标志及其作用,如下:
- SA_RESTART:使被信号打断的系统调用自动重新发起。
- SA_ONSTACK:系统将在调用[[sigalstack|sigalstack]]替代信号栈上运行信号句柄;否则使用用户栈来交付信号。
- SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
- SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
- SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
- SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
- SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
这里主要说一下SA_RESTART,这个标志位可以使被中断的系统调用,在执行完信号处理后自动重新执行,例如read,write系统调用的过程中被中断了,那么在执行完处理函数后将会自动重新发起,不需要手动在进行设置。
使用的话直接进行赋值就可以:
sa.sa_flags = SA_RESTART;
五、自定义信号处理函数的结构图
因为函数中涉及到了多个结构体,所以就画了一张结构图,可以帮助理解
总结
以上就是关于信号的内容,本文仅仅介绍了有关信号的相关知识和简单使用,更多的还需要自己不断摸索,不断学习。若发现错误,请指正,共同进步。