这篇文章主要介绍了Linux的Signal机制是什么的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Linux的Signal机制是什么文章都会有所收获,下面我们一起来看看吧。
Signal机制在Linux中是一个非常常用的进程间通信机制,很多人在使用的时候不会考虑该机制是具体如何实现的。signal机制可以被理解成进程的软中断,因此,在实时性方面还是相对比较高的。
信号的名字和编号: 每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。 信号定义在signal.h
头文件中,信号名都定义为正整数。 具体的信号名称可以使用kill -l
来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
信号的名称
信号的处理: 信号的处理有三种方法,分别是:忽略、捕捉和默认动作
SIGKILL
和SIGSTOP
)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景man 7 signal
来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。也可以参考 《UNIX 环境高级编程(第三部)》的 P251——P256中间对于每个信号有详细的说明。了解了信号的概述,那么,信号是如何来使用呢?
❝
其实对于常用的 kill 命令就是一个发送信号的工具,
kill 9 PID
来杀死进程。比如,我在后台运行了一个 top 工具,通过 ps 命令可以查看他的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了 top 进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。kill 杀死进程
对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段,那么如何来自定义信号的处理函数呢?
信号处理函数的注册不只一种方法,分为入门版和高级版
signal
sigaction
信号发送函数也不止一个,同样分为入门版和高级版 1.入门版:kill 2.高级版:sigqueue
在正式开始了解这两个函数之前,可以先来思考一下,处理中断都需要处理什么问题。 按照我们之前思路来看,可以发送的信号类型是多种多样的,每种信号的处理可能不一定相同,那么,我们肯定需要知道到底发生了什么信号。 另外,虽然我们知道了系统发出来的是哪种信号,但是还有一点也很重要,就是系统产生了一个信号,是由谁来响应? 如果系统通过 ctrl+c 产生了一个 SIGINT(中断信号),显然不是所有程序同时结束,那么,信号一定需要有一个接收者。对于处理信号的程序来说,接收者就是自己。
开始的时候,先来看看入门版本的信号注册函数,他的函数原型如下: signal 的函数原型
#include typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。 对于sighandler_t signal(int signum, sighandler_t handler);
函数来说,signum 显然是信号的编号,handler 是中断函数的指针。 同样,typedef void (*sighandler_t)(int);
中断函数的原型中,有一个参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。我们先来看看简单一个信号注册的代码示例吧。
#include#include#include //typedef void (*sighandler_t)(int); void handler(int signum) { if(signum == SIGIO) printf("SIGIO signal: %d\n", signum); else if(signum == SIGUSR1) printf("SIGUSR1 signal: %d\n", signum); else printf("error\n"); } int main(void) { //sighandler_t signal(int signum, sighandler_t handler); signal(SIGIO, handler); signal(SIGUSR1, handler); printf("%d %d\n", SIGIO, SIGUSR1); for(;;) { sleep(10000); } return 0; }
我们先使用 kill 命令发送信号给之前所写的程序,关于这个命令,我们后面再谈。
通过 kill 命令发送信号
程序接收到的信号的处理结果
简单的总结一下,我们通过 signal 函数注册一个信号处理函数,分别注册了两个信号(SIGIO 和 SIGUSER1);随后主程序就一直“长眠”了。 通过 kill 命令发送信号之前,我们需要先查看到接收者,通过 ps 命令查看了之前所写的程序的 PID,通过 kill 函数来发送。 对于已注册的信号,使用 kill 发送都可以正常接收到,但是如果发送了未注册的信号,则会使得应用程序终止进程。
那么,已经可以设置信号处理函数了,信号的处理还有两种状态,分别是默认处理和忽略,这两种设置很简单,只需要将 handler 设置为 SIG_IGN(忽略信号)或 SIG_DFL(默认动作)即可。
在此还有两个问题需要说明一下:
入门版的信号注册还是比较简单的,只需要一句注册和一个处理函数即可,那么,接下来看看,如何发送信号吧。
kill 的函数原型
#include #include int kill(pid_t pid, int sig);
正如我之前所说的,信号的处理需要有接受者,显然发送者必须要知道发给谁,根据 kill 函数的远行可以看到,pid 就是接受者的 pid,sig 则是发送的信号的类型。从原型来看,发送信号要比接受信号还要简单些,那么我们直接上代码吧~~!Show me the code!!!
#include #include #include#include int main(int argc, char** argv) { if(3 != argc) { printf("[Arguments ERROR!]\n"); printf("\tUsage:\n"); printf("\t\t%s \n", argv[0]); return -1; } int pid = atoi(argv[1]); int sig = atoi(argv[2]); //int kill(pid_t pid, int sig); if(pid > 0 && sig > 0) { kill(pid, sig); } else { printf("Target_PID or Signal_Number MUST bigger than 0!\n"); } return 0; }
发送信号
接收信号的结果
总结一下: 根据以上的结果可看到,基本可以实现了信号的发送,虽然不能直接发送信号名称,但是通过信号的编号,可以正常的给程序发送信号了,也是初步实现了信号的发送流程。
关于 kill 函数,还有一点需要额外说明,上面的程序限定了 pid 必须为大于0的正整数,其实 kill 函数传入的 pid 可以是小于等于0的整数。 pid > 0:将发送个该 pid 的进程 pid == 0:将会把信号发送给与发送进程属于同一进程组的所有进程,并且发送进程具有权限想这些进程发送信号。 pid
关于信号,还有更多的话题,比如,信号是否都能够准确的送达到目标进程呢?答案其实是不一定,那么这就有了可靠信号和不可靠信号
不可靠信号:信号可能会丢失,一旦信号丢失了,进程并不能知道信号丢失 可靠信号:也是阻塞信号,当发送了一个阻塞信号,并且该信号的动作时系统默认动作或捕捉该信号,如果信号从发出以后会一直保持未决的状态,直到该进程对此信号解除了阻塞,或将对此信号的动作更改为忽略。 对于信号来说,信号编号小于等于31的信号都是不可靠信号,之后的信号为可卡信号,系统会根据有信号队列,将信号在递达之前进行阻塞。
信号的阻塞和未决是通过信号的状态字来管理的,该状态字是按位来管理信号的状态。每个信号都有独立的阻塞字,规定了当前要阻塞地达到该进程的信号集。
信号阻塞状态字(block),1代表阻塞、0代表不阻塞;信号未决状态字(pending)的1代表未决,0代表信号可以抵达了;它们都是每一个bit代表一个信号
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数来获取或者设置。该函数管理信号,是通过信号集的数据结构来进行管理的,信号集可以通过以下的函数进行管理。 信号集操作函数(状态字表示)
#include int sigemptyset(sigset_t *set); //初始化 set 中传入的信号集,清空其中所有信号 int sigfillset(sigset_t *set); //把信号集填1,让 set 包含所有的信号 int sigaddset(sigset_t *set, int signum);//把信号集对应位置为1 int sigdelset(sigset_t *set, int signum);//吧信号集对应位置为0 int sigismember(const sigset_t *set, int signum);//判断signal是否在信号集
对于信号集分配好内存空间,需要使用初始化函数来初始化。初始化完成后,可以在该集合中添加、删除特定的信号。 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
其中 how 变量决定了是如何操作该状态字。 SIG_BLOCK:set包含了我们希望添加到当前信号阻塞字的信号,相当于mask=mask|set SIG_UNBLOCK:set包含了我们希望从当前信号阻塞字中解除阻塞的信号,相当于mask=mask&~set SIG_SETMASK:设置当前信号阻塞字为set所指的值,相当于mask=set
pending是由内核来根据block设置的,只可以读取其中的数据,来段判断信号是否会递达。通过设置block可以将希望阻塞的信号进行阻塞,对应的pending会由内核来设置
设置信号阻塞、未达的步骤:
sigset bset;
sigemptyset(&bset);
sigaddset(&bset, SIGINT);
sigprocmask(SIG_UNBLOCK, &bset, NULL);
#include int sigpending(sigset_t *set);
这个函数使用很简单,对于调用他的进程来说,其中信号集中的信号是阻塞不能递送的,那么,也就一定会是当前未决的。
#include int sigsuspend(const sigset_t *mask);
为何会出现原子性的解除阻塞的函数呢? 因为,当信号被阻塞的时候,产生了信号,那么该信号的递送就要推迟到这个信号被解除了阻塞为止。如果此时,应用程序正好处在,解除 SIGINT 的阻塞和 pause 之间,那么此时,会产生问题,可能永远 pause 不能够等到SIGINT 信号来打断他,造成程序永久阻塞在 pause 处。 为了解决这个问题,,需要在一个原子性的操作来恢复信号的屏蔽字,然后才能让进程进入休眠状态,以保证不会出现上述的问题。
进程的信号屏蔽字设置为
我们已经成功完成了信号的收发,那么为什么会有高级版出现呢?其实之前的信号存在一个问题就是,虽然发送和接收到了信号,可是总感觉少些什么,既然都已经把信号发送过去了,为何不能再携带一些数据呢? 正是如此,我们需要另外的函数来通过信号传递的过程中,携带一些数据。咱么先来看看发送的函数吧。
sigaction 的函数原型
#include int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作 void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用 sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。 int sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据 }; //回调函数句柄sa_handler、sa_sigaction只能任选其一
这个函数的原版帮助信息,可以通过man sigaction
来查看。
sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数signum
应该就是注册的信号的编号;第二个参数act
如果不为空说明需要对该信号有新的配置;第三个参数oldact
如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。
在这里额外说一下struct sigaction
结构体中的 sa_mask 成员,设置在其的信号集中的信号,会在捕捉函数调用前设置为阻塞,并在捕捉函数返回时恢复默认原有设置。这样的目的是,在调用信号处理函数时,就可以阻塞默写信号了。在信号处理函数被调用时,操作系统会建立新的信号阻塞字,包括正在被递送的信号。因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。
sigaction 的时效性:当对某一个信号设置了指定的动作的时候,那么,直到再次显式调用 sigaction并改变动作之前都会一直有效。
关于结构体中的 flag 属性的详细配置,在此不做详细的说明了,只说明其中一点。如果设置为 SA_SIGINFO 属性时,说明了信号处理程序带有附加信息,也就是会调用 sa_sigaction 这个函数指针所指向的信号处理函数。否则,系统会默认使用 sa_handler 所指向的信号处理函数。在此,还要特别说明一下,sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。
关于void (*sa_sigaction)(int, siginfo_t *, void *);
处理函数来说还需要有一些说明。void*
是接收到信号所携带的额外数据;而struct siginfo
这个结构体主要适用于记录接收信号的一些相关信息。
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 */ sigval_t 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 */ int si_band; /* Band event */ int si_fd; /* File descriptor */ }
其中的成员很多,si_signo 和 si_code 是必须实现的两个成员。可以通过这个结构体获取到信号的相关信息。 关于发送过来的数据是存在两个地方的,sigval_t si_value这个成员中有保存了发送过来的信息;同时,在si_int或者si_ptr成员中也保存了对应的数据。
那么,kill 函数发送的信号是无法携带数据的,我们现在还无法验证发送收的部分,那么,我们先来看看发送信号的高级用法后,我们再来看看如何通过信号来携带数据吧。
#include int sigqueue(pid_t pid, int sig, const union sigval value); union sigval { int sival_int; void *sival_ptr; };
使用这个函数之前,必须要有几个操作需要完成
sigqueue 函数只能把信号发送给单个进程,可以使用 value 参数向信号处理程序传递整数值或者指针值。
sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了 POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue 发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次。
但是,信号不能无限的排队,信号排队的最大值受到SIGQUEUE_MAX
的限制,达到最大限制后,sigqueue 会失败,errno 会被设置为 EAGAIN。
那么我们来尝试一下,发送一个携带有额外数据的信号吧。 Show me the code!! 接收端
#include#include#include //void (*sa_sigaction)(int, siginfo_t *, void *); void handler(int signum, siginfo_t * info, void * context) { if(signum == SIGIO) printf("SIGIO signal: %d\n", signum); else if(signum == SIGUSR1) printf("SIGUSR1 signal: %d\n", signum); else printf("error\n"); if(context) { printf("content: %d\n", info->si_int); printf("content: %d\n", info->si_value.sival_int); } } int main(void) { //int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction act; /* struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; }; */ act.sa_sigaction = handler; act.sa_flags = SA_SIGINFO; sigaction(SIGIO, &act, NULL); sigaction(SIGUSR1, &act, NULL); for(;;) { sleep(10000); } return 0; }
发送端
#include #include #include#include int main(int argc, char** argv) { if(4 != argc) { printf("[Arguments ERROR!]\n"); printf("\tUsage:\n"); printf("\t\t%s \n", argv[0]); return -1; } int pid = atoi(argv[1]); int sig = atoi(argv[2]); if(pid > 0 && sig > 0) { //int sigqueue(pid_t pid, int sig, const union sigval value); union sigval val; val.sival_int = atoi(argv[3]); printf("send: %d\n", atoi(argv[3])); sigqueue(pid, sig, val); } else { printf("Target_PID or Signal_Number MUST bigger than 0!\n"); } return 0; }
接收到的信号和信息
发送的信号和数据
关于“Linux的Signal机制是什么”这篇文章的内容就介绍到这里,感谢各位的阅读!相信大家对“Linux的Signal机制是什么”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。