【Linux学习】进程信号1

简介: 【Linux学习】进程信号

前言

本文是博主对学习完Linux系统中的进程信号部分的知识点总结,在阅读完该文章之后,我们会对进程信号有更深层次的理解。学习完本文后我们可以掌握以下内容:Linux信号的基本概念、掌握信号产生的一般方式、理解信号递达和阻塞的概念和原理、掌握信号捕捉的一般方式、重新了解可重入函数的概念、了解竞态条件的情景和处理方式、了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制。

一、信号初识

1. 信号的概念

对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号为 Linux 提供了一种处理异步事件的方法。在Linux当中,每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。信号定义在signal.h头文件中,信号名都定义为正整数。具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。

2. Linux中的普通信号

如上文所示,一共有62个信号,实际上缺少了32和33号信号。编号34以上的是实时信号,本文只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal。


以下是对Linux系统中31个普通信号的说明:

1700830440258.png

1700830462632.png

3. 信号的处理

信号的处理方式有三种,分别是:忽略、捕捉和默认动作。

  1. 忽略信号

大多数信号都可以采用这个方式进行处理,但有两种信号不能被忽略(即SIGKILLSIGSTOP)。因为它们向内核和超级用户提供了进程终止和停止的可靠方法。如果忽略了,那么这个要处理的进程就变成了没人能管理的进程,显然是内核设计者不希望看到的场景。

  1. 捕捉信号

捕捉信号,就是需要告诉内核用户希望如何处理某一种信号,其实就是写一个信号处理的函数,然后将这个函数告诉内核。当该信号产生时,内核来调用用户自定义的函数,以此来达到处理信号的目的。

  1. 系统默认

对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。



二、信号产生

1. 终端按键产生信号

对于下面的死循环程序,我们可以通过键盘的ctrl+c进行终止:

#include <stdio.h>
#include <unistd.h>
int main()
{
  while (1){
    printf("hello signal!\n");
    sleep(1);
  }
  return 0;
}

当我们运行程序,在通过键盘ctrl+c就终止了该进程。

但实际上除了按ctrl+c之外,按ctrl+\也可以终止该进程。


二者都是通过按键终止进程,又有什么区别呢?

通过man 7 signal 我们可以发现按ctrl+c实际上是向进程发送2号信号SIGINT,而按ctrl+\实际上是向进程发送3号信号SIGQUIT。查看这两个信号的默认处理动作,可以看到这两个信号的Action是不一样的,2号信号是Term,而3号信号是Core。


Term和Core都代表着终止进程,但是Core在终止进程的时候会进行一个动作,那就是核心转储。

核心转储的概念:

核心转储(core dump),在汉语中有时戏称为吐核,是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件

在云服务器中,核心转储是默认被关掉的,我们可以通过使用ulimit -a命令查看当前资源限制的设定。

我们可以发现第一行是core文件的大小,它的默认大小被设定为0,因此就相当于关闭了核心转储的功能。我们可以通过ulimit -c size命令来设置core文件的大小。


此时我们再次运行程序,并用ctrl+\终止。就会发现终止进程后会显示core dumped

并且会在当前路径下生成一个core文件,该文件以一串数字为后缀,而这一串数字实际上就是发生这一次核心转储的进程的PID。

核心转储的功能:

当我们的代码出错了,我们最关心的是我们的代码是什么原因出错的。如果我们的代码运行结束了,那么我们可以通过退出码来判断代码出错的原因,而如果一个代码是在运行过程中出错的,那么我们也要有办法判断代码是什么原因出错的。


当我们的程序在运行过程中崩溃了,我们一般会通过调试来进行逐步查找程序崩溃的原因。而在某些特殊情况下,我们会用到核心转储,核心转储指的是操作系统在进程收到某些信号而终止运行时,将该进程地址空间的内容以及有关进程状态的其他信息转而存储到一个磁盘文件当中,这个磁盘文件也叫做核心转储文件,一般命名为core.pid。

而核心转储的目的就是为了在调试时,方便问题的定位。我们用下面这段代码进行演示:

#include <cstdio>
#include <unistd.h>
int main()
{
    printf("Hello Linux...\n");
    sleep(3);
    int a = 1/0;
    return 0;
}

该代码当中出现了除0错误,该程序运行3秒后便会崩溃。

