Linux基础——信号

目录

1. 什么是信号?

2. 信号的产生

①键盘的组合键

②kill命令

④产生异常

⑤软件条件

⑥进程等待中的core dump

3. 信号的保存

①信号的发送与接收

②为什么需要保存信号? 

③信号是如何保存的

4. 信号的捕捉处理

①信号的处理与处理时机

②再谈进程地址空间

③信号的捕捉

④可重入函数与不可重入函数 

⑤从信号角度谈谈C++中的volatile

⑥SIGCHLD


在介绍信号之前,我们先用一个浅显的例子来理解信号

我们点了一个外卖,

外卖小哥打了个电话——产生了一个信号

此时可能在打游戏,等待一个合适的时间处理——信号被记录下来

打完游戏后,去取外卖——处理信号

1. 什么是信号?

对于信号的概念,我们还是举几个生活中的例子,比如

照明信号弹,上下课铃声,发令枪,集合哨子,闹钟,外卖电话等等

从这里我们就可以提出几点

1. 我们是怎么认识这些信号的?——有人教 -> 我们记住了这些常见的信号

2. 即便没有信号产生,我们也知道信号产生之后该干什么

3. 信号产生了并不代表我们要立刻处理这个信号,可以挑选一个合适的时间去处理,因为我们有可能正在做更重要的事。因此,信号产生后会为他创建一个时间窗口,在处理信号时,我们需要在这个时间窗口内记住有信号来过。

 我们类比到OS中来,进程就是这里的“我们”,即

1. 进程必须能够识别 + 处理信号——即使信号没有产生,也要有具备处理信号的能力——信号的处理能力属于进程内置功能的一部分

2. 当进程真的收到一个具体信号时,进程可能并不会立即处理这个信号,而是留待合适的时候处理

注:信号的处理方式分为:1. 默认动作; 2. 忽略; 3. 自定义动作

3. 一个信号从产生到信号开始被处理,一定会有一个时间窗口,进程具有临时保存哪些信号已经产生了的能力

我们可以使用

kill -l

来查看所有信号,即

每个信号的序号及其完整的英文名称:

  1. SIGHUP - Hangup (挂断)

  2. SIGINT - Interrupt (中断)

  3. SIGQUIT - Quit (退出)

  4. SIGILL - Illegal Instruction (非法指令)

  5. SIGTRAP - Trace/Breakpoint Trap (跟踪/断点陷阱)

  6. SIGABRT - Abort (中止)

  7. SIGBUS - Bus Error (总线错误)

  8. SIGFPE - Floating-Point Exception (浮点异常)

  9. SIGKILL - Kill (杀死进程)

  10. SIGUSR1 - User-Defined Signal 1 (用户定义信号1)

  11. SIGSEGV - Segmentation Fault (段错误)

  12. SIGUSR2 - User-Defined Signal 2 (用户定义信号2)

  13. SIGPIPE - Broken Pipe (管道破裂)

  14. SIGALRM - Alarm Clock (闹钟)

  15. SIGTERM - Termination (终止)

  16. SIGSTKFLT - Stack Fault (栈错误)

  17. SIGCHLD - Child Status Changed (子进程状态改变)

  18. SIGCONT - Continue (继续)

  19. SIGSTOP - Stop (Cannot be caught or ignored) (停止)

  20. SIGTSTP - Terminal Stop Signal (终端停止信号)

  21. SIGTTIN - Background Read from TTY (后台读取TTY)

  22. SIGTTOU - Background Write to TTY (后台写入TTY)

  23. SIGURG - Urgent Condition on Socket (套接字紧急情况)

  24. SIGXCPU - CPU Time Limit Exceeded (CPU时间限制超出)

  25. SIGXFSZ - File Size Limit Exceeded (文件大小限制超出)

  26. SIGVTALRM - Virtual Alarm Clock (虚拟闹钟)

  27. SIGPROF - Profiling Timer Expired (性能分析时间到期)

  28. SIGWINCH - Window Size Change (窗口大小改变)

  29. SIGIO - I/O Now Possible (I/O操作可能)

  30. SIGPWR - Power Failure (电源故障)

  31. SIGSYS - Bad System Call (错误系统调用)

