Linux -- 进程信号(2)

简介: 1. 信号量1.1 进程互斥概念两个或两个以上的进程,不能同时进入关于同一组共享变量的临界区域,否则可能发生与时间有关的错误,这种现象被称作进程互斥· 也就是说,一个进程正在访问临界资源,另一个要访问该资源的进程必须等待(任何时刻,都只允许一个进程在进行共享资源的访问)任何时刻都只允许一个进程在进行访问的共享资源叫做临界资源临界资源都是通过代码访问的,凡是访问临界资源的代码就叫做临界区一个程序,它要么完整的被执行,要么完全不执行的特性就叫原子性

4. 阻塞信号

4.1 相关概念

  1. 信号递达(delivery):执行信号的处理动作(默认处理:终止进程(Term,Core)、signal函数:自定义处理)

  2. 信号未决(pending):信号从产生到递达之间的状态(暂时保存

  3. 阻塞信号(block):被阻塞的信号产生时将保持在未决状态,直到进程解出对此信号
  4. 的阻塞才执行递达动作(阻塞和忽略是不同的,信号被阻塞就不会递达,忽略是递达后的一种处理动作(什么都不做的动作))

4.2 信号在内核中的示意图

微信图片_20230523230130.png进程维护三张表:

  1. pending表:位图结构;比特位的位置表示哪一个信号,比特位的内容表示是否收到信号
  2. block表:位图结构;比特位的位置表示哪一个信号,比特位的内容表示是否被阻塞
  3. handler表:函数指针数组;数组下标表示信号编号,数组下标对应的内容表示递达动作


如何理解:第一行中,block是0,pending是0,默认处理动作。第二行中block是1,pending是1,忽略来处理。第三行中block是1,pending是0,捕获信号自定义处理。

4.3 函数操作pending和block表

4.3.1 sigset_t信号集

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态**。阻塞信号集也叫做当**

前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。sigset_t来控制block和pending两个位图。

# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
 {
  unsigned long int __val[_SIGSET_NWORDS];
 } __sigset_t;

4.3.2 函数

int sigemptyset(sigset_t *set); //初始化set给出的信号集为空,并从该集合中排除所有信号

int sigfillset(sigset_t *set); //初始化set为full,包括所有信号

int sigaddset(sigset_t *set, int signum); //添加信号符号

int sigdelset(sigset_t *set, int signum); //删除信号符号

int sigismember(const sigset_t *set, int signum); //测试sgn是否是集合的成员

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); //检查和改变阻塞信号


  • sigprocmask函数

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信

号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后

根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。


