当前位置:   article > 正文

【Linux】:信号与信号量以及信号捕捉与处理(为多线程和网络做准备)_linux捕获信号量

linux捕获信号量

在这里插入图片描述

一、什么是 Linux 信号

1.1 信号的定义和作用

信号的定义:信号是 Linux 操作系统中用于进程间通信、处理异常等情况的一种机制。它是由操作系统向一个进程或者线程发送的一种异步通知,用于通知该进程或线程某种事件已经发生,需要做出相应的处理
信号的作用:
进程间通信:进程可以通过向其他进程发送信号的方式进行通信,例如某个进程在完成了某项工作之后,可以向另一个进程发送 SIGUSR1 信号,通知其进行下一步的操作
处理异常:信号可以被用来处理程序中的异常情况,例如当一个进程尝试访问未分配的内存或者除以 0 时,系统会向该进程发送 SIGSEGV 或 SIGFPE 信号,用于处理这些异常情况
系统调试:信号可以用于程序的调试,例如在程序运行时,可以向该进程发送 SIGUSR2 信号,用于打印程序的状态信息等

1.2 信号的分类和编号

在 Linux 中,信号被分类为标准信号和实时信号,每个信号都有一个唯一的编号。标准信号是最基本的信号类型,由整数编号表示,编号范围是 1 到 31。实时信号是 Linux 中的扩展信号类型,由整数编号表示,编号范围是 32 到 64
下面是常见的信号编号和对应的信号名称
在这里插入图片描述
在这里插入图片描述

二、Linux 信号的发送和接收

2.1 发送信号的方法

在 Linux 中,进程可以通过向其他进程或自身发送信号的方式进行通信或处理异常情况。下面介绍几种常见的发送信号的方法

  1. kill 命令
    kill 命令是 Linux 中最常用的发送信号的命令,语法如下:
kill [-signal] PID
  • 1

其中,-signal 可选参数表示要发送的信号类型,如果省略该参数,则默认发送 SIGTERM 信号。PID 表示接收信号的进程 ID
例如,要向进程 ID 123 发送 SIGINT 信号,可以执行以下命令:

kill -SIGINT 123`在这里插入代码片`
  • 1
  1. kill 函数
    除了使用 kill 命令,程序中也可以通过 kill 函数来发送信号。kill 函数的原型如下:
int kill(pid_t pid, int sig);
  • 1

其中,pid 表示接收信号的进程 ID,sig 表示要发送的信号类型。如果函数调用成功,则返回 0,否则返回 -1 并设置 errno
例如,要向进程 ID 123 发送 SIGINT 信号,可以执行以下代码:

#include <signal.h>
#include <unistd.h>
 