其中1~31号信号为普通信号,34~64号信号为实时信号(需要立即处理)

我们之前已经知道了,ctrl+c 能够直接杀掉前台进程,这是为什么呢?——键盘输入是前台输入,首先会被前台进程捕获到,而 ctrl+c 本质上被进程解释成收到了2号信号,我们可以使用signal接口来验证,接口如下

signal() 的行为在不同的 UNIX 版本之间有所不同,并且在不同版本的 Linux 中也有历史上的变化。建议避免使用 signal(),而应使用 sigaction(2)

signal() 将信号 signum 的处理方式设置为 handler,该处理方式可以是 SIG_IGNSIG_DFL,或者是程序员定义的函数的地址(即“信号处理函数”)。

如果信号 signum 被传递给进程,则会发生以下情况之一:

  • 如果处理方式设置为 SIG_IGN,则信号将被忽略。

  • 如果处理方式设置为 SIG_DFL,则会执行与该信号相关的默认操作(请参见 signal(7))。

  • 如果处理方式设置为一个函数,则首先会将处理方式重置为 SIG_DFL,或者信号会被阻塞(请参见下面的可移植性部分),然后调用 handler,并将 signum 作为参数传递。如果调用处理函数导致信号被阻塞,则在返回处理函数后,该信号会被解锁。

信号 SIGKILL 和 SIGSTOP 不能被捕获或忽略。

测试代码如下

int main()
{// 默认处理signal(SIGINT, SIG_DFL);while (true){lg(Debug, "this is a test process\n");sleep(3);}return 0;
}

 运行如下

可以看到,对2号信号的默认动作就是终止自己,接下来我们使用自定义动作试试,测试代码如下

// signo - 信号编号
void myhandler(int signo)
{lg(Info, "get a signal, signo : %d\n", signo);
}int main()
{signal(SIGINT, myhandler);while (true){lg(Debug, "this is a test process\n");sleep(3);}return 0;
}

 运行有

可以看到,我们确实收到了2号信号,且由于我们采取了自定义操作,将处理方式变为了向屏幕打印而不是终止。那我们为什么要将 signal 函数放在代码最前面呢?——设置一次后,再往后都有效。

接下来我们讲讲键盘数据是如何输入给OS的,以及 ctrl+c 是如何变成信号的,这就需要我们谈到硬件了,我们用画图来解释,如图

如图所示,键盘输入内容后,键盘被按下后,键盘通过针脚向CPU发送硬件中断,随后OS根据中断号,将其作为索引在中断向量表中执行对应的方法,最后这个读方法会让OS将键盘中的内容读取到内部的文件缓冲区中。

在这个过程中,如果在最后一步,OS将键盘中内容拷贝到内部文件缓冲区前,OS会先判断数据,即如果识别到数据是 ctrl+c 就会将其转化成2号信号发送给对应进程。我们学习的信号,就是使用软件方式模拟硬件中断,从而给进程发送信号。

2. 信号的产生

信号的产生一般分为如下几种

①键盘的组合键

我们可以使用键盘的组合键向前台进程发送信号,比如:

ctrl+c表示发送2号信号(SIGINT);

ctrl+\表示发送3号信号(SIGQUIT);

ctrl+z表示发送19号信号(SIGSTOP)。

②kill命令

举个例子

kill -19 23541

就表示向pid为23541的进程发送19号信号(SIGSTOP)。 

③系统调用。如:kill, raise, abort等,测试代码如下

void Help(string proc)
{lg(Fatal, "Please follow the format: %s signo pid\n", proc.c_str());
}int main(int argc, char* argv[])
{if (argc != 3){Help(argv[0]);exit(1);}int signo = stoi(argv[1]);pid_t pid = stoi(argv[2]);int n = kill(pid, signo);if (n == -1){lg(Fatal, "kill error : %s", strerror(errno));exit(2);}return 0;
}

运行效果

测试代码2

