Linux学习_信号

首先,要明确信号信号量 两者没有任何关系。信号 :Linux系统提供的让用户(进程)给其它进程发送异步信息的一种方式,属于软中断。信号量属于进程间通信。在进程运行期间,信号可以分为如下阶段:信号的产生、信号的保存、信号的处理。

1. 信号的产生

我们可以使用kill -l 的指令查看系统中的信号列表,这些信号本质上就是宏,宏的定义可以在signal.h 中找到,通过 man 7 signal 可以查看每个信号的具体含义,其中编号34以上的是实时信号,暂不讨论:

1.1 按键产生信号

Ctrl+C 产生的SIGINT 的默认处理动作是终止进程, Ctrl+\ 产生的SIGQUIT 的默认处理动作是终止进程并且Core Dump(云服务器系统中默认关闭),Ctrl+Z 产生的SIGTSTP 的默认处理动作是暂停进程。对于按键输入的信息是由键盘驱动操作系统 联合解释的,而操作系统是通过硬件中断技术知道键盘在输入数据。

1.2 系统调用产生信号

  1. kill 函数
    kill命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号。
cpp 复制代码
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
成功返回0,错误返回-1
  1. raise 函数
    raise函数可以给当前进程发送指定的信号。(类似与 kill(getpid(),信号) )
cpp 复制代码
#include <signal.h>
int raise(int sig);
成功返回0,错误返回-1
  1. abort 函数
    abort 函数使当前进程接收到指定的 SIGABRT信号而异常终止。(类似与 kill(getpid(),6) )
cpp 复制代码
#include <stdlib.h>
void abort(void);
类似exit函数,abort函数总是会成功的,所以没有返回值。

1.3 软件条件产生信号

1.SIGPIPE 是一种由软件条件产生的信号,当管道文件读写 条件不满足时会产生。
2. SIGALRM

cpp 复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

调用alarm 函数可以设定一个闹钟 , 也就是告诉内核在 seconds 秒之后给当前进程发 SIGALRM 信号 , 该信号的默认处理动作是终止当前进程。这个函数的返回值是 0或者是以前设定的闹钟时间还余下的秒数。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

1.4 硬件异常产生信号

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

1.5 Core Dump

status在学习进程等待的时候,我们了解到wait和waitpid都有一个共同的参数status,它是子进程的退出状态信息。这里的core dump标志进程是否接收到信号,默认为0,所以信号没有0信号.

在信号中,我们可以看到一些信号的默认操作是核心转储Core,但在云服务器上,Core和Term都是终止进程。是因为为了防止未知的core dump 一直进行,导致服务器磁盘被打满,所以云服务器默认将core 退出,进行特定处理,默认core是关闭的。

我们可以通过如下指令查看到core是被关闭的:

可以通过如下指令打开core功能:

打开之后,当进程接受到某些默认处理是Core的信号终止后,会把该进程在内存中的与调试有关的核心数据转储到磁盘中形成core、core.pid的文件,方便我们事后进行调试。

2. 信号的保存

2.1 相关概念

首先要明确以下概念:
实际执行信号的处理动作称为信号递达(Delivery)。
信号从产生到递达之间的状态, 称为信号未决 (Pending) 。
进程可以选择阻塞 (Block ) 某个信号。
被阻塞的信号产生时将保持在未决状态, 直到进程解除对此信号的阻塞 , 才执行递达的动作。
阻塞忽略是不同的 , 只要信号被阻塞就不会递达 , 而忽略 是在递达之后 可选的一种处理动作

2.2 在内核中的表示

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

2.3 sigset_t

未决阻塞 标志可以用相同的数据类型sigset_t 来存储 ,sigset_t 称为信号集 , 这个类型可以表示每个信号的" 有效 " 或 " 无效 " 状态:
阻塞信号集中 " 有效 " 和 " 无效 " 的含义是该信号是否被阻塞
未决信号集中 " 有效" 和 " 无效 " 的含义是该信号是否处于未决状态
所以操作系统向进程发送信号,本质上是写入信号,把对应标志位置为1/0。

