IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 信号【1】-linux内核信号的处理过程 -> 正文阅读

[系统运维]信号【1】-linux内核信号的处理过程

系列文章目录

【1】linux内核信号的处理过程


前言

从今天开始在手撸一遍以前没写过的代码同时,认真的对其中的信号知识点做一次总结,主要为纵向和横向比较

  1. 如何使用信号编程以及陷阱在哪里?
  2. 信号从产生到死亡经历了什么?

21.1 设计信号处理器函数

说到信号处理函数的设计就不得不提:线程安全可重入函数异步信号安全。这是不同的三个概念。
线程安全针对竞争而言,数据需要共享就加锁,不需要共享就每个线程私有。
异步信号安全是指在线程编程时要处理异步的信号处理函数,在信号处理函数中以及线程中不管怎样调用你的函数都不会涉及可能变化的全局变量的访问,不发生死锁,就说明这个函数时异步安全的。
可重入函数一般说的可重入函数指的就是异步安全函数。事实上可重入函数又分弱可重入(线程安全),强可重入(异步信号安全)。那么一个函数时可重入的,那一定是线程安全的(反之不一定)。

  • 可重入函数一般需要满足

    • 不连续的调用长时间保持静态变量
    • 不返回指向静态变量的指针
    • 使用本地变量,或者通过制作静态变量的本地拷贝
    • 所有数据都由调用者提供
    • 绝不调用不可重入的函数
  • 线程安全但不可重入

    • 标准C的函数,因为标准C函数malloc/free等函数没有线程进程的概念,但是他们是线程安全的按不保证可重入

    21.1.1 Linux 多线程应用中编写安全的信号处理函数

    • 线程和信号
      linux多线程的应用中,每个线程可以通过pthread_sigmask()设置本线程的信号掩码。
      当一个线程调用pthread_create()创建新的线城时,本线程中的信号掩码被新创建的线程继承。
      应用程序也可以通过调用 pthread_kill(pthread_t thread, int sig) 将信号发送给指定的线程,则线号处理函数会在此指定线程的上下文背景中执行。

    • 安全的异步信号处理函数
      因为信号的异步事件,执行的上下文时不确定的。所以安全的异步信号处理函数有以下几种要求。

      • 信号处理器函数设置全局标志性变量越简单越好。主程序周期性对标志检测,一旦置位随即采取相应动作。
      • 从整个Linux应用的角度,应用使用了异步信号,程序中一些库函数可能会被中断返回异长,要考虑将其重新调用。
    • 在指定的线程中以同步的方式处理异步信号

    • 在指定的线程中处理信号
      sigwait
      sigwait() 提供了一种等待信号的到来,以串行的方式从信号队列中取出信号进行处理的机制。sigwait()只等待函数参数中指定的信号集,即如果新产生的信号不在指定的信号集内,则 sigwait()继续等待。对于一个稳定可靠的程序,我们一般会有一些疑问:

  • 多个相同的信号可不可以在信号队列中排队?

  • 如果信号队列中有多个信号在等待,在信号处理时有没有优先级规则?

  • 实时信号和非实时信号在处理时有没有什么区别?

  • 实时信号和非实时信号在处理时有没有什么区别?

笔者写了一小段测试程序来测试 sigwait 在信号处理时的一些规则。

清单 1. sigwait_test.c

#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void sig_handler(int signum)
{
    printf("Receive signal. %d\n", signum);
}

void* sigmgr_thread()
{
    sigset_t   waitset, oset;
    int        sig;
    int        rc;
    pthread_t  ppid = pthread_self();

    pthread_detach(ppid);

    sigemptyset(&waitset);
    sigaddset(&waitset, SIGRTMIN);
    sigaddset(&waitset, SIGRTMIN+2);
    sigaddset(&waitset, SIGRTMAX);
    sigaddset(&waitset, SIGUSR1);
    sigaddset(&waitset, SIGUSR2);

    while (1)  {
        rc = sigwait(&waitset, &sig);
        if (rc != -1) {
            sig_handler(sig);
        } else {
            printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
        }
    }
}


int main()
{
    sigset_t bset, oset;
    int             i;
    pid_t           pid = getpid();
    pthread_t       ppid;

    sigemptyset(&bset);
    sigaddset(&bset, SIGRTMIN);
    sigaddset(&bset, SIGRTMIN+2);
    sigaddset(&bset, SIGRTMAX);
    sigaddset(&bset, SIGUSR1);
    sigaddset(&bset, SIGUSR2);

    if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
        printf("!! Set pthread mask failed\n");

    kill(pid, SIGRTMAX);
    kill(pid, SIGRTMAX);
    kill(pid, SIGRTMIN+2);
    kill(pid, SIGRTMIN);
    kill(pid, SIGRTMIN+2);
    kill(pid, SIGRTMIN);
    kill(pid, SIGUSR2);
    kill(pid, SIGUSR2);
    kill(pid, SIGUSR1);
kill(pid, SIGUSR1);

    // Create the dedicated thread sigmgr_thread() which will handle signals synchronously
    pthread_create(&ppid, NULL, sigmgr_thread, NULL);

    sleep(10);

    exit (0);
}

