Linux -- 进程信号(1)

简介: 1. 信号量1.1 进程互斥概念两个或两个以上的进程,不能同时进入关于同一组共享变量的临界区域,否则可能发生与时间有关的错误,这种现象被称作进程互斥· 也就是说,一个进程正在访问临界资源,另一个要访问该资源的进程必须等待(任何时刻,都只允许一个进程在进行共享资源的访问)任何时刻都只允许一个进程在进行访问的共享资源叫做临界资源临界资源都是通过代码访问的,凡是访问临界资源的代码就叫做临界区一个程序,它要么完整的被执行,要么完全不执行的特性就叫原子性

1. 信号量

1.1 进程互斥概念

两个或两个以上的进程,不能同时进入关于同一组共享变量的临界区域,否则可能发生与时间有关的错误,这种现象被称作进程互斥· 也就是说,一个进程正在访问临界资源,另一个要访问该资源的进程必须等待(任何时刻,都只允许一个进程在进行共享资源的访问)


任何时刻都只允许一个进程在进行访问的共享资源叫做临界资源


临界资源都是通过代码访问的,凡是访问临界资源的代码就叫做临界区


一个程序,它要么完整的被执行,要么完全不执行的特性就叫原子性


1.2 认识信号量

信号量又是什么呢?

  • 本质就是一个计数器,用来给资源计数

  • 任何一个进程想访问临界资源中的一个子资源的时候都不能直接访问,必须先申请信号量资源;如果有信号量资源,进程在对应的临界区访问临界资源就会申请对应的信号量,类似:count–;使用完后就会释放信号量,类似:count++


信号量是不是共享资源呢?


  • 是的,因为进行需要申请信号量,那么进程就必须先看到对应的信号量,那么信号量就是共享资源


什么来保证信号量的资源呢?


  • 信号量必须保证++和–操作是原子性的

接口认识(不具体说,这里信号量只是做个了解)


  • int semget(key_t key, int nsems, int semflg); —> 获取一个信号量标识符
  • int semctl(int semid, int semnum, int cmd, …); —> 信号量控制操作


2. 信号入门

2.1 信号概念

  1. 生活中的信号到进程信号:


红绿灯、闹钟、下课铃、倒计时、电话等等,这些都是我们生活中的信号。当发生信号的时候,我们就会有对应的行为,比如当红灯亮的时候,我们会停止下来等待,当然信号没有发生的时候,我们也会有知道怎么来处理它。那么我们能处理信号的原因是因为我们可以识别到这些信号。那么进程就相当于是我,信号就相当于一个数字,进程在没有收到信号的时候其实它就已经知道怎么来处理这个信号了!为了能够知道信号处理,那么就需要识别这些信号,那么这些信号怎么来识别呢,操作系统中已经对每个信号进行了设置,如下:(1-31:普通信号,34-64:实时信号,不关心实时信号)


微信图片_20230523212625.png

还有一个问题就是,生活中电话这个信号是不是可能随时就来了,但是我们如果正在和老板谈重要会议呢,那么就不会立马处理,但是这个电话挂断后,我们会记住有个电话之前打来过,这里这个信号就被保存到我们的大脑中,此时的过程就是信号产生 -> 信号保存 -> 信号处理,进程也是如此,当一个信号来了的时候,可能这个进程在执行一个优先级很高的任务,那么此时这个信号就会被进程记录下来,等任务执行完后,再对此信号做出处理


  1. 进程如何记录对应产生的信号?怎么保存这些信号呢?


用结构体对信号进行描述,然后用数据结构对信号管理起来;进程task_struct结构体中存在位图结构来保存信号


2.2 见一见

#include <iostream>
#include <unistd.h>
int main()
{
    while(true)
    {
        std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl;
        sleep(2);
    }
    return 0;
}

演示:

  1. 通过发送信号杀掉进程:


微信图片_20230523212845.png


  1. 前台进程直接ctrl + c终止进程:


微信图片_20230523212927.png

后台进程只能通过发送信号杀掉进程



2.3 signal()系统调用


选项 内容
作用 Signal()将信号信号的处置设置为handler
头文件 #include <signal.h>
函数声明 sighandler_t signal(int signum, sighandler_t handler);


#include <iostream>
int add(int x, int y)
{
    return x + y;
}
void calc(int (*add_fun)(int, int))
{
    int a = 10;
    int b = 20;
    int result = add_fun(a, b);
    std::cout << "result: " << result << std::endl;
}
int main()
{
    calc(add);
    return 0;
}

  1. signal()系统调用获取对信号最处理



#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
    std::cout << "get a signal: " << signum << std::endl;
}
int main()
{
    signal(2, handler);  //handler函数就是回调函数
    while(true)
    {
        std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl;
        sleep(2);
    }
    return 0;
}

