Linux进程信号

目录

互斥的四个概念

​编辑

 查看当前的信号量

认识信号量接口

semget

semctl

理解IPC资源的管理

信号入门

生活角度的信号

技术应用角度的信号

注意 

信号概念

 用kill -l命令可以察看系统定义的信号列表

​编辑

信号处理常见方式概览 

信号产生 

认识常用接口

signal

通过系统接口向进程发送信号

kill

raise

abort

由软件条件产生的信号

alarm

由硬件条件产生的信号


互斥的四个概念

我们把大家都能看到的资源:公共资源

a.互斥:任何一个时刻,都只允许一个执行流在进行公共资源的访问——加锁

b.我们把任何一个时刻都只允许一个执行流在进行访问的共享资源,叫做临界资源

c.临界资源是要通过代码访问的,凡是访问临界资源的代码,叫做临界区

d.原子性

 查看当前的信号量

ipcs -s

认识信号量接口

信号量的获取

semget

int semget(key_t key,int nsems,int semflg)

第一个参数:   键值,用于唯一标识一个信号量集。通常可以通过 ftok 函数生成。

第二个参数:信号量集,代表信号量的个数 

第三个参数: 标志位,用于指定信号量的权限和行为选项。

semctl

得到一个信号量集标识符或创建一个信号量集对象

第一个参数:   信号量集标识符

第二个参数:是要操作的信号量在集合中的索引,通常为0,表示第一个信号量。

第三个参数:

cmd参数列表: 

cmd    解释
IPC_STAT    从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中
IPC_SET    设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值
IPC_RMID    从内核中删除信号量集合
GETALL    从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体成员的一个指针数组中
GETNCNT    返回当前等待资源的进程个数
GETPID    返回最后一个执行系统调用semop()进程的PID
GETVAL    返回信号量集合内单个信号量的值
GETZCNT    返回当前等待100%资源利用的进程个数
SETALL    与GETALL正好相反
SETVAL    用联合体中val成员的值设置信号量集合中单个信号量的值
 

理解IPC资源的管理

在shmctl,semctl,msqctl这些接口的描述里,都有对其数据结构的解释

 操作系统用一个指针数组来统一管理,这也是一种多态的表现

信号入门

生活角度的信号

--你在网上买了很多件商品,在等待不同商品快递的到来。但即便快递还没有到来,你也知道快递到了的时候应该怎么处理快递,也就是你能“识别快递”。
--当快递到达目的地了,你收到了快递到来的通知,但是你不一定要马上下楼取快递,也就是说取快递的行为并不是一定要立即执行,可以理解成在“在合适的时候去取”。
--在你收到快递到达的通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间内你并没有拿到快递,但是你知道快递已经到了,本质上是你“记住了有一个快递要去取”。
--当你时间合适,顺利拿到快递之后,就要开始处理快递了,而处理快递的方式有三种:1、执行默认动作(打开快递,使用商品)2、执行自定义动作(快递是帮别人买的,你要将快递交给他)3、忽略(拿到快递后,放在一边继续做自己的事)。
--快递到来的整个过程,对你来讲是异步的,你不能确定你的快递什么时候到。

技术应用角度的信号

1. 用户输入命令,在Shell下启动一个前台进程。 . 用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程 . 前台进程因为收到信号,进而引起进程退出

注意 

1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程 结束就可以接受新的命令,启动新的进程。

用Ctrl+c只能杀死前台进程,后台进程用kill -9命令可以杀死

2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生 的信号

3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行 到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步 (Asynchronous)的

信号概念

信号是进程之间事件异步通知的一种方式,属于软中断

 用kill -l命令可以察看系统定义的信号列表
信号处理常见方式概览 

可选的处理动作有以下三种:

1. 忽略此信号。

2. 执行该信号的默认处理动作。

3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉 (Catch)一个信号。

信号产生 

a.信号的产生对于进程来说是异步的

b.进程该如何处理对应产生的信号?记录在哪里?
--> 先描述,再组织——>怎么描述一个个信号,用什么数据结构管理这个信号

——> 01来描述信号,用位图这种数据结构管理这个信号

如0000 0000 0000 0000 0000 0001 0000 0000

