本章主要讲解学习Linux中的信号,从信号的产生到识别,再到处理的各个时期的详细学习
用户输入命令,在Shell下启动一个前台进程;用户按下Ctrl-C,这个键盘输入产生一个硬件中断,被OS获取解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出




SIGINT(ctrl+c)的默认处理动作是终止进程,SIGQUIT(ctrl+\)的默认处理动作是终止进程并且Core Dump,这个键盘输入产生一个硬件中断,被OS获取解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
注:在开发调试阶段可以用ulimit -c 1024命令限制,允许产生core文件(允许core文件最大为1024K)

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
int main()
{
while(1)
{
cout<<"getpid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
注:使用gdb对当前可执行程序进行调试,然后直接使用
core-file core文件命令加载core文件,即可判断出该程序在终止时的信号,并且定位错误代码
waitpid函数的第二个参数status是一个输出型参数,用于获取子进程的退出状态。status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同

首先在后台执行死循环程序,然后用kill命令给它发信号
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
using namespace std;
int main()
{
if(fork()==0)
{
//child
while(1)
{
cout<<"I am child getpid:"<<getpid()<<"ppid:"<<getppid()<<endl;
sleep(1);
}
exit(0);
}
//father
cout<<"I am father getpid:"<<getpid()<<"ppid:"<<getppid()<<endl;
int status=0;
int ret=waitpid(-1,&status,0);
if(ret>0&&WIFEXITED(status))
{
cout<<"wait success exit code:"<<WEXITSTATUS(status)<<endl;
}
else if(ret>0)
{
cout<<"exit signal:"<<(status&0x7F)<<" core dump:"<<((status>>7)&1)<<endl;
}
return 0;
}
#include <signal.h>
int kill(pid_t pid, int signo);
//第一个参数为对应进程的id,第二个参数为想要发送的信号编号
int raise(int signo);
//这两个函数都是成功返回0,错误返回-1
#include <stdlib.h>
void abort(void);
//就像exit函数一样,abort函数总是会成功的,所以没有返回值SIGPIPE信号实际上就是一种由软件条件产生的信号,当进程在使用管道进行通信时,读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么此时写端进程就会收到SIGPIPE信号进而被操作系统终止
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int fd[2] = { 0 };
if (pipe(fd) < 0){ //使用pipe创建匿名管道
perror("pipe");
return 1;
}
pid_t id = fork(); //使用fork创建子进程
if (id == 0){
//child
close(fd[0]); //子进程关闭读端
//子进程向管道写入数据
const char* msg = "hello father, I am child...";
int count = 10;
while (count--){
write(fd[1], msg, strlen(msg));
sleep(1);
}
close(fd[1]); //子进程写入完毕,关闭文件
exit(0);
}
//father
close(fd[1]); //父进程关闭写端
char buffer[128]={0};
ssize_t s=read(pipe_id[0],buffer,sizeof(buffer)-1);//给结束符留一个位置
if(s>0)
{
buffer[s]=0;//设置结束符
printf("msg from child:%s",buffer);
}
else if(s==0)
{
printf("子进程写端关闭...\n");
}
close(fd[0]); //父进程读一次直接关闭读端(导致子进程被操作系统杀掉)
int status = 0;
waitpid(id, &status, 0);
printf("child get signal:%d\n", status & 0x7F); //打印子进程收到的信号
return 0;
}
alarm函数原型:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int cnt=0;
void handler(int signo)
{
cout<<"get a signal:"<<signo<<" cnt:"<<cnt<<endl;
exit(0);
}
int main()
{
//对信号SIGALRM进行捕获
signal(SIGALRM,handler);
alarm(1);//1秒后唤醒
while(1)
{
cnt++;
}
return 0;
}
注:这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号
示例:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程;当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
using namespace std;
int main()
{
if(fork()==0)
{
//child
int cnt=0;
while(cnt<5)
{
cout<<"I am child getpid:"<<getpid()<<"ppid:"<<getppid()<<endl;
sleep(1);
cnt++;
}
int* p=NULL;
*p=100;
sleep(1);
exit(0);
}
//father
cout<<"I am father getpid:"<<getpid()<<"ppid:"<<getppid()<<endl;
int status=0;
int ret=waitpid(-1,&status,0);
if(ret>0&&WIFEXITED(status))
{
cout<<"wait success exit code:"<<WEXITSTATUS(status)<<endl;
}
else if(ret>0)
{
cout<<"exit signal:"<<(status&0x7F)<<" core dump:"<<((status>>7)&1)<<endl;
}
return 0;
}
在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的
注:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

POSIX.1允许系统递送该信号一次或多次,Linux是这样实现的:常规信号在递达之前产生多次只计一次,信号数据存在丢失,而实时信号在递达之前产生多次可以依次放在一个队列里,信号数据不会丢失
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
#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)调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1
注:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则sigprocmask返回前,至少将其中一个信号递达
int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出
//调用成功则返回0,出错则返回-1#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
void PrintPending(sigset_t* p)
{
int i=0;
for(;i<32;i++)
{
if(sigismember(p,i))
printf("1");
else
printf("0");
}
printf("\n");
}
void handler(int signo)
{
printf("get a signal:%d\n",signo);
}
int main()
{
//捕获2号信号
signal(2,handler);
sigset_t set,oset;
sigemptyset(&set);
sigemptyset(&oset);
sigaddset(&set,2);//添加
sigprocmask(SIG_SETMASK,&set,&oset);//设置block位图,阻塞2号信号
sigset_t pending;
sigemptyset(&pending);
int cnt=0;
while(1)
{
sigpending(&pending);
PrintPending(&pending);
sleep(1);
cnt++;
if(cnt==5)
{
sigprocmask(SIG_SETMASK,&oset,&set);//还原为之前的block位图,不再阻塞2号信号
}
}
return 0;
}
注:程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

注:用户和内核态的转换是具有状态标识变量存在的,对于这样的状态转换是为了更好的管理和确保不同状态的各项的权限
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);struct sigaction {
void(*sa_handler)(int);
void(*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void(*sa_restorer)(void);
};#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
struct sigaction act,oact;
void handler(int signo)
{
printf("get a signal :%d\n",signo);
//恢复block位图
sigaction(2,&oact,&act);
}
int main()
{
//初始化
memset(&act,0,sizeof(struct sigaction));
memset(&oact,0,sizeof(struct sigaction));
//设置block位图
act.sa_handler=handler;
sigaddset(&act.sa_mask,4);
sigaction(2,&act,&oact);
while(1)
{
printf("I an running...\n");
sleep(1);
}
return 0;
}

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入 insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数 如果一个函数只访问自己的局部变量或参数,则称为可重入函数(可以被多个执行流访问,并不会造成数据错乱)
保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0){//child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1){
printf("father proc is doing some thing!\n");
sleep(1);}
return 0;
}
父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程
注:系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main()
{
signal(SIGCHLD, SIG_IGN);
if (fork() == 0){
//child
printf("child is running, child dead: %d\n", getpid());
sleep(3);
exit(1);
}
//father
while (1);
return 0;
}