程序编译运行在 RHEL4 的结果如下:

图 1. sigwait 测试程序执行结果sigwait 测试程序执行结果
从以上测试程序发现以下规则:

对于非实时信号,相同信号不能在信号队列中排队;对于实时信号,相同信号可以在信号队列中排队。
如果信号队列中有多个实时以及非实时信号排队,实时信号并不会先于非实时信号被取出,信号数字小的会先被取出:如 SIGUSR1(10)会先于 SIGUSR2 (12),SIGRTMIN(34)会先于 SIGRTMAX (64), 非实时信号因为其信号数字小而先于实时信号被取出。
sigwaitinfo() 以及 sigtimedwait() 也提供了与 sigwait() 函数相似的功能。

  • Linux 多线程应用中的信号处理模型
    在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑调用 sigwait()使用同步模型进行处理。其程序流程如下:
    • 主线程设置信号掩码,阻碍希望同步处理的信号;主线程的信号掩码会被其创建的线程继承;
    • 主线程创建信号处理线程;信号处理线程将希望同步处理的信号集设为 sigwait()的第一个参数。
    • 主线程创建工作线程。
      图 2. 在指定的线程中以同步方式处理异步信号的模型在指定的线程中以同步方式处理异步信号的模型
      代码示例
      以下为一个完整的在指定的线程中以同步的方式处理异步信号的程序。

主线程设置信号掩码阻碍 SIGUSR1 和 SIGRTMIN 两个信号,然后创建信号处理线程sigmgr_thread()和五个工作线程worker_thread()。主线程每隔10秒调用 kill() 对本进程发送 SIGUSR1 和 SIGTRMIN 信号。信号处理线程 sigmgr_thread()在接收到信号时会调用信号处理函数 sig_handler()。

程序编译:gcc -o signal_sync signal_sync.c -lpthread

程序执行:./signal_sync

从程序执行输出结果可以看到主线程发出的所有信号都被指定的信号处理线程接收到,并以同步的方式处理。

清单 2. signal_sync.c
#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
 
void sig_handler(int signum)
{
    static int j = 0;
    static int k = 0;
    pthread_t  sig_ppid = pthread_self(); 
    // used to show which thread the signal is handled in.
   
    if (signum == SIGUSR1) {
        printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
        j++;
    //SIGRTMIN should not be considered constants from userland, 
    //there is compile error when use switch case
    } else if (signum == SIGRTMIN) {
        printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
        k++;
    }
}

void* worker_thread()
{
    pthread_t  ppid = pthread_self();
    pthread_detach(ppid);
    while (1) {
        printf("I'm thread %d, I'm alive\n", ppid);
        sleep(10);
    }
}

void* sigmgr_thread()
{
    sigset_t   waitset, oset;
    siginfo_t  info;
    int        rc;
    pthread_t  ppid = pthread_self();

    pthread_detach(ppid);

    sigemptyset(&waitset);
    sigaddset(&waitset, SIGRTMIN);
    sigaddset(&waitset, SIGUSR1);

    while (1)  {
        rc = sigwaitinfo(&waitset, &info);
        if (rc != -1) {
            printf("sigwaitinfo() fetch the signal - %d\n", rc);
            sig_handler(info.si_signo);
        } else {
            printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
        }
    }
}


