Linux进程信号二

发布于:2025-03-11 ⋅ 阅读:(13) ⋅ 点赞:(0)

1.软件条件产生信号

SIGPIPE是一种由软件条件产生的信号,在管道中出现过。

alarm函数

设置一个定时器,当时间到达时,会向进程发送SIGALRM信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

seconds参数:表示定时时间,单位为秒,指定了从调用函数开始,经过多少秒后向进程发送SIGALRM信号

返回值:如果之前设置过定时器,返回值为之前定时器剩余的时间,如果没有设置定时器就返回0

接收信号后,默认处理操作是终止当前进程。

alarm验证IO效率问题

// IO 多
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
    int count = 0;
    alarm(1);
    while(true)
    {
        std::cout << "count : "<< count << std::endl;
        count++;
        }
    return 0;
}
//IO少
#include <iostream>
#include <unistd.h>
#include <signal.h>
int count = 0;
void handler(int signumber)
{
    std::cout << "count : " <<
    count << std::endl;
    exit(0);
}
int main()
{
    signal(SIGALRM, handler);
    alarm(1);
    while (true)
    {
        count++;
    }
    return 0;
}

IO效率问题

这两个程序的主要区别在于IO操作的频率和方式。在第一个程序中,每次循环都会执行一次IO操作,这大大降低了程序的性能。而在第二个程序中,IO操作只在信号处理函数中执行一次,这大大提高了程序的性能。

这是因为IO操作通常涉及到磁盘、网络等外部设备的交互,这些设备的速度通常比CPU慢得多。因此,频繁的IO操作会大大降低程序的性能。相反,如果尽量减少IO操作,程序的性能就会大大提高。

设置重复闹钟

包装三个函数,然后设置一秒闹钟,pause函数会阻塞住,直到接收到了信号,到了后就指向自定义方法,返回后就指向pause后面的代码。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <vector>
#include <functional>
using func_t = std::function<void()>;
int gcount = 0;
std::vector<func_t> gfuncs;
// 把信号 更换 成为 硬件中断
void hanlder(int signo)
{
for(auto &f : gfuncs)
    {
        f();
    }
    std::cout << "gcount : " << gcount << std::endl;
    int n = alarm(1); // 重设闹钟,会返回上⼀次闹钟的剩余时间
    std::cout << "剩余时间 : " << n << std::endl;
}
int main()
{
gfuncs.push_back([](){ std::cout << "我是⼀个内核刷新操作" << std::endl; });
gfuncs.push_back([](){ std::cout << "我是⼀个检测进程时间⽚的操作,如果时间⽚到了,我会切换进程" << std::endl; });
gfuncs.push_back([](){ std::cout << "我是⼀个内存管理操作,定期清理操作系统内部的内存碎⽚" << std::endl; });
    alarm(1); // ⼀次性的闹钟,超时alarm会⾃动被取消
    signal(SIGALRM, hanlder);
    while (true)
    {
        pause();
        std::cout << "我醒来了..." << std::endl;
        gcount++;
    }
    return 0;
}

pause函数

使进程挂起,直到有信号为止

#include <unistd.h>
int pause(void);

理解软件条件

在操作系统中,信号的软件条件是由软件内部状态或特定软件操作触发的信号产生机制。有定时器超时,软件异常(向已经关闭的管道写数据,产生SIGPIPE信号)等。软件条件满足时,操作系统会向相关进程发送相应的信号,以通知进程进行相应的处理。

理解系统闹钟

系统闹钟,本质是OS必须自身具有定时功能,并能让用户设置这种定时功能。

多个定时器的出现,就要对定时器进行管理,内核中定时器的数据结构是:

struct timer_list {
struct list_head entry ;
        unsigned long expires;
        void (*function)( unsigned long );
        unsigned long data;
        struct tvec_t_base_s * base ;
};

比如用堆结构来进行管理,把所有的闹钟按小堆的形式管理起来,堆顶就是最小的时间的闹钟,就去堆顶数据,处理这个闹钟,因为它是最先到时间的,然后剩下的结点就重新排序,每次都取堆顶的数据处理。

 

保存信号

信号的一些相关概念

