信号是给进程发送的,那么进程也有对应的信号处理的机制(这个好比我们知道交规一样,进程信号处理机制是程序员预先设定好的!),一样的道理,即便是信号还没有产生,但是进程已然存在对应信号的处理机制!
[hb@localhost code_test]$ cat sig.c
#include
int main()
{
while(1){
printf("I am a process, I am waiting signal!\n");
sleep(1);
}
}
[hb@localhost code_test]$ ./sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C
[hb@localhost code_test]$
假设你是一个进程,而快递员代表操作系统。你的任务是一直等待快递员送信号(快递)给你。在这里,快递员相当于操作系统,会向你发送各种信号,如Ctrl-C信号。
代码中的while(1)表示你一直在等待,就像一直在家里等待快递。每秒你都打印出一条消息:“I am a process, I am waiting signal!”,表示你不断地检查是否有快递到达。
当你收到了Ctrl-C信号时(就像快递员按响了你家门铃),你会看到在终端上出现"^C",并且你的程序会终止执行。
这就是代码信号处理过程的模拟。你作为一个进程一直在等待信号,而操作系统会发送不同的信号给你,如Ctrl-C信号,你需要对这些信号做出相应的处理,比如终止程序的执行。
总结来说,代码中的进程等待信号的过程就像你一直在家等待快递的到来。当操作系统发送信号给你,你需要对信号做出相应的处理,就像按下了门铃后你会去开门签收快递。不同的信号可以触发不同的处理动作,让你的程序做出相应的反应。
- Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的
异步是指事件之间不需要严格的同步和等待,而是可以独立地进行处理。在异步操作中,一个事件的触发并不会导致程序的立即停顿或阻塞,而是允许程序继续执行其他任务,而后在合适的时间点再去处理该事件或结果。
异步操作通常用于处理耗时较长的任务,如网络请求、文件读写、数据库查询等。在传统的同步操作中,当执行这些耗时任务时,程序会一直等待任务完成才能继续执行后续代码。而在异步操作中,程序可以先发起这些耗时任务,然后继续执行其他代码,等待任务完成后再进行后续处理。
异步操作可以提高程序的响应性能和效率,尤其在涉及到多任务并行处理的场景下,异步操作能够更好地利用系统资源和提高系统的并发能力。
在编程中,异步操作通常通过回调函数、事件驱动机制、多线程或异步IO等方式来实现。一些编程语言和框架提供了异步编程的支持,使得开发者能够更方便地处理异步操作。
== 因为信号的产生是异步的,当一个信号产生的时候,对应的进程可能正在处理其他的更加重要的事情,那么进程可以暂时不去处理这个信号 ==
当一个信号产生时 进程可能执行的操作:
在操作系统中,当进程收到信号后,如果不设置忽略,操作系统会在进程的 PCB(进程控制块)中记录该信号的待处理状态。这通常通过在 PCB 中的位图(或类似的数据结构)来实现。
在 Linux 中,进程的 PCB 数据结构中有一个名为 sigpending 的位图,用于表示当前已经到达但还未处理的待处理信号。当进程收到信号但还未处理时,相应信号的位会被设置为 1。一旦进程开始处理该信号,操作系统会将对应位重新设置为 0,表示信号已经处理完成。
具体来说,sigpending 位图在 PCB 数据结构中用于存储当前进程收到但还未处理的信号。当进程收到信号时,相应信号的位会被设置为 1,表示信号已经到达。当进程准备处理信号时,会检查 sigpending 位图,找到所有待处理的信号,并依次处理它们。处理完成后,相应信号的位会被重新设置为 0,表示信号已经处理完毕。
这样,即使进程在收到信号后没有立即处理,操作系统也能够记录信号的状态,并在适当的时候通知进程处理相应的信号。这种方式实现了异步信号处理,允许进程在合适的时候处理优先级较高的信号,而不会被阻塞在处理低优先级的信号上。
#include
void (*signal(int signum, void (*handler)(int)))(int);
使用signal函数时,一般会先定义一个自定义的信号处理函数,然后通过signal函数将其注册到指定的信号上。当进程收到相应的信号时,操作系统会调用该信号处理函数来处理该信号。函数回调机制在这里体现在信号发生时,系统通过函数指针调用我们提供的处理函数。
测试代码
#include
#include
void sigHandler(int signum) {
printf("Received signal %d\n", signum);
}
int main() {
// 注册SIGINT信号处理函数为sigHandler
//只是注册 当singint产生的时候才会被调用,如果不产生,就不会被调用
signal(SIGINT, sigHandler);
printf("Waiting for SIGINT...\n");
while (1) {
// 进程持续运行,等待信号发生
}
return 0;
}
在上述示例中,当用户在终端中按下Ctrl+C(产生SIGINT信号)时,进程会调用sigHandler函数来处理该信号,并输出"Received signal 2"(因为SIGINT的编号是2)。
注意:使用signal函数时,需要注意信号的可重入性问题。在一些情况下,建议使用更加安全可靠的sigaction函数来替代signal函数。
signal函数可以自定义信号的处理机制,如上面所示,当在终端按下Ctrl+c(也就是2号进程)时会打印一个: Received signal 2 这就是我们的自定义行为
那么所有的信号都可以被自定义吗?
答案:不是 9号信号不可以被定义 (管理员信号 )
Linux中的9号信号是SIGKILL,也称为强制终止信号。SIGKILL用于立即终止一个进程,并且该信号无法被捕获或忽略。当进程收到SIGKILL信号时,它会立即终止,不会有任何处理和清理工作。
通常情况下,应该避免直接使用SIGKILL信号来终止进程,除非有特殊原因需要强制终止进程。因为进程没有机会进行资源清理和善后工作,可能会导致数据丢失或其他不稳定的情况。
相比之下,可以使用SIGTERM信号来通知进程进行正常退出,这样进程有机会在收到信号后进行资源释放和善后工作,保证系统的稳定性。
函数原型如下
#include
int kill(pid_t pid, int sig);
参数说明:
//手写一个kill命令
static void Usage(const std::string &proc)
{
cerr<<"Usege:\n\t"<<proc<<"signo pid"<<endl;
}
int main(int argc,char *argv[]) {
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
if(kill(static_cast<pid_t>(atoi(argv[2])),atoi(argv[1]))==-1) //类型转换 调用函数
{
//失败报错
cerr<<"kill"<<strerror(errno)<<endl;
exit(2);
}
return 0;
}
if (argc != 3): 这行代码判断命令行参数的数量是否为3,即程序名本身和两个额外参数。如果不是3个参数,说明用户输入有误,程序没有正确使用,因此调用Usage函数输出使用方法,并通过exit(1)终止程序运行。
kill(static_cast
(atoi(argv[2])), atoi(argv[1])): 这行代码使用kill函数向目标进程发送信号。argv[2]是第二个命令行参数,即目标进程的进程ID,通过atoi函数将字符串转换为整数,并使用static_cast 进行类型转换,以满足kill函数的参数要求。argv[1]是第一个命令行参数,即要发送的信号编号,也通过atoi函数将字符串转换为整数。最终调用kill函数发送信号,如果发送失败,kill函数会返回-1,此时程序会输出相应的错误信息,使用cerr输出错误消息,然后通过exit(2)终止程序运行。
总的来说,这段代码用于向指定进程发送信号,并根据发送结果输出相应的错误信息,是一个简单的进程通信示例
using namespace std;
#include
#include
#include
int main()
{
while(1)
{
cout<<"这是一个进程,pid是:"<<getpid()<<endl;
}
return 0;
}
#include
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
#include
#include
#include
void handler(int sig)
{
printf("catch a sig : %d\n", sig);
}
int main()
{
//signal(2, handler); // 信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的
sleep(1);
//野指针使用
int *p = NULL;
*p = 100;
while(1);
return 0;
}
== Segmentation fault 对应系统第11号信号 SIGSEGV ==
通过前文的介绍,我们知道,当进程接收到信号后处理有三种行为:
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
== 相当于对相关数据结构进行增删查改 ==
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
#include
int main() {
sigset_t new_mask, old_mask;
// 初始化新的信号屏蔽字,屏蔽SIGINT和SIGQUIT信号
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigaddset(&new_mask, SIGQUIT);
// 阻塞新的信号屏蔽字,并保存旧的信号屏蔽字
if (sigprocmask(SIG_BLOCK, &new_mask, &old_mask) == -1) {
perror("sigprocmask");
return 1;
}
// 这里的代码执行时,SIGINT和SIGQUIT信号会被阻塞
// 解除对SIGINT信号的阻塞,保持SIGQUIT信号仍然被阻塞
sigdelset(&new_mask, SIGINT);
if (sigprocmask(SIG_SETMASK, &new_mask, NULL) == -1) {
perror("sigprocmask");
return 1;
}
// 这里的代码执行时,只有SIGQUIT信号会被阻塞,SIGINT信号会被接收
return 0;
}
在示例中,首先使用sigprocmask函数将SIGINT和SIGQUIT信号添加到进程的信号提示字中,然后解除对SIGINT信号的阻塞,保持SIGQUIT信号仍然被上述阻塞。这样,在不同阶段执行代码时,就会根据信号信号字的设置决定是否接收相应的信号。
sigpending是一个系统调用,用于获取当前被阻塞的未决信号集,即当前进程接收但尚未处理的被阻塞信号集合。
函数原型为:
int sigpending(sigset_t *set);
set:一个指向sigset_t类型的指针,用于存储
调用sigpending函数后,被阻塞的未决信号集将会被填充到set指向的sigset_t类型的信号中。如果进程没有设置信号提示字或者没有未决信号,则set中的信号集将会被清空。
使用示例:
#include
#include
int main() {
sigset_t blocked_set;
// 创建一个信号集,并将SIGINT信号添加到信号集中
sigemptyset(&blocked_set);
sigaddset(&blocked_set, SIGINT);
// 设置信号屏蔽字,阻塞SIGINT信号
if (sigprocmask(SIG_BLOCK, &blocked_set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("Waiting for SIGINT signal...\n");
// 这里可以做一些其他的工作
// 获取被阻塞的未决信号集
sigset_t pending_set;
if (sigpending(&pending_set) == -1) {
perror("sigpending");
return 1;
}
// 检查是否有SIGINT信号在未决信号集中
if (sigismember(&pending_set, SIGINT)) {
printf("Received SIGINT signal.\n");
} else {
printf("SIGINT signal is not pending.\n");
}
return 0;
}
在示例中,首先设置信号提示字,阻止SIGINT信号。然后获取被阻塞的未决信号集,并检查其中是否有SIGINT信号。如果有,则表示进程收到了上述但开始处理的SIGINT信号,否则表示该信号不在信号集中