c.所谓的发送信号,本质就是写入信号,直接修改特定进程的信号位图中的特定比特位0->1

d.数据内核结构,只能由OS进行修改,无论后面我们有多少种信号产生的方式,最终必须有OS来完成最后的发送过程

认识常用接口
signal

这里的第一个参数表示信号的编号,第二个参数则是一个函数指针,可以指向要执行的函数 (信号处理的方法)

这里2号信号的默认执行方法是杀死进程,用signal函数可以改变信号的处理方式,这里直接用了自定义的信号处理动作

这里提出一个小问题,在调用signal方法的时候,handler方法被调用了吗?

答案是并没有,他知识在操作系统内部更改了2号信号的处理动作,并没有调用handler

那么handler方法在什么时候才被调用呢?当2号信号产生的时候!

用signal(2,handler)来执行用户动作的自定义捕捉

这里两个信号都做捕捉

注:ctrl+c 2号信号 ctrl+\ 3号信号

以此类推,是不是可以对所有的信号都走自定义动作的设置

所有信号都自定义是不行的

这里9号信号之所以能够杀死进程,是因为9号信号是管理员信号,是不能被自定义动作的

通过系统接口向进程发送信号
kill

写一个我们自己的kill命令

loop.cc

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

int main()
{
    while (true)
    {
        std::cout << "我是一个进程,我正在运行 ..., pid: " << getpid() << std::endl;
        sleep(1);
    }
}

mykill.cc

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

int count = 0;

void Usage(std::string proc)
{
    std::cout << "\tUsage: \n\t";
    std::cout << proc << " 信号编号 目标进程" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signo = atoi(argv[1]);
    int target_id = atoi(argv[2]);
    int n = kill(target_id,signo);
    if(n!=0)
    {
        std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
        exit(2);
    }

    }
raise
int raise(int sig)

 参数代表信号的编号

用法:“谁调用我,我就给谁发这个信号” 

abort

用来直接终止进程,谁调用就终止谁,在信号里是6号信号,这个接口信号在被自定义捕捉后依然会使进程退出,作用雷同与exit

由软件条件产生的信号
alarm

这里的信号是14号信号SIGALRM

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后 响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

由硬件条件产生的信号

我们平时在输入的时候,计算机怎么知道我们从键盘输入了数据呢?键盘是通过硬件中断的方式,通知系统,我们的键盘已经按下了

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

如除0这个运算操作(8号信号),还有野指针问题等(11号信号)

核心转储

在这之前,要了解信号的这个Term和Core分别代表的是什么

Term:终止就是终止,无其他动作

Core:终止,会先进行核心转储,然后终止进程

什么是核心转储?

Linux系统级别提供了一种能力,可以将一个进程在异常的时候,OS可以在该进程在异常的时候,核心代码进行核心转储,将内存中进程的相关数据,全部dump到磁盘中,一般会在当前进程的运行目录下,形成core.pid这样的二进制文件——>核心转储文件

(在云服务器上确实看不到,而是云服务器是默认关闭这个功能的)

 怎么打开呢?

这里认识一个命令

ulimit -a

 这个命令用来查看当前系统当中特定资源的上限

可以看到有一个core file size在这里是0的,也就意味着没有设置

用2号信号来给进程发送异常信号,目录下面并没有发生变化

但是当我们换一个core类型的信号,当前目录下面出现的了一个core.pid的二进制文件

核心转储有什么用?

支持调试

直接将core文件load到gdb中直接自动定位事后调试,不用自己定位

核心转储为什么在云服务器都是关闭的?

核心转储形成的文件大小比较大,如果一个程序挂掉导致服务器崩溃,那么重启服务器,然后又因为这个程序挂掉,如此循环,每次都生成core.pid文件,磁盘直接被干满了,操作系统可能直接挂掉

补充

在之前的进程退出的信号中,有一个core dump标志位,这个标志位置1表示有core.pid

文件,0表示没有生成

阻塞信号(信号保存)

信号其他相关常见概念

实际执行信号的处理动作称为信号递达(Delivery)

信号从产生到递达之间的状态,称为信号未决(Pending)。

