科技颠覆者 发表于 2024-10-22 08:03:40

【Linux】进程信号(上)

目次
一、信号概念
1.1 生存中的信号
1.2 进程中的信号  
1.3 信号的概念
二、信号的产生
2.1 通过组合键产生信号
2.2 通过函数发送信号
kill命令
kill函数
raise函数 
abort函数 
Core Dump
2.3 特定软件条件下产生信号

一、信号概念

1.1 生存中的信号

在日常生存中,我们的身边处处是信号,比方电话铃、红绿灯、闹钟等等,这些信号的设置是为了提示我们某种信息
又比方,点的外卖到了,我们接收到了外卖员发送的“信号”,知道此时应该对该信号进行处置处罚即取外卖,但大概我们正在做一些重要事情脱不开身,于是过了一会才下楼将外卖取走。这说明即使我们接收到了信号,也可以选择差别的处置处罚该信号的方式。
上面的情景说明,通过他人的教导,我们可以大概识别某种信号的作用以及怎样处置处罚该信号,比方绿灯就应该通行,闹钟响了代表该起床了,这些都是对信号的默认处置处罚动作。但同时我们还可以自定义对信号的处置处罚方式,比方闹钟响了但我仍然继承睡觉。
一般在接收到信号后我们有三种处置处罚信号的方式:


[*]执行默认动作
[*]执行自定义动作
[*]忽略信号

1.2 进程中的信号  

上面的概念,在进程中也同样实用,进程必须可以大概识别并用某种方式处置处罚差别的进程信号。
当我们在Shell中启动了一个前台进程,并按下ctrl+c,此时进程会直接退出,比方:
https://i-blog.csdnimg.cn/direct/d4ba26b77521411eb4c3468e1559704c.png
这是因为当我们按下ctrl+c后,键盘产生硬件停止被操纵系统获取,并将我们的操纵表明成信号发送给前台进程,进程收到该信号后执行退出的动作。
就像你在打游戏的过程中,你妈忽然喊你用饭,此时你就得老老实实关掉游戏去用饭
前台进程在运行的过程中,用户随时都大概按下ctrl+c产生信号停止进程,因此信号相对于进程的运行流程是异步的

1.3 信号的概念

进程信号是Linux中用于进程间通讯和控制的一种机制,是进程之间变乱异步通知的一种方式,属于软停止。进程信号通常应用于进程间通讯、异常处置处罚、线程同步等场景。
进程信号有许多种,我们可以通过 kill -l 命令查看
https://i-blog.csdnimg.cn/direct/c61aee80f3b94a2291f29e77e04e51c9.png
而我们上面输入的ctrl+c,本质上是被表明为了2号信号即SIGINT信号,被进程接收 
此中,1-31号信号为平常信号,34号今后的信号是实时信号。这些差别的信号分别在哪些情况下产生,默认的处置处罚动作是什么,通过 man 7 signal 命令可以查看
https://i-blog.csdnimg.cn/direct/52fd0d2c13544ea5b6777373e83e971c.png
同时我们也可以通过signal函数来修改进程对于某个信号的处置处罚动作
#include <signal.h>
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler); 此中handler是用户自定义的信号处置处罚动作
比方:
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>

using namespace std;

void myhandler(int sig)
{
    cout << "process get a signal: " << sig << endl;
    exit(1);
}

int main()
{
    signal(SIGINT, myhandler);
    while(1)
    {
      cout << "process running..." << endl;
      sleep(1);
    }
    return 0;
} 我们将SIGINT信号的处置处罚方式修改为了我们的myhandler函数,然后运行代码:
https://i-blog.csdnimg.cn/direct/86744a1b06694db185c5684f8b4a91c4.png
可以看到,在我们输入ctrl+c后,进程收到的信号的确是2号信号 
   拓展:我们在键盘上输入的ctrl+c,是怎么被表明为对应信号的呢?
CPU上有许多针脚,通过这些针脚硬件就能向CPU发送硬件停止来告诉CPU,我这个硬件当中已经有数据输入了
同时,电脑当中有许多硬件,要进行区分,每个硬件都有自己对应的停止号。接收到对应硬件发送的硬件停止后,操纵系统就根据停止号在停止向量表中查找对应的函数指针指向的方法,通过该方法来读取硬件当中的数据。
在把硬件中的数据读取到内存前,操纵系统会先判断当前的数据是否是组合控制键,比方我们输入的ctrl+c,就会被转化为对应的信号发送给进程。在读取完毕后,硬件又会向CPU发送硬件停止来停止读取
硬件向CPU发送硬件停止,和我们向进程发送差别的信号是不是很类似?实际上我们要学习的信号就是用软件方式对进程模拟硬件停止。