使用gdb对当前可执行程序进行调试,然后直接使用core-file core文件命令加载core文件,即可判断出该程序在终止时收到了8号信号,并且定位到了产生该错误的具体代码。


说明:

事后用调试器检查core文件以查清错误原因,这种调试方式叫做事后调试。

core dump 标志:

此时我们回忆前面所学习的【进程控制】中的获取子进程的退出码部分。

进程等待函数waitpid函数的第二个参数:

pid_t waitpid(pid_t pid, int *status, int options);

waitpid函数的第二个参数status是一个输出型参数,用于获取子进程的退出状态。status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只关注status低16位比特位):

若进程是正常终止的,那么status的次低8位就表示进程的退出状态,即退出码。若进程是被信号所杀,那么status的低7位表示终止信号,而第8位比特位是core dump标志,即进程终止时是否进行了核心转储

打开Linux的核心转储功能,通过以下的程序查看core dump 的标志位信息。代码中父进程使用fork函数创建了一个子进程,子进程所执行的代码当中存在野指针问题,当子进程执行到*p = 1000时,必然会被操作系统所终止并在终止时进行核心转储。此时父进程使用waitpid函数便可获取到子进程退出时的状态,根据status的第7个比特位便可得知子进程在被终止时是否进行了核心转储。

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //子进程
        int *p = nullptr;
        *p = 1000;
        exit(1);
    }
    //父进程
    int status = 0;
    waitpid(id, &status, 0);
    printf("exit code %d, sigo: %d, core dump flag: %d\n", (status >> 8) & 0xFF, (status >> 7) & 0x7F, (status >> 7) & 0x1);
    return 0;
}

可以看到,所获取的status的第7个比特位为1,即可说明子进程在被终止时进行了核心转储。

因此,core dump标志实际上就是用于表示程序崩溃的时候是否进行了核心转储。


2. 系统调用发送信号

2.1 kill函数

当我们继续运行死循环程序,可以使用kill -信号 进程ID的方式向进程发送信号。

实际上kill命令是通过调用kill函数实现的,kill函数可以给指定的进程发送指定的信号,kill函数的函数原型如下:

#include <signal.h>
int kill(pid_t pid, int sig);

kill函数用于向进程ID为pid的进程发送sig信号,如果发送成功返回0,失败则返回-1。

此时我们可以之间模拟实现一个kill命令:

#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
using namespace std;
static void Usage(const string& proc)
{
    cerr << "Usage:\n\t" << proc << " signo pid" << endl;
}
int main(int argc, char *argv[])
{
    if(argc !=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    if(kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == 1)
    {
        cerr << " kill: " << strerror(errno) << endl;
    }
    return 0;
}


【注意】

此时要想执行mykill程序时不带路径,就需要我们提前导入环境变量:

2.2 raise函数

raise函数原型:

#include <signal.h>
int raise(int sig);

raise函数用于给当前进程发送信号,发送成功返回0,发送失败则返回非0。例如,下列代码当中用raise函数每隔一秒向自己发送一个2号信号。

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int cnt = 0;
void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << " cnt: " << cnt << endl;
}
int main()
{
    signal(2, handler);
    while(true)
    {
        sleep(1);
        cnt++;
        raise(2);
    }
    return 0;
}

运行结果就是该进程每隔一秒收到一个2号信号。

2.3 abort函数

abort函数的函数原型如下:

#include <stdlib.h>
void abort(void);

abort函数使当前进程接收到信号而异常终止。就像exit函数一样,abort函数总是会成功的,所以没有返回值。

例如使用下面的程序,运行5秒后调用abort终止程序。

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
using namespace std;
int cnt = 0;
void handler(int signo)
{
    cout << "我是一个进程,刚刚获取了一个信号: " << signo << " cnt: " << cnt << endl;
}
int main()
{
    signal(2, handler);
    while (true)
    {
        sleep(1);
        cnt++;
        if (cnt == 5)
            abort();
    }
    return 0;
}


【注意】

abort函数的作用是异常终止进程,exit函数的作用是正常终止进程,而abort本质是通过向当前进程发送SIGABRT信号而终止进程的,因此使用exit函数终止进程可能会失败,但使用abort函数终止进程总是成功的。

3. 由软件条件产生信号

3.1 SIGPIPE信号

SIGPIPE信号是一种由软件条件产生的信号,当进程在使用管道通信时,如果读端进程将读端关闭后,另一个进程还在不断向管道写入数据,那么此次写端进程就会收到SIGPIPE信号而终止程序。