演示:


微信图片_20230523213604.png


观察现象:当Ctrl + c时,会执行会执行对应的handler方法


  1. (9)号信号的特殊


#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signum)
{
    std::cout << "get a signal: " << signum << std::endl;
}
int main()
{
    for(int i = 1; i <= 31; ++i)
    {
        signal(i, handler); 
    }
    while(true)
    {
        std::cout << "I am a process, excuting .... , PID:" << getpid() << std::endl;
        sleep(3);
    }
    return 0;
}

演示:

微信图片_20230523213842.png

(9)号信号是管理员信号,是不可被定义的,所以可以直接杀掉进程

2.4 宏定义信号

微信图片_20230523214042.png

3. 信号产生方式

3.1 键盘产生信号

当我们命令上按Ctrl+c时就表示终止进程,键盘如何发送这里请查阅资料


3.2 系统调用产生信号

  • kill()系统调用

选项 内容
作用 发送信号给进程
头文件 #include <sys/types.h> #include <signal.h>
函数声明 int kill(pid_t pid, int sig);
返回值 成功返回0,失败返回-1


systemcall.cc文件


#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <assert.h>
#include <errno.h>
#include <cstring>
void manual(std::string process)
{
    std::cout << "manual: \n\t";
    std::cout << process << " <number> <process>\n" << std:: endl;
}
int main(int argc, char* argv[])
{
    if(argc != 3) //必须带两个选项
    {
        manual(argv[0]);
    }
    int signum = atoi(argv[1]); //atoi():字符串转整数
    pid_t id = atoi(argv[2]);
    int ret = kill(id, signum);  //kill():发送信号给进程
    assert(ret == 0);
    if(ret != -1){
        std::cerr << errno << " : " << strerror(errno) << std::endl;
    }
    return 0;
}

test.cc文件

#include <iostream>
#include <unistd.h>
int main()
{
    while(true)
    {
        std::cout << "I am a process, is excuting ..... , PID: " << getpid() << std::endl;
        sleep(2);
    }
    return 0;
}

演示:

微信图片_20230523214348.png


  • raise()C语言接口


选项 内容
作用 发送信号给调用者
头文件 #include <signal.h>
函数声明 int raise(int sig);
返回值 成功返回0,失败返回非0


  • abort()C语言接口


选项 内容
作用 导致进程异常终止
头文件 #include <stdlib.h>
函数声明 void abort(void);
返回值 无返回值


3.3 软件条件产生信号

  • abort()C语言接口

选项 内容
作用

设置一个告警信号

头文件 #include <unistd.h>
函数声明 unsigned alarm(unsigned seconds);
返回值

如果在剩余时间内有先前的alarm()请求,则alarm()应返回一个非零值,表示距离前一个的秒数请求将生成一个SIGALRM信号。否则,报警()应返回0


  • 验证IO效率

#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
int main()
{
    alarm(1); //1秒后发信号
    while(true)
    {
        std::cout << "count: " << count++ << std::endl;
    }
    return 0;
}
//大概count等于20000左右
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signum)
{
    std::cout << "get a signal: " << signum << " count:" << count << std::endl;
}
int main()
{
    signal(SIGALRM, handler);
    alarm(1); //1秒后发信号
    while(true)
    {
        ++count;
    }
    return 0;
}
//大概count等于500000000左右
  • alarm()返回值理解



#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signum)
{
    std::cout << "get a signal: " << signum << " count:" << count << std::endl;
    int ret = alarm(10);
    std::cout << "ret: " << ret << std::endl;
}
int main()
{
    std::cout << "PID: " << getpid() << std::endl;
    signal(SIGALRM, handler);
    alarm(10); //1秒后发信号
    while(true)
    {
        ++count;
    }
    return 0;
}

返回的就是上次设置闹钟时间到给闹钟发信号的时间的时间差。也就是假如我设置了一个20分钟的闹钟,此时我的小猫把房间里面的东西给我吵醒了,我起床一看我只睡了10分钟,那么剩余的10分钟就是这里的时间差。这个闹钟是个信号,那么对应的操作系统中就会有对应的闹钟结构体对其描述和管理。


3.4 硬件异常产生信号

test.cc

#include <iostream>
int main()
{
    int a = 10;
    int b = 0;      
    int c = a / b;
    std::cout << "division operation" << std::endl;
    return 0;
}
//运行结果如下:

微信图片_20230523220102.png

这里其实是硬件异常导致产生的信号,运行会通过内存把数据加载到CPU来运算,CPU中有一种寄存器是用来记录是否数据溢出的,这里除0操作会溢出,那么这个寄存器就会被置为对应的数值来表示这个状态,最后CPU检测到异常然后给到操作系统,操作就会给该signal进程发送信号(8)号信号SIGFPE。


  • 这里可不可以不让这个进程因为SIGFPE信号退出呢?可以的