int main()
{int cnt = 5;while (true){cout << "this is a test process, my pid : " << getpid() << endl;sleep(1);cnt--;if (cnt == 0) raise(9);}return 0;
}

 运行效果

abort类似于raise,不同点在于发送的是6号信号。

④产生异常

我们举个例子,测试代码如下

int main()
{sleep(1);lg(Debug, "point error before\n");int *p;*p = 5;sleep(1);lg(Debug, "point error after\n");sleep(1);return 0;
}

运行效果

可以发现,进程由于使用了野指针崩溃了,我们对其捕捉并自定义有

void handler(int signo)
{lg(Info, "process %d get a signal, signo : %d\n", getpid(), signo);
}int main()
{signal(SIGSEGV, handler);sleep(1);lg(Debug, "point error before\n");int *p;*p = 5;sleep(1);lg(Debug, "point error after\n");sleep(1);return 0;
}

运行有

为什么这里一直调用了handler函数呢?信号又为什么一直触发呢?——进程未退出(未变成Z状态)。而我们之前学到过try ... catch{} 肯定与信号有关,不过它发出的信号应该是很温和的,因为我们可以自己对这种情况进行处理。此外,还有除0错误等硬件异常。但是,异常并不只由硬件产生,比如在使用管道的时候,写端正在写入,读端被关闭也会产生异常。

⑤软件条件

在Linux中,我们可以使用alarm接口来向进程发出14号信号(SIGALRM),它的接口如下

alarm() 函数安排在指定的秒数后将一个 SIGALRM 信号发送给调用进程。

如果秒数为零,任何待处理的警报将被取消。

无论如何,之前设置的任何 alarm() 都会被取消。

测试代码如下

int main()
{int n = alarm(5);while (true){lg(Info, "this is a process, my pid : %d\n", getpid());sleep(1);}return 0;
}

 运行效果如下

我们可以通过重复设置闹钟,使代码每5s响一次,每几秒执行一次自己的任务。修改后代码如下

void handler(int signo)
{lg(Info, "pocess %d get a signal, signo : %d\n", getpid(), signo);alarm(5);
}int main()
{signal(SIGALRM, handler);int n = alarm(5);while (true){lg(Info, "this is a process\n");sleep(1);}return 0;
}

运行效果如下

⑥进程等待中的core dump

之前我们进程控制中提到,对于一个进程来说,其正常退出与被信号杀死退出是不一样的,具体表现在 waitpid 的第二个参数status,图解如下

这个core dump具体大致分为两种形式,我们可以在man 7 signal 中查看到

我们可以用下面的代码来验证

int main()
{pid_t id = fork();// childif (id == 0){while (true){lg(Info, "this is child, my pid: %d", getpid());sleep(1);}exit(0);}// fatherint status = 0;pid_t rid = waitpid(id, &status, 0);if (rid == id){lg(Info, "child quit! rid: %d, exit code: %d, exit signal: %d, core dump: %d\n", rid, (status>>8)&0xFF, (status&0x7F), (status>>7)&1);}return 0;
}

我们向其发送2号信号有

 

向其发送8号信号有

可以看到,两种退出方式中的core dump各不相同,而在我们发送信号后,会生成一个core.xxx的文件,如图

这里的32734其实就是运行时进程的pid。

当我们打开系统中的core dump功能时,一旦进程出现异常,OS会将进程在内存中的运行信息 dump(转储) 到进程的当前目录中,形成 core.pid 文件,我们将其称为核心转储(core dump)

形成 core dump 属于运行时出错,而出错了我们一定是想知道在哪一行出了问题。而我们可以在gdb调试中使用 core_file core.pid 来直接定位到出错行。

但是这个功能在云服务器中是默认关闭的,为什么?——因为如果服务器因为出问题陷入了停机就会重启,而重启之后又停机的话,会在这个过程中产生大量的core dump文件从而冲击到磁盘。

3. 信号的保存

①信号的发送与接收

在讲信号的保存前,我们先来讲讲信号是如何发送与接收的。