例如使用下面的一段代码来模拟以上这种情况,创建管道进行父子进程间通信,其中父进程是读端,子进程是写端,但父进程关闭了自己的读端,那么此时子进程在向管道写入数据时就会收到SIGPIPE信号,进而被终止。

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main()
{
    int fd[2] = {0};
    if (pipe(fd) < 0)
    {
        cerr << "pipe error" << endl;
        exit(1);
    }
    pid_t id = fork();
    if (id == 0)
    {
        //子进程 --- 写端
        close(fd[0]);
        const char *msg = "父进程你好,我是子进程...";
        int count = 5;
        while (count)
        {
            write(fd[1], msg, strlen(msg));
            sleep(1);
            count--;
        }
        close(fd[1]);
        exit(0);
    }
    //父进程 --- 读端
    close(fd[1]);
    close(fd[0]); //父进程关闭自己的写端,导致子进程写入会被操作系统终止
    int status = 0;
    waitpid(id, &status, 0);
    cout << "子进程收到信号:" << (status & 0x7F) << endl;
    return 0;
}

结果子进程收到13号信号,即SIGPIPE信号。

3.2 alarm函数

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该函数的原型如下:

#include <unistd.h>
unsigned alarm(unsigned seconds);

alarm函数的作用就是让操作系统在seconds秒之后给当前进程发送SIGALRM信号,, 该信号的默认处理动作是终止当前进程。


返回值:

  • 若调用alarm函数之前,进程已经设置了闹钟,则返回上一个闹钟剩余的时间,并且本次闹钟会覆盖上次闹钟的设置。
  • 若调用alarm函数之前,没有设置闹钟,则返回0。

以下是一个小例子,统计一个我们的进程1S cnt++ 多少次。

#include <iostream>
#include <unistd.h>
using namespace std;
int cnt = 0;
int main()
{
    alarm(1); //设置1s
    while (true)
    {
        cnt++;
        printf("cnt = %d\n", cnt);
    }
    return 0;
}

执行结果如下:

【Linux学习】进程信号2:https://developer.aliyun.com/article/1383970

目录
相关文章
|
17天前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
2月前
|
消息中间件 存储 网络协议
从零开始掌握进程间通信:管道、信号、消息队列、共享内存大揭秘
本文详细介绍了进程间通信(IPC)的六种主要方式:管道、信号、消息队列、共享内存、信号量和套接字。每种方式都有其特点和适用场景,如管道适用于父子进程间的通信,消息队列能传递结构化数据,共享内存提供高速数据交换,信号量用于同步控制,套接字支持跨网络通信。通过对比和分析,帮助读者理解并选择合适的IPC机制,以提高系统性能和可靠性。
255 14
|
7天前
|
存储 IDE Linux
零基础保姆级教程!手把手教你免费玩转Linux CentOS安装+学习环境搭建(附避坑指南)
本文详细介绍了在VMware虚拟机中安装CentOS 6.8的全过程。首先,需确保已安装VMware并开启V-CPU虚拟化功能,可通过BIOS设置或使用LeoMoon CPU-V工具检测。接着,下载CentOS镜像文件,并在VMware中新建虚拟机,配置CPU、内存、硬盘等参数。最后,加载ISO镜像启动虚拟机,按照提示完成CentOS的安装,包括语言、键盘、存储方式、地区、密码设置及硬盘分区等步骤。安装完成后,以root用户登录即可进入系统桌面,开始学习Linux命令和操作。
52 12
零基础保姆级教程!手把手教你免费玩转Linux CentOS安装+学习环境搭建(附避坑指南)
|
1月前
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
78 34
|
13天前
|
Linux
Linux:守护进程(进程组、会话和守护进程)
守护进程在 Linux 系统中扮演着重要角色,通过后台执行关键任务和服务,确保系统的稳定运行。理解进程组和会话的概念,是正确创建和管理守护进程的基础。使用现代的 `systemd` 或传统的 `init.d` 方法,可以有效地管理守护进程,提升系统的可靠性和可维护性。希望本文能帮助读者深入理解并掌握 Linux 守护进程的相关知识。
28 7
|
12天前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
32 5
|
12天前
|
Linux 应用服务中间件 nginx
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
20 3
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
49 17
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
60 26
|
1月前
|
消息中间件 Linux C++
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
68 16