Linux之进程信号(上)(一)

简介: Linux之进程信号(上)(一)

前言

生活中有各种各样的信号,比如:闹钟、红绿灯、上下课铃声……我们可以知道信号产生时对应的要做些什么,幼儿园的小朋友也明白红灯停、绿灯行的道理。

但是,人是怎么识别出这些信号的呢?人是只有通过认识,才能产生行为:有人通过教育的手段让我们在大脑里记住了红绿灯属性及其对应行为。

但是,当信号产生时,我们并不是总能及时去处理这个信号。信号的发生是随时的(异步),但是我们去处理信号并不都是即时的。因为,我们在信号来临时可能会有其他更重要的事情要做(优先级更高的事情),所以从信号发生到信号被处理中间会有一个时间窗口,当然我们在未处理这个信号时需要将这个信号记录下来,等能处理时再处理。

当我们处理信号时,处理信号的方式也是有所不同的(不同的信号有不同的处理方式,不同的人对对同一个信号的处理方式也可能不同,相同的人对相同的信号在不同的场景下处理信号方式也可能不同)。处理信号的方式大致分为以下三种:

  1. 默认动作:例如,红灯停,绿灯行等。
  2. 自定义动作:例如,红灯唱歌,绿灯跳舞等。
  3. 忽略动作:例如,早晨闹钟响了,我们默认动作是起床,忽略动作是忽略闹钟继续睡觉。
    那么,进程与人处理信号的方式有什么异同呢?信号又是如何产生的呢?本文我们来了解Linux中的进程信号。

一、进程信号

前言中,我们通过生活中的信号引入了进程中的信号,下面我们简单了解以下进程信号的概念。进程本身是被程序员编写的代码,是属性和逻辑的组合,所以进程处理信号的识别和对应的动作都是程序员所赋予的。

  1. 信号是给进程发送的,那么进程是如何识别信号的? 认识 + 动作
  2. 进程在处理信号的时候有三种动作:默认动作、自定义动作、忽略动作。
  3. 处理信号也被称为信号被捕捉
  4. 如果进程收到信号的时候,有优先级更高的代码需要执行,我们就不能即时的处理信号,因此进程需要有保存信号的能力
  5. 进程是如何保存不能即时处理的信号的?
    信号会被保存在进程的PCB(task_struct)中,我们用比特位来表示信号的编号,比特位的内容表示是否收到对应信号(0表示没收到,1表示收到了)。
  6. 如何理解信号的发送和接收?
    信号的发送和接收,实际上就是改变PCB中的信号位图。PCB是内核维护的数据结构对象,所以PCB的管理者是OS,因此只有OS可以改变PCB中的内容,因此无论我们之后学习到多少种发送信号的方式,本质上都是OS向目标进程发送信号。当然,这也说明了系统必须要提供发送信号、处理信号相关的系统调用。(我们之前使用的kill命令就是一种系统调用)

二、查看命令kill -l与信号解释man 7 signal

1.kill -l

查看系统定义的信号列表;

每一个信号都有与之对应的编号和宏定义名称,这些宏定义可以在signal.h中找到。

2.man 7 signal

查看信号详细信息。

Term表示正常结束(OS不会做任何额外工作);

Core表示OS出了终止的工作;

其他的见下文。

三、信号的产生

文件test.c

1 #include<stdio.h>
  2 int main()
  3 {
  4         int cnt = 0;
  5         while(1)
  6         {       
  7                 printf("hello %d, i am %d\n",cnt++, getpid());
  8                 sleep(1);
  9         }
 10         return 0;
 11 }

1.按键

ctrl + c

ctrl + c:热键,它实际上是个组合键,OS会将它解释为2信号。

进程对3信号的默认行为是终止进程,所以当我们按ctrl + c时当前运行的进程将被直接终止。

用ctrl + c:

用kill -2

ctrl + z

ctrl + z:热键,实际上是20号信号(即,按ctrl + \和kill -20 (进程pid)是一样的)。

ctrl + \

ctrl + \:热键,实际上是3信号。

2.系统调用

用键盘向前台进程发送信号,前台进程会影响shell,Linux规定跟shell交互时只允许有一个前台进程,实际上当我们运行自己的进程时,我们的进程就变成了前台进程,而sbash会被自动切到后台(默认情况下bash也是一个进程)。

当然,除了用键盘向前台进程发送信号外,我们可以用系统调用向进程发送信号。

kill——向任意进程发送信号

发送信号的能力是OS的,但是有这个能力并不一定有使用这个能力的权利,一般情况下要由用户决定向目标进程发送信号(通过系统提供的系统调用接口来向进程发送信号)。

通过kill与命令行参数相结合:

文件mykill.cc