进程可以选择阻塞 (Block )某个信号。

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

在内核中的表示 

信号在内核中的表示示意图

这里面有三张表,分别是pending,block,handler表

pending表:表示是否收到该信号

block表:表示对应的信号是否被阻塞

handler表:表示信号的递达动作

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号
产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子
中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前
不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次
或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可
以依次放在一个队列里。

信号递达的若干种动作,之前只描述了信号的自定义捕捉

 

这个是默认动作,时候退出进程,用ctrl+c会直接退出,效果就不演示了

 这个是忽略动作

可以看到,按ctrl+c发送2号信号不能结束进程,原因是虽然接收到了信号,但是被忽略了

sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当 前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数 

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

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系 统支持的所有信号。

注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1

sigprocmask 

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1 

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信 号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。 

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递 达。 

sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

 用一个小样例来理解,对2号信号屏蔽,然后再解除block屏蔽,通过当前进程的pending位图来直观的感受

#include<iostream>
#include<cassert>
#include<signal.h>
#include<unistd.h>
using namespace std;
static void PrintPending(const sigset_t &pending)
{
    cout <<"当前进程的pending位图: ";
    for(int signo =1 ;signo<=31;signo++)
    {
        if(sigismember(&pending,signo))cout<<"1";
        else cout<<"0";
    }
    cout << "\n";
}

void handler(int signo)
{
    cout<<"对特定信号:"<<signo<<"的捕捉动作"<<endl;
}

int main()
{

    //1.屏蔽2号信号
    sigset_t set,oset;
    //1.1初始化
    sigemptyset(&set);
    sigemptyset(&oset);

    //1.2将2号信号添加到set中
    sigaddset(&set,2/*SIGINT*/);
    //1.3将新的信号屏蔽字设置到进程中
    sigprocmask(SIG_BLOCK,&set,&oset);

    //2. while获取进程的pending信号集合,并01打印
    //2.0 设置对2号信号的自定义捕捉
    signal(2,handler);
    int cnt = 0;
    while(true)
    {
        //2.1 先获取pending集合
        sigset_t pending;
        sigemptyset(&pending);//不是必须的
        int n = sigpending(&pending);
        assert(n==0);
        (void)n;//保证不会出现编译的是warning

        //2.2 打印,方便我们查看
        PrintPending(pending);

        //2.3休眠一下
        sleep(1);

        //2.3 10s后,回复对所有信号的block动作
        if(cnt++ == 10)
        {
            cout<<"解除对2号信号的屏蔽"<<endl;//先打印
            sigprocmask(SIG_SETMASK,&oset,nullptr);
        }
    }
}

 捕捉信号(信号处理)

信号处理,可以不是立即处理,而是“合适”的时候处理。

信号可以被立即处理吗?如果一个信号之前被block,当他解除block的时候,对应的信号会被立即递达!

大部分信号,都不是立即处理,为什么呢?

原因是:信号的产生是异步的,当前进程可能正在做更重要的事情!

什么时候是合适的时候呢?

当进程从内核态 切换回 用户态 的时候,进程会在OS的指导下,进行信号的检测与处理

补充:

内核态:执行OS的代码的时候,进程所处的状态

用户态:执行你写的代码的时候,进程所处的状态

 

内核如何实现信号的捕捉 

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码 是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返 回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了

sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

——sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。

参数说明:

● signum代表指定信号的编号。

● 若act指针非空,则根据act修改该信号的处理动作。

● 若oldact指针非空,则通过oldact传出该信号原来的处理动作

signo 是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传 出该信号原来的处理动作。act和oact指向sigaction结构体: 

——将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动 作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回 值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信 号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需 要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都 把sa_flags设为0,sa_sigaction是实时信号的处理函数,

v

其,参数act和oldact都是结构体指针变量,该结构体的定义如下:

struct sigaction {
	void(*sa_handler)(int);//要设置进的回调方法
	void(*sa_sigaction)(int, siginfo_t *, void *);//实时信号(不管)
	sigset_t   sa_mask;
	int        sa_flags;//设置为0
	void(*sa_restorer)(void);//不管
};

 

可重入函数 

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

酷帅且洋仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值