int main()
{
    sigset_t bset, oset;
    int             i;
    pid_t           pid = getpid();
    pthread_t       ppid;
    

    // Block SIGRTMIN and SIGUSR1 which will be handled in 
    //dedicated thread sigmgr_thread()
    // Newly created threads will inherit the pthread mask from its creator 
    sigemptyset(&bset);
    sigaddset(&bset, SIGRTMIN);
    sigaddset(&bset, SIGUSR1);
    if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
        printf("!! Set pthread mask failed\n");
    
    // Create the dedicated thread sigmgr_thread() which will handle 
    // SIGUSR1 and SIGRTMIN synchronously
    pthread_create(&ppid, NULL, sigmgr_thread, NULL);
  
    // Create 5 worker threads, which will inherit the thread mask of
    // the creator main thread
    for (i = 0; i < 5; i++) {
        pthread_create(&ppid, NULL, worker_thread, NULL);
    }

    // send out 50 SIGUSR1 and SIGRTMIN signals
    for (i = 0; i < 50; i++) {
        kill(pid, SIGUSR1);
        printf("main thread, send SIGUSR1 No. %d\n", i);
        kill(pid, SIGRTMIN);
        printf("main thread, send SIGRTMIN No. %d\n", i);
        sleep(10);
    }
    exit (0);
}
  • 注意事项
    在基于 Linux 的多线程应用中,对于因为程序逻辑需要而产生的信号,可考虑使用同步模型进行处理;而对会导致程序运行终止的信号如 SIGSEGV 等,必须按照传统的异步方式使用 signal()、 sigaction()注册信号处理函数进行处理。这两种信号处理模型可根据所处理的信号的不同同时存在一个 Linux 应用中:

    • 不要在线程的信号掩码中阻塞不能被忽略处理的两个信号 SIGSTOP 和 SIGKILL。
    • 不要在线程的信号掩码中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
    • 确保 sigwait() 等待的信号集已经被进程中所有的线程阻塞。
    • 在主线程或其它工作线程产生信号时,必须调用 kill() 将信号发给整个进程,而不能使用 pthread_kill() 发送某个特定的工作线程,否则信号处理线程无法接收到此信号。
    • 因为 sigwait()使用了串行的方式处理信号的到来,为避免信号的处理存在滞后,或是非实时信号被丢失的情况,处理每个信号的代码应尽量简洁、快速,避免调用会产生阻塞的库函数。
  • 小结
    在开发 Linux 多线程应用中, 如果因为程序逻辑需要引入信号, 在信号处理后程序仍将继续正常运行。在这种背景下,如果以异步方式处理信号,在编写信号处理函数一定要考虑异步信号处理函数的安全; 同时, 程序中一些库函数可能会被信号中断,错误返回,这时需要考虑对 EINTR 的处理。另一方面,也可考虑使用上文介绍的同步模型处理信号,简化信号处理函数的编写,避免因为信号处理函数执行上下文的不确定性而带来的风险。

22.3 可中断和不可中断的进程睡眠状态

由内核可知当使用阻塞式IO的时候,进程可分为可中断唤醒和不可中断唤醒,都是指的信号中断。

  • 那么在用户态进程和信号的关系是什么?
    其实内核经常令进程进入休眠状态,而休眠状态又分为两种。

    • TASK_INTERRUPTIBLE: 例如进程因为调用read等待数据到来而进入休眠状态,这个状态可长可短,这时候为该进程创造一个信号,进程被唤醒当前状态被打断而进入信号处理器。通过PS命令查看可被打断的进程其STAT段被标记为S。
    • TASK_UNINTERRUPTIBLE :进程正在等待某种特定类型的事件,将磁盘I/O读写完成,如果为该进程产生一个信号,那么在进程摆脱当前,信号会被阻塞不会传递给当前进程。通过PS命令查看可被打断的进程其STAT段被标记为D。

5. 信号从产生到死亡经历了什么?

22.5 信号的同步生成和异步生成

信号无论是从其他进程发送过来,还是由内核发送过来,都是无法预测的,都属于异步产生。然而有时候信号的产生者是自身信号并发送给自己。

  • 执行特定的机器语言指令,可导致硬件异常
  • 进程可使用kill,raise,killpg向自身发送信号

以上这两种信号就是同步产生。对于同步产生的信号,其行为不仅可以预测还可以重现。

22.6 信号传递的时机与顺序

  • 进程再调度超时后,再度获得调度时(即一个时间片开始时)
  • 系统调用完成时

22.6.1 进程如何发现和接受信号

这里主要讨论异步信号的传递。一个进程的不可能等待信号的到来也不知道信号什么时候到来。**信号的传递不是由进程传递的,而是由内核代理,**例如,进程p2向进程p1发送信号,内核将信号放在p1的信号队列中,等p1再次被调度时会首先执行,会调用do_notify_resume()来处理信号队列中的信号。信号处理主要就是调用sighand_struct结构中对应的信号处理函数。do_notify_resume()(arch/arm/kernel/signal.c)函数的定义如下:linux内核中的信号机制–信号处理

asmlinkage void
do_notify_resume(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
	if (thread_flags & _TIF_SIGPENDING)
		do_signal(&current->blocked, regs, syscall);
}

在这里插入图片描述

22.6.2 信号检测和响应时机

[root@localhost stack_dump]# cat /proc/20254/task/20255/stack
[<ffffffff81432637>] __skb_recv_datagram+0x237/0x290
[<ffffffff8149861b>] udp_recvmsg+0x8b/0x310
[<ffffffff8149f85a>] inet_recvmsg+0x5a/0x90
[<ffffffff81426693>] sock_recvmsg+0x133/0x160
[<ffffffff81426d5e>] sys_recvfrom+0xee/0x180
[<ffffffff8100b0d2>] system_call_fastpath+0x16/0x1b
[<ffffffffffffffff>] 0xffffffffffffffff
 
 
 
