进程 信号

生活中,信号相关的场景

红绿灯、闹钟、转向灯、狼烟……

  1. 对于生活中的信号,我们会潜移默化的记住对应场景下信号的含义,以及信号出现后要执行的动作。
  2. 即便特定的信号没有产生,但我们也知道应该如何处理这个信号。
  3. 我们在收到信号的时候,可能正在执行某个动作,并不会立刻去处理这个信号。
  4. 信号本身,在我们无法立即处理的时候,一定会临时的记住这个信号。

技术应用角度的信号

信号

  1. 本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,某些事件已经发生,可以在后续进行处理。
  2. 进程要处理信号,必须具备信号的识别能力。
  3. 信号的产生是随机的,进程可能正在执行其他动作。所以,信号的后续处理,可能不是立即处理的。临时记录下信号,方便后续处理。
  4. 信号是进程之间事件异步通知的一种方式,属于软中断。

用户输入命令,在Shell下启动一个前台进程,并用Ctrl-c终止。

用户按[Ctrl c],这个键盘输入会产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程。
前台进程因为收到吸纳后,进而引起进程退出。
Ctrl-c本质就是发送2号信号。
#include<stdio.h>

int main()
{
  while (1)
  {
    printf("I am a process, I am waiting signal\n");
    sleep(1);
  }
  return 0;
}

在这里插入图片描述

  1. Ctrl-c产生的信号只能发给前台进程。一个命令后面加上&,就可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接收到像Ctrl-c这样控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能安县Ctrl-c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止。所以信号相对于进程的控制流程来说是异步的。

系统定义的信号列表
用kill -l命令可以查看系统定义的信号列表
在这里插入图片描述

信号列表
- 每个信号都一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到 - 编号[1,31]的信号称之为普通信号,编号[34,64]以上的是实时信号。 - 不论是实时信号还是普通信号,这些信号各自在什么条件下产生,默认处理动作是什么,在signal(7)中都有说明。`man 7 signal`

在这里插入图片描述

man 7 signal

信号常见的处理方式:

  1. 忽略此信号(忽略也是信号处理的一种方式)。
  2. 执行该信号的默认处理动作(进程自带的)。
  3. 自定义处理方式(捕捉信号):提供一个信号处理函数,要求内核在处理该信号时,切换到用户态执行这个处理函数。

如何理解信号被进程保存:
进程必须具有保存信号的相关数据结构(位图 , unsigned int)。unsigned int有三十二个比特位,发送的是什么信号用第几位bit位表示,是否产生该信号用对应bit位为0为1来表示。
PCB内部保存了信号位图字段。
信号位图是在task_struct中的,task_struct是内核数据结构,所以发送信号是由OS去发的,因为只有OS才有资格通过修改目标进程的task_struct的位图结构,来完成“发送”信号的过程。


信号的产生

通过终端按键产生的信号

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

Core Dump核心转储
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
进程异常终止通常是因为有Bug,比如非法访问内存导致段错误。事后可以用调试器检查core文件,以查清错误原因,这叫做Post-moterm Debug(事后调试)
一个进程允许产生多大的core文件取决于进程的Resource Limit(这个消息保存在PCB中)。
默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感消息,不安全。
在开发调试阶段,可以用ulimit命令改变这个限制,允许产生core文件。
允许core文件最大文件为1024k: $ ulimit -c 1024
关闭或阻止core文件生成:$ulimit -c 0

在这里插入图片描述

允许core文件最大文件为1024k

在这里插入图片描述

运行这个死循环程序,前台运行这个程序,然后在终端输入Ctrl-c(貌似不行)或Ctrl-\(这个可以)

在这里插入图片描述

ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的Resource Limit值,这样子就可以产生CoreDump了

在这里插入图片描述

使用core文件

du(disk usage)命令可以查看文件大小
-h选项可以显示单位
在这里插入图片描述


进程控制中获取子进程status中的core dump
在这里插入图片描述
core dump标志表示,进程异常退出时,是否发生核心转储。

如下代码故意让子进程除以0。
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //子进程
        sleep(1);
        int a = 100;
        a /= 0;
        exit(0);
    }
    int status = 0;
    waitpid(id, &status, 0);
    cout << "父进程:" << getpid() << "子进程:" << id << \
    "exit sig:" << (status & 0x7F) << "is core:" << ((status >> 7) & 1)<< endl;
    return 0;
}

在这里插入图片描述


调用系统函数向进程发送信号

用户调用系统接口->执行OS对应的系统调用代码->OS提取参数,或者设置特定的数值->OS向目标进程写信号->修改对应进程的信号标记->进程后续会处理信号->执行对应的处理动作
kill函数&raise函数

//kill函数											//raise函数
功能:给指定的进程发送指定的信号							功能:给当前进程发送指定信号(自己给自己发)
#include<sys/types.h>								#include<signal.h>
#include<signal.h>
int kill(pid_t pid, int signo);						int raise(int signo);
返回值:
这俩函数成功返回0;错误返回-1.