二、信号的产生

2.1 通过组合键产生信号

除了ctrl+c会被表明为2号信号,ctrl+\会被表明为3号信号SIGQUIT,ctrl+z会被表明为19号信号SIGSTOP
https://i-blog.csdnimg.cn/direct/8fe4bf56436d4663b2bbf6ee91824b80.png

2.2 通过函数发送信号

kill命令

除了组合键,我们还可以通过 kill -信号 pid 命令向进程发送信号
不是全部的信号都可以大概被修改默认处置处罚动作的,否则假设一个进程将全部的信号都忽略,我们怎样停止该进程?
接下来测试一下有哪些信号可以被捕捉,哪些信号不能被捕捉
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>

using namespace std;

void myhandler(int sig)
{
    cout << "process get a signal: " << sig << endl;
}

int main()
{
    for (int i = 1; i <= 31; i++) //修改所有普通信号的默认处理动作
    {
      signal(i, myhandler);
    }
    while (1)
    {
      cout << "process running..., pid: " << getpid() << endl;
      sleep(1);
    }
    return 0;
} https://i-blog.csdnimg.cn/direct/d8fba55256924301bec087e52d72c2e0.png
https://i-blog.csdnimg.cn/direct/d24489b0c90d4e6f9a92f4930a1a8f5c.png
像这样,通过测试,我们最后可以发现只有9号信号SIGKILL和19号信号SIGSTOP无法被修改处置处罚动作

kill函数

kill命令本质上是调用了kill系统调用实现的
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
通过传入pid和信号码sig,就可以向指定进程发送指定信号
用kill函数,我们就可以搭配命令行参数实现我们自己的kill命令了
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <sys/types.h>
#include <signal.h>

using namespace std;

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " signo pid" << std::endl;
}

// ./mykill signo pid
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
      Usage(argv);
      exit(1);
    }

    int signo = atoi(argv);
    pid_t pid = atoi(argv);

    kill(pid, signo);

    return 0;
} https://i-blog.csdnimg.cn/direct/c5d33a48dea6446a815355a4f4dcb75b.png

raise函数 

另有一个函数可以给进程自身发送信号
#include <signal.h>

int raise(int sig);
因为是给自身发送信号,因此raise函数不需要传入pid,只需要指定信号码
测试:
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>

using namespace std;

void myhandler(int sig)
{
    cout << "process get a signal: " << sig << endl;
}

int main()
{
    for (int i = 1; i <= 31; i++)
    {
      signal(i, myhandler);
    }
    cout << "process running..., pid: " << getpid() << endl;
    for (int i = 1; i <= 31; i++) //发送1-31号信号
    {
      raise(i);
    }
    return 0;
} https://i-blog.csdnimg.cn/direct/180e353c136446158ab8a771f5dabe0b.png
可以再次证实,9号信号的处置处罚方式无法被修改,一旦信号收到9号信号就立即被杀死

abort函数 

另有一个函数可以让进程立即停止
#include <stdlib.h>

void abort(void);
测试:
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>

using namespace std;

void myhandler(int sig)
{
    cout << "process get a signal: " << sig << endl;
}

int main()
{
    for (int i = 1; i <= 31; i++)
    {
      signal(i, myhandler);
    }
    int cnt = 10;
    while (cnt--)
    {
      cout << "process runnning..., pid: " << getpid() << endl;
      if(cnt == 5)
            abort();
      sleep(1);
    }
    return 0;
} https://i-blog.csdnimg.cn/direct/184831d90331417ea28c1ec1d848e861.png
可以看到abort函数会向进程发送6号信号SIGABRT,但是我们不是可以对6号信号的处置处罚方式进行修改吗?为什么进程照旧会被停止?
这是由abort函数的行为决定的,该函数不止会向进程发送6号信号,还会让进程停止。也就是说导致进程停止的不是abort函数发送的6号信号,所以我们不能用kill函数或raise函数代替abort函数

Core Dump

在三个月之前讲怎样获取子进程的退出状态的时间,曾经出现过Core Dump这个名词
【Linux】进程等待-CSDN博客https://csdnimg.cn/release/blog_editor_html/release2.3.7/ckeditor/plugins/CsdnLink/icons/icon-default.png?t=O83Ahttps://blog.csdn.net/Eristic0618/article/details/140667331?spm=1001.2014.3001.5502
当时没有细讲,这里我们来聊聊什么是Core Dump
在signal(7)中,关于默认处置处罚方式:
Term   Default action is to terminate the process.
Ign    Default action is to ignore the signal.
Core   Default action is to terminate the process and dump core (see core(5)).
Stop   Default action is to stop the process.
Cont   Default action is to continue the process if it is currently stopped.
此中:


