【Linux】信号
创始人
2024-04-05 10:54:29

文章目录

      • 1.信号基础
        • 1.1信号的原理
      • 2.信号的产生
        • 2.1signal函数
        • 2.2kill指令和函数
        • 2.3信号产生的方式
          • 2.3.1键盘产生
          • 2.3.2由系统向进程发送信号
        • 2.4由软件条件产生信号
          • 2.4.1alarm函数
          • 2.2.4setitimer函数
          • 2.2.5IO优化
        • 2.5由硬件异常产生信号
        • 2.6code dump
        • 2.7前台进程和后台进程
      • 3.阻塞信号
        • 3.1信号其他相关常见概念
        • 3.2内核中的信号表现
        • 3.3信号集函数
        • 3.4sigprocmask函数
        • 3.5sigpending函数
      • 4.理解信号捕捉
        • 4.1内核空间和用户空间
        • 4.2内核态和用户态
        • 4.3信号捕捉流程
        • 4.4sigaction函数
      • 5.可重入函数
        • 5.1volatile关键字
        • 5.2SIGCHLD信号

1.信号基础

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

信号机制

信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。

信号是给进程发送的,进程具备处理信号的能力,接收信号的能力。识别信号的能力。

#include
#include
using namespace std;
int main(){int cnt=0;while(1){cout<<"cnt:"<

image-20221107104609206

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

image-20221107104951633

  • 前31个信号是常用信号,后面的信号属于实时信号,常用于嵌入式开发中

  • 其中比较特殊的是SIGKILL和SIGSTOP信号不能被捕捉、忽略、阻塞。分别表示杀死信号和暂停信号。

常见信号的处理方式

image-20221107112044529

1.1信号的原理

信号的异步机制:信号是随时随地可以产生的,当信号产生时,进程可能还在做更重要的事情,进程可以暂时不处理这个信号,等事情处理完在处理信号。

结论:

  • 进程可以暂时不处理信号
  • 进程需要记住需要处理什么信号,并且按照相应的动作处理信号。1.默认动作【系统默认】2.忽略信号 3.自定义动作。

进程任何记住信号,在哪里记住信号。

在每一个进程的PCB中,存在一个位图,记录该进程有那些信号需要处理。

struct task_struct{uint32- t sig; //一个位图,比特位的位置记录是了什么信号,比特位的值记录有没有该信号。
}

OS是进程的管理者,PCB的数据需要OS进行修改。

2.信号的产生

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。

2.1signal函数

上面我们提到,进程对一个信号有三种处理方式。而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:"<

image-20221107114304422

2.2kill指令和函数

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;
}

image-20221107125032895

2.3信号产生的方式

2.3.1键盘产生

键盘参数----> 参数硬件中断----> OS识别中断-----> 发送信号

#include
#include
using namespace std;
int main(){int cnt=0;while(1){cout<<"cnt:"<

上面介绍过,可以Ctrl+C或者Ctrl+\终止进程。

2.3.2由系统向进程发送信号

当我们要使用kill命令向一个进程发送信号时,我们可以以kill -信号名 进程ID的形式进行发送。

int main(){cout<<"进程设置完:"<cout<<"pid:"<

image-20221107120023274

raise()函数:向该进程发送信号

#include 
int raise(int sig);

示例:

int main()
{printf("I will die\n");sleep(2);raise(SIGSEGV);sleep(3);return 0;
}

image-20221107120727901

abort()函数:给自己发送异常终止信号 6) SIGABRT 信号,终止并产生core文件

 void abort(void); 该函数无返回

2.4由软件条件产生信号

2.4.1alarm函数
#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;
}

image-20221107131218551

2.2.4setitimer函数

设置定时器,可以替代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:传出型参数,可以得到上一个计时器的信息
*/

image-20220730165725142

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:"<

image-20221107133057535

2.2.5IO优化

程序实际执行时间(real)=系统时间(sys)+用户时间(user)+等待时间。我们可以观察一秒钟cnt++的次数进行判断。

假如输出到标准输出中:

int main(){int cnt=0;alarm(1);while(1){cout<<"cnt:"<

image-20221107133443198

不进行IO:

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

image-20221107201543767

可以看到,加法的执行次数达到了5亿多次。这样表明,计算机CPU与外设进行IO交互时,速度非常慢。

2.5由硬件异常产生信号

C/C++程序进程崩溃的本质是,该进程收到了异常信息。比如发生了段错误,野指针,除0错误等。

image-20221107203355907

原理:

  • 除0错误:CPU内部,有一组寄存器叫做状态寄存器,标记本次计算的状态。当进程发生除0等计算错误时,CPU内部的状态寄存器中的标志位会修改为错误。
  • 越界/野指针:程序使用的是虚拟地址,当进程运行时,虚拟地址会通过MMU和页表的作用,转换为真实的物理地址,读取对应的数据;如果虚拟地址有问题,那么在转化的过程中,MMU就会发现问题并发生硬件中断,产生信号。

2.6code dump

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

image-20220814174325445

ulimit -a #查看OS配置信息

image-20221107204627391

修改code的大小。

image-20221107204732511

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;
}

image-20221107205459245

生成了对应的code文件

image-20221107205527704

2.7前台进程和后台进程

./proc&		#可以将进程proc变成一个后台进程jobs		#可以查看当前的后台进程fg	工作编号	#将对应的编号变成变为前台进程bg	工作编号	#变为后台进程

3.阻塞信号

3.1信号其他相关常见概念

  • 信号递达:实际执行信号的处理动作
  • 信号未决(Pending) :信号从产生到递达之间的状态
  • 信号阻塞(Block):进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

阻塞和忽略的区别:

  • 忽略:信号已经递达,处理动作是什么都不做
  • 阻塞:信号未递达,进程等待信号。

3.2内核中的信号表现

image-20221107211157005

block和pending是一个位图,分别表示阻塞信号集(也叫信号屏蔽字)和未决信号集;handler表示一个函数指针数组,每个元素存放下标对应信号的信号处理方式

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志 。
  • 如果信号被阻塞,那么阻塞信号集对应的比特位设置为1;在没有解除阻塞之前不能忽略这个信号 。

如果一个信号被传递多次:

  • POSIX:允许系统递送该信号多次
  • Linux:常规信号在递达前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列中。

3.3信号集函数

#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);  

函数用法:

  • sigemptyset函数:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • sigfillset函数:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
  • sigaddset函数:在set所指向的信号集中添加某种有效信号。
  • sigdelset函数:在set所指向的信号集中删除某种有效信号。
  • sigismember函数:判断在set所指向的信号集中是否包含某种信号,若包含则返回1,不包含则返回0,调用失败返回-1。

3.4sigprocmask函数

用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(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_UNBLOCKhow设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
SIG_SETMASK当how设置为此,set表示用于替代原始屏蔽集的新屏蔽集。相当于 mask = set,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

3.5sigpending函数

读取当前进程的未决信号集

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:"<

image-20221107215548122

4.理解信号捕捉

进程处理信号,不是立即处理,而是需要在合适的时间处理。

当当前进程从内核态,切换回到用户态的时候,该进程会处理信号。

4.1内核空间和用户空间

image-20221107222713661

  • OS中的所有进程的内核区共享一个内核物理地址
  • 每个进程的用户区都有自己的用户物理地址

用户态下,进程只能访问用户级页表;内核态下,进程才有权限访问系统级页表。

4.2内核态和用户态

当进程变为用户态时,进程才有权限访问内核的页表和数据结构。

CPU内部有对应的状态寄存器CR3,标志位为0时对应内核态,3对应用户态。

什么情况下,从用户态转变为内核态?

  • 系统调用,进程调用系统调用时,会陷入内核中。
  • 时间片到了,进行进程间切换。
  • 异常、中断、陷阱等处理完毕。

其中,由用户态切换为内核态我们称之为陷入内核。每当我们需要陷入内核的时,本质上是因为我们需要执行操作系统的代码,比如系统调用函数是由操作系统实现的,我们要进行系统调用就必须先由用户态切换为内核态。

4.3信号捕捉流程

信号捕捉流程图

image-20221108195321786

  • 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
  • 由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
  • 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。
  • sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
  • 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

4.4sigaction函数

捕捉信号除了用前面用过的signal函数之外,我们还可以使用sigaction函数对信号进行捕捉,sigaction函数的函数原型如下:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数

  • signum:捕捉的信号
  • 若act指针非空,则根据act修改该信号的处理动作。
  • 若oldact指针非空,则通过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_handler:函数指针,捕捉信号的处理方式。赋值为常数SIG_IGN传给sigaction函数,表示忽略信号。赋值为常数SIG_DFL传给sigaction函数,表示执行系统默认动作。如果传递一个函数指针,就是自定义方式
  • sa_sigaction:实时信号处理函数,通常为NULL
  • sa_mask:阻塞信号集
  • sa_flags:一般为0
  • sa_restorer:该参数没有被使用

关于sa_mask

  • 首先需要注意的是,当某个信号的处理函数被调用。内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。【Linux环境下,信号在递达前计数为1次】
  • 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用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"<

image-20221109001432796

5.可重入函数

考虑下面常见:

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

image-20221109002622841

分析执行过程:

image-20221109002754295

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

image-20221109003038240

捕捉信号:

image-20221109003329726

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

image-20221109003423845

最后左右一个结点插入到链表中,在销毁链表时会发生内存泄漏。

  • 像上例这样,insert函数被不同的控制流调用(main函数和sighandler函数使用不同的堆栈空间,它们之间不存在调用与被调用的关系,是两个独立的控制流程),有可能在第一次调用还没返回时就再次进入该函数,我们将这种现象称之为重入。

  • insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

**如果一个函数符合以下条件之一则是不可重入的: **

  • 内部调用了malloc和free的函数,因为malloc是用全局链表管理内存的
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

5.1volatile关键字

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;
}

image-20221109004218112

代码中的main函数和handler函数是两个独立的执行流,而while循环是在main函数当中的,在编译器编译时只能检测到在main函数中对flag变量的使用。

当捕捉到信号时,全局变量flag被修改为1,进程退出。

gcc ....... -O2/O3 选项可以优化编译器,只从当前执行流的寄存器中取数据。

image-20221109005704520

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;
}

image-20221109010011357

5.2SIGCHLD信号

子进程在终止时会给父进程发生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"<

image-20221109012329036

不只是子进程退出,子进程暂停和再允许都会向父进程发生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"<

image-20221109012725757

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

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...