abort函数

功能:使当前进程接受到信号而异常终止。
#include<stdlib.h>
void abort(void);
如exit函数一般,abort函数总是会成功的,所以没有返回值。

在这里插入图片描述

在后台执行执行死循环程序,然后用kill命令给他发送SIGSEGV信号。
  • ./test &中的&代表后台执行,即执行这个程序的同时,终端还能同时做其他事情。
  • 30282是test进程的id。之所以要再次回车才显示Segmentation fault,是因为在30282进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用户的输入交错在一起,所以等待用户输入命令之后才显示。
  • SIGSEGV: SIG 是信号名的通用前缀, SEGV 是segmentation violation,也就是存储器区段错误。而上面的死循环程序没有非法访问内存的错误,给他发送SIGSEGV也能产生段错误。

软件条件产生的信号

alarm函数

功能:设定一个闹钟,告诉内核在seconds秒后,给当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
返回值是0或者之前设定的闹钟时间还余下的秒数。

在这里插入图片描述


硬件异常产生的信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
如:当前进程执行了除以0的运行的指令,CPU运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。
再如:当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。


信号捕捉

signal函数

#include<signal.h>
typedef void (*sighandler_t)(int);//返回值为void*,参数为int的函数指针
sighandler_t signal(int signum, sighandler_t handler);
参数:
	signum:信号编号
信号SIGINT
产生方式:键盘Ctrl-c
产生结果:只对当前前台进程,和他所在的进程组的每个进程发送SIGINT信号。之后这些进程会执行信号处理再终止。

signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作。
如下代码中,特定信号的处理动作一般只有一个。将进程对2号信号的处理动作修改为打印信号编号,致使Ctrl-c无法终止程序。

#include<iostream>
#include<signal.h>
#include<unistd.h>

using namespace std;

void catchSig(int signum)
{
    cout << "进程捕捉到了一个信号,正在处理:" << signum << "Pid:" << getpid() << endl;
}

int main()
{
    //signal的第一个参数写信号名称、信号编号都可以
    //signal(2, catchSig);//#define	SIGINT		2	/* Interrupt (ANSI).  */
    signal(SIGINT, catchSig);
	//如果后续没有任何的SIGINT信号产生,catchSig永远也不会被调用。
    while(true)
    {
        cout << "我是一个进程,我正在运行……,Pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

如上代码运行截图
  • 运行程序时,进程对2号信号的处理动作已经被修改为打印信号编号,致使Ctrl-c无法终止程序。
  • 这时可以用Ctrl-\,Ctrl-\是向目标进程发送3号信号。

阻塞信号

信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达
  • 信号从产生到递达之间的状态称为信号未决
  • 进程可以选择阻塞某个信号
  • 被阻塞的信号产生时将保持未决状态,直到进程接触对此信号的阻塞,才执行递达的动作
  • 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

信号在内核中的表示

在这里插入图片描述

信号在内核中的表示示意图
  • 每个信号都有两个标识位分别表示阻塞(block)和未决(pending)还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然他的处理动作是忽略,但是在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再接触阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

信号集操作函数

sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志 可以用相同的数据类型sigset_t来表示,sigset_t称为信号集。
这个类型可以表示每个信号的“有效”“无效”状态,在阻塞信号集中“有效”“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”“无效”的含义是该信号是否处于未决状态
阻塞信号集也叫做当前进程的信号屏蔽字。

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include<signal.h>
int sigemptyset(sigset_t *set);//清空,把所有bit位全置0
int sigfillset(sigset_t *set);//把所有bit位全置1
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
返回值
	头四个函数,成功返回0,出错返回-1。
	sigismember是一个bool函数,用于判断一个信号集的有效信号中是否包含某种信号,
	若包含则返回1,不包含则返回0,出错返回-1
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号对应的bit位置零,表示该信号集不包含任何有效信号。
  • 函数sigfilllset初始化set所指向的信号集,使其中所有信号的对应bit位置一,表示该信号集的有效信号包括系统支持的所有信号。
  • ps.在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以调用sigaddset和sigdelset在该信号集合中添加或删除某种有效信号。

sigprocmask

功能:读取或更改进程的阻塞信号集(信号屏蔽字)
#include<signal.h>
int sigprocmask(int how, coonst sigset_t *set, sigset_t *oldset);
参数:
	如果oldset是非空指针,则读取进程的当前阻塞信号集
	如果set是非空指针,则更改进程的阻塞信号集,参数how指示如何更改信号屏蔽字
	如果oldset和set都是非空指针,则先将原来的阻塞屏蔽字备份到oset里,然后根据set和how参数更改阻塞信号集
	how参数的可选值:
	1.SIG_BLOCK:set包含了我们希望添加到当前阻塞信号集的信号,相当于mask = mask|set
	2.SIG_UNBLOCK:set包含了希望从当前阻塞信号集中解除阻塞的信号,相当于mask = mask&~set
	3.SIG_SETMASK:设置当前阻塞信号集为set所指向的值,相当于mask=set

sigpending

功能:读取当前进程的未决信号集,通过set参数传出。
#include<signal.h>
int sigpending(sigset_t *set);
返回值:
	调用成功返回0;出错返回-1.

如果对所有信号都进行自定义捕捉,那么该进程是否不会被异常或者用户杀掉?

 并不是
 9号信号属于管理员信号,无法设定自定义捕捉动作

在这里插入图片描述
在这里插入图片描述


对2号信号block且突然发送一个2号信号,并不断获取并打印当前进程的pending信号集

#include<iostream>
#include<assert.h>
#include<unistd.h>
#include<signal.h>

static void handler(int signum)
{
    std::cout << "捕捉2号信号 " << signum << std::endl;
}

static void showPending(sigset_t &pending)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(&pending, sig))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

int main()
{
    //0. 为了更方便看到2号信号被恢复,这里进行对2号信号的捕捉,不要直接退出
    signal(2, handler);
    //1. 定义信号集对象
    sigset_t bset, obset;
    sigset_t pending;
    //2. 初始化
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);
    //3. 添加要进行屏蔽的信号
    sigaddset(&bset, 2/*SIGINT*/);
    //4. 设置set到内核中对应的进程内部(默认情况下,进程不会对任何信号进行block)
    int n =sigprocmask(SIG_BLOCK, &bset, &obset);
    assert(n == 0);
    (void)n;
    std::cout << "block 2号信号成功" << getpid() << std::endl;
    //5. 重复打印当前进行的pending信号集
    int count = 0;
    while(true)
    {
        //5.1 获取当前进行的pending信号集
        sigpending(&pending);
        //5.2 显示pending信号集中没有被递达的信号
        showPending(pending);
        sleep(1);
        count++;
        if(count == 10)
        {
            //默认情况下,恢复2号0信号的block的时候,确实会进行递达
            //但是2好信号的默认处理动作是终止进程
            //所以为了更方便看到2号信号被恢复,这里进行对2号信号的捕捉
            int m = sigprocmask(SIG_SETMASK, &obset, nullptr);
            (void*)m;
            std::cout << "解除对于2号信号的block" << std::endl;
        }
    }
    return 0;
}

