Linux|信号
- 信号的概念
- 信号处理的三种方式
-
- [捕捉信号的System Call -- signal](#捕捉信号的System Call -- signal)
- 1.产生信号的5种方式
- 2.信号的保存
-
- [2.1 core 标志位](#2.1 core 标志位)
- 2.信号的保存
-
- [2.1 对pending 表 和 block 表操作](#2.1 对pending 表 和 block 表操作)
- [2.2 阻塞SIGINT信号 并打印pending表例子](#2.2 阻塞SIGINT信号 并打印pending表例子)
- 捕捉信号
-
- [sigaction 函数](#sigaction 函数)
- 验证当前正在处理某信号,则该信号会自动被屏蔽
- 验证当前信号被处理完之后,会自动解除屏蔽
- 地址空间中操作系统态
- 谈谈键盘输入的过程
- 两个深刻的问题
- 可重入函数
- volatile
- sigchild信号
信号的概念
信号:是进程之间异步通知的一种方式,属于软中断。
所谓异步就是 a 和 b 之间没有联系,比如同学a 去上厕所了,老师b还是继续讲课,这称为异步。
信号处理的三种方式
一般情况下是三选一
- 忽略此信号
- 执行该信号的默认处理动作
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号
捕捉信号的System Call -- signal
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
sighandler_t signal(int signum, sighandler_t handler);
当我们在键盘中 按ctrl + c 的时候 就会发送一个SIGINT信号,
我们可以用 signal 这个系统调用验证
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void hander(int sig)
{
std::cout<<"catch sig:"<< sig<<std::endl;
}
int main()
{
signal(2,hander);
while(true)
return 0;
}
有同学会想我把所有的信号都捕捉了,那个这个进程是不是就刀枪不入了?不是的 因为9号信号 无法捕捉
1.产生信号的5种方式
1. 通过 kill 命令,向指定的进程发信号
2. 通过键盘 ctrl + c
3. 系统调用 kill
raise(sign) 和 kill(getpid(),sign) 是等价的
alrm 也可以产生信号 alrm的返回值是上一个闹钟的剩余时间
同一个进程同一个时间只能有一个闹钟!
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void hander(int sig)
{
std::cout<<"catch sig:"<< sig<<std::endl;
}
int main()
{
signal(2,hander);
//kill(getpid(),2);
raise(2);
sleep(3);
return 0;
}
4.软件条件
比如 管道 我们读端关闭 , 写端还在写,那么就会产生一个SIGPIPE的信号。
5. 异常
a.
cpp
void hander(int sg)
{
std:: cout<< "捕捉到:"<<sg<<std::endl;
}
int main()
{
signal(8,hander);
int b = 10 / 0;
return 0;
}
可能有同学会问为什么会一直死循环打印捕捉到的8号信号呢?
当处理器检测到除法错误时,它会暂停正常的指令流,保存 当前的状态(包括程序计数器和其他寄存器的内容),然后跳转到一个预定义的地址来处理这个异常。这个地址指向的是操作系统的异常处理程序,它可以记录错误、终止进程或采取其他恢复措施,由于进程没有退出,又恢复当前的状态,到cpu中 ,cpu中的溢出标记位又置为1了。(这也回答cpu是怎么检测到除以0的)总的来说就是因为进程一直被调度,所以才出现死循环的情况。
终止进程的本质:释放进程的上下文数据,报告溢出标志数据或其他异常数据
b. 野指针问题:
CR3 + MMU : 将虚拟地址转换为物理地址
CR2:保存主要用于存储最近一次发生的页面错误(page fault)时的线性地址。
当异常的时候,操作系统检测到CR2中的地址,开始发送信号。
2.信号的保存
2.1 core 标志位
当时在进程控制时 waitpid 函数中的 status参数 core dump 标志位 我们现在就马上知道什么意思了。当程序被信号杀死时,会生成一个core的debug文件。 这个core标记位 ,为0不允许生成,为1运行生成debug文件。
在云服务上 生成这个core文件的功能默认是被关闭的
ulimit - a 查看core file size 的大小
ulimit -c 【size】 设置一下就好了
也有 可能 生成的core 文件不在当前目录
echo ./core > /proc/sys/kernel/core_pattern 就欧克啦
一重启就会生成一个core.进程号的文件 如果无限制的重启 就会生成非常多的core文件 所以云服务器就把这个功能关闭了
调试的时候,我们core-file core文件 把这个debug文件加载进去,调试器就直接显示出错的那一行了!
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int sum(int star, int end)
{
int ret = 0;
for (int i = star; i <= end; i++)
{
ret /= 0;
ret += i;
}
return ret;
}
int main()
{
pid_t id = fork();
if(id == 0)
{
sleep(1);
sum(1, 100);
exit(0);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
printf("exit code: %d, sig: %d, core dump:%d\n",(status >> 8) &0xff, status &0x7f,(status >> 7) &1);
}
return 0;
}
当我们把ulimit -c设置为 0时 coredump 标记位就为0了 表示 不生成core dump(核心转储)文件
2.信号的保存
信号的保存就保存在这三张表中,block表,peding表,handler表。
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
执行信号处理的动作称为信号的递达。
信号产生到递达之间称为未决
如果一个信号被阻塞了,那么它永远未决。
我们用的signal方法sighandler_t signal(int signum, sighandler_t handler); 其中 我们写的handler 就是把函数地址写进对应的handler表下标中 。
两张位图+函数指针数组 == 让进程识别信号
2.1 对pending 表 和 block 表操作
先介绍几个函数
cpp
#include <signal.h>
// 清空位图
int sigemptyset(sigset_t *set);
// 所有bit位全为1
int sigfillset(sigset_t *set);
// 把某一bit位置为1
int sigaddset (sigset_t *set, int signo);
// 把某一bit位置为0
int sigdelset(sigset_t *set, int signo);
// 判断某一比特位是不是1
int sigismember(const sigset_t *set, int signo);
signal.h 给我们提供了 用户级别的位图,这些函数可以用来操作这个位图 sigset_t
cpp
//调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
// 获取pending 表
int sigpending (sigset_t * set);
2.2 阻塞SIGINT信号 并打印pending表例子
cpp
// 利用上面的函数,我们就是验证 某一信号被阻塞后,是否一直未决
#include <iostream>
#include <signal.h>
#include <unistd.h>
void PrintPending( sigset_t & pending)
{
for(int sig = 31; sig >= 1; sig--)
{
if(sigismember(&pending,sig))
{
std::cout<<1;
}else
{
std::cout<<0;
}
}
std::cout<<std::endl;
}
int main()
{
sigset_t block_set , old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set , SIGINT);
//
sigprocmask(SIG_BLOCK,&block_set,&old_set);
while(true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
return 0;
}
捕捉信号
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号 。
信号可能不会立即被处理,而是在合适的时候处理
,这个合适的时候指的是,从用户态 返回内核态 的时候进行处理。
用户态:执行我们自己的数据和代码的时候
内核态:执行操作系统的代码和数据的时候
当信号的处理动作是自定义的信号处理函数时才返回时先到用户态再从内核态到用户态(因为hander方法 和 main 函数不是调用关系并不能直接返回)。
如果是默认 则直接杀死进程了。 忽略则 除了修改pending 表 由 1 变为 0,其他什么也不干。
举例:
户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号
SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler
和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返
回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
sigaction 函数
**int sigaction(int signum, const struct sigaction act, struct sigaction oldact);
和 signal一样都是捕捉信号的,它有一个同名的结构体,但这个结构体我们只关心ssiginfo_t 这个函数指针方法字段
cpp
void handler(int signal)
{
std::cout<<"捕捉到:"<<signal<<std::endl;
// while(true)
// {
// sigset_t pending;
// sigpending(&pending);
// Print(pending);
// sleep(1);
// }
exit(1);
}
int main()
{
struct sigaction act, oact;
act.sa_flags = 0;//在这种情况下,信号处理将遵循默认的行为,
//也就是说,信号处理函数将作为一个普通的函数执行,
//而不会触发任何 sa_flags 标志所定义的特殊行为。
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
//sigaddset(&act.sa_mask,3); // 顺带屏蔽三号信号
sigaction(2,&act,&oact);
while(true)
{
std::cout<<"pid: "<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
验证当前正在处理某信号,则该信号会自动被屏蔽
我们在hander方法中一直sleep,不退出hander方法,我们再按ctrl + c信号也不会被处理了。这就验证了当前信号正在被处理,则该信号会被自动屏蔽。
验证当前信号被处理完之后,会自动解除屏蔽
我们设置hander方法睡三秒自动退出。 退出之后又可以捕捉到2号信号则证明了该结论
地址空间中操作系统态
内核级页表所有进程共享一份用户级页表每一个进程都有一份。操作系统的代码数据都通过内核级页表映射在物理内存中。
谈谈键盘输入的过程
操作系统怎么知道键盘摁下了? 是一直问键盘吗?当然不是,那不然太浪费cpu资源了
每一个硬件都有一个中断号,硬盘也不例外,当按下一个键后,通过8529这个芯片向cpu 发出硬件中断,某一个寄存器上就有了键盘的中断号,再在中断向量表中查询对应的键盘读入方法~这样就完成了cpu知道键盘输入的一个过程。
我们学习的信号就是模拟硬件中断实现的!
两个深刻的问题
如何操作系统是怎么运行的
操作系统调用进程谁由来调度操作系统呢?
硬件上有一个时钟,时钟到了就通过中断提醒操作系统该检测进程的时间片,时间片到了就切换进程,否则什么也不做
如何理解系统调用
- 有一个函数指针数组,通过下标 可以找到系统调用,这个下标我们称为系统调用号。
- 我们使用系统调用如fork时,会产生内部中断(陷阱),执行系统调用的方法,让cpu找这个函数指针数组。eax 中保存这个函数系统调用号,然后cpu就找到这个系统调用了
可重入函数
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数
volatile
cpp
#include <iostream>
#include <signal.h>
int gflag = 0;
void changeData(int signo)
{
std::cout<<"gflg:0 -> 1"<<std::endl;
gflag = 1;
}
int main()
{
signal(2,changeData);
while(!gflag);
std::cout<<"process quit!"<<std::endl;
return 0;
}
当我们用编译器O1的优化时,main函数 里面又没有修改 gflag的值,于是编译器把内存中的值拷贝到寄存器后,就只看寄存器中的值了。
怎么解决这个问题呢?
我们可以在gval前 加一个volatile关键字 保证内存的可见性就行了。
sigchild信号
子进程退出的时候会给父进程发送一个sigchild信号
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
void notice(int sig)
{
std::cout<<"I am fatherprocess,pid: "<<getpid()<<std::endl;
std::cout<<"get sig:"<<sig<<std::endl;
}
int main()
{
signal(SIGCHLD,notice);
pid_t id = fork();
if(id == 0)
{
std::cout<<"I am childprocess,pid: "<<getpid()<<std::endl;
sleep(3);
exit(1);
}
sleep(100);
return 0;
}
如果不关心 子进程的退出信息则可以把SIGCHLD 的捕捉动作改为SIG_IGN
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
signal(SIGCHLD,SIG_IGN);
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt--)
{
std::cout<<"child process runing"<<std::endl;
std::cout<<"cnt:"<<cnt<<std::endl;
sleep(1);
}
exit(1);
}
while(true)
{
std::cout<<"father process runing"<<std::endl;
sleep(1);
}
return 0;
}