做网站背景图片要多大,上饶建设网站,网页设计html教程,建筑企业资质文章目录进程信号信号的产生方式#xff08;信号产生前#xff09;1. 硬件产生2.调用系统函数向进程发信号3.软件产生4.定位进程崩溃的代码#xff08;进程异常退出产生信号#xff09;信号保存的方式#xff08;信号产生中#xff09;获取pending表修改block表…
文章目录进程信号信号的产生方式信号产生前1. 硬件产生2.调用系统函数向进程发信号3.软件产生4.定位进程崩溃的代码进程异常退出产生信号信号保存的方式信号产生中获取pending表修改block表信号的处理信号处理时修改handler函数补充—— sigaction 系统调用进程信号
信号在生活中无处不在例如闹钟、红绿灯快递到达发的短信等等 信号的例子 例如在网上你买了一个东西就是信号的注册 快递员该你打电话要你拿一下快递就是给你发送了一个信号 你收到信号之后你知道怎么去处理这个信号在这里就是去拿快递 但是你也不一定立马去拿你可能会等你忙完现在的事在去处理 从技术的角度说平时电脑上按altf4就是一种信号它会关闭当前的窗口Linux中ctrlc可以终止进程
信号是进程之间事件异步通知的一种方式属于软中断
信号的种类 信号的种类可以通过kill -l命令来查看 Linux 系统的信号列表 如图其中1-31是普通信号也是我们在这里要重点学习的信号34-64是实时信号 信号的产生方式信号产生前
1. 硬件产生
例如当我们的程序发生死循环时按下CTRLC就可以终止进程。CTRLC的本质其实是向进程发送2号信号SIGINT而SIGINT的默认处理动作是从键盘中断man 7 signal查看手册
那我们可以把信号2的默认处理动作更换成我们自己的验证一下这里需要用到
sighandler_t signal(int signum, sighandler_t handler); 这里我让收到2号信号后先打印一段话再退出
#include stdio.h
#include stdlib.h
#include signal.h
#include unistd.hvoid mysignal_2(int signo)
{printf(你好signo:%d\n,signo);exit(1);
}int main()
{signal(2,mysignal_2); //相当于一个函数指针这里填的函数名相当于函数地址while(1){printf(hello\n);sleep(1);}return 0;
}可以看到运行结果按下ctrlc后先打印再终止进程
例如野指针问题引起的段错误只要运行程序就会崩溃那么下面验证一下
先把1-31的信号处理动作全部换成自定义之后捕捉信号查看野指针对应的信号
#include stdio.h
#include stdlib.h
#include signal.h
#include unistd.hvoid handler(int signo)
{printf(signo:%d\n,signo);exit(1);
}int main()
{int sig 1;for(;sig 31; sig){signal(sig,handler);}while(1){int* p NULL;*p 100;printf(hello\n);sleep(1);}return 0;
} 运行结果可知野指针引起的进程崩溃是收到了11信号
再例如除0错误把上面的代码修改一下进行验证 while(1){int i 0;i / 0;printf(hello\n);sleep(1);}可以看到进程也是直接就崩溃了是因为收到了8号信号
上述提到的ctrlc、除0错误、野指针的段错误为什么会产生退出信号呢
首先要知道os是硬件的管理者负责管理好硬件资源监视硬件状态等等
ctrlc是由键盘发出的os检测到键盘发出的信号就会向进程发出2号信号除0时是在CPU中运算的CPU中有一些寄存器会记录运行的状态除0时的运算会产生异常寄存器就会记录这个状态OS检测到这个异常状态就会向进程发送8号信号当我们对空指针进行直接赋值时OS会发现我们的进程地址空间与物理内存的映射对不上就会向进程发送11号信号
综上这些错误分别体现在键盘、CPU、内存中所以软件层面上的错误。
也就是体现在硬件或其他软件上而OS是硬件的管理者发现硬件出现一些异常就会反馈回出现异常的代码所在的进程上进程根据对应的信号进行处理上面提到的错误都是默认终止进程 2.调用系统函数向进程发信号
系统提供了一些函数接口可以手动发送信号
int kill(pid_t pid, int signo);
pid 表示目标进程的 pid 。sig 表示要发给目标进程的信号。成功返回0失败返回-1
#include stdio.h
#include stdlib.h
#include signal.h
#include unistd.hstatic void Usage(const char* proc)
{printf(Usage:\n\t %s signo who \n,proc);
}int main(int argc ,char* argv[])
{if(argc ! 3){Usage(argv[0]);return 1;}int signo atoi(argv[1]);int who atoi(argv[2]);kill(who,signo);printf(signo:%d who:%d\n,signo,who);return 0;
} int raise(int signo); 给自己发送信号signo要发送的信号 int main()
{sleep(1);raise(3);return 0;
}void abort(void); 给自己发送固定的信号6 3.软件产生
unsigned int alarm(unsigned int seconds);
seconds是代表几秒后执行执行成功返回0取消定时则返回剩余秒数
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
int main()
{alarm(30);sleep(4);int ret alarm(0); //取消定时printf(%d\n,ret);return 0;
}还有管道通信时写端退出读端收到13号信号等等
4.定位进程崩溃的代码进程异常退出产生信号
进程退出一般分为三种
代码运行完毕结果正常代码运行完毕结果不正常代码异常直接终止 如图退出码退出信号core dump标志位共16位waitpid函数接口的第二个参数可以获取到并查看退出码和退出信号
具体用法自行搜索这里不做介绍
在Linux中是可以定位到具体崩溃到哪一行的代码的当一个进程退出的时候它会根据退出的情况置退出码或者退出信号表明退出的原因如果必要OS会设置退出信息中的core dump标志位并将进程在内存中的数据转存到磁盘上以便后期调试
云服务器上的core dump是默认关闭的可以输入 ulimit -c 102400打开core dumpulimit -a 可以查看 这里用除0错误演示一下
int main()
{while(1){int i 0;i / 0;printf(hello\n);sleep(1);}return 0;
} 在我们运行程序后除了显示退出原因还会显示core dumped并且多了一个 core.5195文件之后利用gdp调试打开我的可执行程序Linux默认是release版本的需要在编译时加上 -g 之后输入core-file core.5195加载 core.5195文件 也不是所有信号都会core dump例如死循环使用ctrlc的2号信号或者 kill -9杀掉进程就没有core dump
总结信号产生的方式有很多但本质都是由OS向目标进程发送的 信号保存的方式信号产生中
在Linux中一个进程收到信号也不一定马上处理待当前工作处理完成后寻找合适时机处理那就需要PCB有保存信号的能力
先来解释三个名词 实际执行信号的处理动作称为信号递达delivery 信号递达可以是自定义捕捉、默认、忽略 信号从产生到递达之间的状态称为信号未决Pending 本质是这个信号因为一些原因如优先级低等被暂时存在了task_struct中 进程可以选择阻塞某个信号block 本质是OS允许进程暂时屏蔽指定的信号被阻塞的信号产生时将保持在未决状态直到进程解除对此信号的阻塞才执行递达的动作 注意阻塞和忽略是不同的只要信号被阻塞就不会递达而忽略是在递达之后可选的一种处理动作。
在进程的task_struct内有三张表是用来保存信号状态的 pending 表和 block 表都是位图而 handler 表是一个函数指针数组指向的是信号递达时对应的处理动作(默认/忽略/自定义) pending和block位图的比特位的位置代表信号的编号 pending位图的内容代表是否收到信号 block位图内容代表是否被阻塞阻塞位图也叫信号屏蔽字 信号处理过程示例 只要block为1那么pending不管是1还是0该信号都是阻塞状态
有了这三张表进程就可以识别信号了
因为task_struct是内核的没有人能写入到内核只有OS可以所以信号的产生方式都是由OS统一向PCB发送的
不是所有信号都会被屏蔽例如9号信号
获取pending表修改block表
pending表和block表的位图结构就是由这个sigset_t的类型来存储的。sigset_t是一个位图结构类型称为信号集这个类型可以表示每个信号的有效或无效状态在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决定状态。
由于每个平台的sigset_t信号集实现位图的方法不一定一样所以不推荐用户对这个信号集直接进行修改信号集需要通过OS提供的信号集操作函数进行修改。
#include signal.h
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 sigemptyset(sigset_t *set) 初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号 成功返回0失败返回-1 int sigfillset(sigset_t *set) 初始化 set 将其中所有信号对应的比特位置 1 。 成功返回0失败返回-1 int sigaddset (sigset_t *set, int signo) 把signo信号添加到 set 集合里其实就是把signo信号对应的比特位由 0 置 1 成功返回0失败返回-1 int sigdelset(sigset_t *set, int signo) 把 signo 信号从 set 集合里删去其实就是把 signo 信号对应的比特位由 1 置 0 成功返回0失败返回-1 int sigismember(const sigset_t *set, int signo) 判定 signo 信号是否在 set 集合里其实就是判定 signo 信号对应的比特位是否为 1 包含就返回1不包含返回0失败返回-1 有了上面的信号集操作函数后想要修改block表就需要用到sigprocmask 系统调用 sigprocmask获取或更改当前进程的信号屏蔽字 how表示要对信号屏蔽字进行的操作类型 SIG_BLOCKset 包含了我们希望添加到当前信号屏蔽字的信号。 mask mask | set SIG_UNBLOCKset 包含了我们希望从当前信号屏蔽字中解除阻塞的信号。 mask mask ~set SIG_SETMASK设置当前信号屏蔽字为 set 。 mask set 常用 set输入型参数就是我们要设置的信号集的指针 oldset输出型参数用来获取原信号集可设置NULL 返回值成功返回0失败返回-1 而想要获取pending表就要用到sigpending 系统调用 参数传出型参数用于存储进程pending表返回值成功返回0失败返回-1 示例阻塞2号信号
void test1()
{sigset_t iset,oset;//1.先清空sigemptyset(iset);sigemptyset(oset);//2.给iset指定信号编号sigaddset(iset,2);//3.设置屏蔽字sigprocmask(SIG_SETMASK,iset,oset);while(1){printf(hello\n);sleep(1);}}这时按ctrlc是无效的只能通过kill -9来结束进程
示例先阻塞2号信号后获取pending表并打印20秒后放开2号信号
void test2()
{//设置信号集sigset_t iset,oset;sigemptyset(iset);sigemptyset(oset);//初始化信号集sigaddset(iset,2);sigprocmask(SIG_SETMASK,iset,oset); //阻塞sigset_t pending;//用于获取打印用int count 0;while(1){if(count 20)sigprocmask(SIG_SETMASK,oset,NULL); //解除2号的阻塞更改阻塞的信号集为osetoset为全0sigemptyset(pending); //先清空sigpending(pending); //在获取sleep(1); //延时1scount; //计数show_pending(pending); //打印}
}20s后2号被取消阻塞信号递达进程退出
综上步骤
想要获取pending表或者更改block表的必备步骤是
设置信号集初始化信号集 信号的处理信号处理时
每个进程都有自己的地址空间和对应的页表地址空间一般一共4G3G的用户空间而剩下的1G空间是内核空间存储的是OS的数据和代码且内核空间也有对应的内核页表映射到物理内存上但是内核页表不管有多少个进程都只共享一份保证无论进程怎么切换都能够找到同一个OS 进程是可以看到内核和用户的内容的但是不一定能够访问需要有权限来证明进程处于哪种工作模式在进程里面是有对应的相关数据来标识进程的工作模式的用户模式 / 内核模式 想要访问内核数据就需要切换至内核态想要访问用户数据就需要切换至用户态 这个数据会被加载到 CPU 的其中一个寄存器当中CR3用于保存当前进程是处于用户态还是处于内核态。 内核态执行 OS 的代码和数据时所处的状态。OS 的代码的执行全部都是在内核态。用户态用户的代码和数据被访问或执行时所处的状态。也就是说我们写的代码全部都是在用户态执行的。 而系统调用就是将进程切换至内核态去调用系统函数 信号是如何处理的
信号是被保存在PCB中的pending位图里面处理的工作分为检测、递达默认、忽略、自定义当进程从内核态返回到用户态的时候进行信号的处理工作
处理的流程大致如下图所示 抽象图 为什么不能由OS直接执行捕捉函数方法
要知道OS是不相信任何人的handler方法是用户定义的内核态只能执行OS的代码数据所以首先再身份上就不合适。其次因为OS只相信自己也就是只相信自己的代码和数据是安全的假如handler的函数方法定义一些恶意代码那么OS等于以它的权限去执行造成安全隐患。所以OS要保护好自己
什么时候处理信号从内核态切换回用户态的时候进行信号检测并处理
**当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字。**这样就保证了在处理某个信号时,如果这种信号再次产生,那么它第二次会被阻塞到当前处理结束为止
在 Linux 中如果把进程的某一个普通信号屏蔽了然后 OS 给这个进程多次发送该信号该进程只能记住一次因为记录信号的标记位只有一个比特位。也就是说在 Linux 中普通信号是可能会被丢失的。而实时信号不会被丢失因为内核是以链表队列的形式把所有的实时信号组织起来的来一个就链一个。数据结构的差别 修改handler函数补充—— sigaction 系统调用
和signal函数的功能是一样的都是修改handler表中的方法 signum 表示要设置的信号act 输入型参数需填充该结构体里面 signum 的对应处理动作。oldact 输出型参数若为非空则带回内含老的处理动作的结构体若不关心则可设为 NULL 。成功返回0失败返回-1
与信号集sigset_t相关的操作类似
sa_handler 代表 signum 的对应处理动作sa_mask 代表在调用信号处理函数时需要额外屏蔽的信号sa_flags 代表选项我们在这里设为 0sa_sigaction 和 sa_restorer 通常与实时信号相关联我们在这里不关心
如果在调用信号处理函数时除了当前信号被自动屏蔽之外还希望自动屏蔽另外一些信号则用 struct sigaction 结构体中的 sa_mask 字段说明这些需要额外屏蔽的信号mask是信号集可以用信号集操作函数