Linux系统:进程信号

阅读: 评论:0

Linux系统:进程信号

Linux系统:进程信号

进程信号

本章将系统地介绍 Linux 系统中信号的概念,以信号的生命周期为路线,先后学习信号的产生,信号的保存,信号的处理。

1. 信号的概念# 进程信号

1. 信号的产生

1.1 信号的概念

生活中也存在信号,如红绿灯,除了信号本身还有信号触发时人们的动作,如:闻鸡起舞,红灯停绿灯行。

系统中也存在信号,信号是系统发送给进程的,进程需要在合适的时候处理信号,因此进程必须明确各种信号对应的动作。

Linux共有62种信号,前31个是普通信号,后31个是实时信号。实时信号不作考虑。

$ kill -l1) SIGHUP        2) SIGINT      3) SIGQUIT      4) SIGILL      5) SIGTRAP6) 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

Linux中信号用宏来表示,数字是宏值,单词是宏名。

信号的产生

当然,进程必须能够分辨信号和明确指定信号的处理方式,以便在信号到来时能够及时处理。也就是说,进程在信号产生前就应该具有识别和处理信号的能力

信号可能随时产生,也就是说,信号的产生对进程来说是异步的

信号的保存

实际生活人们收到某种信号时,可能并不会立即处理因为此时还在处理其他事情。比如闹钟响起时赖床。

进程在收到信号时也可能无法立即处理,因为进程可能正在处理优先级更高的任务,只能将信号暂存起来,等到合适的时候再进行处理。也就是说**,进程具有暂时保存信号的能力**。

信号本身也是数据,可被暂存在PCB中。系统提供多种发送信号的方式,但本质都是让系统向进程控制块中写入数据

我们已经知道,产生信号实际是操作系统在向进程发送信号。信号本身也是数据,发送信号的本质就是向PCB中写入信号数据

进程只关心是否收到信号及是几号信号,因此进程中采用整型位图来标识进程是否收到某种信号,比特位位置代表信号编号,比特位内容代表是否收到信号。

系统向进程发送信号就是向进程 task_struct 的信号位图的对应位置写入1。

Linux信号说明

1.2 注册信号处理动作

前面已经说过,进程在信号产生前就必须明确对信号的处理方式。

  • 分辨信号:进程能够分辨系统设计好的共62种信号。
  • 处理方式:一般信号有默认动作、忽略动作、自定义动作。
    • 默认动作:信号的默认行为,如SIGKILL的杀死自身进程。
    • 忽略动作:忽略掉该信号,不做出任何反应,也是一种信号处理的方式。
    • 自定义动作:要求内核返回用户态执行用户注册的行为,我们称之为捕捉一个信号。
// signal 注册进程对信号 signum 的处理动作为函数 handler,并返回旧的处理方法
#include <signal.h>
typedef void (*sighandler_t)(int); // handler 函数指针
sighandler_t signal(int signum, sighandler_t handler);
void handler(int sig)
{printf("process:%d, signal number:%dn", getpid(), sig);
}
int main()
{signal(2, handler); // 通过signal注册对2号信号的处理动作,并没有实际信号产生while (1){printf("hello proc:%dn", getpid());sleep(1);}return 0;
}

通过signal接口注册对某个信号的处理动作,相当于是一种预定的注册机制,并没有任何信号产生。

9号SIGKILL信号和19号SIGSTOP信号无法被注册。

1.3 信号产生的方式

产生信号共有4种大方式,信号产生的方式繁多,但底层都是系统向进程发送信号。

方式具体解释
系统指令指令kill或者键盘发送快捷键^C,^Z等。
系统调用系统提供多种发送信号的接口,方便我们在代码中设置信号。
软件条件某些软件条件不具备,触发信号发送的条件,操作系统向进程发送信号。
硬件异常进程运行中出现错误导致软硬件出现异常,被操作系统甄别并处理。
系统指令
输入含义
kill向进程发送任意号信号
^C向进程发送2号信号SIGINT,默认是终止进程。
^向进程发送3号信号SIGQUIT,默认是终止进程并进行core dump。
^Z向进程发送20号信号SIGTSTP,默认是停止进程。
$ ./a.out &   # 将进程放到后台执行
$ fg          # 将后台进程调至前台

键盘产生的信号,只能作用于前台进程,后台进程无标准输入所以无法使用键盘发送信号。

