Linux C 系统编程(08)进程管理 信号与信号处理

该系列文章总纲链接:专题分纲目录 LinuxC 系统编程​​​​​​​


本章节思维导图如下所示(思维导图会持续迭代):

第一层:

Linux C 系统编程(08)进程管理 信号与信号处理_第1张图片

第二层:

Linux C 系统编程(08)进程管理 信号与信号处理_第2张图片


信号与信号处理

信号是一种典型的异步通讯方式,也是linux下常用的进程通讯方式之一,也称做软中断。利用kill -l命令可以查看系统所支持的信号列表。一般有:

Linux C 系统编程(08)进程管理 信号与信号处理_第3张图片


1 信号基础

1.1 信号的产生

linux下有5种方式可以产生信号。

  1. 对终端进行操作,比如ctrl+C、ctrl+D。
  2. 硬件异常。
  3. 执行命令kill发送信号给一个进程。
  4. 调用kill函数发送命令给另一个进程。
  5. 当内核检测到某种软件条件发生的时候,可以通过信号通知进程。

注意:无论上述5种方法哪一种发送了信号,接收信号的进程都会暂停执行程序,转而处理接收到的信号。如果进程处于就绪状态/阻塞状态,那么一旦得到CPU的时间片将首先处理信号。如果进程处于挂起状态,那么接收信号将唤醒挂起进程,进程将首先处理信号。总之就是优先处理信号的原则,哪怕程序是死循环。

1.2 信号的处理

linux下进程有3种处理方式:

  1. 忽略此信号:置之不理。
  2. 注册一个信号处理函数:要求内核在接收到信号后切换到用户态调用该处理函数,这种方式叫做捕捉信号。用户程序经常需要对某些信号做一些自定义处理来解决问题。
  3. 执行系统默认动作:不同的信号有不同的动作,系统所知用的默认动作有两种:终结进程/忽略信号。

1.3 常用信号的使用方法

系统设置的向进程发送信号的快捷键。

ctrl+C:结束当前的进程。
ctrl+/:程序运行结束,并且产生了core文件。core文件用于程序的调试,可以用gdb工具对其进行调试。

使用kill命令向一个指定的进程发送指定信号。


2 信号的影响

不论当前进程执行到何处,都会跳到信号处理函数中执行,所以信号也会产生副作用,这是避免不了的。例如,在一个进程中,main函数、其他线程以及信号处理函数都是各自独立的执行流程,它们是并行的。如果一个进程有多个执行流程并且这些流程访问相同的资源,就有可能出现冲突。

2.1 重入

基本概念:

  • 重入:对于一个函数,有不同的执行流程调用,有可能第一次调用还没有返回就又一次进入该函数。
  • 可重入函数:如果一个函数只访问局部变量/参数,则称为可重入函数/纯代码/线程安全的函数。
  • 非可重入函数:函数访问了全局的变量/参数,以及可能因为重入而导致错误的函数。

如果一个函数满足以下条件之一则是不可重入的:

  • 使用了全局数据,包括全局变量和静态变量。
  • 调用动态方法得到内存,因为动态分配内存的方法也是以链表来管理内存分配的,这种书据也是全局作用域的。
  • 使用了标准I/O库,标准I/O库中很多实现都以不可重入的方式使用全局数据结构。

总之,使用了具有全局作用域的数据/函数都是不可重入的,这种函数/代码被称为非纯代码。

2.2 原子操作

  • 对于一个进程的一些操作而言,有时候为了防止信号对其造成的影响,指令需要一次执行完,这也就是原子操作的由来。所谓原子操作,是相对汇编来说的一个指令。
  • 对于赋值语句,要想得到原子操作,需要使用类型:sig_atomic_t。
  • 对于具有多个流程的程序,sig_atomic_t和volatile这两个关键字总是同时使用。
  • 关键字volatile的作用是防止优化。

2.3 中断系统调用

  • 使用signal函数加载的信号处理程序总是会在信号处理结束后重新启动被中断的系统调用。
  • 使用sigaction函数可以设置信号处理程序返回的时候是否重启被中断的系统调用。

3 信号处理函数

3.1 设置信号处理函数

linux下允许用户提供自己的信号处理函数,使用signal函数将处理函数加载,并且通知系统:

#include 
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

详细见linux函数参考手册。实际上在signal.h文件中是这样的:

#define SIG_IGN ((void *) (*)()) 1
#define SIG_DFL ((void *) (*)()) 0
#define SIG_ERR ((void *) (*)()) -1

系统中信号处理程序的整体框架是这样的:

void signal_handler(int signo)
{
   switch(signo){
   case SIG1:          /*处理信号1*/
        ...
   case SIG2:          /*处理信号2*/
        ...
   case SIGn:          /*处理信号n*/
        ...
   }
}

linux系统下不允许用户创建新的信号,但是提供两个信号SIGUSR1和SIGUSR2专门用于应用程序之间进行信号通讯。这两个信号没有特殊的意义,系统默认的方式是忽略。

3.2 发送信号与向进程本身发送信号

