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;
}







相关文章
|
11天前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
32 5
|
11天前
|
Linux 应用服务中间件 nginx
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
20 3
|
12天前
|
Linux
Linux:守护进程(进程组、会话和守护进程)
守护进程在 Linux 系统中扮演着重要角色,通过后台执行关键任务和服务,确保系统的稳定运行。理解进程组和会话的概念,是正确创建和管理守护进程的基础。使用现代的 `systemd` 或传统的 `init.d` 方法,可以有效地管理守护进程,提升系统的可靠性和可维护性。希望本文能帮助读者深入理解并掌握 Linux 守护进程的相关知识。
27 7
|
17天前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
1月前
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
77 34
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
49 17
|
1月前
|
消息中间件 Linux C++
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
68 16
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
60 26
|
2月前
|
消息中间件 存储 网络协议
从零开始掌握进程间通信:管道、信号、消息队列、共享内存大揭秘
本文详细介绍了进程间通信(IPC)的六种主要方式:管道、信号、消息队列、共享内存、信号量和套接字。每种方式都有其特点和适用场景,如管道适用于父子进程间的通信,消息队列能传递结构化数据,共享内存提供高速数据交换,信号量用于同步控制,套接字支持跨网络通信。通过对比和分析,帮助读者理解并选择合适的IPC机制,以提高系统性能和可靠性。
250 14
|
2月前
|
消息中间件 Linux
Linux:进程间通信(共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
通过上述讲解和代码示例,您可以理解和实现Linux系统中的进程间通信机制,包括共享内存、消息队列和信号量。这些机制在实际开发中非常重要,能够提高系统的并发处理能力和数据通信效率。希望本文能为您的学习和开发提供实用的指导和帮助。
185 20