最全Linux应用开发八股文(四)——信号

你好,我是拉依达。

这是我的Linux应用开发八股文详细解析系列

本系列最开始是我在csdn上更新的文章全文总字数超3w字,现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。

【下面是拉依达推荐学习相关专栏:】
一、Linux驱动学习专栏:拉依达的Linux驱动八股文 - 牛客网
二、Linux应用学习专栏:拉依达的Linux应用八股文 - 牛客网
【我的嵌入式学习和校招经验】 拉依达的嵌入式学习和秋招经验-CSDN博客
嵌入式学习规划/就业经验指导,可私信咨询

———————————————————————————————————————————————————

三、信号

3.1 信号概述

Linux 中的信号是一种消息处理机制,它本质上是一个整数,不同的信号对应不同的值,由于信号的结构简单所以天生不能携带很大的信息量,但是信号在系统中的优先级是非常高的。

信号也可以实现进程间通信,但是信号能传递的数据量很少,不能满足大部分需求,另外信号的优先级很高,并且它对应的处理动作是回调完成的,它会打乱程序原有的处理流程,影响到最终的处理结果。因此非常不建议使用信号进行进程间通信。

信号编号

kill -l 命令

# 执行shell命令查看信号
$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

查看信号信息

# 查看man文档的信号描述
$ man 7 signal

在信号描述中介绍了对产生的信号的五种默认处理动作,分别是:

  1. Term:信号将进程终止
  2. Ign:信号产生之后默认被忽略了
  3. Core:信号将进程终止,并且生成一个 core 文件 (一般用于 gdb 调试)
  4. Stop:信号会暂停进程的运行
  5. Cont:信号会让暂停的进程继续运行

关于对信号的介绍有一句非常重要的描述: 9号信号和19号信号不能被 捕捉, 阻塞, 和 忽略

  • 9号信号: 无条件杀死进程
  • 19号信号: 无条件暂停进程

信号的状态

Linux 中的信号有三种状态,分别为:产生,未决,递达

  • 产生:键盘输入,函数调用,执行 shell 命令,对硬件进行非法访问都会产生信号
  • 未决:信号产生了,但是这个信号还没有被处理掉,这个期间信号的状态称之为未决状态
  • 递达:信号被处理了 (被某个进程处理掉)

3.2 信号相关函数

kill/raise/abort

这三个函数的功能比较类似,可以发送相关的信号给到对应的进程

  1. kill 发送指定的信号到指定的进程,函数原型如下:
#include <signal.h>
// 给某一个进程发送一个信号
int kill(pid_t pid, int sig);

参数:

  • pid: 进程 ID(man 文档里边写的比较详细)
  • sig: 要发送的信号
  1. raise:给当前进程发送指定的信号,函数原型如下:
// 给自己发送某一个信号
#include <signal.h>
int raise(int sig);	// 参数就是要给当前进程发送的信号
  1. abort:给当前进程发送一个固定信号 (SIGABRT),函数原型如下:
// 这是一个中断函数, 调用这个函数, 发送一个固定信号 (SIGABRT), 杀死当前进程
#include <stdlib.h>
void abort(void);

定时器——alarm

alarm () 函数只能进行单次定时,定时完成发射出一个信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 参数:倒计时 seconds 秒,倒计时完成发送一个信号 SIGALRM , 当前进程会收到这个信号,这个信号默认的处理动作是中断当前进程
  • 返回值:大于 0 表示倒计时还剩多少秒,返回值为 0 表示倒计时完成,信号被发出

定时器——setitimer

setitimer () 函数可以进行周期性定时,每触发一次定时器就会发射出一个信号。

// 这个函数可以实现周期性定时, 每个一段固定的时间, 发出一个特定的定时器信号
#include <sys/time.h>

struct itimerval {
	struct timeval it_interval; /* 时间间隔 */
	struct timeval it_value;    /* 第一次触发定时器的时长 */
};
//  - it_value: 当前设置闹钟的时间点 到 明天早晨7点 对应的总秒数
//  - it_interval: 闹钟第一次响过之后, 每隔5分钟响一次