[root@localhost stack_dump]# kill -11 20255
[root@localhost stack_dump]# Stack trace (non-dedicated):
./funstack() [0x400c16]
/lib64/libpthread.so.0() [0x31cf40f7e0]
/lib64/libpthread.so.0(recvfrom+0x33) [0x31cf40eca3]
./funstack(func1+0xd8) [0x400de5]
./funstack(test_func+0xe) [0x400e11]
./funstack(task_entry+0x16) [0x400e29]
/lib64/libpthread.so.0() [0x31cf407aa1]
/lib64/libc.so.6(clone+0x6d) [0x31ce8e893d]
End of stack trace
recvfrom over

经过实验可知,信号检测并非再系统调用返回前夕而是在本进程再次获得调度机会时

22.6.3 进入信号处理函数

由以上可知,信号处理函数在用户态,但是发现信号处理函数在内核态,如何进程状态变换呢?
有以下图可知,系统先从内核态转到用户态,再回到内核态,再跳到用户态。
在这里插入图片描述
如图中所见,处理信号的整个过程是这样的:进程由于 系统调用或者中断 进入内核,完成相应任务返回用户空间的前夕,检查信号队列,如果有信号,则根据信号向量表找到信号处理函数,设置好“frame”(栈帧)后,跳到用户态执行信号处理函数。信号处理函数执行完毕后,返回内核态,设置“frame”,再返回到用户态继续执行程序。

22.6.4 为什么要设置frme,为什么执行完信号函数之后还要回到内核态?

什么叫Frame?

在调用一个子程序时,堆栈要往下(逻辑意义上是往上)伸展,这是因为需要在堆栈中保存子程序的返回地址,还因为子程序往往有局部变量,也要占用堆栈中的空间。此外,调用子程序时的参数也是在堆栈中。子程序调用嵌套越深,则堆栈伸展的层次也越多。在堆栈中的每一个这样的层次,就称为一个”框架”,即frame。

一般来说,当子程序和调用它的程序在同一空间中时,堆栈的伸展,也就是堆栈中框架的建立,过程主要如下:

call指令将返回地址压入堆栈(自动)
用push指令压入调用参数
调整堆栈指针来分配局部变量
为什么以及怎么设置frame?

我们知道,当进程陷入内核态的时候,会在堆栈中保存中断现场。因为用户态和内核态是两个运行级别,所以要使用两个不同的栈。当用户进程通过系统调用刚进入内核的时候,CPU会自动在该进程的内核栈上压入下图所示的内容:(图来自《Linux内核完全注释》)
在这里插入图片描述
在处理完系统调用以后,就要调用do_signal()函数进行设置frame等工作。这时内核堆栈的状态应该跟下图左半部分类似(系统调用将一些信息压入栈了):
在这里插入图片描述
在找到了信号处理函数之后,do_signal 函数首先把内核堆栈中存放返回执行点的 eip 保存为old_eip,然后将 eip 替换为信号处理函数的地址,然后将内核中保存的“原ESP”(即用户态栈地址)减去一定的值,目的是扩大用户态的栈,然后将内核栈上的内容保存到用户栈上,这个过程就是设置frame.值得注意的是下面两点:

之所以把EIP的值设置成信号处理函数的地址,是因为一旦进程返回用户态,就要去执行信号处理程序,所以EIP要指向信号处理程序而不是原来应该执行的地址。

之所以要把 frame 从内核栈拷贝到用户栈,是因为进程从内核态返回用户态会清理这次调用所用到的内核栈(类似函数调用),内核栈又太小,不能单纯的在栈上保存另一个frame(想象一下嵌套信号处理),而我们需要EAX(系统调用返回值)、EIP这些信息以便执行完信号处理函数后能继续执行程序,所以把它们拷贝到用户态栈以保存起来。

以上这些搞清楚之后,下面的事情就顺利多了。这时进程返回用户空间,就会根据内核栈中的EIP值执行信号处理函数。那么,信号处理程序执行完后,怎么返回程序继续执行呢?

22.6.5 信号处理函数执行完后怎么办?

信号处理程序执行完毕之后,进程会主动调用 sigreturn() 系统调用再次回到内核(可以通过strace来进行验证),查看有没有其他信号需要处理,如果没有,这时内核就会做一些善后工作,将之前保存的frame恢复到内核栈,恢复eip的值为old_eip,然后返回用户空间,程序就能够继续执行。至此,内核遍完成了一次(或几次)信号处理工作。

总结

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-05-12 16:43:29  更:2022-05-12 16:46:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 15:17:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码