信号是进程之间事件异步通知的一种方式,属于软中断。
信号机制
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
信号是给进程发送的,进程具备处理信号的能力,接收信号的能力。识别信号的能力。
#include
#include
using namespace std;
int main(){int cnt=0;while(1){cout<<"cnt:"<

我们按Ctrl+\也可以终止程序。Ctrl+C和Ctrl+\的区别:Ctrl+C发送的是2号信号SIGINT,Ctrl+\发送是3号信号SIGOUT。

前31个信号是常用信号,后面的信号属于实时信号,常用于嵌入式开发中
其中比较特殊的是SIGKILL和SIGSTOP信号不能被捕捉、忽略、阻塞。分别表示杀死信号和暂停信号。
常见信号的处理方式

信号的异步机制:信号是随时随地可以产生的,当信号产生时,进程可能还在做更重要的事情,进程可以暂时不处理这个信号,等事情处理完在处理信号。
结论:
进程任何记住信号,在哪里记住信号。
在每一个进程的PCB中,存在一个位图,记录该进程有那些信号需要处理。
struct task_struct{uint32- t sig; //一个位图,比特位的位置记录是了什么信号,比特位的值记录有没有该信号。
}
OS是进程的管理者,PCB的数据需要OS进行修改。
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。
上面我们提到,进程对一个信号有三种处理方式。而signal函数可用于在该进程中捕捉一个信号,并注册该信号的处理方式。
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);参数:signum:注册的信号编号handler:一个函数指针,注册信号的处理方式返回值:返回函数指针,返回上一次处理该信号的方式
下面我们修改信号SIGINT的处理方式
#include
#include
#include
using namespace std;
void handler(int signo){cout<<"捕捉信号sign:"<signal(SIGINT,handler);sleep(3);cout<<"进程设置完:"<cout<<"pid:"<

kill -signal pid #表示向pid进程发送信号signal
kill接口
#include
#include
int kill(pid_t pid, int sig);用法和kill指令一样,pid是目标进程,sig是发送的信号编号
实现一个mykill指令,格式为:mykill signo pid
using namespace std;
void Useag(){cout<<"mykill signo pid"<if(argc!=3){Useag();}int sign=atoi(argv[1]);int pid=atoi(argv[2]);if(kill(static_cast(atoi(argv[2])), atoi(argv[1])) == -1){cerr << "kill: " << strerror(errno) << endl;exit(2);}return 0;
}

键盘参数----> 参数硬件中断----> OS识别中断-----> 发送信号
#include
#include
using namespace std;
int main(){int cnt=0;while(1){cout<<"cnt:"<
上面介绍过,可以Ctrl+C或者Ctrl+\终止进程。
当我们要使用kill命令向一个进程发送信号时,我们可以以kill -信号名 进程ID的形式进行发送。
int main(){cout<<"进程设置完:"<cout<<"pid:"<

raise()函数:向该进程发送信号
#include
int raise(int sig);
示例:
int main()
{printf("I will die\n");sleep(2);raise(SIGSEGV);sleep(3);return 0;
}

abort()函数:给自己发送异常终止信号 6) SIGABRT 信号,终止并产生core文件
void abort(void); 该函数无返回
#include
unsigned int alarm(unsigned int seconds);
参数:seconds:过多少秒发送一个SIGALRM信号返回值:返回上一个alarm函数还剩下多少时间发送信号
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理
动作是终止当前进程。进程收到信号SIGALRM,默认动作为终止进程。
int main()
{int ret=alarm(7);printf("alarm返回值:%4d\n",ret);sleep(2);ret=alarm(4);printf("alarm返回值:%4d\n",ret);int cnt=0;while(1){printf("cnt:%4d\n",cnt++);sleep(1);}return 0;
}
设置定时器,可以替代alarm函数。
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
/*
参数说明:which有三个宏,可以指定定时的方式1.自然对数:ITIMER_REAL 函数调用发送信号 14)SIGALRM2.虚拟空间计时(用户空间):ITIMER_VIRTUAL 函数调用发送信号 26)SIGVTALRM3.运行时计时(用户+内核):ITIMER_PROF 函数调用发送信号 27)SIGPROFnew_value:该计时器的信息old_value:传出型参数,可以得到上一个计时器的信息
*/

struct itimerval类型嵌入了两个struct timeval结构体。it_interval表示周期性的值,每多少时间为一个周期发送信号。it_value是设置第一次发送信号的时间。
stuct timeval中的 tv_sec表示秒,tv_usec表示微秒
模拟实现alarm
using namespace std;
int myalarm(int second){struct itimerval old_value,new_value={{0,0},{0,0}};new_value.it_value.tv_sec=second;setitimer(ITIMER_REAL,&new_value,&old_value);return old_value.it_value.tv_sec;
}
int main(){int ret1=myalarm(7);printf("第一次返回值:%d\n",ret1);sleep(2);int ret2=myalarm(5);printf("第二次返回值:%d\n",ret2);int cnt=0;while(1){cout<<"cnt:"<

程序实际执行时间(real)=系统时间(sys)+用户时间(user)+等待时间。我们可以观察一秒钟cnt++的次数进行判断。
假如输出到标准输出中:
int main(){int cnt=0;alarm(1);while(1){cout<<"cnt:"<

不进行IO:
int cnt=0;
void handler(int signo){ //捕捉信号cout<<"cnt:"<signal(SIGALRM,handler);alarm(1);while(1){cnt++;}return 0;
}

可以看到,加法的执行次数达到了5亿多次。这样表明,计算机CPU与外设进行IO交互时,速度非常慢。
C/C++程序进程崩溃的本质是,该进程收到了异常信息。比如发生了段错误,野指针,除0错误等。

原理:
code dump机制:又叫做核心转储机制。当程序异常退出时,会把进程运行过程中对应的异常上下文数据code dump到磁盘上。并产生一个code.pid文件。【在云服务器上,该选项常常是关闭的】

ulimit -a #查看OS配置信息

修改code的大小。

int main(){pid_t pid=fork();if(pid==0){int *p=nullptr;*p=1;exit(1);}int status=0;waitpid(pid,&status,0);printf("exitcode:%d,signo:%d,code%d\n",(status>>8)&0xFF,status&0x7F,(status>>7)&0x1);return 0;
}

生成了对应的code文件

./proc& #可以将进程proc变成一个后台进程jobs #可以查看当前的后台进程fg 工作编号 #将对应的编号变成变为前台进程bg 工作编号 #变为后台进程
阻塞和忽略的区别:

block和pending是一个位图,分别表示阻塞信号集(也叫信号屏蔽字)和未决信号集;handler表示一个函数指针数组,每个元素存放下标对应信号的信号处理方式
如果一个信号被传递多次:
#include
int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set);int sigaddset(sigset_t *set, int signum);int sigdelset(sigset_t *set, int signum);int sigismember(const sigset_t *set, int signum);
函数用法:
用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);成功:0;失败:-1,设置errno
参数:1.set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。2.oldset:传出参数,保存旧的信号屏蔽集。
参数how
| 选项 | 作用 |
|---|---|
| SIG_BLOCK | 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask |
| SIG_UNBLOCK | how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set |
| SIG_SETMASK | 当how设置为此,set表示用于替代原始屏蔽集的新屏蔽集。相当于 mask = set,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。 |
读取当前进程的未决信号集
int sigpending(sigset_t *set);set:传出参数
打印未决信号集
int main(){sigset_t pend,sigproc;sigemptyset(&pend);sigemptyset(&sigproc);//设置阻塞信号集sigaddset(&sigproc,SIGINT);sigaddset(&sigproc,SIGQUIT);sigaddset(&sigproc,SIGABRT);sigprocmask(SIG_BLOCK,&sigproc,nullptr);while (1){sigpending(&pend);for(int i=0;i<32;i++){if(sigismember(&pend,i)==1){std::cout<<1<<" ";}else{std::cout<<0<<" ";}}std::cout<<"pid:"<

进程处理信号,不是立即处理,而是需要在合适的时间处理。
当当前进程从内核态,切换回到用户态的时候,该进程会处理信号。

用户态下,进程只能访问用户级页表;内核态下,进程才有权限访问系统级页表。
当进程变为用户态时,进程才有权限访问内核的页表和数据结构。
CPU内部有对应的状态寄存器CR3,标志位为0时对应内核态,3对应用户态。
什么情况下,从用户态转变为内核态?
其中,由用户态切换为内核态我们称之为陷入内核。每当我们需要陷入内核的时,本质上是因为我们需要执行操作系统的代码,比如系统调用函数是由操作系统实现的,我们要进行系统调用就必须先由用户态切换为内核态。
信号捕捉流程图

捕捉信号除了用前面用过的signal函数之外,我们还可以使用sigaction函数对信号进行捕捉,sigaction函数的函数原型如下:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数
struct sigaction结构体解析:
struct sigaction {void(*sa_handler)(int);void(*sa_sigaction)(int, siginfo_t *, void *);sigset_t sa_mask;int sa_flags;void(*sa_restorer)(void);
};
关于sa_mask
实例
//阻塞2号信号和3号信号
void handler(int signo){std::cout<<"捕捉到一个信号,信号的编号是:"<sigpending(&pending);for(int i=1;i<=31;i++){if(sigismember(&pending,i)){std::cout<<1<<" ";}else{std::cout<<0<<" ";}}std::cout<struct sigaction act,oact;sigset_t mask;//添加阻塞信号sigaddset(&mask,SIGINT);sigaddset(&mask,SIGQUIT);act.sa_handler=handler;act.sa_flags=0;act.sa_mask=mask;sigaction(SIGINT,&act,&oact);while(1){std::cout<<"main running"<

考虑下面常见:
主函数对一个全局链表进行头插时,insert()函数执行到一半,接收到信号发送中断;而信号的捕捉函数也是向链表进行头插。

分析执行过程:

执行主函数中的insert(&node1);由于发生了信号中断,所以后半部分重新需要在处理信号后再继续执行。

捕捉信号:

执行insert(&node1)的后部分程序:

最后左右一个结点插入到链表中,在销毁链表时会发生内存泄漏。
像上例这样,insert函数被不同的控制流调用(main函数和sighandler函数使用不同的堆栈空间,它们之间不存在调用与被调用的关系,是两个独立的控制流程),有可能在第一次调用还没返回时就再次进入该函数,我们将这种现象称之为重入。
insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
**如果一个函数符合以下条件之一则是不可重入的: **
volatile的作用是:
volatile是C语言的一个关键字,该关键字的作用强制从内存中取数据,保持内存的可见性。
int flag = 0;
void handler(int signo)
{printf("捕捉到一个信号:%d\n", signo);flag = 1;
}
int main()
{signal(2, handler);while (!flag);printf("进程是正常退出的!\n");return 0;
}

代码中的main函数和handler函数是两个独立的执行流,而while循环是在main函数当中的,在编译器编译时只能检测到在main函数中对flag变量的使用。
当捕捉到信号时,全局变量flag被修改为1,进程退出。
gcc ....... -O2/O3 选项可以优化编译器,只从当前执行流的寄存器中取数据。

C99标准会进行优化,只能捕捉一次信号。
使用volatile关键字,强制进程从真实的内存中取数据,而不是寄存器
//阻塞2号信号和3号信号
#include
#include
volatile int flag = 0;
void handler(int signo)
{printf("捕捉到一个信号:%d\n", signo);flag = 1;
}
int main()
{signal(SIGINT, handler);while (!flag);printf("进程是正常退出的!\n");return 0;
}

子进程在终止时会给父进程发生SIGCHLD信号,该信号的默认处理动作是忽略。
void handler(int signo)
{std::cout<<"捕捉到信号:"<pid_t pid=fork();if(pid==0){int cnt=5;while (cnt){std::cout<<"我是子进程:pid=%d"<std::cerr<<"waitpid error"<

不只是子进程退出,子进程暂停和再允许都会向父进程发生SIGCHLD信号。
void handler(int signo)
{std::cout<<"捕捉到信号:"<pid_t pid=fork();if(pid==0){int cnt=5;while (cnt){std::cout<<"我是子进程:pid=%d"<std::cerr<<"waitpid error"<

发送SIGSTOP,子进程暂停一样会给父进程发送SIGCHLID信号。