@1 信号可以很好地应用到进程间的通信。linux下使用kill函数向进程/进程组发送信号,函数的原型如下:

#include 
#include 
int kill(pid_t pid, int sig);

详细见linux函数参考手册。一个进程向另一个进程发送信号时必须注意以下几点:

  1. 该进程有向指定进程发送信号的权限。如果信号可以随便发送,会给恶意程序带来可乘之机。
  2. 系统进程不能接收信号。恶意程序会导致系统崩溃。
  3. 根用户可以向系统内任意进程发送信号,因此根用户可以杀死任何恶意进程,但是同时得到根用户权限的恶意程序也可以杀死系统内其他的进程。
  4. 进程也可以用kill(getpid(),signo)来给自己发送信号signo。

@2 linux下单独提供了向进程本身发送信号的函数,该函数可以替代上述形式。函数原型如下:

#include 
int raise(int sig);

详细见linux函数参考手册。利用raise函数可以实现exit函数的功能,所不同的是它不做任何善后处理(比如冲洗流、关闭文件等)。因此,如果信号的处理方式是终结进程,那么应该编写一个处理所有善后事宜的函数作为此信号的信号处理程序,使用signal函数设置后使用。

3.3 设置linux定时器

有些场合需要设置一个定时器,经过若干时间后通知设置定时器的进程。linux下使用alarm函数设置一个定时器,函数原型如下:

#include 
unsigned int alarm(unsigned int seconds);

详细见linux函数参考手册。alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。当然,有些点还是需要注意的:

  1. 一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。
  2. alarm信号经过指定的秒数后,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一些时间,因此alarm函数所设置的不会是一个十分精确的定时器。
  3. alarm函数也可以用来实现定时阻塞,即:当需要读写的设备未就绪时,只等待有限的时间。当然,对于一个负载不是很严重的系统这样做是可以的,但是一旦负载很严重,那么当alarm函数设置了定时器后失去了CPU权限,由于系统中的年代该执行的进程太多,导致alarm函数容易超时,那么一些操作便不再受alarm函数的约束。

3.4 挂起进程

当一个进程的运行条件已经具备时仍然需要使进程阻塞(例如期望进程延迟执行,sleep函数经常起到这个作用),这种由进程资源进入阻塞状态的1情况称为进程挂起。linux下使用pause函数实现挂起一个进程,函数原型如下:

#include 
int pause(void);
/*pause函数执行成功返回-1,同时将errno设置为EINTR。
pause函数使调用该函数的进程进入挂起状态,直到有一个终止进程的信号/一个信号处理程序从其返回后,pause函数才返回。*/

详细见linux函数参考手册。

3.5 进程休眠

pause函数是使进程无时间限制的挂起,若想使进程在一定时间内恢复运行则使用sleep函数,函数原型如下:

#include 
unsigned int sleep(unsigned int seconds);
int usleep(useconds_t usec);

详细见linux函数参考手册。


4 信号集与屏蔽信号

4.1 信号集和信号集处理函数

屏蔽信号是linux系统下一个重要的功能,允许用户进程有选择的接收并处理信号。需要屏蔽的信号使用信号集表示。这种集合是一种位向量,其中每一位对应一个信号。这种数据类型是sigset_t。对信号进行操作不可以直接进行移位,需要系统函数来实现相关功能。在linux下,提供了一组信号集处理函数:

#include 
int sigemptyset(sigset_t *set);     //将信号集清空
int sigfillset(sigset_t *set);          //将所有信号添加到信号集
int sigaddset(sigset_t *set, int signum);     //将信号signum添加到信号集set中
int sigdelset(sigset_t *set, int signum);     //将信号signum从信号集set中删除
int sigismember(const sigset_t *set, int signum);     //在信号集set中查看signum信号是否被设置,被设置函数返回1,没被设置函数返回0,查看失败返回-1。

详细见linux函数参考手册。

注意:信号的编号是从1开始的,而信号处理函数中的位编号是从0开始的,因此有:

signum==信号编码-1

4.2 屏蔽信号

程序中需要对信号进行处理,自然就有一些情况不需要对信号进行处理。所以需要屏蔽信号的函数。每个进程都有一个信号屏蔽字,标记被屏蔽的信号。信号屏蔽字的本质是同信号集一样的,是一个位向量,信号编码对应的位是1表示屏蔽该信号,对应的位是0表示处理该信号。linux下使用sigpromask函数实现检测或更改进程的信号屏蔽字。sigpromask函数的原型如下:

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

详细见linux函数参考手册。(注意:SIG_KILL和SIG_STOP这两个信号不能被屏蔽。如果这两个信号被屏蔽,恶意进程就不用担心被根用户的kill命令杀死而可以肆意破坏了)

4.3 处理未决信号

信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间;可以通过 int sigpending (sigset_t *set) 和 int sigismember (const sigset_t *set, int signum) 查看某个信号是未决的。linux下使用sigpending函数来检查未决信号,函数的原型如下:

#include 
int sigpending(sigset_t *set);

详细见linux函数参考手册。

你可能感兴趣的:(Linux,系统)