在这里插入图片描述

可以通过pidof+进程名称的方式获取pid
在这里插入图片描述


捕捉信号

内核如何实现信号的捕捉

在这里插入图片描述

信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

sigaction

sigaction函数

功能:可以读取和修改指定信号相关联的处理动作。
#include<signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
参数:
	signo:指定信号的编号
	若act指针非空,则根据act修改该信号的处理动作。
	若oldact指针非空,则通过oldact传出该信号原来的处理动作。
	act和oldact都指向sigaction结构体。;
返回值:
	成功返回0;出错返回1-

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表示执行系统默认动作;赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前参数的编号。这样做就可以用同一个函数处理多种信号。显然这是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时,自动恢复原来的信号屏蔽字。这样就保证了在处理某个信号时,如果这种信号再次产生,它会被阻塞到当前信号处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
在这里插入图片描述

在进程处理2号信号时,不断发送2号信号

可重入函数

信号捕捉,并没有创建新的进程或者线程。
栗子:
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步。
刚做完第一步的时候因为硬件中断使进程切换到了内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数。sighandler函数也调用insert函数向同一个链表head中插入节点node2。
插入操作的两步都做完之后,从sighandler返回内核态,再次回到用户态时,就从main函数调用的insert函数中继续向下执行,先前做的第一步之后被打断,现在继续做完第二步。
结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入了链表中。

在这里插入图片描述

栗子
像上面的栗子,insert函数被不同的控制流程调用,有可能在第一次调用还没有返回时,就再次进入该函数,这称之为重入。 insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数。 反之,如果一个函数之访问自己的局部变量或参数,则称为可重入函数。

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

  • 调用了malloc或free。因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数

volatle

标准情况下,键入Ctrl-c,2号信号被捕捉,执行自定义动作,修改flag=1,while条件不满足,进程退出。
在这里插入图片描述

优化情况下,键入Ctrl-c,2号信号被捕捉,执行自定义动作,修改flag=1,但是while条件依旧满足,进程依旧继续运行。
很明显,while循环检查的flag,并不是内存中最新的flag,这就存在数据二异性问题。
在这里插入图片描述

makefile

在这里插入图片描述

mysignal

volatile作用:保持内存的可见性,告知编译器,被改关键字修饰的变量不允许被优化,对该变量的任何操作,都不许在真是的内存中进行操作。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值