系统调用
kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig); // kill函数向指定进程发送指定信号
int main(int argc, char** argv)
{if (argc != 3) {std::cout << "Usage:nt./mykill -signo pid" << std::endl;exit(1);}int sig = stoi(argv[1] + 1);int pid = stoi(argv[2]);if (kill(pid, sig) < 0) perror("mykill");elsestd::cout << "kill -" << sig << " " << pid << std::endl;return 0;
}
raise
#include <signal.h>
int raise(int signum); // raise函数给自身进程发送指定信号
void handler(int signo)
{std::cout << "get a signal: " << signo << endl;
}int main()
{signal(SIGINT, handler);while (true) {sleep(1);raise(SIGINT);}return 0;
}
abort
#include <stdlib.h>
void abort(void); // abort函数给自身进程发送6号信号SIGABRT
int main()
{if (fork() == 0){std::cout << "i am child process" << std::endl;abort();}int status = 0;waitpid(-1, &status, 0);std::cout << "exit code: " << (status >> 8) << ", signal: " << (status & 0x7F) << endl;return 0;
}

abort是C库函数,不仅会发送信号还会调用exit终止程序。

软件条件

由于软件条件的不就绪而触发信号。例如管道所有读端关闭,写端的条件不就绪,系统就会向其发送SIGPIPE信号。

alarm

alarm设置一个计时器,计时结束后向自身进程发送14号SIGALRM信号终止进程。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
  • 参数是设置闹钟的时间。参数为0表示取消闹钟。
  • 返回值是上一次alarm闹钟的剩余时间,如果之前没有设定过则返回0。每次调用alarm都会覆盖之前的闹钟并重新设定。
int main()
{int cnt = 5;alarm(cnt);while (true){std::cout << "remaining time: " << cnt-- << std::endl;sleep(1);}return 0;
}
进程异常
int main()
{*(int*)11223344 = 100; // 野指针
}$ ./test
Segmentation fault //带有野指针等错误的程序运行起来,进程会崩溃。

运行存在异常问题的程序时,会导致CPU、MMU等硬件会出现异常,致使操作系统就会向进程发出对应的信号。

  • 如地址越界、权限不足等段错误,MMU在查找页表后会直接报错,进程会收到11号SIGSEGV信号。
  • 除零等浮点数溢出错误会导致CPU中状态寄存器出现异常,进程就会收到8号SIGFPE信号。
void handler(int signum) {printf("get a sigal num:%dn", signum);exit(1);
}int maln()
{signal(SIGSEGV, handler);*(int*)11223344 = 100; // 野指针
}
$ ./test
get a sigal num:11int main()
{signal(SIGFPE,  handler);int a = 1 / 0;
}
$ ./test
get a sigal num:8

 

2. 信号的保存

进程可能正在处理优先级更高的事情故不能及时处理信号,所以进程使用信号位图来暂存已收到的信号。

2.1 信号状态的概念

概念解释
信号递达执行信号处理动作就叫做信号递达。
信号未决信号产生但并未递达的状态称为信号未决,就是信号被暂存在pcb信号位图中。
信号阻塞系统允许进程屏蔽/阻塞某些信号。此时信号是未决的,因阻塞故无法递达,解除阻塞方可递达。

执行信号的忽略动作就说明信号已被处理,只是处理动作就是什么都不做,但已递达。

阻塞表示信号被屏蔽,信号被阻塞就代表信号不可能被递达,但可以是未决的,直至解除阻塞方可递达。

2.2 信号保存的方式

这些概念在 pcb 中是如何表示的呢?

进程PCB中有三张表,分别是block表,pending表,handler表,如图所示:

表名内容本质
pending为1表示收到信号,为0反之pending位图表示是否收到信号
block为0表示信号被阻塞,为1反之block位图表示是否阻塞信号。信号屏蔽字。
handlerSIGDFL(0)SIGIGN(1),自定义函数地址handler函数指针数组存储信号处理动作。

三张表横着看,一行对应一个信号,首先是否收到,然后是否屏蔽,其次是其处理方法。

//伪代码
if (block & (1 << (signo - 1))) { //被阻塞//...
}
else { //未阻塞if (pending & (1 << (signo - 1))) { //未阻塞且已收到handler[signo - 1](signo);return 0;}
}

  • 一个信号如果被 block,即使其被 pending,也不能被 handler。
  • 一个信号没有被 block,如果其被 pending,进程将会在合适的时候进行 handler。
  • 一个信号如果被 handler,那么其一定被 pending,且一定不能被 block。

这三张表涵盖了信号是否被屏蔽,是否收到,处理方法是什么,此时进程就具有识别信号的能力。

2.3 信号集的操作

系统为用户提供了修改信号的数据类型和接口。

信号位图类型 sigset_t

为防止用户误操作,信号的各种操作必须使用系统提供的数据类型:

sigset_t set; // 信号位图结构类型// source code
typedef struct {unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;typedef __sigset_t sigset_t;

可以使用sigset_t变量,操作内核中的未决和阻塞位图。虽然sigset_t本质是位图,但不允许自行位运算,只能用系统提供的接口。

信号调用接口

一般操作信号的步骤是:

  1. 先设置好自定义信号位图,也就是使用sigset_t类型定义的用户变量。
  2. 再将该变量写入内核数据结构中。

用来修改用户信号变量的一系列接口:

#include <signal.h>
int sigemptyset( sigset_t *set );  // 全部置0
int sigfillset ( sigset_t *set );  // 全部置1int sigaddset( sigset_t *set, int signo );  // 加入信号
int sigdelset( sigset_t *set, int signo );  // 删除信号int sigismember( const sigset_t *set, int signo );  // 判断是否存在

用来将位图变量设置进进程PCB的信号位图的接口:

读取/修改信号阻塞集 sigprocmask

sigprocmask用来读取或者修改进程PCB中的信号阻塞位图。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
变量解释
set设置进内核的信号阻塞集,不需要可以设为NULL
oldset获取内核中的信号阻塞集,不需要可以设为NULL
how设置信号的方式,选项有三种:SIG_BLOCKSIG_UNBLOCKSIG_SETMASK
how参数的选项解释
SIG_BLOCK将set中值为1的信号设置进内核阻塞表中,相当于`mask
SIG_UNBLOCK将set中值为1的信号在内核中解除阻塞,相当于mask&=(~set)
SIG_SETMASK直接将内核中的阻塞位图替换为set,相当于mask=set
void showBlockset(sigset_t* set)
{for (int signo = 1; signo < 32; signo++)if (sigismember(set, signo)) std::cout << "1";else                         std::cout << "0";std::cout << std::endl;
}int main()
{sigset_t set, oset;sigemptyset(&set);sigemptyset(&oset);sigaddset(&set, SIGINT);sigprocmask(SIG_SETMASK, &set, &oset);while (true){showBlockset(&set);sleep(1);}return 0;
}
读取信号未决集 sigpending

pending表由系统自行管理无需设置,sigpending只能获取进程的pending位图。

#include <signal.h>
int sigpending(sigset_t *set);
int main()
{sigset_t set, pset;sigemptyset(&set);sigaddset(&set, SIGINT);sigprocmask(SIG_BLOCK, &set, nullptr);int cnt = 0;while (true){sigpending(&pset);showBlockset(&pset);if (cnt++ == 10)sigprocmask(SIG_UNBLOCK, &set, nullptr);sleep(1);}
}
读取/修改信号处理集 sigaction

signalsigaction接口没有区别,都是注册对单个信号的处理方法。

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);struct sigaction {void   (*sa_handler)(int);sigset_t sa_mask;int      sa_flags;// ...
};
参数解释
signum需要设置的信号
act设置进内核的信号处理集,不需要可以设为NULL
oldact获取内核中的信号处理集,不需要可以设为NULL

当某个信号的处理方法正在被调用时,系统会暂时将该信号屏蔽,等处理完该信号时,再对该信号解除阻塞。

如果想在处理某个信号的同时,将其他信号也作屏蔽,则可以使用sa_mask字段。

void handler(int signo)
{std::cout << "get a signal: " << signo << std::endl;sigset_t set;sigprocmask(SIG_BLOCK, nullptr, &set);for (int sig = 1; sig < 32; sig++){if (sigismember(&set, sig)) cout << '1';else cout << '0';}cout << endl;
}int main()
{struct sigaction act;act.sa_handler = handler;act.sa_flags = 0;sigaddset(&act.sa_mask, 1);sigaction(SIGINT, &act, nullptr);raise(SIGINT);return 0;
}

 

3. 信号的处理

3.1 信号处理的时机

信号的产生是异步的,信号产生时当前进程可能正在进行更重要的工作,信号必须要被延时处理,具体要取决于操作系统和进程的状态。

进程会在合适的时候处理未决的信号,那什么是合适的时候呢?

当进程从内核态切换到用户态的时候,进程会在系统的指导下,会进行信号的检测和处理

内核态和用户态的概念

进程在执行代码时会不断的切换用户态和内核态,执行用户代码时处于用户态,执行内核代码时处于内核态

  • 执行用户态的代码时,进程必须为用户态,必须受到操作系统的监管。
  • 执行内核级的代码时,进程必须为内核态,提升身份权限以执行内核代码。再次执行用户代码时必须转为用户态,否则可能影响系统安全。

调用系统调用,会发生进程状态转变,并不是只有这一种情况才会发生状态转变。

程序运行起来,用户和内核的代码数据都是要被加载到内存中的。

进程地址空间分为用户空间和内核空间,分别通过用户级页表和内核级页表映射到用户代码数据和内核代码数据。

用户页表每个进程都有有一份,而内核页表只有一份,被所有进程空间共享。这样既保证了进程独立性,也保证了每个进程都能访问到内核代码。

CPU中的CR3寄存器中的数值表示代码执行级别。如果执行级别不够,CPU中状态寄存器出错,进而系统发送信号终止进程。

3.2 信号处理的过程

当进程从内核态返回到用户态的时候,会进行信号的检测和处理工作。以调用系统接口为例:

  1. 用户代码中调用系统接口,进程由用户态进入内核态;
  2. 系统接口调用完毕后,检测并处理信号;
  3. 如果信号处理动作是默认或忽略,直接处理并返回。如果是自定义捕捉,需返回用户态,调用handler方法。
  4. 自定义捕捉方法执行后,不能直接返回用户代码,需返回内核态,调用sys_sigreturn返回用户代码

  • 执行信号处理自定义方法时,必须返回用户态。如果使用内核态执行用户代码,会对系统安全造成威胁。
  • 信号处理完后,无法直接返回用户代码,因为处理信号时用户代码执行到什么位置是不清楚的,需要通过sys_sigreturn()返回对应位置。

进程进行信号检测处理的具体流程如上图所示,类似无穷大符号:

每次经过分界线都是一次状态的切换,而交叉点可以看作信号检测并处理的点。

从处理逻辑来看,信号的产生是异步的,信号的处理是同步的。进入内核态后处理信号,处理后返回用户代码,始终是一个执行流。

生活中也存在信号,如红绿灯信号,公鸡打鸣信号等等,伴随这些信号的还有当信号触发时人们该做什么,比如:鸡鸣时该起床,红灯停绿灯行等。

在系统中也存在信号,信号是发送给进程的,进程需要在合适的时候,执行信号对应的动作。进程必须明确各种的信号的对应的动作,和该信号是否产生无关。

信号的产生

当然,进程在运行之前就必须能够分辨出哪个信号,以及指定对该信号的处理方式,以便信号到来之时,能够及时处理。也就是说,进程在信号产生之前就应该具有识别信号并处理信号的能力

在实际生活中,人们收到某种信号,但可能并不会立即处理因为此时还在处理其他事情。比如早晨闹钟响起,但我们很困,就会关掉闹钟继续睡觉了。但我们知道闹钟已经响起,起床是迟早的事情。

信号的保存

也有可能,进程已经收到信号但并不能立即处理,因为当前进程正在处理优先级更高的事情,只能将信号暂存起来,并等到合适的时候再进行处理。也就是说,进程具有暂时保存信号的能力

信号本身也是数据,可以被暂存在进程的 PCB 中。所以说,发送信号的本质就是向进程控制块task_struct中写入信号数据。PCB 只有操作系统能够读写,操作系统会向上提供多种信号的发送方式,但本质都是操作系统向进程发送信号

 

2. 信号的产生

Linux 系统中所有信号如图所示:共有62种信号,前31个是普通信号,后31个是实时信号,我们只学习普通信号。

2.1 注册信号处理动作

前面已经说过,进程在运行之前就必须能够分辨出哪个信号,以及指定对该信号的处理方式,以便信号到来之时,能够及时处理。

  • 分辨信号,进程能够分辨上述列表中的31种信号,这是系统设计好的。
  • 处理方式:可以执行默认动作,也可以通过signal接口来设置当前进程对某个信号的处理方式。

修改进程对信号signum的默认处理动作,为自定义的函数handler,该函数必须满足规定的函数声明形式。

NAMEsignal - ANSI C signal handling
SYNOPSIS#include <signal.h>typedef void (*sighandler_t)(int); //自定义函数指针sighandler_t signal(int signum, sighandler_t handler);
DESCRIPTIONsignal()  sets  the  disposition of the signal signum to handler, which is either SIG_IGN, SIG_DFL, or the address of a  programmer-defined  function (a "signal handler").The signals SIGKILL and SIGSTOP cannot be caught or ignored.
void handler(int signum) {printf("handle proc:%d, signum:%dn", getpid(), signum);
}
int main()
{signal(2, handler); //通过signal注册对2号信号的处理动作,并没有实际信号产生while (1) {printf("hello proc:%dn", getpid());sleep(1);}return 0;
}

如图所示,键盘输入^C能够对进程发送2号信号SIGINT,输入^C就会触发进程提前注册好的对2号信号的处理动作。

通过signal接口注册对某个信号的处理动作,相当于是一种预定机制,并没有产生任何实际信号。

处理信号的三种方案

进程收到的信号,就要对信号的处理,一般有三种处理情况:

  1. 默认动作:执行信号的默认注册行为,如SIGKILL的杀死自身进程。
  2. 忽略动作:忽略掉该信号,不做出任何反应,也是一种信号处理的方式。
  3. 自定义捕捉:执行用户注册的行为,即调用修改后的handler方法。

2.2 产生信号的方式

产生信号共有4种方式:

方式具体解释
系统指令输入指令kill或者键盘发送快捷键^C,^Z等。
进程异常进程运行中出现错误导致软硬件出现异常,被操作系统甄别并处理。
系统调用系统提供多种发送信号的接口,方便我们在代码中设置信号。
软件条件某些软件条件不具备,触发信号发送的条件,操作系统向进程发送信号。

虽产生信号的方式繁多,但其底层都是操作系统在向进程发送信号数据。

系统指令
输入含义
Ctrl+C^C向进程发送2号信号SIGINT,默认是终止进程。
Ctrl+^Quit向进程发送3号信号SIGQUIT,默认是终止进程并进行核心转储 Core Dump。
^Z向进程发送20号信号SIGSTP,默认是停止进程。

键盘产生的信号,只能作用于前台进程,后台进程由于不影响输入指令无法使用键盘发送信号。

$ ./a.out &   # 将进程放到后台执行
$ fg          # 将后台进程调至前台
$ kill -signum proc_pid

使用kill命令,向指定进程发送指定信号,如:

$ kill -9 12138 # 向pid为12138的进程发送9号信号

就算进程注册了对9号信号的处理,也无法生效,因为9号信号是无法被捕捉的。因为9号信号能够直接杀死进程,捕捉9号信号是很不安全的行为。

Linux信号说明

进程异常

如图所示,运行带有野指针等内存错误的程序,会出现进程异常崩溃的现象。报错也是Segmentation fault段错误。

实际上是进程收到了11号信号SIGSEVG而终止程序。

那为什么出现段错误的进程会收到11号信号呢?是因为硬件出现异常。CPU 在执行进程的时候发现了程序中存在段错误并记录下来,然后操作系统发现了 CPU 出现异常,故向该进程发出了11号信号。

软件上面的错误,通常会体现在硬件或其他软件上程序中存在异常问题,导致硬软件出现异常,致使操作系统向目标进程发送对应信号

Core Dump

Linux下,进程正常退出时,会设置其退出码和退出信号,进程异常退出时,会设置其退出信号,表明进程退出的原因。必要的话,操作系统会将进程在内存的中的数据以文件的形式转储在磁盘上,以便后期调试,并设置 core dump 标志位

core dump 可以帮助我们事后调试更加方便,可以直接显示错误出现的行数。

云服务器上默认关闭Core Dump 功能,设置方式如图所示:

waitpid可以获取子进程的退出信息,退出码,退出信号,还有就是Core Dump 标志位。如果进程异常退出,且core dump 标志位被设置的话,可能会形成core dump 文件。

if (fork() == 0) {while (1) {printf("hello proc:%dn", getpid());sleep(1);}exit(1);
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code: %dn" , (status >> 8) & 0xff);
printf("exit signal:%dn",  status       & 0x7f);
printf("core dump: %dn" , (status >> 7) & 1   );  

不是所有进程异常退出都会设置 core dump 标志位。

系统调用
kill

kill函数向指定进程发送指定信号。

NAMEkill - send signal to a process
SYNOPSIS#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig);
DESCRIPTIONThe  kill()  system  call  can  be  used  to  send any signal to any process group orprocess.
RETURN VALUEOn  success,  zero is returned.  On error, -1 is  returned, and errno is set appropriately.
int main(int argc, char* argv[])
{if (argc != 3) {printf("Usagent./test signum pidn");exit(1);}int signum = atoi(argv[1]);int pid = atoi(argv[2]);kill(pid, signum);printf("kill -%d %dn", signum, pid);return 0;
}
raise

raise函数给自身进程发送指定信号。

NAMEraise - send a signal to the caller
SYNOPSIS#include <signal.h>int raise(int signum);
DESCRIPTIONThe  raise()  function sends a signal to the calling process or thread.  In a single-
threaded program it is equivalent to
RETURN VALUE raise() returns 0 on success, and nonzero for failure.
int main()
{if (fork() == 0) {printf("hello raisen");raise(9);}int status = 0;waitpid(-1, &status, 0);printf("exit signal:%dn", status & 0x7f); //获取 signalreturn 0;
}
abort

abort函数给自身进程发送6号信号SIGABRT

NAMEabort - cause abnormal process termination
SYNOPSIS#include <stdlib.h>void abort(void);
DESCRIPTIONThe  abort()  first  unblocks the SIGABRT signal, and then raises that signal for thecalling process.  
RETURN VALUEThe abort() function never returns.
int main()
{if (fork() == 0) {printf("hello raisen");abort(9);}int status = 0;waitpid(-1, &status, 0);printf("exit signal:%dn", status & 0x7f); //获取 signalreturn 0;
}
软件条件

通过某种软件条件的触发,来促使信号的产生。如:系统层面上设置定时器,由某种操作导致条件不满足这些场景。最经典的例子是管道通信,读端已经关闭,写端会立马收到13号信号SIGPIPE而停止写入,这就是由于软件条件的不就绪,而触发产生信号。

系统调用alarm

我们可以通过系统调用alarm设置一个计时器,计时结束后向自身进程发送14号SIGALRM信号。

NAMEalarm - set an alarm clock for delivery of a signal
SYNOPSIS#include <unistd.h>unsigned int alarm(unsigned int seconds);
DESCRIPTIONalarm()  arranges for a SIGALRM signal to be delivered to the calling process in seconds seconds.If seconds is zero, any pending alarm is canceled.
RETURN VALUEalarm() returns the number of seconds remaining until any previously scheduled  alarmwas due to be delivered, or zero if there was no previously scheduled alarm.

    size_t ret = alarm(10);printf("alarm(10)->ret:%ldn", ret);ret = alarm(5); //重新设置闹钟,并返回上一个闹钟的剩余时间//如果没有之前没有设置过闹钟,则返回0printf("alarm(5) ->ret:%ldn", ret);

再次调用alarm就会覆盖之前的闹钟并重新设定。返回值是上一个alarm闹钟的剩余时间,如果在此之前没有设定过则返回0。

2.3 发送信号的实质

我们已经知道,产生信号实际是操作系统在向进程发送信号。信号本身也是数据,发送信号的本质就是向 pcb 中写入信号数据

如图所示,信号的编号是有规律的,从1到31依次排列。既然信号是写入到进程 pcb 中的,那么 pcb 一定要有对应的数据变量,来存储记录是否收到(二元性)了对应的信号。

这个数据变量就是整型变量位图,比特位的位置代表对应信号的编号,比特位内容代表是否收到该信号。

如图所示,进程中采用位图来标识进程是否收到某种信号。操作系统向进程发送信号也就是向进程 task_struct 的信号位图的对应位置写入1。

3. 信号的保存

前面已经说过,进程有可能已收到信号但不能立即处理,因为进程正在处理优先级更高的事情,所以我们要求,进程具有暂时保存信号的能力。

我们已经知道,信号是如何保存在进程 pcb 中的,为进一步深入信号的保存,先明确几个概念。

3.1 信号状态的概念

概念解释
信号递达实际执行信号的处理动作就叫做信号递达。一般有三种处理情况:默认,忽略,自定义捕捉。
信号未决信号产生后到递达前的状态称为信号未决。本质是信号被暂存在pcb的信号位图中。
信号阻塞系统允许进程暂时屏蔽某些信号即叫做阻塞。信号此时仍然是未决的,但因为被阻塞所以一直无法递达,直至解除阻塞方可被递达。

信号递达中的忽略动作代表信号已被处理,只是处理动作就是什么都不做。而阻塞表示信号暂时被屏蔽,信号被阻塞就代表信号不可能被递达,一直是未决的,直至解除阻塞方可递达。

这些概念对应的信号在 pcb 中的情况是怎样的呢?这就要深入探究 pcb 中用来保存信号的数据结构。

3.2 信号保存的方式

进程 pcb 中有三张表,分别是block表,pending表,handler表,如图所示:

表名内容本质
pending值为1表示收到信号,为0反之操作系统向进程发送信号,就是在写入 pcb 中的pending表,本质是一个无符号整数位图
block为0表示信号被阻塞,为1反之block表存储信号是否阻塞的信息,和pending一样是位图。因为其具有屏蔽信号的效果,block 表又被称为信号屏蔽字。
handler默认SIGDFL为1,忽略SIGIGN为0,自定义捕捉函数的地址handler表用来存储进程对指定信号的处理动作,由于有三种值所以不能再用位图存储,handler表本质是一个函数指针数组void *handler[31](int)

这三张表应该横着看,一行对应一个信号,首先看是否被屏,然后才是是否已发送,以及其处理方法。

如果信号未阻塞,且已收到,就能执行对应的处理方法。如果信号已阻塞,不管是否收到都不能执行处理方法。

//伪代码
if (block & signo) { //被阻塞//...
}
else { //未阻塞if (pending & signo) { //未阻塞且已收到handler_array[signo - 1](signo);return 0;}
}

系统中关于信号的各种宏值的定义如下图所示:

  1. 一个信号如果被 handler,那么其一定被 pending,且一定不能被 block。
  2. 一个信号如果被 block,即使其被 pending,但就是不能被 handler。
  3. 一个信号没有被 block,如果其被 pending,进程将会在合适的时候进行 handler。

进程有了这三张表,就能知道信号是是否被屏蔽,是否已发送,处理方法是什么,也就是说,此时进程具有识别信号的能力。

3.2 信号集的操作

上述保存信号的三张表都是内核中的信号数据结构,系统为用户提供了修改信号的系统调用和数据类型。

信号位图类型 sigset_t

为防止用户误操作导致系统错误,信号的各种操作必须使用系统提供的数据类型:

sigset_t set; //信号位图结构类型

block 表和 pending 表都是只记录信号的有效无效两种值,非零即一,无需记录信号的次数,所以这两种表可以采用位图的结构表示。

故未决和阻塞可以采用相同的数据类型sigset_t表示,虽然sigset_t类型的变量本质是位图,但不可以自行采用二进制运算,只能用系统提供的接口。sigset_t就是系统提供的信号位图的数据类型,配合它才能使用信号的各种系统调用接口:

信号调用接口

一般操作信号的步骤是:

  1. 先设置好用户栈中的信号位图变量,也就是使用sigset_t类型定义的用户栈变量。
  2. 再将该变量写入内核数据结构中。

以下是用来操作用户栈中的信号变量的一系列接口:

NAMEsigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set oper‐ations.
SYNOPSIS#include <signal.h>int sigemptyset ( sigset_t *set );  // 全部置0int sigfillset  ( sigset_t *set );  // 全部置1int sigaddset   ( sigset_t *set, int signo );  // 加入信号置1int sigdelset   ( sigset_t *set, int signo );  // 删除信号置0int sigismember ( const sigset_t *set, int signo );  // 判断是否存在
DESCRIPTIONsigemptyset() initializes the signal set given by set to empty. sigfillset() initializes set to full, including all signals.sigaddset() and sigdelset() add and delete respectively signal signum from set.sigismember() tests whether signum is a member of set.
RETURN VALUEsigemptyset(), sigfillset(), sigaddset(), and sigdelset() return 0 on  success  and-1 on error.sigismember()  returns 1 if signum is a member of set, 0 if signum is not a member,and -1 on error. 

以下是用来将定义的sigset_t信号变量设置进进程的数据结构的接口:

修改信号屏蔽集 sigprocmask

sigprocmask函数用来读取和修改进程中的信号屏蔽集位图的。

NAMEsigprocmask - examine and change blocked signals
SYNOPSIS#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
DESCRIPTIONsigprocmask() is used to fetch and/or change the signal mask of the calling thread.If oldset is non-NULL, the previous value of the signal mask is stored in oldset.
RETURN VALUEsigprocmask() returns 0 on success and -1 on error.  In  the  event  of  an  error,errno is set to indicate the cause.
  • set —— 输入型参数,传入需要设置进内核的栈位图变量,不需要可以设为NULL
  • oldset —— 输出型参数,输出内核中的原来信号屏蔽位图,不需要可以设为NULL
  • how —— 表示设置信号的方式,选项有三种:SIG_BLOCKSIG_UNBLOCKSIG_SETMASK
选项解释
SIG_BLOCK将 set 中值为1的信号添加进内核中,相当于`mask
SIG_UNBLOCK将 set 中值为1的信号在内核中解除阻塞,相当于mask&=(~set)
SIG_SETMASK直接将内核中的阻塞位图替换为 set,相当于mask=set
int main()
{sigset_t set;sigset_t oset;sigemptyset(&set);sigemptyset(&oset);sigaddset(&set, 2);sigaddset(&set, 3);sigaddset(&set, 9); //Errsigprocmask(SIG_BLOCK, &set, &oset); //屏蔽 2 3 9 号信号,9号无法被屏蔽while (1) {printf("hello blockn");sleep(1);}return 0;
}
读取信号未决集 sigpending

pending 表由操作系统自行管理无需设置,故sigpending不对系统 pending 位图做修改,只获取进程的 pending 位图。

NAMEsigpending - examine pending signals
SYNOPSIS#include <signal.h>int sigpending(sigset_t *set);
DESCRIPTIONsigpending()  returns the set of signals that are pending for delivery to the call‐ing thread  The  mask  of pending signals is returned in set.
RETURN VALUEsigpending() returns 0 on success and -1 on error.  In the event of an error, errnois set to indicate the cause.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int signum) {printf("hello signum:%dn", signum);
}
void show(sigset_t* set) {printf("pending->");int i = 0;for (i = 1; i < 32; i++) {printf("%d", sigismember(set, i));}printf("n");}
int main()
{signal(SIGINT, handler);sigset_t set;sigset_t oset;sigemptyset(&set);sigemptyset(&oset);sigaddset(&set, 2);sigaddset(&set, 3);sigaddset(&set, 4);sigprocmask(SIG_SETMASK, &set, &oset);printf("2) 3) 4) is blockedn");int cnt = 15;while (1) {sigpending(&set);show(&set);if (cnt-- == 0) {printf("2) SIGINT is unblockedn");sigprocmask(SIG_SETMASK, &oset, NULL); //解除2号信号的阻塞}sigemptyset(&set);sleep(1);}return 0;
}
修改信号处理集 sigaction

除了一开始学到的signal方法,还有一个就是sigaction,本质没有区别都是注册对单个信号的处理方法。

NAMEsigaction - examine and change a signal action
SYNOPSIS#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
DESCRIPTIONThe  sigaction()  system  call  is  used to change the action taken by a process onreceipt of a specific signal.  (See signal(7) for an overview of signals.)signum specifies the signal and can be any valid signal except SIGKILL and SIGSTOP.If act is non-NULL, the new action for signal signum is  installed  from  act.   Ifoldact is non-NULL, the previous action is saved in oldact.The sigaction structure is defined as something like:struct sigaction {void     (*sa_handler)(int); //处理方法sigset_t   sa_mask;int        sa_flags; //选项不关系,设置为0即可};
void handler(int signum) {printf("hello signum:%dn", signum);
}
int main()
{struct sigaction  act;struct sigaction oact;memset( &act, 0,  sizeof(act));memset(&oact, 0, sizeof(oact));act.sa_handler = handler;sigaction(2, &act, &oact);while (1) {printf("hello worldn");sleep(1);}return 0;
}

当某个信号的处理方法正在被调用时,系统会暂时将该信号屏蔽,等处理完该信号时,再对该信号解除阻塞。

 

4. 信号的处理

4.1 信号处理的时机

原先我们一直在讲进程会在合适的时候,处理未决的信号。现在,是时候谈谈“什么是合适的时候”和“为什么是合适的时候”了。

为什么是合适的时候?

信号的产生是异步的,信号产生时当前进程可能正在进行更重要的工作,信号必须要被延时处理,具体要取决于操作系统和进程的状态。

什么是合适的时候?

因为信号是被保存在进程的 pcb 的 pending 位图中的,操作系统检测信号位图的时候就是处理信号的时候。当进程从内核态返回到用户态的时候,会进行信号的检测和处理工作

内核态和用户态的概念

进程在执行代码时会不断的切换用户态和内核态,执行用户代码时处于用户态,执行操作系统的内核代码时处于内核态。

  • 执行用户态的代码时,进程必须为用户态,必须受到操作系统的监管。
  • 执行内核级的代码时,进程必须为内核态,提升身份权限以执行内核代码。再次执行用户代码时必须转为用户态,否则可能影响操作系统的安全。

当处理进程代码中的调用系统接口的时候,进程会从用户态转变为内核态,以进入内核执行内核代码。系统调用结束后会返回到用户代码中继续执行用户代码,进程也会从内核态转变为用户态。

调用系统调用接口,只是进程执行状态转变的一种体现,并不是只有这一种情况才会发生状态转变。

在系统中,用户和内核的代码和数据都是要被加载到内存中的,进程地址空间分为用户空间和内核空间,分别通过用户级页表和系统级页表映射到对应的物理内存上。

用户页表每个进程都有有一份,而内核页表只有一份,被所有进程空间共享。这样既保证了进程独立性,也保证了每个进程都能访问到内核代码。

4.2 信号处理的步骤

当进程从内核态返回到用户态的时候,操作系统会进行信号的检测和处理工作。具体如下,以调用系统接口为例:

  1. 用户代码中调用系统接口,进程由用户态进入内核态;
  2. 系统接口代码调用完毕后,检测进程信号是否需要处理,
    1. 如果信号的处理动作是默认或忽略,直接处理并返回即可。
    2. 如果信号的处理动作是自定义捕捉,则需要返回用户态,调用用户自定义的handler方法。
    3. 自定义捕捉方法执行完毕后,不可直接返回用户代码,需返回内核态,调用sys_sigreturn接口返回到用户代码中

进程进行信号检测处理的具体流程如上图所示,可高度抽象简化成莫比乌斯环或无穷大的符号:

每次经过分界线都是一次状态的切换,而交叉点可以看作信号检测并处理的点。

用户的代码只能由用户态执行,内核态时进程具有的权限较高,能够但不允许执行用户代码,因为用户代码不安全。

本文发布于:2024-02-01 08:58:30,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170674911035469.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:进程   信号   系统   Linux
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23