信号是什么时候被处理?
进程从内核态,切换到用户态的时候,信号会被检测处理。
内核态:操作系统的状态,权限级别高
用户态:你自己的状态
内核态和用户态
进程地址空间第三次
所谓的系统调用本质其实是一堆函数指针数组。
1.我们使用系统调用或者访问系统数据,其实还是在我们进程的地址空间内进行跳转的。
2.进程无论如何切换,总能找到OS
我们访问OS,本质是通过我的进程的地址空间的[3,4]GB来访问的。
3.操作系统是如何运行的
信号技术本来就是通过软件的方式,来模拟的硬件中断。
OS的周期时钟中断:非常高的频率,非常短的时间,给CPU发送中断——CPU不断进程处理中断。
操作系统是一个死循环,不断在接受外部的其他硬件中断。
4.操作系统不相信任何用户
必须要能区分当前用户的运行模式,所以就有了用户态和内核态。
信号是如何被处理?
捕捉信号还有其他方式吗?
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数说明
signum: 要操作的信号编号(如 SIGINT、SIGTERM 等)
act: 指向新信号动作结构的指针,如果为 NULL 则不改变当前处理方式
oldact: 输出型参数,用于保存原信号动作结构的指针,如果为 NULL 则不保存
返回值
成功时返回 0,失败时返回 -1 并设置 errno。
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 替代的信号处理函数
sigset_t sa_mask; // 执行处理函数时要阻塞的信号
int sa_flags; // 修改行为的标志
void (*sa_restorer)(void); // 已废弃
};
关于sa_mask变量
- 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(屏蔽该信号)
- 如果我们处理完对应的信号,该信号默认也会从信号屏蔽字中进行移除。(解除屏蔽该信号)
为什么会这样?原因:不想让信号,嵌套式地对同一个信号进行捕捉处理。
用例子解释:
#include<iostream>
#include<signal.h>
void Print(sigset_t pending)
{
std::cout<<" curr process pending: ";
for(int sig = 31;sig > 0;sig--)
{
if(sigismember(&pending,sig)) std::cout << "1";
else std::cout <<"0";
}
std::cout << std::endl;
}
void handler(int signo)
{
std::cout << "signal: " << signo <<std::endl;
//不断获取当前进程的pending信号集并打印
sigset_t pending;
sigemptyset(&pending);
while(true)
{
sigpending(&pending);
Print(pending);
sleep(1);
}
}
int main()
{
struct sigaction act,oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2,&act,&oact);
while(true)sleep;
}
在调用信号处理函数时,除了当前信号被自动屏蔽外,还希望自动屏蔽另外一些信号,则要用sa_mask字段说明。
例子:
int main()
{
struct sigaction act,oact;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
//除了屏蔽当前正在处理的函数,还屏蔽3,4,5号信号
sigaction(2,&act,&oact);
while(true)sleep;
}
子进程信号版的进程退出
子进程退出,父进程不wait,子进程就会僵尸。
子进程退出,不是默默退出的,会在退出的时候,向父进程发送信号(17.SIGHLD)
如何证明?
#include<iostream>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
std::cout <<"child quit, father get a signo: "<< signo << std::endl;
}
int main()
{
signal(SIGCHLD,handler);
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt--)
{
std::cout<<"I am child process: "<<getpid()<<std::endl;
sleep(1);
}
std::cout<<"child process died"<<std::endl;
exit(0);
}
//father
while(true) sleep(1);
return 0;
}
结果:确实子进程退出时向父进程发送了17号信号
所以当子进程退出时,发送17号信号,刚好我们将信号进行捕获,自定义处理信号,将该子进程进行回收,就有如下代码。
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void handler(int signo)
{
std::cout <<"child quit, father get a signo: "<< signo << std::endl;
}
void CleanupChild(int signo)
{
//v1
// if(signo == SIGCHLD)
// {
// pid_t rid = waitpid(-1,nullptr, 0);//-1表示任意一个子进程
// if(rid >0)
// {
// std::cout << "wait child success: " << rid << std::endl;
// }
// }
//v2-同时退出一百个进程
// if(signo == SIGCHLD)
// {
// //但是如果同时要回收100个子进程,这时pending位图中在短时间内只能保存一次信号,所以加上循环,不停的回收
// while(true)
// {
// pid_t rid = waitpid(-1,nullptr, 0);//-1:表示任意一个子进程
// if(rid >0)
// {
// std::cout << "wait child success: " << rid << std::endl;
// }
// else if(rid <= 0) break;
// }
// }
//v3-50个进程退出,50个进程没有退出
if(signo == SIGCHLD)
{
//但是如果同时要回收100个子进程,这时pending位图中在短时间内只能保存一次信号,所以加上循环,不停的回收
while(true)
{
pid_t rid = waitpid(-1,nullptr, WNOHANG);//-1:表示任意一个子进程//以非阻塞方式等待
if(rid >0)
{
std::cout << "wait child success: " << rid << std::endl;
}
else if(rid <= 0) break;
}
}
std::cout <<"wait sub process done"<<std::endl;
}
int main()
{
signal(SIGCHLD,CleanupChild);
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 5;
while(cnt--)
{
std::cout<<"I am child process: "<<getpid()<<std::endl;
sleep(1);
}
std::cout<<"child process died"<<std::endl;
exit(0);
}
//father
while(true) sleep(1);
return 0;
}
版本v1
特点:
- 使用阻塞方式等待(options=0)
- 每次只能回收一个子进程
问题:
- 如果有多个子进程同时退出,可能会丢失信号
- 阻塞调用可能会影响主程序执行
版本v2
改进:
- 通过循环可以回收多个子进程,解决了多个子进程同时退出的问题。
问题:
- 仍然是阻塞调用,如果子进程没有退出会一直阻塞。
版本v3
优点:
- 使用 WNOHANG 非阻塞选项,不会阻塞主程序执行。
- 可以一次性回收所有已退出的子进程。
更简单的回收子进程的方式
直接忽略它,将SIGCHLD
设置成SIG_IGN
,即 signal(SIGCHLD,SIG_IGN);
这样子进程在终止时会被自动清理掉,不会产生僵尸进程,也不会通知父进程。
缺点:仅仅只是退出进程,无法通过自定义函数的方式获取子进程的id,退出码等相关信息。
注:SIGCHLD的默认处理动作是IGN(忽略),为什么还要手动设置成SIG_IGN?
这是系统的一个特性,仅在Linux环境下。
设置系统层的IGN,进程终止时,会产生僵尸进程。
设置用户层的SIG_IGN,系统会将进程终止,不产生僵尸进程。