#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signalnum)
{
    printf("PID:%d received signal:%d crash\n", getpid(),signalnum);
    //exit(1);
}
int main()
{
    signal(SIGFPE, handler); //捕捉信号
    int a = 10;
    int b = 0;      
    int c = a / b;
    std::cout << "division operation" << std::endl;
    return 0;
}
//这段代码的现象:循环打印printf内容,为什么呢?
//原因是CPU中这个溢出状态检测寄存器检测到溢出后操作系统接受到错误给该进程发送信号,但是操作系统并没有修复CPU溢出检测寄存器,所以操作系统不断就给进程发送信号,这里进程不断捕捉信号(自定义行为),就死循环了。怎么改正呢?很简单绶捕捉信号后终止掉进程
  • 野指针问题同样是硬件异常产生信号
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signalnum)
{
    printf("PID:%d received signal:%d crash\n", getpid(),signalnum);
    //exit(1);
}
int main()
{
    signal(SIGSEGV, handler);
    int* p1 = nullptr;
    //p1 = (int*)100;
    *p1 = 100; //野指针访问写入
    std::cout << "wild pointer!" << std::endl;
    return 0;
}
//输出结果:segmentation fault (11号信号:SIGSEGV)
//为什么会出现这种错误呢?程序运行会变成进程,进程由操作系统管理,虚拟内存会通过页表建立key/value关系映射到物理内存,硬件上MMU主要完成虚拟地址到物理地址的映射,所以虚表是实现MMU的手段,这里的页表中不仅仅有kv关系,同时也有读写权限,p1=(void*)0,也就是0号地址,这里有两种可能导致异常,可能p1虚拟地址并没有物理地址,也可能p1虚拟地址有映射的物理地址但是并没有写入或者读取权限。,这两种可能都会导致报错。
//这里为什么会出现死循环呢?原因还是因为操作系统并没有修复MMU硬件错误,使得进程不断捕获信号,导致死循环打印。解决方法:捕获后终止进程

3.5 Core dump

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。虚拟机上是可以直接看到的,但是云服务器上此功能是默认关闭的,使用ulimit -a命令查看:

微信图片_20230523224216.png

  • 如何打开呢?

使用ulimit -c size(大小)使得这个磁盘核心转储有大小就是把它打开了。

如何验证core file size的存在?

#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signalnum){
    printf("PID:%d received signal:%d termination\n",getpid(), signalnum);
    exit(1);
}
int main()
{
    while(true){
        std::cout << "PID:" << getpid() << " doing ....." << std::endl;
        sleep(1);
    }
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aglLGiPi-1684114794264)(https://typora130.oss-cn-nanjing.aliyuncs.com/QQ截图20230508191812.png)]

又上述观察得到一个结果:Term就是普通终止,没有任何操作;Core终止会先进行核心转储再终止进程,如何验证?


微信图片_20230523224506.png

  • 核心转储有什么用?方便异常后进行调试

#include <iostream>
int main()
{
    std::cout << "wild pointer!" << std::endl;
    std::cout << "wild pointer!" << std::endl;
    std::cout << "wild pointer!" << std::endl;
    int* p1 = nullptr;
    *p1 = 100; //野指针访问写入
    std::cout << "wild pointer!" << std::endl;
    return 0;
}

微信图片_20230523224602.png

  • 这里有了这个core dump那么调试就很轻松,那为什么云服务器会关闭core dump呢,它有什么坏处呢?

它会占用磁盘空间,一是本身程序就很大,出错后形成core dump文件很大;二是每次程序瓜重启都会形成core dump文件,如果很多次重启程序就会导致形成很多个core dump文件。如何关闭呢?ulimit -c 0命令。


另外,进程退出时可以获取信号,也可以获取退出码,也可以知道core dump是否发生(依靠core dump标记比特位):

#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        std::cout << "wild pointer!" << std::endl;
        std::cout << "wild pointer!" << std::endl;
        std::cout << "wild pointer!" << std::endl;
        int* p = nullptr;
        *p = 100;
        std::cout << "wild pointer!" << std::endl;
        std::cout << "wild pointer!" << std::endl;
        std::cout << "wild pointer!" << std::endl;
        exit(0);
    }
    int status = 0;
    waitpid(id, &status, 0); //阻塞等待
    printf("exit code: %d | exit signal: %d | core dump flag: %d\n",  \
    ((status >> 8) & 0xFF), status & 0x7F, (status >> 7) & 0x1);
    return 0;
}
//运行结果如下:

微信图片_20230523224744.png

微信图片_20230523224806.png








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