> 技术文档 > linux信号的产生和保存

linux信号的产生和保存

目录

信号

信号说明

前台进程与后台进程

产生信号

系统调用产生信号

软件条件产生信号

硬件异常产生信号

除0操作

野指针操作

Core Dump

信号的保存

sigset_t

信号集操作函数

sigemptyset

sigprocmask

sigpending

sigaddset

sigismember


信号

讲进程信号之前,我们需要先做一个预备工作。讲一下信号基本结论。

1.

进程为什么能识别信号?处理信号?

OS程序员在设计进程的时候,早已内置了信号的识别和处理方式。也就是说在进程在没有信号产生的时候,早就知道信号如何处理了。

2.

进程是怎么处理信号的呢?

进程在收到信号,可能不会立即处理,也可能等一会再处理,只会在合适的时候处理信号。

处理信号的方式有三种:默认处理动作,自定义信号处理动作,忽略处理。其中自定义处理信号也叫信号捕捉。

一个简单样例:

Ctrl+c本质是给前台进程发送2号信号,前台进程收到信号,从而进程退出。

信号说明

man 7 signal中都有详细说明!

注意:1~31是普通信号,34~64是实时信号。

每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,信号的本质其实就是一个数字!

自定义信号处理函数:

再看样例:

执行:

要注意的是,signal函数仅仅是设置了特定信号的捕捉⾏为处理⽅式,并不是直接调⽤处 理动作。如果后续特定信号没有产⽣,设置的捕捉函数永远也不会被调⽤!!

前台进程与后台进程

1.

不管是前台进程还是后台进程,都可以向标准输出上打印,而前台进程本质就是要从键盘获取数据的。命令行shell进程就是一个典型的前台进程,⼀个命令后⾯加个&可以放到后台运⾏。

2.

我们知道,父进程fork一个子进程,父进程先退出了,子进程会自动变成后台进程,后台进程只能使用kill命令才能收到信号。

3.

前台进程是不会能被暂停的,当使用ctrl+z(SIGSTOP,19号信号)暂停,这个进程就会被提到后台。

信号是进程之间事件异步通知的⼀种⽅式,属于软中断。

下面通过三个步骤详细说明信号:信号的产生,信号的保存,信号的处理。

产生信号

产生信号的方式有多种,上述是键盘产生信号,还有系统调用产生信号,也可以使用kill命令来向进程发送信号,当然也可以异常发送信号。

注意:不管哪种方式产生信号,本质上都是OS给进程发送信号的。

系统调用产生信号

一个函数:kill函数

使用:

void hander(int signumber){ printf(\"我是进程,pid:%d,我获得了信号:%d\\n\",getpid(),signumber);}int main(){ signal(SIGINT,hander); while(true) { sleep(1); printf(\"我是进程,pid:%d\\n\",getpid()); printf(\"发送了一个信号\\n\"); kill(getpid(),SIGINT);//发送信号 } return 0;}

我们可以模拟一个kill,实现自己的kill命令:

//testsig.ccvoid hander(int signumber){ printf(\"我是进程,pid:%d,我获得了信号:%d\\n\", getpid(), signumber);}int main(){ signal(SIGINT,hander); while(true) { sleep(1); printf(\"我是进程,pid:%d\\n\",getpid()); }}
//mykill.ccint main(int argc,char* argv[]){ if(argc!=3) { std::cout<<\"输入错误\"<<std::endl; return 1; } pid_t target=std::stoi(argv[1]); int signum=std::stoi(argv[2]); kill(target,signum); return 0;}

一个函数:raise函数

可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)。

使用:

void hander(int signumber){ printf(\"我是进程,pid:%d,我获得了信号:%d\\n\",getpid(),signumber);}int main(){ signal(SIGINT,hander); while(true) { sleep(1); printf(\"我是进程,pid:%d\\n\",getpid()); raise(2);//不断发送2号信号 } return 0;}

一个函数:abort函数

abort函数是给当前进程发送SIGABRT信号(6号信号),虽然会捕捉,但是还是会退出。

使用:

void hander(int signumber){ printf(\"我是进程,pid:%d,我获得了信号:%d\\n\", getpid(), signumber);}int main(){ signal(SIGABRT, hander); abort();//发送6号信号 return 0;}

我们看到虽然自定义捕捉了,但是还是会退出!

以上例子就是系统调用可以产生信号。

软件条件产生信号

之前学习管道的时候,就已经接触过了软件条件产生信号。

当一个进程向管道写入,另一个进程向管道读数据,当写端进程不写了,或者被杀掉了,读就没有意义了,OS就会向读端进程发送SIGPIPE信号(13号信号),终止掉这个进程。

一个函数:alarm函数

使用:我们可以利用这个函数体会IO效率问题。

IO多:

int main(){ int count = 0; alarm(1); while (true) { count++; std::cout << \"count:\" << count << std::endl; } return 0;}

在这1s内,count会一直往显示器上打印,我们使用的是云服务器,运行的count值需要通过网络传到本地,然后IO到本地显示器,所以才只有count值只有3w。

IO少:

int count = 0;void hander(int signumber){ std::cout << \"count:\" << count << std::endl; exit(1);}int main(){ signal(SIGALRM, hander); alarm(1); while (true) count++; return 0;}

在这1s内,count一直运算,最后才会从网络传到本地,再IO到显示器,最后才会打印,比较快,所以count有4亿多。

一个函数:pause函数

使用:

void hander(int signumber){ std::cout<<\"signumber:\"<<signumber<<std::endl; alarm(1);}int main(){ signal(SIGALRM,hander); alarm(1); while(true) pause(); return 0;}

这段代码可以每隔1s就发送一个信号,完成一些任务。

硬件异常产生信号

硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前 进程执⾏了除以0的指令,CPU的运算单元会产⽣异常,内核将这个异常解释为SIGFPE信号发送给进 程。再⽐如当前进程访问了⾮法内存地址,MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送 给进程。

除0操作

void hander(int signumber){ printf(\"我是进程,pid:%d,我获得了信号:%d\\n\", getpid(), signumber); sleep(1);}int main(){ signal(SIGFPE,hander); //Floating point exception 表示浮点数异常 sleep(1); int a=1; a/=0;//浮点数异常信号,8号信号 return 0;}

野指针操作

void hander(int signumber){ printf(\"我是进程,pid:%d,我获得了信号:%d\\n\", getpid(), signumber); sleep(1);}int main(){ signal(SIGSEGV,hander); //Segmentation fault,表示段错误 int* p=nullptr; *p=100;//野指针,OS发送段错误信号,11号信号 return 0;}

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层⾯上,是被当成信号处理的。

为什么叫硬件异常?

cpu在调度进程时,会将进程的变量,存放在cpu的寄存器上,当数据出现除0(其实也是溢出)或野指针(cpu在利用虚拟地址通过页表访问其物理地址时,没有物理地址),OS就会给进程发送对应的信号。这就是典型的硬件中断触发的信号。

Core Dump

  • ⾸先解释什么是CoreDump。当⼀个进程要异常终⽌时,可以选择把进程的⽤⼾空间内存数据全部 保存到磁盘上,⽂件名通常是core,这叫做CoreDump。
  • 进程异常终⽌通常是因为有Bug,⽐如⾮法内存访问导致段错误,事后可以⽤调试器检查core⽂件以 查清错误原因,这叫做 Post-mortem Debug (事后调试)。
  • ⼀个进程允许产⽣多⼤的 core ⽂件取决于进程的 中)。默认是不允许产⽣ core ⽂件的,因为 Resource Limit (这个信息保存在PCB core ⽂件中可能包含⽤⼾密码等敏感信息,不安全。
  • 在开发调试阶段可以⽤ 改变 S ulimit 命令改变这个限制,允许产生core文件

主要用途就是调试。

core和term区别:

例子:

当子进程异常退出的时候,父进程等待子进程,有个status,core dump标志。

int main(){ if (fork() == 0) { sleep(1); int a = 10; a /= 0; exit(0); } int status = 0; waitpid(-1, &status, 0); printf(\"exit signal: %d, core dump: %d\\n\", status & 0x7F, (status >> 7) & 1); return 0;}

信号的保存

  • 实际执⾏信号的处理动作称为信号递达(Delivery)
  • 信号从产⽣到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动 作。

理解:

1.

信号的编号表示三张表的下标,前两张表都可以看成位图,而handler表存放的是函数指针,block表示信号阻塞(阻塞为1),pending表示信号是否收到(收到为1),handler表示信号的动作(忽略,默认,自定义),其实也就是函数。

2.

handler表中,SIG_DFL和SIG_IGN都是宏,分别表示默认(0),忽略(1).

3.

举个例子:当有一个信号到来时,pending表就会将对应的信号比特位变成1,如果这个信号没有被阻塞,就会执行对应的信号动作,在执行动作之前,会先将pending表对应信号比特位由1变0,再去执行。

sigset_t

从上图来看,每个信号只有⼀个bit的未决标志,⾮0即1,不记录该信号产⽣了多少次,阻塞标志也是这样 表⽰的。因此,未决和阻塞标志可以⽤相同的数据类型sigset_t来存储, sigset_t称为信号集 , 这个类型 可以表⽰每个信号的“有效”或“⽆效”状态,在阻塞信号集中“有效”和“⽆效”的含义是该信号 是否被阻塞,⽽在未决信号集中“有效”和“⽆效”的含义是该信号是否处于未决状态。

信号集操作函数

sigset_t类型对于每种信号⽤⼀个bit表⽰“有效”或“⽆效”状态,⾄于这个类型内部如何存储这些 bit则依赖于系统实现,从使⽤者的⻆度是不必关⼼的,使⽤者只能调⽤以下函数来操作sigset_t变量, ⽽不应该对它的内部数据做任何解释,⽐如⽤printf直接打印sigset_t变量是没有意义的。

  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含 任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰该信号集的有效信号 包括系统⽀持的所有信号。
  • 注意,在使⽤sigset_t类型的变量之前,⼀定要调⽤sigemptyset或sigfillset做初始化,使信号集处于 确定的状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删 除某种有效信号。
  • 这四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信 号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

sigemptyset

sigprocmask

调⽤函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是⾮空指针,则更改 进程的信号屏蔽字,参数how指⽰如何更改。如果oset和set都是⾮空指针,则先将原来的信号屏蔽字 备份到oset⾥,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了 how参数的可选值。

如果调⽤sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀ 个信号递达。

sigpending

sigaddset

sigismember

使用:

void PrintPending(sigset_t &pending){ printf(\"我是一个进程(%d), pending: \", getpid()); for (int signo = 31; signo >= 1; signo--) { //查询某个信号是否在pending表中 if (sigismember(&pending, signo)) std::cout << \"1\";//在打印1 else std::cout << \"0\";//不在打印0 } std::cout << std::endl;}void handler(int sig){ std::cout << \"#######################\" << std::endl; std::cout << \"递达\" << sig << \"信号!\" << std::endl; sigset_t pending; int m = sigpending(&pending); PrintPending(pending); std::cout << \"#######################\" << std::endl;}int main(){ signal(SIGINT, handler); // 1. 屏蔽2号信号 sigset_t block, oblock; //初始化 sigemptyset(&block); //初始化 sigemptyset(&oblock); // 将2号信号添加进block中 sigaddset(&block, SIGINT); //将block覆盖原有的block表,oblock就是原有的 //oblock参数是一个输出型参数 int n = sigprocmask(SIG_SETMASK, &block, &oblock); (void)n; // 4. 重复获取打印过程 int cnt = 0; while (true) { // 2. 获取pending信号集合 sigset_t pending; int m = sigpending(&pending); // 3. 打印pending PrintPending(pending); if (cnt == 10) { // 5. 恢复对2号信号的block情况 std::cout << \"解除对2号的屏蔽\" << std::endl; sigprocmask(SIG_SETMASK, &oblock, nullptr); } sleep(1); cnt++; } return 0;}

我们下期见!!!