1 #include<iostream>
  2 #include<stdio.h>
  3 #include<errno.h>
  4 #include<stdlib.h>
  5 #include<sys/types.h>
  6 #include<signal.h>
  7 using namespace std;
  8 
  9 static void Usage(const string &proc)
 10 {
 11         cout<<"\nUsage:"<<proc<<" pid signo\n"<<endl;
 12 }
 13 
 14 int main(int argc, char* argv[])
 15 {
 16         if(argc != 3)
 17         {
 18                 Usage(argv[0]);
 19                 exit(1);
 20         }
 21         pid_t pid = atoi(argv[1]);
 22         int signo = atoi(argv[2]);
 23         int n = kill(pid, signo);
 24         if(n != 0)
 25         {
 26                 perror("kill");
 27         }
 28         return 0;
 29 }

kill命令底层实际上就是kill系统调用,信号的发送由用户发起而OS执行的。

raise——进程给自己发送任意信号

文件mysignal.cc

1 #include<iostream>
  2 #include<signal.h>
  3 #include<unistd.h>
  4 using namespace std;
  5 int main(int argc, char* argv[])
  6 {
  7         int cnt = 0;
  8         while(cnt <= 10)
  9         {
 10                 sleep(1);
 11                 cout<<cnt++<<endl;
 12                 if(cnt >= 5)
 13                 {
 14                         raise(3);//发送3号信号
 15                 }
 16         }
 17         return 0;
 18 }

abort——进程给自己指定的信号(6号信号)

文件mysignal.cc

1 #include<iostream>
  2 #include<stdlib.h>
  3 #include<signal.h>
  4 #include<unistd.h>
  5 using namespace std;
  6 int main(int argc, char* argv[])
  7 {
  8         int cnt = 0;
  9         while(cnt <= 10)
 10         {
 11                 sleep(1);
 12                 cout<<"cnt:"<<cnt++<<",pid:"<<getpid()<<endl;
 13                 if(cnt >= 5)
 14                 {
 15                         abort();//发送6号信号
 16                 }
 17         }
 18         return 0;
 19 }

不同的信号代表不同的事件,但是对事件发送后的处理动作是可以一样的。大多数信号处理的默认动作都是终止进程。

3.硬件异常产生信号

信号的产生,不一定非要用户显示的发送,有些情况下,信号会自动在OS内部产生。

除零,发送8号信号

文件test.cc

1 #include<iostream>
  2 using namespace std;
  3 #include<unistd.h>
  4 int main(int argc, char* argv[])
  5 {
  6         while(true)
  7         {
  8                 cout<<"i am running..."<<endl;
  9                 sleep(1);
 10                 int a = 10;
 11                 int b = 0;
 12                 a /= b;
 13         }
 14         return 0;
 15 }

  1. 为什么进程除零会终止进程?
    因为除零会导致当前进程收到来自OS的SIGFPE信号。
  2. OS怎么知道应该给当前进程发送8号信号呢?
    因为CPU出现异常,除零错误。
    CPU中由很多寄存器(eax/edx等),执行int a = 10;int b = 0;a /= b;时CPU内除了数据保存,还要保证运行有没有问题。因此CPU内有状态寄存器,状态寄存器可以用来衡量本次运算结果,10/0的结果是无穷大,它会引起状态寄存器溢出标记位用0变为1,CPU就发送了运算异常。OS得知CPU发送运算异常,就要识别异常:状态寄存器的标记位置为1,是由当前进程导致的,因此会向当前进程发送信号,最后就终止了进程。

通过signal接口,将SIGFPE信号自定义捕捉。

文件test.cc

1 #include<iostream>
  2 using namespace std;
  3 #include<unistd.h>
  4 #include<signal.h>
  5 void catchSig(int signo)
  6 {
  7         cout<<"获取到一个信号,信号编号是:"<<signo<<endl;
  8 }
  9 int main(int argc, char* argv[])
 10 {
 11         signal(SIGFPE, catchSig);//捕捉任意信号
 12         int a = 10;
 13         int b = 0;
 14         a /= b;
 15         while(true)
 16         {
 17                 cout<<"i am running..."<<endl;
 18                 sleep(1);
 19         }
 20         return 0;
 21 }

通过上面的例子,我们可以知道:收到信号并不一定会引起进程退出。

如果进程没有退出,则还有被调度的可能。CPU内的寄存器只有一份,寄存器内的内容是独立属于当前进程的上下文,一旦出现异常,我们没有能力去修正这个问题,所以当进程被切换时,会有无数次状态寄存器被保存和恢复的过程,每一次恢复的时候都会让OS识别到CPU内的状态寄存器中的溢出标志位为1,所以每一次都会发送8号信号。

相关文章
|
3月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
195 67
|
2月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
81 16
|
4月前
|
安全 Linux
【Linux】阻塞信号|信号原理
本教程从信号的基本概念入手,逐步讲解了阻塞信号的实现方法及其应用场景。通过对这些技术的掌握,您可以更好地控制进程在处理信号时的行为,确保应用程序在复杂的多任务环境中正常运行。
207 84
|
2月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
74 20
|
1月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
49 0
|
1月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
43 0
|
1月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
38 0
|
1月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
51 0
|
4月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。
|
4月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
155 4
AI助理
登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问

你好,我是AI助理

可以解答问题、推荐解决方案等