之前我们说过,OS可以使用 kill 命令来向指定的进程发送对应信号。那么对于一个进程来说,我怎么知道自己有没有收到一个信号呢?其实OS给进程发信号,其实就是给进程的PCB中发信号,我们可以在PCB(task_struct)中查看到

其实OS是将一个 int 的32位作为一个位图,并使用它来管理普通信号(1~31号)。那就意味着如果一次性发十几个信号的话会遗失一部分。

注:实时信号不会遗失,且会立刻执行,虽然它的设计理念与普通信号相同,但是它们实际是使用双链表来管理的。

我们来谈谈这个位图,一个 int 可以表示成 0000 0000 0000 0000 0000 0000 0000 0000,其中

1. 比特位的内容是0还是1,表示是否收到该信号

2. 比特位的位置(第几个)表示信号的编号

3. 所谓的向进程“发信号”,其本质就是在OS去修改PCB中的信号位图对应的比特位!

②为什么需要保存信号? 

因为进程在收到信号后,可能不会在现在处理这个信号,而信号不被处理,就会产生一个时间窗口

③信号是如何保存的

我们先来介绍一些信号的常见概念

1. 实际执行信号的处理动作称为信号递达(Delivery)。

2. 信号从产生到递达之间的状态,称为信号未决(Pending)。

3. 进程可以选择阻塞(Block)某个信号。
4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。 

因为普通信号一共有31种,每一种信号都要有自己的处理办法,那么我们可以猜测一下,OS中一定有下面这种类似的设计

typedef void (*handler_t)(int); // -- 对应操作的函数指针
handler_t handler[31];          // -- 31种信号的操作集

实际上,在Linux中是这样设计的,如图

即在PCB中,对于信号方面有三个表,分别是阻塞表,保存表,处理表。

在阻塞表中,对应位置的数字为1则表示该信号被阻塞,数字为0则则表示该信号未被阻塞;

在保存表中,对应x位置的数字被设置成1时,表示进程收到了x号信号;

在处理表中,对应位置的处理一般分为SIG_DFL(默认处理),SIG_IGN(忽略处理),自定义处理,而在内核中我们可以看到SIG_DFL与SIG_IGN分别是由0和1强制类型转换来的。

注:阻塞和忽略是不同的,举个简单的例子,忽略就是已读不回,而阻塞是未读。

那我们有没有办法来修改这几个表呢?——有的,我们可以使用信号集操作函数来修改block表与pending表。 

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);

我们简单介绍一下上面这些函数

1. 函数 sigemptyset 初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
2. 函数 sigfillset 初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
注: 在使用 sigset_ t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化, 使信号集处于确定的状态。

3. 初始化 sigset_t 变量之后就可以调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
4. sigismember 是一个布尔函数, 用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

在介绍完后,我们讲讲如何修改 Block 表和pending表,我们需要使用

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

sigprocmask() 函数用于获取和/或更改调用线程的信号屏蔽字。信号屏蔽字是指当前对于调用者被阻塞(即无法接收)的信号集合(有关更多详细信息,请参阅 signal(7))。

此调用的行为取决于 how 参数的值,具体如下:

  • SIG_BLOCK
    • 被阻塞的信号集合是当前集合与 set 参数的并集。
  • SIG_UNBLOCK
    • 从当前被阻塞的信号集合中移除 set 中的信号。尝试解除一个未被阻塞的信号的阻塞是允许的。
  • SIG_SETMASK
    • 被阻塞的信号集合被设置为 set 参数指定的集合。

如果 oldset 非空,则信号屏蔽字之前的值会被存储在 oldset 中。

如果 set 为空,则信号屏蔽字保持不变(即 how 被忽略),但如果 oldset 非空,当前信号屏蔽字的值仍会被存储在 oldset 中。

在多线程进程中,sigprocmask() 的使用未指定;请参见 pthread_sigmask(3)

还有

int sigpending(sigset_t *set);

 sigpending() 函数返回调用线程(即调用该函数的线程)待处理的信号集合(也就是那些在阻塞期间被引发但尚未交付的信号)。待处理信号的掩码会返回在 set 中。

测试代码如下

