Linux信号是一种更高层的软件形式的异常,它允许进程和内核中断其他进程。
一个信号就是一条小消息,它由内核发出,通知进程系统中发生了一个某种类型的事件。Linux系统支持30种不同类型的信号,信号类型用小整数ID来标识,每种信号类型都对应某种系统事件。
其中,信号SIGKILL(序号为9)、SIGSTOP(序号为19)不可忽略、阻塞、捕获。
信号在内核中的表示如下图所示。pending位向量中维护着待处理信号的集合;blocked位向量中维护着被阻塞地信号集合。
? ? ? ? 比如上面的ctrl + c 就给进程发送了2号信号SIGINT。而ctrl + \可以给进程发送3号信号SIGQUIT。
? ? ? ? 通过按键组合的方式来给进程发送信号。
kill 命令是 Linux 中最常用的发送信号的命令
除了使用 kill 命令,程序中也可以通过 kill 函数来发送信号。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cstdio>
#include<cstring>
#include<string>
static void Usage(const std::string &proc){
std::cout<<"Usage:"<<proc<<"pid signo\n"<<std::endl;
}
int main(int argc,char *argv[]){
if(argc!=3){
Usage(argv[0]);
exit(1);
}
pid_t pid=atoi(argv[1]);
int signo=atoi(argv[2]);
int n=kill(pid,signo);
if(n!=0)
perror("kill");
while(true){
std::cout<<"hello"<<std::endl;
sleep(1);
}
}
作用:给当前进程发送信号
代码演示
#include<cstdio>
#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>
void Usage(const char* proc){
std::cout<<"Usage:"<<proc<<" pid signo"<<std::endl;
}
int main(int argc,char *argv[]){
int cnt=0;
while(cnt<=10){
printf("cnt:%d\n",cnt++);
sleep(1);
if(cnt>=5)
raise(3);
}
return 0;
}
?????硬件异常产生信号就是硬件发现进程的某种异常,而硬件是被操作系统管理。硬件会将异常通知给系统,系统就会向当前进程发送适当的信号。
除0错误理解
野指针的情况
???原因:由于p是野指针,p指针变量里保存的是随机值,进程执行到野指针这一行。进程在页表中找映射的物理内存时,硬件mmu会发现该虚拟地址是一个?野指针,会产生异常,由于操作系统管理硬件,硬件会将异常发送给系统。系统会发送适当的信号给当前进程
? ? ? ? 在匿名管道中,当读进程关闭时,写进程会收到系统发来的13号信号,终止写进程。系统发给写进程的13号信号就是软件条件生成的信号。
这里也有一个函数alarm,相当于设置一个闹钟,告诉内核多少秒后,发送一个SIGALRM信号给当前进程。
这里有一个现象:
同样将count++,1秒后发送SIGALARM信号给进程,同样时间:上面count才加到21095,下面加到了491153364,差了1000倍。
这是因为上面的代码要不断往屏幕打印,屏幕是外设,在不断进行I/O,时间消耗多。
core要核心存储 term直接终止
核心转储(Core Dump)是操作系统在程序发生严重错误(如访问违规或段错误)时生成的一种文件。它记录了程序崩溃时刻的内存映像和程序执行状态,包括程序计数器和寄存器的内容、堆栈信息、正在使用的内存段等。核心转储通常用于随后的调试和分析,以确定程序崩溃的原因。
核心转储的特点:
详细信息:
调试用途:
生成时机:
文件大小:
实际执行的信号的处理动作称为信号递达
信号从产生到递达之间的状态称为信号未决
进程可以选择阻塞某个信号(Block)
被阻塞的信号产生时将保持在未决状态,知道进程解除对此信号的阻塞,才执行递达动作
阻塞和忽略是不同的,只要信号被阻塞就不会被递达,而忽略是在递达之后可选的一种处理动作
忽略:本质是处理信号的一种方式
阻塞:本质是不让信号递达,直到解除阻塞
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前信号屏蔽字为mask,下图为how参数的可选值
sigpending
函数用于检查当前进程的信号挂起集(pending signal set)。这个挂起集包含了那些已经发送给进程但尚未被处理的信号,通常是因为这些信号当前被阻塞了。
sigemptyset初始化set所指向的信号集,使其中所有信号对应的bit清0,表示该信号集不包含任何有效信号。
sigfillset初始化set所指向的信号集,使其中所有信号对应的bit置位,表示该信号集的有效信号包括系统支持的所有信号。
注意:在使用sigset_t类型的变量之前,一定要调用sigemptyset或signfillset做初始化,使信号集处于确定的状态。初始化sigset_t信号变量之后就可以在调用sigaddset和sigdelset在信号集中添加或删除某种有效信号
这四个函数成功返回0,错误返回-1,
sigismember是布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,包含返回1,不包含返回0,出错返回-1.
sigaddset
#include<cstdio>
#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>
const int max_signum=31;
static void show_pending(const sigset_t &pending){
for(int signo=1;signo<=max_signum;signo++){
if(sigismember(&pending,signo)){
std::cout<<"1";
}
else{
std::cout<<"0";
}
}
std::cout<<"\n";
}
void Usage(const char* proc){
std::cout<<"Usage:"<<proc<<" pid signo"<<std::endl;
}
int main(int argc,char *argv[]){
sigset_t block,oblock,pending;
//1.初始化
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
//2.添加屏蔽信号
sigaddset(&block,SIGINT);
//3.开始屏蔽设置进内核
sigprocmask(SIG_SETMASK,&block,&oblock);
//遍历打印pending信号集
while(1){
//获取
sigpending(&pending);
//打印
show_pending(pending);
}
return 0;
}
代码演示
#include<cstdio>
#include<iostream>
#include<cstring>
#include<signal.h>
#include<unistd.h>
void handler(int signo){
std::cout<<"get a signo"<<signo<<std::endl;
}
int main(int argc,char *argv[]){
struct sigaction act,oact;
act.sa_flags=0;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
sigaction(SIGINT,&act,&oact);
while(true){
sleep(1);
}
return 0;
}
当某个信号的处理函数被调用时,内核自动将当前信号加入信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另一些信号,则用sa_mask字段说明这些需要额外的屏蔽信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,一般设置为0;sa_sigation是实时信号的处理函数。
一个进程可能有多个执行流。比如说信号。当自定义捕捉信号函数,当信号没来时,信号不会执行处理信号函数,只会执行自己的代码。当信号来了,会去执行处理信号的函数。
? ? ? ? 可重入函数就是,当一个执行流进入一个函数,当中又进入了这个函数,不会出现错误的函数。
? ? ? ? 不可重入函数就是,当一个执行流进入一个函数,当中又进入了这个函数,会出现错误的函数
作用:保持内存可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对改变量的任何操作,都必须在真实的内存中进行操作。(解决因编译器优化导致不正确执行)
父进程可以阻塞等待子进程结束,也可以非阻塞的查询是否有子进程结束等待清理(轮询)。采用第一种方式,父进程阻塞就不能处理自己的工作了;采用第二种方式,父进程在子进程处理自己的工作的同时还要记得时不时轮询,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLDA信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关系子进程,而子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可
不产生僵尸进程的另一种方法:父进程调用sigaction将SIGCHLD的处理动作设置为SIG_IGN,这样fork出来的子进程在终止时会自动清理,不会产生僵尸进程也不会通知父进程。系统,偶然的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void sigchld_handler(int sig) {
int status;
pid_t pid;
// waitpid() 必须用 WNOHANG 做为参数,确保不会阻塞
while ((pid = waitpid((pid_t)(-1), &status, WNOHANG)) > 0) {
printf("Child with PID %ld exited.\n", (long)pid);
}
}
int main() {
signal(SIGCHLD, sigchld_handler);
pid_t pid = fork();
if (pid == 0) { // 子进程
// 子进程做一些工作,然后退出
printf("Child process working...\n");
sleep(2);
printf("Child process done.\n");
exit(0);
} else if (pid > 0) { // 父进程
// 父进程继续做其他工作
for (int i = 0; i < 5; ++i) {
printf("Parent working...\n");
sleep(1);
}
} else {
// fork 失败
perror("fork");
exit(1);
}
return 0;
}
?