// 这个结构体表示的是一个时间段: tv_sec + tv_usec
struct timeval {
	time_t      tv_sec;         /* 秒 */
	suseconds_t tv_usec;        /* 微妙 */
};

int setitimer(int which, 
			const struct itimerval *new_value, 
              struct itimerval *old_value);

参数:

  • which: 定时器使用什么样的计时法则,不同的计时法则发出的信号不同
    • ITIMER_REAL: 自然计时法,最常用,发出的信号为 SIGALRM, 一般使用这个宏值,自然计时法时间 = 用户区 + 内核 + 消耗的时间 (从进程的用户区到内核区切换使用的总时间)
    • ITIMER_VIRTUAL: 只计算程序在用户区运行使用的时间,发射的信号为 SIGVTALRM
    • ITIMER_PROF: 只计算内核运行使用的时间,发出的信号为 SIGPROF
  • new_value: 给定时器设置的定时信息,传入参数
  • old_value: 上一次给定时器设置的定时信息,传出参数,如果不需要这个信息,指定为 NULL

3.3 信号集

阻塞 / 未决信号集

PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集”,另一个称之为 “未决信号集”。这两个信号集体现在内核中就是两张表。但是操作系统不允许我们直接对这两个信号集进行任何操作,而是需要自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。

  • 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。

  • 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了 防止信号打断某些敏感的操作。

阻塞信号集和未决信号集在内核中的结构是相同的,它们都是一个整形数组 (被封装过的), 一共 128 字节 (int [32] == 1024 bit),1024 个标志位,其中前 31 个标志位,每一个都对应一个 Linux 中的标准信号,通过标志位的值来标记当前信号在信号集中的状态。

  • 在阻塞信号集中,描述这个信号有没有被阻塞

    • 默认情况下没有信号是被阻塞的,因此信号对应的标志位的值为 0
    • 如果某个信号被设置为了阻塞状态,这个信号对应的标志位 被设置为 1
  • 在未决信号集中,描述信号是否处于未决状态

    • 如果这个信号被阻塞了,不能处理,这个信号对应的标志位被设置为 1
    • 如果这个信号的阻塞被解除了,未决信号集中的这个信号马上就被处理了,这个信号对应的标志位值变为 0
    • 如果这个信号没有阻塞,信号产生之后直接被处理,因此不会在未决信号集中做任何记录

信号集函数

因为用户是不能直接操作内核中的阻塞信号集和未决信号集的,必须要调用系统函数:

  • 阻塞信号集可以通过系统函数进行读写操作
  • 未决信号集只能对其进行读操作。

读 / 写阻塞信号集的函数:

#include <signal.h>
// 使用这个函数修改内核中的阻塞信号集
// sigset_t 被封装之后得到的数据类型, 原型:int[32], 里边一共有1024给标志位, 每一个信号对应一个标志位
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

  • how:
    • SIG_BLOCK: 将参数 set 集合中的数据追加到阻塞信号集中
    • SIG_UNBLOCK: 将参数 set 集合中的信号在阻塞信号集中解除阻塞
    • SIG_SETMASK: 使用参 set 结合中的数据覆盖内核的阻塞信号集数据
    • oldset: 通过这个参数将设置之前的阻塞信号集数据传出,如果不需要可以指定为 NULL 返回值:函数调用成功返回 0,调用失败返回 - 1

sigprocmask () 函数有一个 sigset_t 类型的参数,对这种类型的数据进行初始化需要调用一些相关的操作函数

#include <signal.h>
// 如果在程序中读写 sigset_t 类型的变量
// 阻塞信号集和未决信号集都存储在 sigset_t 类型的变量中, 这个变量对应一块内存
// 阻塞信号集和未决信号集, 对应的内存中有1024bit = 128字节

// 将set集合中所有的标志位设置为0
int sigemptyset(sigset_t *set);
// 将set集合中所有的标志位设置为1
int sigfillset(sigset_t *set);
// 将set集合中某一个信号(signum)对应的标志位设置为1
int sigaddset(sigset_t *set, int signum);
// 将set集合中某一个信号(signum)对应的标志位设置为0
int sigdelset(sigset_t *set, int signum);
// 判断某个信号在集合中对应的标志位到底是0还是1, 如果是0返回0, 如果是1返回1
int sigismember(const sigset_t *set, int signum);