[*]Term:信号的默认操纵是停止进程
[*]Ign:信号的默认操纵是忽略信号
[*]Core:信号的默认操纵是停止进程并转储焦点
[*]Stop:信号的默认操纵是停止进程
[*]Cont:信号的默认操纵是让已经停止的进程继承运行
然后是差别信号的默认处置处罚方式:
https://i-blog.csdnimg.cn/direct/4daeb20af51e4b459b56084c336f7dab.png
可以看到,差别信号的默认处置处罚方式也大概差别
起首我们来表明一下何为Core Dump。当进程异常停止时,可以选择将进程的用户空间内存数据全部生存到磁盘上,形成一个core文件,这个过程叫做Core Dump
通过调试器查抄core文件可以帮助我们查清进程异常停止时的错误原因,这叫做过后调试(Post-mortem Debug)。一个进程可以大概生成多大的core文件取决于进程的core file size,我们可以通过 ulimit -a 命令查看
https://i-blog.csdnimg.cn/direct/04ea7da924f34a4abdf5ea781ab49274.png
可以看到此时进程的core file size为0,说明进程不被允许创建core文件。
这是因为生成core文件也并不是那么安全的,一方面是core文件中大概包罗某些隐私信息,另一方面是如果默认允许进程生成core文件,假设某个带有自重启功能的服务挂掉了,一刹时就会产生大量的core文件把磁盘挤爆
我们可以通过 ulimit -c 大小 来修改core file size的数值
https://i-blog.csdnimg.cn/direct/33c2d98e0f3e48e785578ce274802120.png
而我们之条件到的Core Dump标记位,就是用来标记进程是Term停止照旧Core停止。通过位运算我们也可以将该标记位解析出来:
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
      while(true)
      {
            cout << "child process running..., pid: " << getpid() << endl;
            sleep(1);
      }
      exit(0);
    }

    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
      cout << "waitpid success, exit code: " << ((status >> 8) & 0xFF)
      << ", exit signal: " << (status & 0x7F)
      << ", core dump: " << ((status >> 7) & 1) << endl;
    }

    return 0;
} https://i-blog.csdnimg.cn/direct/807b6ae3ee254d9ea237408b73171a87.png
可以看到,因为2号信号的默认处置处罚方式是Term,因此Core Dump标记位为0且没有core文件生成
https://i-blog.csdnimg.cn/direct/6cca5846aee042829f0e0a4f847acf46.png
而8号信号的默认处置处罚方式为Core,此时Core Dump标记位为1,且生成了core文件
关于core文件的利用,我们运行一段包罗除零错误的代码作为例子
https://i-blog.csdnimg.cn/direct/115c6957325f42d6ad951dd986502f03.png
用gdb调试我们发生异常的可执行文件
https://i-blog.csdnimg.cn/direct/cb08b16d4c49462d91b47fb830b1a5ea.png
在调试时打开core文件,就可以查看进程异常退出时收到的信号和发生异常的位置

2.3 特定软件条件下产生信号

在讲进程间通讯时,我们认识了管道,以及管道在读端被关闭时会SIGPIPE信号,这也属于软件条件下产生信号
除此之外,我们通过alarm函数可以向进程发送SIGALRM信号即第14号信号,这也属于在特定软件条件下产生信号
#include <unistd.h>

unsigned int alarm(unsigned int seconds);
alarm函数的作用是在seconds秒后向进程发送SIGALRM信号,类似一个闹钟,默认动作是停止当进步程
若进程在传入函数的时间定时收到信号,则alarm函数的返回值为0,否则返回闹钟剩余的时间
验证:
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int t;

void myhandler(int sig)
{
    cout << "process get a signal, signo: " << sig << endl;
    t = alarm(30);
    cout << t << endl;
}

int main()
{
    for (int i = 1; i <= 31; i++)
    {
      signal(i, myhandler);
    }
    alarm(30);
    while(true)
    {
      cout << "process running..., pid: " << getpid() << endl;
      sleep(1);
    }
    return 0;
} https://i-blog.csdnimg.cn/direct/4f4cbb9f7e2f4235b76784ea7491fc35.png
可以看到,当我们手动发送14号信号给进程时,闹钟还剩27秒
有没有想过,死循环运行一秒能循环多少次?
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    int cnt = 0;
    alarm(1);
    while(true)
    {
      cout << "cnt = " << cnt++ << endl;
    }
    return 0;
} https://i-blog.csdnimg.cn/direct/c389bd86237c415a90cac4344238b73b.gif
进程信号(上)完.

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux】进程信号(上)