int main() {
  pid_t pid = 123;
  int sig = SIGINT;
  if (kill(pid, sig) == -1) {
    perror("kill");
    return 1;
  }
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  1. raise 函数
    raise 函数是一个简单的发送信号的函数,可以用来向当前进程发送信号。raise 函数的原型如下:
int raise(int sig);
  • 1

其中,sig 表示要发送的信号类型。如果函数调用成功,则返回 0,否则返回 -1 并设置 errno
例如,要向当前进程发送 SIGTERM 信号,可以执行以下代码:

#include <signal.h>
 
int main() {
  int sig = SIGTERM;
  if (raise(sig) == -1) {
    perror("raise");
    return 1;
  }
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  1. pthread_kill 函数
    如果在多线程程序中需要向另一个线程发送信号,可以使用 pthread_kill 函数。pthread_kill 函数的原型如下:
int pthread_kill(pthread_t thread, int sig);
  • 1

其中,thread 表示接收信号的线程 ID,sig 表示要发送的信号类型。如果函数调用成功,则返回 0,否则返回错误码
例如,要向线程 ID 456 发送 SIGUSR1 信号,可以执行以下代码:

#include <pthread.h>
#include <signal.h>
 
void* thread_func(void* arg) {
  // 线程函数
  return NULL;
}
 
int main() {
  pthread_t tid = 456;
  int sig = SIGUSR1;
  if (pthread_kill(tid, sig) != 0) {
    perror("pthread_kill");
    return 1;
  }
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

2.2 接收信号的方法

在 Linux 中,进程可以通过注册信号处理函数来接收信号并处理。下面介绍几种常见的接收信号的方法

  1. signal 函数
    signal 函数可以用来注册信号处理函数。signal 函数的原型如下:
void (*signal(int sig, void (*handler)(int)))(int);
  • 1

其中,sig 表示要注册的信号类型,handler 是一个函数指针,指向信号处理函数。signal 函数返回一个函数指针,指向之前注册的信号处理函数。如果注册信号处理函数失败,则返回 SIG_ERR
例如,要注册 SIGINT 信号的处理函数,可以执行以下代码:

#include <stdio.h>
#include <signal.h>
 
void sigint_handler(int sig) {
  printf("Received SIGINT signal\n");
}
 
int main() {
  if (signal(SIGINT, sigint_handler) == SIG_ERR) {
    perror("signal");
    return 1;
  }
 
  // 程序主逻辑
 
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. sigaction 函数
    在Linux中,sigaction函数是用于设置和检索信号处理器的函数。
    sigaction函数有以下语法:
#include <signal.h>
 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 1
  • 2
  • 3

其中,sig 表示要注册的信号类型,act 是一个指向 struct sigaction 结构体的指针,表示新的信号处理函数和信号处理选项,oldact 是一个指向 struct sigaction 结构体的指针,用于获取之前注册的信号处理函数和信号处理选项
struct sigaction 结构体的定义如下:

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

其中,sa_handler字段指定信号处理函数的地址。如果设置为SIG_IGN,则表示忽略该信号。如果设置为SIG_DFL,则表示使用默认处理器,也可以自己设置需处理的函数逻辑
sa_sigaction字段指定一个信号处理器函数,这个函数包含三个参数:一个整数表示信号编号,一个指向siginfo_t结构体的指针,和一个指向void类型的指针
sa_mask字段指定了在执行信号处理函数期间要阻塞哪些信号
sa_flags字段是一个标志位,可以包括以下值:
SA_NOCLDSTOP:如果设置了该标志,则当子进程停止或恢复时不会生成SIGCHLD信号
SA_RESTART:如果设置了该标志,则系统调用在接收到信号后将被自动重启
SA_SIGINFO:如果设置了该标志,则使用sa_sigaction字段中指定的信号处理器
sa_restorer字段是一个指向恢复函数的指针,用于恢复某些机器状态
调用sigaction函数后,如果成功,则返回0,否则返回-1,并设置errno错误号。可以使用以下代码来检查errno:
例如,要注册 SIGINT 信号的处理函数,可以执行以下代码:

#include <stdio.h>
#include <signal.h>
 
void sigint_handler(int sig) {
  printf("Received SIGINT signal\n");
}
 
int main() {
  struct sigaction newact = {
     newact.sa_handler = sigint_handler
  };
  struct sigaction oldact;
 
  if (sigaction(SIGINT, &newact, &oldact) == -1) {
    perror("sigaction");
    return 1;
  }
 
  // 程序主逻辑
 
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在使用sigaction函数之前,应该先定义一个信号处理函数并将其注册。可以使用signal函数来注册一个信号处理函数,但是signal函数在某些情况下可能会出现问题,建议使用sigaction函数来注册信号处理函数
3. sigwait 函数
sigwait 函数可以用于阻塞等待一个或多个信号,并在信号到达时唤醒。sigwait 函数的原型如下

int sigwait(const sigset_t* set, int* sig)
  • 1
#include <stdio.h>
#include <signal.h>
 
int main() {
  sigset_t set;
  int sig;
 
  sigemptyset(&set);
  sigaddset(&set, SIGINT);
 
  if (sigwait(&set, &sig) == -1) {
    perror("sigwait");
    return 1;
  }
 
  printf("Received signal: %d\n", sig);
 
  // 程序主逻辑
 
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
#include <stdio.h>
#include <signal.h>
 
void sigint_handler(int sig) {
  printf("Received SIGINT signal\n");
}
 
int main() {
  if (signal(SIGINT, sigint_handler) == SIG_ERR) {
    perror("signal");
    return 1;
  }
 
  printf("Waiting for SIGINT signal...\n");
 
  pause();
 
  // 程序主逻辑
 
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

三、信号的处理函数

3.1 信号的默认处理函数

当程序收到一个信号时,如果没有设置该信号的处理函数,则会按照默认的处理方式进行处理。每个信号都有自己的默认处理方式,下面列出了一些常见信号的默认处理方式:
SIGHUP:终止进程
SIGINT:终止进程
SIGQUIT:终止进程,并生成 core dump 文件
SIGILL:终止进程,并生成 core dump 文件
SIGABRT:终止进程,并生成 core dump 文件
SIGFPE:终止进程,并生成 core dump 文件
SIGKILL:强制终止进程
SIGSEGV:终止进程,并生成 core dump 文件
SIGPIPE:终止进程
SIGALRM:终止进程
SIGTERM:终止进程
SIGUSR1:终止进程
SIGUSR2:终止进程
注意,有一些信号是不可以被捕获和处理的,例如 SIGKILL 和 SIGSTOP,这两个信号的处理方式无法被改变,只能直接终止进程
除了上述列举的常见信号,还有一些其他的信号也具有默认的处理方式,如下:
SIGBUS:终止进程,并生成 core dump 文件
SIGCHLD:忽略该信号,即不进行处理
SIGCONT:继续进程,如果进程被挂起,则恢复运行
SIGSTOP:暂停进程
SIGTSTP:暂停进程,并发送 SIGSTOP 信号给进程组中所有进程
SIGTTIN:暂停进程
SIGTTOU:暂停进程
SIGURG:忽略该信号
SIGXCPU:终止进程,并生成 core dump 文件
SIGXFSZ:终止进程,并生成 core dump 文件
需要注意的是,在 Linux 中,如果一个进程收到了某个信号,并且该信号的默认处理方式是终止进程,那么这个进程的资源可能不会被完全释放,这时需要进行清理工作,例如关闭文件、释放锁等,以避免资源泄漏
core dump文件的定义
当一个程序在运行时发生严重错误,如访问非法内存地址或除以零等,操作系统会向该程序发送一个信号,通知它出现了一个错误。如果程序没有处理该信号,操作系统会将该程序终止,并将程序的内存映像保存到一个称为 core dump 文件的文件中
Core dump 文件通常包含程序在崩溃时的内存快照和一些其他有用的调试信息,例如程序的寄存器状态、堆栈跟踪信息和调用栈信息等。这些信息可以帮助程序员在调试时定位错误
Core dump 文件通常非常大,可以是程序内存使用量的几倍甚至几十倍。因此,在生产环境中,可以通过禁用 core dump 生成来减少磁盘空间的使用。在开发和测试环境中,生成 core dump 文件可以提供有用的调试信息,帮助程序员解决问题
在Linux系统中,默认情况下,默认是关闭的,core dump 文件被保存在当前工作目录下的名为 core 或者 core. 的文件中。其中 是崩溃程序的进程ID。可以通过 ulimit -c unlimited 命令来打开 core dump 生成。此外,还可以使用 gdb 或其他调试工具来分析 core dump 文件中的信息

3.2 注册信号处理函数

在 Linux 中,可以通过 signal() 函数或者 sigaction() 函数来注册信号处理函数
signal() 函数用于注册信号处理函数,其原型如下:

typedef void (*sighandler_t)(int); 
sighandler_t signal(int signum, sighandler_t handler);
  • 1
  • 2

其中,signum 表示要注册的信号编号,handler 表示信号处理函数的地址。该函数返回一个指向原信号处理函数的指针,如果出现错误,则返回 SIG_ERR。
例如,下面的代码将捕获 SIGINT 信号,并在捕获到信号时打印一条消息:

#include <stdio.h>
#include <signal.h>
 
void sigint_handler(int signum)
{
    printf("Caught signal %d (SIGINT)\n", signum);
}
 
int main()
{
    signal(SIGINT, sigint_handler);
 
    while (1) {
        /* do something */
    }
 
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

3.3 信号处理函数的编写技巧

编写信号处理函数需要注意一些技巧,下面是一些建议:
简洁明了:信号处理函数应该尽量简短,只包含必要的操作,并且不能阻塞太长时间,否则可能会影响系统的稳定性
不要调用不可重入函数:因为信号处理函数是在异步上下文中执行的,不能保证当前进程处于什么状态,如果在信号处理函数中调用了不可重入函数,可能会导致死锁等问题
保证对共享数据的访问安全:如果信号处理函数需要访问共享数据,必须保证访问的安全性,通常可以使用互斥锁等机制来保护共享数据
不要在信号处理函数中使用动态分配内存:因为信号处理函数是在异步上下文中执行的,不能保证当前进程处于什么状态,如果在信号处理函数中使用动态分配内存,可能会导致内存泄漏等问题
使用 sigaction() 函数注册信号处理函数:相比于 signal() 函数,sigaction() 函数提供了更多的选项,例如是否启用信号排队等,更加灵活
在信号处理函数中尽量不要进行 IO 操作:因为 IO 操作是阻塞的,如果在信号处理函数中进行 IO 操作,可能会导致信号排队等问题
在信号处理函数中不要使用 printf() 等标准 IO 函数:因为这些函数是不可重入的,可能会导致死锁等问题
可以使用 siglongjmp() 函数来进行非局部跳转:在一些情况下,需要从信号处理函数中跳转到某个特定位置,可以使用 siglongjmp() 函数来实现非局部跳转
总之,在编写信号处理函数时需要注意保证代码的简洁明了,尽可能避免使用不可重入函数,保证对共享数据的访问安全,尽量不进行 IO 操作,使用 sigaction() 函数注册信号处理函数等

四、内核如何实现信号的捕捉

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

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

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo
是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传
出该信号原来的处理动作。act和oact指向sigaction结构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动
作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回
值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信
号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动
作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回
值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信
号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因
为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函
数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从
sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步
之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只
有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称
为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,
如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的
控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/785385
推荐阅读
相关标签
  

闽ICP备14008679号