未决信号集不需要程序猿修改,如果设置了某个信号阻塞,当这个信号产生之后,内核会将这个信号的未决状态记录到未决信号集中,当阻塞的信号被解除阻塞,未决信号集中的信号随之被处理,内核再次修改未决信号集将该信号的状态修改为递达状态(标志位置 0)

因此,写未决信号集的动作都是内核做的,这是一个读未决信号集的操作函数

#include <signal.h>
// 这个函数的参数是传出参数, 传出的内核未决信号集的拷贝
// 读一下这个集合就指定哪个信号是未决状态
int sigpending(sigset_t *set);


## 3.4  信号捕捉
Linux 中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略这个信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。

程序中进行信号捕捉可以看做是一个注册的动作,**提前告诉应用程序信号产生之后做什么样的处理**,当进程中对应的信号产生了,这个处理动作也就被调用了。

###  signal
使用 signal () 函数可以捕捉进程中产生的信号,并且修改捕捉到的函数的行为,这个信号的自定义处理动作是一个回调函数,内核通过 signal () 得到这个回调函数的地址,在信号产生之后该函数会被内核调用。

```cpp
#include <signal.h>
// 在程序中什么时候产生信号, 程序猿是不知道的, 因此不能在信号产生之后再去处理
// 在信号产生之前, 提供一个注册函数, 用来捕捉信号
//	  - 假设在将来这个信号产生了, 就委托内核进行捕捉, 这个信号的默认动作就不能被执行
//	  - 执行什么样的处理动作 ==> 在signal函数中指定的处理动作
//	  - 如果这个信号不产生, 回调函数永远不会被调用
sighandler_t signal(int signum, sighandler_t handler);   

参数:

  • signum: 需要捕捉的信号

  • handler: 信号捕捉到之后的处理动作,这是一个函数指针,函数原型 这个回调函数是需要程序猿写,由内核调用,内核调用回调函数的时候,会给它传递一个实参,这个实参的值就是捕捉的那个信号值。

    typedef void (*sighandler_t)(int);
    

sigaction

sigaction () 函数和 signal () 函数的功能是一样的,用于捕捉进程中产生的信号,并将用户自定义的信号行为函数(回调函数)注册给内核,内核在信号产生之后调用这个处理动作。sigaction () 可以看做是 signal () 函数是加强版,函数参数更多更复杂,函数功能也更强一些。函数原型如下:

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

参数:

  • signum: 要捕捉的信号

  • act: 捕捉到信号之后的处理动作

  • oldact: 上一次调用该函数进行信号捕捉设置的信号处理动作,该参数一般指定为 NULL

返回值:函数调用成功返回 0,失败返回 - 1

函数的参数是一个结构体类型,结构体原型如下:

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);  //不用
};

结构体成员介绍

  • sa_handler: 函数指针,指向的函数就是捕捉到的信号的处理动作

  • sa_sigaction: 函数指针,指向的函数就是捕捉到的信号的处理动作

  • sa_mask: 在信号处理函数执行期间,临时屏蔽某些信号 , 将要屏蔽的信号设置到集合中即可

    • 当前处理函数执行完毕,临时屏蔽自动解除
    • 假设在这个集合中不屏蔽任何信号,默认也会屏蔽一个(捕捉的信号是谁,就临时屏蔽谁)
  • sa_flags:使用哪个函数指针指向的函数处理捕捉到的信号

    • 0:使用 sa_handler (一般情况下使用这个)
    • SA_SIGINFO:使用 sa_sigaction (使用信号传递数据 == 进程间通信)
  • sa_restorer: 被废弃的成员

#嵌入式##校招##八股文##Linux##Linux应用开发#
拉依达的Linux应用八股文 文章被收录于专栏

你好,我是拉依达。 这是我的Linux应用开发八股文详细解析系列。 本系列最开始是我在csdn上更新的文章全文总字数超3w字,现重新对内容进行整理,希望可以帮助到更多学习嵌入式的同学。

全部评论

相关推荐

2 6 评论
分享
牛客网
牛客企业服务