2.4 信号集操作函数

2.4.1 操作sigset_t类型的函数

sigset_t 类型对于每种信号用一个 bit 表示 " 有效 " 或 " 无效 " 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_t 变量 ,而不应该对它的内部数据做任何解释。即用户不能直接对直接操作信号这种内核数据结构,只能通过操作 sigset_t 类型数据来间接影响内部的信号。

cpp 复制代码
#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);

成功返回0,出错返回-1

函数sigemptyset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 清零 , 表示该信号集不包含任何有效信号。
函数sigfillset 初始化 set 所指向的信号集 , 使其中所有信号的对应 bit 置1 , 表示该信号集的有效信号包括系统支持的所有信号。
在使用sigset_t 类型的变量之前 , 一定要调用 sigemptysetsigfillset 做初始化 , 使信号集处于确定的状态。
初始化sigset_t变量之后就可以在调用 sigaddsetsigdelset 在该信号集中添加删除某种有效信 号。
sigismember是一个布尔函数,用来判断某个信号是否在一个有效信号集中,若存在就返回1,不存在就返回0,出错返回-1。

2.4.2 sigprocmask

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

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


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

2.4.3 sigpending

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

3. 信号的处理

3.1 内核对信号的捕捉

首先,我们要明确信号就是通过软件的方式,来模拟硬件中断。通过高频率、不间断的向cpu发送中断,cpu就会不断地处理中断。cpu根据中断向量表,就会去执行对应的功能(比如操作系统、响应键盘),所以操作系统就会一直执行。

每个进程都有自己的PCB和进程空间如下。可以看到除了常用的用户空间,还有内核空间。这两个空间都有一张对应的页面来映射物理地址空间,分别是用户级页表内核级页表 。要知道我们访问OS,本质就是通过进程的内核空间来访问的,这也是为什么无论进程怎么切换,总能找到OS。

当CPU处理进程的时候,因为内核数据不暴露给外面,所以需要区分是用户级别 还是内核级别 ,从而访问不同级别的页表,在CPU中就有了CS寄存器 中存放了权限标识(0:内核态 3:用户态),CR3寄存器 存放对应级别页表的物理地址(关于这两个寄存器不做详述)。所以这时就必须要区分当前用户的运行模式用户态内核态

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

3.2 sigaction

cpp 复制代码
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
调用成功则返回0,出错则返回- 1

signo 是指定信号的编号, sigaction 函数可以读取和修改与指定信号相关联的处理动作
actoact 指向 sigaction 结构体
act 指针非空 , 则根据 act 修改该信号的处理动作
oact 指针非 空 , 则通过 oact 传出该信号原来的处理动作

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

4. 可重入函数


main函数调用 insert 函数向一个链表 head 中插入节点 node1, 插入操作分为两步, 刚做完第一步的时候 , 因为硬件中断使进程切换到内核, 再次回用户态之前检查到有信号待处理 , 于是切换到 sighandler 函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点 node2, 插入操作的两步都做完之后从sighandler返回内核态 , 再次回到用户态就从 main 函数调用的 insert 函数中继续往下执行 , 先前做第一步之后被打断, 现在继续做完第二步。结果是 ,main 函数和 sighandler 先后向链表中插入两个节点 , 而最后只有一个节点真正插入链表中了。
像上例这样,insert 函数被不同的控制流程调用 , 有可能在第一次调用还没返回时就再次进入该函数, 这称为重入,insert 函数访问一个全局链表 , 有可能因为重入而造成错乱 , 像这样的函数称为不可重入函数。 反之,如果一个函数只访问自己的局部变量参数, 则称为可重入函数
如果一个函数符合以下条件之一则是不可重入的:
调用了mallocfree, 因为 malloc 也是用全局链表来管理堆的
调用了标准I/O库函数,标准 I/O 库的很多实现都以不可重入 的方式使用全局数据结构