实际执行信号的处理动作叫信号递达(Delivery)

信号从产生到递达之间的状态,叫信号未决(Pending)

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

注意:阻塞和忽略是不同的,只要信号阻塞就不会递达,而忽略是在递达之后的一种处理动作

内核中表示

信号在内核中的表示示意图

pending就是前面提到的位图,block也是一个位图,可以表示第几个信号以及阻塞情况。 

每个信号都有俩个标志位分别表示阻塞(block)和未决(Pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置改信号的未决标志,直到信号递达才清楚该标志。在上图的例子,SIGHUP信号为阻塞也为产生过,当它递达时就是默认处理动作。

所以信号有或者没有,对应的处理方法是确定的。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达,虽然处理方法是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

如果进程解除对某信号的阻塞之前,这种信号产生过多次,POSIX.1允许系统递送该信号一次或者多次,如果是常规信号则在递达之前只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

函数执行中默认和忽略方法的地址是位于0和1的位置

sigset_t信号集

 

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

信号集操作函数

sigset_t类型对于每种信号用一个bit表示有效和无效,这个类型内部如何存储这些bit则依赖于系统实现。

1.sigemptyset函数

#include <signal.h>
int sigemptyset(sigset_t *set);

作用:初始化或者清空一个信号集

参数:传sigset_t类型信号集的指针

返回值:成功0,失败-1

2.sigfillset函数

#include <signal.h>
int sigfillset(sigset_t *set);

作用:将所有可能的信号都加入到信号集中

参数:set指向一个sigset_t类型信号集指针

返回值:成功1,失败-1

signo:要从信号集中删除的信号编号

3.sigaddset函数

#include <signal.h>
int sigaddset(sigset_t *set, int signo);

作用:向已经有的信号集中添加特定的信号

参数:signo要添加到信号集的信号编号

返回值:成功1,失败-1

4.sigdeleset函数

#include <signal.h>
int sigdelset(sigset_t *set, int signo);

作用:从信号集中删除某一个特定的信号

参数:signo要从信号中删除的信号编号

返回值:成功1,失败0

5.sigismember函数

#include <signal.h>
int sigismember(const sigset_t *set, int signo);

作用:检查一个特定的信号是否在信号集中

参数:signo要检查的信号编号

返回值:如果信号是信号集的成员返回1,不是为0

注意:

在使⽤sigset_ t类型的变量之前,⼀定要调 ⽤sigemptyset或sigfillset做初始化,使信号集处于 确定的 状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删 除某种有效信号。

sigprocmask函数

调⽤函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:how是指定信号集进行操作的方式,有:

  • SIG_BLOCK:将 set 参数中指定的信号添加到当前的信号屏蔽集中,阻塞这些信号。

  • SIG_UNBLOCK:将 set 参数中指定的信号从当前的信号屏蔽集中移除,允许这些信号。

  • SIG_SETMASK:用 set 参数中指定的信号集替换当前的信号屏蔽集。

set:指向sigset_t类型的信号集指针,指定要操作的信号集。

oldset:指向sigset_t类型信号集的指针,用于存储操作前的当前信号集。

sigpending函数

获取当前进程中被阻塞且未决的信号集,就是发送信号还没有处理的信号

#include <signal.h>
int sigpending(sigset_t *set);

示例代码

先用sigaddset函数把2号改成1,阻塞集中的2号就变为1了,然后用sigprocmask函数把我们想要的阻塞集传过去,然后一直打印pending的信号集,计数到10把2号变回0,注意,如果在计数未到十时,发送了信号2,这是打印的第二位就是1,然后等十到的时候,就会立即执行处理方法,执行完后,pending表的2号位置就重新变为0,处理完才变回0的。

void PrintPending(sigset_t &pending)
{
    printf("我是一个进程(%d), pending: ", getpid());
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

void handler(int sig)
{
    std::cout << "#######################" << std::endl;
    std::cout << "递达" << sig << "信号!" << std::endl;
    //sigset_t pending;
    //int m = sigpending(&pending);
    //PrintPending(pending); // 0000 0010(处理完,2号才回被设置为0),0000 0000(执行handler方法之前,2对应的pending已经被清理了)
    //std::cout << "#######################" << std::endl;
}

int main()
{
    signal(SIGINT, handler);
    // 1. 屏蔽2号信号
    sigset_t block, oblock;
    sigemptyset(&block);
    sigemptyset(&oblock);

    sigaddset(&block, SIGINT); // 已经对2号信号进行屏蔽了吗?没有!
    // for(int i = 1; i<32; i++)
    //     sigaddset(&block, i);

    int n = sigprocmask(SIG_SETMASK, &block, &oblock);
    (void)n;

    // 4. 重复获取打印过程
    int cnt = 0;
    while (true)
    {
        // 2. 获取pending信号集合
        sigset_t pending;
        int m = sigpending(&pending);

        // 3. 打印
        PrintPending(pending);
        if (cnt == 10)
        {
            // 5. 恢复对2号信号的block情况
            std::cout << "解除对2号的屏蔽" << std::endl;
            sigprocmask(SIG_SETMASK, &oblock, nullptr);
        }

        sleep(1);
        cnt++;
    }

    return 0;
}

Core和term

SIGINT的默认操作处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。

Core Dump是一个进程异常终止时,可以选择把进程的用户空间内存数据全部保持到磁盘上,核心转储,文件名通常是core。

进程异常终止通常是因为有bug,事后可以通过调试器检查core文件来弄清楚错误原因,Post-mortem Debug

一个·进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件可能包含用户密码等敏感信息,不安全,且要是错误多次就会产生多个core文件,占满磁盘空间。

在开发调试阶段可以⽤ ulimit 命令改变这个限制,允许产⽣ core ⽂件。 ⾸先⽤ ulimit 命令
改变 Shell 进程的 Resource Limit ,如允许 core ⽂件最⼤为 1024K: $ ulimit -c
1024

 

代码示例

#include <iostream>
#include <cstdio>
#include <vector>
#include <functional>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        sleep(2);
        printf("hello bit\n");
        printf("hello bit\n");
        printf("hello bit\n");
        printf("hello bit\n");
        printf("hello bit\n");
        int a = 10;
        a /= 0;
        printf("hello bit\n");

        exit(1);
    }
    int status = 0;
    waitpid(id, &status, 0);
    printf("signal: %d, exit code: %d, core dump: %d\n",
           (status & 0x7F), (status >> 8) & 0xFF, (status >> 7) & 0x1);
}