how参数可选值 功能
SIG_BLOCK 添加信号屏蔽字信号,相当于mask = mask | set
SIG_UNBLOCK 删除信号屏蔽字信号,相当于mask = mask & ~set
SIG_SETMASK 设置信号屏蔽字为set所指向的值,相当于mask = set


  • 使用
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void showNew(sigset_t* newSet)
{
    int signalnum = 1;
    std::cout << "newSet:";
    for(; signalnum <= 31; ++signalnum)
    {
        if(sigismember(newSet, signalnum)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}
void showOld(sigset_t* oldSet)
{
    int signalnum = 1;
    std::cout << "oldSet:";
    for(; signalnum <= 31; ++signalnum)
    {
        if(sigismember(oldSet, signalnum)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}
int main()
{
    //栈操作-->并没有设置进进程
    sigset_t newSet, oldSet;
    sigemptyset(&newSet); //初始化
    sigemptyset(&oldSet);
    sigaddset(&newSet, 2); //将2号信号添加到newSet信号集
    //设置进进程
    sigprocmask(SIG_SETMASK, &newSet, &oldSet); //阻塞信号集被设置为参数集
    int time = 0;
    while(true)
    {
        showNew(&newSet); //这里打印是一直不变的,因为newSet和oldSet并没有改变
        showOld(&oldSet);
        ++time;
        if(time == 10) //不再屏蔽2号信号
        {
            sigprocmask(SIG_SETMASK, &oldSet, &newSet); //把old信号集设置到进程
        }
        sleep(1);
    }
    return 0;
}

4.3.3 sigpending未决信号集

函数:int sigpending(sigset_t *set);//检测未决信号(set参数是输出型参数

#include <iostream>
#include <signal.h>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
//任务:屏蔽二号信号不断获取进程pending信号集并不断打印,发送二号信号观察pending信号集变化并解出二号信号阻塞,递达处理动作是自定义动作
static void printtPending(const sigset_t &pending)
{
    std::cout << "PID:" << getpid() << " pending: ";
    for(int signalnum = 1; signalnum <= 31; ++signalnum)
    {
        if(sigismember(&pending, signalnum)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}
static void handler(int signalnum)
{
    std::cout << "catched:" << signalnum << std::endl;
}
int main()
{
    sigset_t newSet, oldSet;
    //初始化
    sigemptyset(&newSet);
    sigemptyset(&oldSet);
    //信号集中设置2信号
    sigaddset(&newSet, SIGINT); 
    //信号屏蔽字设置进进程中
    sigprocmask(SIG_BLOCK, &newSet, &oldSet);
    //获取进程pending信号集并打印
    int count = 0;
    signal(SIGINT, handler); //2号信号捕捉后执行自定义动作
    while(true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        //获取pending信号集
        int ret = sigpending(&pending);
        assert(ret == 0);
        (void)ret;
        //打印
        printtPending(pending);
        //解出对2号信号的屏蔽
        if(count++ == 10) 
        {
            std::cout << "SIGINT signal unblocked!" << std::endl;
            sigprocmask(SIG_SETMASK, &oldSet, nullptr); //对2号信号解出阻塞后,默认递达动作时终止进程
        }
        sleep(1);
    }
    return 0;
}

运行结果

微信图片_20230523231141.png

现象描述:给进程发送2号信号后,并没有采取信号默认处理方式而是处于信号未决状态,也就是本来没发送信号,此时发送2号信号后,屏蔽信号集中2号信号被设置,所以进程收到2号信号时被阻塞了,也就是处于未决状态没有递达,所以此时pending信号集的第2个比特位变成了1,过了10秒后信号递达先是解除2号信号阻塞,然后执行自定义处理动作,随后打印解出2号信号后的pending信号集。

5. 捕捉信号

5.1 引出

生活中,当我们和某个人说的十分重要的事的时候突然来了个电话,我们不会去立即处理,当和这个人说完事后再回电话处理。那么信号会被立即处理吗?也可能不会,但是当一个信号解除了阻塞状态时,就会立即递达。这里需要引出的问题就是,什么时候合适解出阻塞状态呢?正是进程从内核态用户态的时候,进程会在OS指导下进行信号的检测和处理(处理三种方式:默认、忽略、自定义行为处理)。用户态是执行用户的代码进程所处的状态,内核态是执行内核的代码进程所处的状态,这句话什么意思呢?我们在linux上写代码的时候往往会调用系统调用接口,这些系统接口是Linux操作系统中的代码来封装得到的,那么当我们写代码的时候就会会执行内核中的代码。 那么再回顾地址空间:

微信图片_20230523231259.png

所有进程的虚拟地址空间[0GB, 3GB]是不同的,每个进程都有自己的用户级页表

所有进程的虚拟地址空间[3GB, 4GB]是不同的,每个进程都有相同的内核级页表

OS运行的本质:都是在进程的虚拟地址空间运行

系统调用的本质:在进程自身地址空间中进行函数跳转并返回即可

OS本质?1.OS是软件,是systemd进程,只是这个进程是死循环 2. OS时钟每个很短时间给OS发送时钟中断,OS执行对应中断处理方法来检测当前进程时钟中断。进程如何被调度?时间片到了,进程对应的上下文等等保存并切换,选择合适用的进程(进程调度就是一个系统函数schedule()来完成的)


问题:既然进程中包含内核地址空间和用户地址空间,那么一个进程不就可以随意访问内核中的代码和数据吗?


这里为了解决这个问题就有了内核态和用户态的出现,怎么来识别身份的呢?CPU中有CR3寄存器,其中3表示用户态,0表示内核态,这里身份切换并不是我们用户来完成的,用户无法更改,所以,OS中的系统调用内部中会修改执行级别,这样就能进行访问内核中的代码了。

5.2 信号捕捉

微信图片_20230523231421.png

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码

是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行

main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号

SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler

和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返

回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复

main函数的上下文继续执行了。信号捕捉中用户态和内核态状态转换有四次转换

5.3 sigaction

函数:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //检测和更改信号,如果act不为nullptr,signum被设置到act中,如果oldact不为nullptr,之前的act会被保存到oldact中


oldact是输出型参数,act是输入型参数。其中struct sigaction结构体:

struct sigaction {
  void     (*sa_handler)(int);
    //sa_handler指定与signum相关联的操作,默认操作可以是SIG_DFL,忽略该信号的SIG_IGN,或者指向信号的指针处理函数。
  void     (*sa_sigaction)(int, siginfo_t *, void *);
  sigset_t   sa_mask;
    //sa_mask指定在execu‐期间应该被阻塞的信号的掩码(即,添加到调用信号处理程序的线程的信号掩码中)信号处理程序的连接。此外,触发处理程序的信号将被阻塞,除非使用了SA_NODEFER标志。
  int        sa_flags;
    //sa_flags指定一组修改信号行为的标志
  void     (*sa_restorer)(void);
};
  • 使用
include <iostream>
#include <cstring>
#include <csignal>
#include <cassert>
#include <unistd.h>
static void printtPending(const sigset_t &pending)
{
    std::cout << "PID:" << getpid() << " pending: ";
    for(int signalnum = 1; signalnum <= 31; ++signalnum)
    {
        if(sigismember(&pending, signalnum)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}
//现象:2/3/4/5号信号都被block了
//第一次发送2号信号,此时2号信号正在被自定义处理,在此期间如果再发送2号信号,此时再发送的2号信号就会被暂存处于pending状态,3/4/5号信号也会暂存
//pending信号集是在执行handler之前被置零的
static void handler(int signalnum)
{
    printf("PID:%d catched signalnum:%d\n", getpid(), signalnum);
    int time = 30;
    while(time--)
    {
        sigset_t pending;
        sigemptyset(&pending);
        sigpending(&pending);
        printtPending(pending);
        sleep(2);
    }
}
int main()
{
    std::cout << "Process PID: " << getpid() << std::endl;
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act)); //初始化
    memset(&oldact, 0, sizeof(oldact));
    act.sa_handler = handler; //对2信号递达后采用自定义处理动作
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask); //初始化
    sigaddset(&act.sa_mask, SIGQUIT); //3号信号屏蔽
    sigaddset(&act.sa_mask, SIGILL); //4号信号屏蔽
    sigaddset(&act.sa_mask, SIGTRAP); //5号信号屏蔽
    int ret = sigaction(SIGINT, &act, &oldact); //检测2号信号 --> 等价于signla(SIGINT)
    assert(ret == 0);
    (void)ret;
    while(true)
    {
        sleep(1);
    }
}

运行截图

微信图片_20230523231625.png

6. 其他知识

6.1 可重入函数

#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signalnum);
typedef struct singleLinkListNode
{
    struct singleLinkListNode* _next;
    int _val;
    singleLinkListNode(const int& val)
        :_val(val)
    {
        _next = nullptr;
    }
}node;
node* head = new node(0);
node node1(1), node2(2);
void printLink(node* phead)
{
    node* cur = phead;
    while(cur)
    {
        printf("node:%d->", cur->_val);
        cur = cur->_next;
    }
    std::cout << "nullptr" << std::endl;
}
void insert(node* newnode)
{
    newnode->_next = head;
    std::cout << "wait signal......." << std::endl;
    sleep(10);
     //10秒期间发送2号信号让其递达执行自定义处理动作
    head = newnode;
}
void handler(int signalnum)
{
    printf("PID:%d, call handler!\n", getpid());
    insert(&node2);
}
int main()
{   
    printf("Process PID:%d\n", getpid());
    signal(SIGINT, handler);
    insert(&node1);
    std::cout << "head: ";
    printLink(head);
    std::cout << "node1: ";
    printLink(&node1);
    std::cout << "node2: ";
    printLink(&node2);
    return 0;
}
  1. 画图理解

微信图片_20230523231750.png

运行结果:

微信图片_20230523231819.png

像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。


  • 符合以下条件之一则是不可重入

  1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的
  2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构


6.2 volatile关键字

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int quit = 0;
void handler(int signalnum)
{
    printf("change quit form 0 to 1\n");
    quit = 1;
}
int main()
{
    printf("Process PID:%d\n", getpid());
    signal(SIGINT, handler);
    while(!quit); //欺骗编译器
    printf("normal exit!\n");
    return 0;
}
//makefile
valatile_keyword:valatile_keyword.cc
  g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
  rm -f valatile_keyword

运行结果:

image-20230510200542633

其实gcc编译器有很多优化选项:-O1、-O2、-O3、-O0 (man gcc查找):

下面换用-O1优化选项进行编译,运行截图:

微信图片_20230523232007.png

  • 为什么这里-O2选项优化后发送2号信号并不会终止进程呢?

首先要知道上面代码哪里优化了,其实这里while(!quit)这个语句时别优化了,如何优化呢?CPU执行运算的时候,quit初始值为0,那么0就被Load到寄存器中,此时寄存器就是0值,当发送2号信号,quit被赋值变成1,但是这里寄存器中的值并随之改变,所以一直死循环。这里就是一个内存位置不可见的问题,怎么来解决这个问题呢?告诉编译器保证每次检测都要从内存中读取数据,不要让内存数据不可见。解决方法:变量前加上volatile关键字。volatile关键字作用:保证内存可见性。

6.3 SIGCHLD信号

引出:子进程退出,父进程如何得知的呢?父进程阻塞式等待或者非阻塞式等待都需要父进程主动检测,其实子进程退出的时候会向父进程发送SIGCHLD信号,父进程收到信号采用的是忽略的处理方式。验证SIGCHLD信号:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t id;
void handler(int signalnum)
{
    sleep(1);
    printf("catched a signal:%d, who:%d\n", signalnum, getpid());
    pid_t result = waitpid(-1, nullptr, 0); //等待任意子进程
    if(result > 0)
    {
        printf("wait success!, result:%d, id:%d\n", result, id);
    }
}
int main()
{   
    signal(SIGCHLD, handler);
    id = fork();
    if(id == 0)
    {
        int time = 5;
        while(time--)
        {
            printf("child process, PID:%d, PPID:%d\n", getpid(), getppid());
            sleep(1);
        }
        exit(1);
    }
    while(true)
    {
        sleep(1);
    }
    return 0;
}

运行结果:(监控脚本:examine.sh,使用:bash examine.sh)


微信图片_20230523232152.png

场景:假如如果有多个子进程同时退出呢?多个子进程同时退出会发送多个SIGCHLD信号,但是这里父进程的信号集中的SIGCHLD信号只有一个比特位来标记,所以此时就需要循环等待子进程来回收所有子进程(基于信号回收进程):

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t id;
void handler(int signalnum)
{
    sleep(1);
    printf("catched a signal:%d, who:%d\n", signalnum, getpid());
    while (true) //循环回收
    {
        pid_t result = waitpid(-1, nullptr, WNOHANG); //等待回收子进程
        if (result > 0)
        {
            printf("wait success!, result:%d, id:%d\n", result, id);
        }
        else
        {
            break;
        }
    }
    printf("handler done!\n");
}
int main()
{
    signal(SIGCHLD, handler);
    for (int i = 0; i < 5; ++i) //创建5个子进程
    {
        id = fork();
        if (id == 0)
        {
            int time = 5;
            while (time--)
            {
                printf("child process, PID:%d, PPID:%d\n", getpid(), getppid());
                sleep(1);
            }
            exit(1);
        }
    }
    while (true)
    {
        sleep(1);
    }
    return 0;
}

优雅的处理僵尸进程,直接让操作系统回收,而不是父进程等待回收(只保证Linux下有效):

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t id;
int main()
{
    signal(SIGCHLD, SIG_IGN); //收到SIGCHLD信号默认处理动作为忽略
    for (int i = 0; i < 5; ++i)
    {
        id = fork();
        if (id == 0)
        {
            int time = 5;
            while (time--)
            {
                printf("child process, PID:%d, PPID:%d\n", getpid(), getppid());
                sleep(1);
            }
            exit(1);
        }
    }
    while (true)
    {
        sleep(1);
    }
    return 0;
}







相关文章
|
29天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
66 1
|
18天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
83 13
|
25天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
1月前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
2月前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
153 4
linux进程管理万字详解!!!
|
2月前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
2月前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
90 8
|
2月前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
162 1
|
2月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
2月前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
76 4