5. volatile

作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量 的任何操作,都必须在真实的内存中进行操作。
下面我们通过代码,来观察volatile在信号中的作用:

cpp 复制代码
#include <stdio.h>
#include <signal.h>

int flag = 0;
//volatile int flag = 0;

void handler(int sig)
{
    (void)sig;
    printf("chage flag 0 to 1\n");
    flag = 1;
}

int main()
{
    signal(2, handler);
    while (!flag);
    printf("process quit normal\n");
    return 0;
}

标准情况下,当按下Ctrl+C时2号信号被捕捉,执行handler 函数,从而导致while循环退出。但现在我们可以在gcc编译时加上参数强制编译器优化,使用man gcc 可以看到,默认优化级别是0。优化逻辑是:while循环中没有对flag 进行操作,为了提高效率,每次对flag 进行逻辑运算时,不再从内存中读取,而是直接在cpu的相关寄存器中读取。这时,我们在handler 函数中对flag 的修改是对内存中的flag 修改,寄存器 中的flag 还是0,存在数据二义性,所以按下Ctrl+C也不能终止循环。

而当我们使用volatile后,强制编译器读取flag 时,从内存中读取,就可以解决上面的问题。

**6.**SIGCHLD信号

waitwaitpid 函数清理僵尸进程时, 父进程可以阻塞等待子进程结束 , 也可以非阻塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 , 父进程阻塞了就不能处理自己的工作了; 采用第二种方式 ,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实, 子进程在终止时会给父进程发 SIGCHLD 信号, 该信号的默认处理动作是忽略, 父进程可以自定义 SIGCHLD 信号 的处理函数 , 这样父进程只需专心处理自己的工作 , 不必关心子进程了 , 子进程终止时会通知父进程 , 父进程在信号处理 函数中调用 wait 清理子进程即可。
要想不产生僵尸进程还有另外一种办法; 父进程调用 sigactionSIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略通常是没有区别的 , 但这是一个特例。此方法对于 Linux 可用,对于其它Unix系统不保证可行。
下面代码实现: 父进程 fork 出子进程 , 子进程调用 exit(2) 终止 , 父进程自定义 SIGCHLD 信号的处理函数, 在其中调用 wait 获得子进程的退出状态并打印。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
    pid_t id;
    while ((id = waitpid(-1, NULL, WNOHANG)) > 0)
    {
        printf("wait child success: %d\n", id);
    }
    printf("child is quit! %d\n", getpid());
}
int main()
{
    signal(SIGCHLD, handler);
    pid_t cid;
    if ((cid = fork()) == 0)
    { // child
        printf("child : %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while (1)
    {
        printf("father proc is doing some thing!\n");
        sleep(1);
    }
    return 0;
}
相关推荐
南种北李3 分钟前
Linux自动化构建工具Make/Makefile
linux·运维·自动化
百里香酚兰5 分钟前
【AI学习笔记】基于Unity+DeepSeek开发的一些BUG记录&解决方案
人工智能·学习·unity·大模型·deepseek
小飞猪Jay9 分钟前
面试速通宝典——10
linux·服务器·c++·面试
布丁不叮早起枣祈20 分钟前
10.3学习
学习
结衣结衣.32 分钟前
python中的函数介绍
java·c语言·开发语言·前端·笔记·python·学习
暗恋 懒羊羊1 小时前
Linux 生产者消费者模型
linux·开发语言·ubuntu
LN-ZMOI1 小时前
c++学习笔记1
c++·笔记·学习
五味香1 小时前
C++学习,信号处理
android·c语言·开发语言·c++·学习·算法·信号处理
安红豆.2 小时前
Linux基础入门 --13 DAY(SHELL脚本编程基础)
linux·运维·操作系统
..空空的人2 小时前
linux基础指令的认识
linux·运维·服务器