void PrintPending(sigset_t &pending)
{// 打印1~31号信号cout << "pending: ";for (int signo = 1; signo <= 31; signo++){if (sigismember(&pending, signo)){cout << "1";}else{cout << "0";}}cout << endl;
}int main()
{// 1. 我们想阻塞2号信号sigset_t set, oset;sigemptyset(&set);sigemptyset(&oset);sigaddset(&set, 2); // 到这里只是预备好了资源,并没有设置进内核中// 2. 设置进内核sigprocmask(SIG_SETMASK, &set, &oset); // 已经将2号信号设置为阻塞了!// 3. 打印pendingsigset_t pending;int cnt = 10;while (true){int n = sigpending(&pending);if (n < 0){lg(Fatal, "sigpending error!\n");continue;}PrintPending(pending);// 10秒后解除阻塞2号信号sleep(1);cnt--;if (cnt == 0){lg(Info, "unblock the signo 2\n");sigprocmask(SIG_SETMASK, &oset, nullptr);}}return 0;
}

运行效果如下

接下来,我们对2号信号进行自定义捕捉,有

那么我们可以屏蔽所有信号吗?——当然不行,前面我们提到过的9号与19号信号都是不可被屏蔽的!

4. 信号的捕捉处理

①信号的处理与处理时机

我们先来看看信号是如何被处理的,在这里我们可以使用一张图来描述

在这个过程中我们也可以看到,当进程从内核态返回到用户态的时候,进行信号的检测与处理。那么什么时候才会切换身份呢?我们举几个例子

1. 调用系统调用。OS是会自动做身份切换的,即 用户身份 ↔ 内核身份

2. 汇编指令 int 80。这条汇编指令会使用户态陷入内核态

②再谈进程地址空间

在一个正常运转的OS中,用户页表有几份呢?——因为进程具有独立性,因此有几个进程就有几个用户级页表。那么内核页表有几份呢?——仅有1份!因为每个进程看到的3~4GB(即内核空间)的东西是一样的,整个OS中进程再怎么切换,内核空间的内容是不会变的。

对于进程来说,自己调用系统中的方法,就是在我进程地址空间中的代码区执行的;

对于OS来说,在任意一个时刻,都有进程正在运作,它不管什么时候想执行OS的代码,就可以马上执行。

因此,对于一个进程的工作状态来说,内核态就是能够访问OS的代码与数据,用户态就是只能访问自己的代码与数据。 

③信号的捕捉

我们已经知道,信号在收到后会保存在三个表中,而信号的捕捉实际上就是在研究 pending 表什么时候由1->0,我们可以使用 sigaction 函数来对每个信号的行为,其接口如下

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

其中,signum 表示信号序号,act 是一个输入型参数,oldact 是一个输出型参数,而这个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);
};

 测试代码如下

void handler(int signo)
{lg(Info, "catch a signal, signal number: %d", signo);
}int main()
{struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(oact));act.sa_handler = handler;sigaction(2, &act, &oact);while(true){lg(Info, "this is a process: %d", getpid());sleep(1);}return 0;
}

运行效果如下

要研究 pending 表中的信号什么时候由1->0,我们在 handler 中加上一个打印函数来验证,即