下图是一起16位的,虽然分成俩分了。

在计算机中,当您使用按位与操作符 `&` 时,您实际上是在对两个操作数的每一位进行逻辑与操作。操作数的大小可以不同,但操作会基于每个位的位置来进行。

对于您提到的 `status & 0x7F` 操作:

- `status` 是一个16位的值,其中包含了子进程的退出状态信息。
- `0x7F` 是一个8位的掩码,其二进制表示为 `0111 1111`。

当您执行 `status & 0x7F` 时,您实际上是在对 `status` 的低8位进行掩码操作,因为 `0x7F` 只有低8位是1,高8位是0。这意味着:

- `status` 的低8位将与 `0x7F` 的对应位进行逻辑与操作。
- `status` 的高8位将与 `0x7F` 的高8位(即0)进行逻辑与操作,结果为0,因此不会影响最终结果。

所以,`status & 0x7F` 的确是用来提取 `status` 的低8位信息的,而忽略高8位。这是在处理16位或更多位宽的数值时常见的一种操作,用于提取或修改特定部分的位。

对于您的问题,`status` 是一个16位的值,但 `0x7F` 只影响低8位,因此结果是只留下低8位与8位掩码进行按位与操作的结果,高8位的信息在这个操作中被忽略。

希望这次的解释更加清晰。如果还有其他问题或需要进一步的解释,请随时告诉我。

补充:

对于共享内存,shmget可以构建一个结构体,里面有struct file指针,就执行一个文件,而进程里面也有一个struct file,这样就可以把这俩个file都指向同一个地方,并把vm area struct结构体的start和end都初始化,就可以虚拟地址和物理地址进行映射了,就把这个共享内存挂载到了进程地址空间里面,另一个进程也这样做就可以两个进程都有同一块共享内存,进而可以通信了。