信号的概念
- 进程一定要有识别和处理信号的能力,信号的处理能力属于进程内置功能。
- 当进程接收到信号有可能不会立即处理。
为什么ctrl + c 会导致进程终止?
linux中,一次登录,只允许一个进程是前台进程,可以多个进程是后台进程,(区别是,前台进程可以获取键盘输入)
ctrl + c 产生一个信号,发送给前台进程,前台进程收到信号后,就会终止。
本质是被进程解释为收到的信号,2号信号(SIGINT)。
键盘数据如何输入给内核,ctrl + c 如何给内核发送信号?
- 当用户输入数据时,键盘驱动程序将数据写入到终端设备文件中,如:/dev/tty1
写入内核相应的缓冲区,如果缓冲区满就给CPU发出中断申请,操作系统根据中断向量表找到相应的中断处理程序,中断处理程序将数据从缓冲区拷贝到内核的读缓冲区中。 - 当用户按下ctrl + c时,键盘驱动程序将ctrl + c解释为2号信号,写入到终端设备文件中,如:/dev/tty1
- 当终端设备文件有数据可读时,内核会读取数据,并判断数据是否是信号,如果是信号,则发送给前台进程。
信号的产生
首先不是所有的信号都可以被signal捕捉,SIGKILL和SIGSTOP信号不能被捕捉,也不能被忽略,只能被阻塞。
快捷键产生信号
ctrl + c 产生2号信号(SIGINT)
ctrl + \ 产生3号信号(SIGQUIT)
ctrl + z 产生18号信号(SIGTSTP)
ctrl + d 产生4号信号(SIGTERM)
系统调用产生信号
kill()函数产生信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <sys/types.h>
#include <string>
using namespace std;
void myhandler(int signo) {
cout << "Process received signal: " << signo << endl;
exit(2);
}
void Usage(string proc) {
cout << "Usage:\n\t" << proc << " signum pid\n\n";
}
int main(int argc, char *argv[]) {
signal(2, myhandler);
if (argc != 3) {
Usage(argv[0]);
exit(1);
}
int signum = stoi(argv[1]); // 字符串转为 INT
pid_t pid = stoi(argv[2]);
int n = kill(pid, signum);
if (n == -1) {
perror("kill");
exit(2);
}
return 0;
}
raise()函数产生信号
SYNOPSIS
#include <signal.h>
int raise(int sig);
//本质是
kill(getpid(), sig);
abort()函数产生信号
SYNOPSIS
#include <signal.h>
void abort(void);
信号的捕捉和处理
信号范围[1,31],每种信号都有自己的一种处理方法
类型是typedef void (*handler_t)(int);函数指针
handler_t handler[31];这个类型的数组,其中每个指针都可以通过信号处理函数修改为自定义处理函数
信号处理方式
- 忽略信号
- 执行默认操作
- 执行用户自定义的信号处理函数
信号处理函数
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
第一个参数为信号编号,第二个参数为信号处理函数的指针。
三张表
每个进程对应的结构体中对应三个表
分别是block表,作用是表面当前信号是否被屏蔽
pending表,作用是表面当前信号是否被挂起(也就是是否到达,如果到达并且没被屏蔽,就会进行信号的处理)。
handler表,表面当前信号的处理方式
接口
#include <signal.h>
int sigemptyset(sigset_t *set);
将一个信号集(sigset_t 类型)清空,也就是说,将该信号集中所有的信号都设置为不包含状态。
#include <signal.h>
int sigaddset(sigset_t *set, int signum);
sigaddset 是一个用于向信号集(sigset_t 类型)中添加特定信号的函数。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigprocmask 是一个用于修改当前进程的信号屏蔽字的系统调用。信号屏蔽字定义了哪些信号在进程执行期间被阻塞(即不被处理)。
how: 指定如何修改信号屏蔽字的操作。可以是以下值之一:
SIG_BLOCK: 将 set 中的信号添加到当前的信号屏蔽字中。
SIG_UNBLOCK: 从当前的信号屏蔽字中删除 set 中的信号。
SIG_SETMASK: 将当前的信号屏蔽字设置为 set 中的信号。
set: 指向一个 sigset_t 类型的变量,表示要添加、删除或设置的信号集。
oldset: 指向一个 sigset_t 类型的变量,用于存储修改前的信号屏蔽字(如果不需要,可以传递 NULL)。
#include <signal.h>
int sigpending(sigset_t *set);
sigpending 是一个用于检查当前进程中被阻塞但尚未处理的信号的系统调用。也就是得到pending表中的内容
小案例(先屏蔽2号信号,后释放)
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handler(int singno){
cout<<"catch a signo: "<<singno<<endl;
}
void PrintPending(sigset_t &pending){
for(int signo=31;signo>=1;signo--){
if(sigismember(&pending,signo))
{
cout<<"1";
}else{
cout<<"0";
}
}
cout<<"\n\n";
}
int main(){
//对2号新号进行自定义处理
signal(2,handler);
//初始化两个信号集
sigset_t bset,oset;
sigemptyset(&bset);
sigemptyset(&oset);
sigaddset(&bset,2);
sigprocmask(SIG_SETMASK,&bset,&oset);
sigset_t pending;
int cnt=10;
while(true){
int n=sigpending(&pending);
if(n<0){
continue;
}
PrintPending(pending);
sleep(1);
cnt--;
if(cnt==0){
sigprocmask(SIG_SETMASK,&oset,nullptr);
cout<<"unblock 2 signo "<<endl;
}
}
//继续发送2号信号结果应该为
//全0
}
信号什么时候被处理
当进程从内核态返回到用户态的时候,进行信号的检测和处理
内核态:允许访问操作系统对应的代码和数据
用户态:允许访问自己的代码和数据
信号的捕捉
- 用户态,在主控制流程中因为执行某条指令因为中断,异常或者系统调用进入内核。
- 内核态,内核处理完异常准备返回用户模式之前可以先处理当前进程中pending的信号
- do_signal(),如果函数的处理动作是自定义的信号,处理函数则返回用户态执行
- 用户态,执行自定义函数,当调用sigreturn返回内核态
- 内核态,sys_sigreturn(),返回上一次被中断的地方继续执行
子进程退出会发送信号
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号
的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理
函数中调用wait清理子进程即可。
#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void handler(int signo)
{
sleep(5);
pid_t rid;
while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
{
cout << "I am proccess: " << getpid() << " catch a signo: " << signo << "child process quit: " << rid << endl;
}
}
int main()
{
//signal(17, SIG_IGN); // SIG_DFL -> action -> IGN
// 如果我们有10个子进程呢??如果同时退出呢?
signal(17, handler);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
while (true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
cout << "child quit!!!" << endl;
exit(0);
}
sleep(rand()%5+3);
//sleep(1);
}
// father
while (true)
{
cout << "I am father process: " << getpid() << endl;
sleep(1);
}
return 0;
}
将17号信号捕捉,可以直观看出退出的时候子进程不是悄悄地退出,会主动的向父进程发送17号信号。