void PrintPending(sigset_t &pending)
{cout << "pending: ";for (int signo = 1; signo <= 31; signo++){if (sigismember(&pending, signo)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int signo)
{sigset_t set;sigpending(&set);PrintPending(set);lg(Info, "catch a signal, signal number: %d", signo);
}int main()
{struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(oact));act.sa_handler = handler;sigaction(2, &act, &oact);while(true){lg(Info, "this is a process: %d", getpid());sleep(1);}return 0;
}

运行效果如下

从结果中,我们可以看到在打印 pending 表时,信号已经变为0,结论就是在执行捕捉方法时,是先将 pending 表清零,再调用 handler 方法。且在收到2号信号后,如果当前正在处于捕捉状态,2号信号的block表会设置成1,捕捉结束后变为0,这样可以防止出现各种嵌套调用,我们可以以下代码来测试

void PrintPending(sigset_t &pending)
{cout << "pending: ";for (int signo = 1; signo <= 31; signo++){if (sigismember(&pending, signo)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int signo)
{sigset_t set;sigpending(&set);lg(Info, "catch a signal, signal number: %d", signo);while (true){PrintPending(set);sleep(1);}
}int main()
{struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(oact));act.sa_handler = handler;sigaction(2, &act, &oact);while(true){lg(Info, "this is a process: %d", getpid());sleep(1);}return 0;
}

运行效果如下

可以看到,正在捕捉的时候,如果再传来相同的信号会被block表中的1阻塞。那在处理2号信号时,2号信号被自动屏蔽了,我们可不可以屏蔽更多信号呢?——可以设置 sigaction 结构体中的 sa_mask 参数即可!

④可重入函数与不可重入函数 

假如有这样一个场景,我们想向链表中插入一个节点,即

如果现在 main 函数中调用了一次 insert (newnode1),如果在执行了①后,此时来了一条信号(这个信号的处理方法也是调用 insert(newnode2) ),执行后 head 节点指向 newnode2,newnode2 指向 node1,此时回到 main 函数的执行流,执行②此时就会将 head->next = newnode1,即

由于 insert 函数被 main 和 sighandler 执行流重复进入,导致了节点丢失与内存泄漏。

如果一个函数被重复进入的情况下出错了或者可能会出错,我们将其称为不可重入函数。反之,称为可重入函数。

目前,我们使用的大部分函数都是不可重入函数! 

⑤从信号角度谈谈C++中的volatile

我们先来看下面这段代码

int flag = 0;void handler(int signo)
{lg(Info, "catch a signal, signo: %d", signo);flag = 1;
}int main()
{signal(2, handler);while(!flag); // flag 0, !flag真lg(Info, "process quit success!");return 0;
}

g++ 可能会对这段代码进行优化,在优化条件下,flag 变量可能会被直接优化到CPU的寄存器中,比如我们使用 -O1进行优化,有如下情况

在未优化情况下,我们使用2号信号可以直接退出,其情况如下

使用优化后,我们不能退出,其情况如下

因为优化,导致我们在内存中不可见了!所以,我们可以在定义 flag 时加上 volatile 关键字,其核心作用就是防止编译器过度优化,保持内存的可见性!加上此关键字后我们再次运行有

⑥SIGCHLD

我们之前提到过,子进程在退出的时候并非是静悄悄地退出的。实际上子进程在退出的时候,会主动向父进程发送SIGCHLD(17号)信号,我们使用下面的代码验证

void handler(int signo)
{lg(Info, "%d proccess catch a signal, signo: %d", getpid(), signo);
}int main()
{signal(17, handler);pid_t id = fork();if(id == 0) // child{while(true){lg(Info, "this is child process, pid: %d, ppid: %d", getpid(), getppid());sleep(1);break;}exit(0);}// fatherwhile(true){lg(Info, "this is father process, pid: %d", getpid());sleep(1);}return 0;
}

运行效果如下 

所以,父进程在进行等待的时候,我们可以采用基于信号的方式进行等待。进程等待有如下的好处

1. 获取子进程的退出状态,释放子进程的僵尸

2. 即使不知道父子进程谁先运行,但一定是 father 进程先退出

我们还需要让父进程调用wait/waitpid接口,即父进程必须保证自己是一直在运行的。既然如此,我们也可以试着将子进程的进程等待写入信号捕捉中,如下

void handler(int signo)
{pid_t rid = waitpid(-1, nullptr, 0);lg(Info, "%d proccess catch a signal, signo: %d", getpid(), signo);
}

如过有多个子进程可以使用 while 循环捕捉,即

void handler(int signo)
{pid_t rid;while(rid  = waitpid(-1, nullptr, WNOHANG) > 0){lg(Info, "%d proccess catch a signal, signo: %d, %d child process quit succcess!", getpid(), signo, rid); }}

换一种说法,我们必须要进行进程等待吗?或者说必须调用 wait 接口吗?

也不是必须要这样做,我们可以使用 signal(17, SIG_IGN) 即可!

注:此方法对 Linux 有效,但是对类 Unix 不一定有效。

我们查看 signal 的手册

可以看到,OS 对17号信号默认执行的就是忽略操作,即17号信号的 SIG_DFL 的行为就是 SIG_IGN!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/57476.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis Time Series介绍和命令大全

Redis Time Series介绍和命令大全 Redis Time Series1 时序序列是什么2 Redis Time Series的特性3 內存模型4 命令详解命名链接表命名详解表TS.CREATETS.ADDTS.ALTERTS.CREATERULETS.DECRBYTS.DELTS.DELETERULETS.GETTS.INCRBYTS.INFOTS.MADDTS.MGETTS.MRANGETS.MREVRANGETS.QU…

WORFBENCH:一个创新的评估基准,目的是全面测试大型语言模型在生成复杂工作流 方面的性能。

2024-10-10,由浙江大学和阿里巴巴集团联合创建的WORFBENCH&#xff0c;一个用于评估大型语言模型&#xff08;LLMs&#xff09;生成工作流能力的基准测试。它包含了一系列的测试和评估协议&#xff0c;用于量化和分析LLMs在处理复杂任务时分解问题和规划执行步骤的能力。WORFBE…

微信小程序文本收起展开

这里写自定义目录标题 微信小程序文本收起展开常见问题的梯形背景框 微信小程序文本收起展开 参考 https://juejin.cn/post/6963904955262435336 <!-- 常见问题解答 --><view classcontentBottom><view classBottomFirst><text id0 data-id0 class&quo…

安装buildkit,并使用buildkit构建containerd镜像

背景 因为K8s抛弃Docker了,所以就只装了个containerd,这样就需要一个单独的镜像构建工具了,就用了buildkit,这也是Docker公司扶持的,他们公司的人出来搞的开源工具,官网在 https://github.com/moby/buildkit 简介 服务端为buildkitd,负责和runc或containerd后端连接干活,目前…

魔音音乐 5.0.1 | 界面优美,可无损下载,可播放

魔音Morin 是一款免费下载付费音乐和免费播放音乐的软件。现在听歌都需要付费&#xff0c;不想付费听音乐就来魔音Morin&#xff0c;完全免费的音乐资源非常丰富&#xff0c;可同步四大音乐平台歌单&#xff0c;还有各类音乐榜单&#xff0c;自带音乐社、同步歌单以及搜索音乐功…

iTOP-RK3568开发板独立NPU通过算法加特应用到以下的场景

iTOP-3568开发板采用瑞芯微RK3568处理器&#xff0c;内部集成了四核64位Cortex-A55处理器。主频高达2.0Ghz&#xff0c;RK809动态调频。集成了双核心架构GPU&#xff0c;ARM G52 2EE、支持OpenGLES1.1/2.0/3.2、OpenCL2.0、Vulkan1.1、内嵌高性能2D加速硬件。 内置独立NPU,算力…

国内大语言模型哪家更好用?

大家好&#xff0c;我是袁庭新。 过去一年&#xff0c;AI大语言模型在爆发式增长&#xff0c;呈现百家争鸣之态。国内外相关厂商积极布局&#xff0c;并相继推出自家研发的智能化产品。 我在工作中已习惯借助AI来辅助完成些编码、创作、文生图等任务&#xff0c;甚至对它们产…

基于Springboot在线视频网站的设计与实现

基于Springboot视频网站的设计与实现 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;idea 源码获取&#xff1a;https://do…

Android 开发 TabLayout 自定义指示器长度

前言 原生 TabLayout 的指示器长度是充满整个屏幕的&#xff0c;但在实际开发中 UI 会设计成 指示器的长度等于或者小于标题字体长度&#xff0c;如图 如果设置成跟字体长度一样即使用 API: mTabLayout.setTabIndicatorFullWidth(false);或者在 xml 布局文件中的TabLayout标签…

vscode配色主题推荐:Andromeda !

vscode配色主题推荐:Andromeda ! 图标库 vscode-icons ! Andromeda:Dark theme with a taste of the universe&#xff1b; 仙女座&#xff1a;一套宇宙深空体验的哑暗色主题; 高对比度,色彩饱和; Easy Installation Open the extensions sidebar on Visual Studio CodeSearc…

判断自己的mac是macOS x64 还是macOS ARM64

在终端输入 uname -a 这样的是x64 这样的是ARM64

Skydel 24.9版本震撼发布,升级五大关键功能

在信号传播与仿真领域&#xff0c;Skydel软件一直是行业内的佼佼者。我们与您分享升级快讯&#xff0c;Skydel软件24.9.0版本已正式发布&#xff0c;此次更新不仅带来了五大全新功能&#xff0c;还在性能优化、用户体验以及远程API方面进行了全面升级&#xff0c;为用户带来更高…

面试题:如何能够保证T2在T1执行完后执行,T3在T2执行完后执行?——CountDownLatch原理

CountDownLatch的使用方式 CountDownLatch用于某个线程等待其他线程执行完任务再执行&#xff0c;与thread.join()功能类似。常见的应用场景是开启多个线程同时执行某个任务&#xff0c;等到所有任务执行完再执行特定操作&#xff0c;如汇总统计结果。 面试题&#xff1a;如何…

最新Java零基础知识(持续更新中......)

1. 学习前的准备 一个好的学习方法&#xff08;如何更高效学习&#xff09;&#xff1a; 成为一名合格的程序员&#xff0c;需要具备两个关键能力&#xff1a; 指法速度&#xff1a;高效的代码输入速度。编程思想&#xff1a;能够用编程的方式解决现实问题的能力。 指法速度&am…

SpringBoot中的Component和ComponentScan注解工作原理

Spring IoC 容器的工作是通过管理对象的生命周期和配置来保持业务逻辑清晰&#xff0c;但是 Spring 容器并不会自动知道要管理哪些 bean。所以我们来告诉 Spring 应该处理哪些 bean 以及如何处理&#xff0c;很简单这就是 Spring 的 Component 和 ComponentScan 注释的作用所在…

算法题总结(二十)——并查集

并查集理论基础 并查集常用来解决集合连通性问题&#xff0c;两个节点在不在一个集合&#xff0c;也可以将两个节点添加到一个集合中。 大白话就是当我们需要判断两个元素是否在同一个集合里的时候&#xff0c;我们就要想到用并查集。 并查集主要有两个功能&#xff1a; 将…

linux介绍与基本指令

前言 本次博客将会讲解linux的来源历史、linux操作系统的理解以及它的一些基本指令。 1.linux的介绍 linux的来源 linux的来源最初还是要说到unix操作系统的。 1968年&#xff0c;一些来自通用电器公司、贝尔实验室和麻省理工学院的研究人员开发了一个名叫Multics的特殊操作…

C语言 | Leetcode C语言题解之第502题IPO

题目&#xff1a; 题解&#xff1a; #define MIN(a, b) ((a) < (b) ? (a) : (b)) #define MAX_INT_NUMBER 0x7FFFFFFEtypedef struct {int capital;int profit; } ProNode;int CompareProfit(const ProNode *a, const ProNode *b) { /* 从大到小排序 */return b->pr…

根据发生异常的汇编指令以及函数调用堆栈,从内存的角度出发,估计出问题的可能原因,确定排查方向,快速定位C++软件问题

目录 1、前言 2、初步分析dump文件 3、加载更多模块的pdb文件&#xff0c;可能能看到更多行的函数调用堆栈 4、从内存的角度去看&#xff0c;估计是访问了野指针导致的&#xff0c;沿着这个怀疑的方向快速地定位了问题 5、最后 C软件异常排查从入门到精通系列教程&#xf…

力扣OJ算法题:合并两个有序链表

—————————————————————————————————————————— 正文开始 OJ算法题&#xff1a;合并两个有序链表 思路 创建一个新的空链表&#xff08;可以用malloc优化&#xff09;和两个指针L1、L2分别指向两个链表&#xff0c;遍历两个链表&am…