0
点赞
收藏
分享

微信扫一扫

linux之信号操作(九千字长文详解)

linux之信号操作

sigset_t

这是信号在内核中的表示

image-20230804111055508

==block和pending都是位图——即用bit位来表示信号编号!==

每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用==相同的数据类型sigset_t==来存储,==sigset_t称为信号集==,这个类型可以表示每个信号 的“有效”或“无效”状态(在软件层面上就是将bit为从0置为1,或者从1置为0,硬件上就是从充放电来表示)

在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。

信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释(如用printf直接打印sigset_t变量是没有意义的)

sigprocmask

==这个函数的作用就是用来更该进程的block表!==——那个进程调用就修改那个进程!

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

sigpengding

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

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

我们接下来可与用上面的函数做一些实验!

我们知道,一般情况:我们所有的信号都是不被阻塞的!如果一个信号被阻塞,那么该信号就不会被抵达!如果收到了为阻塞的信号,那么就会被存入pending位图里面!

#include<iostream>
#include <signal.h>
#include<string>
#include<vector>
#include<unistd.h>
// #define BLOCK_SIGNAL 2
#define MAX_SIGNUM 31

static std::vector<int> sigv = {2};//想要屏蔽那些信号往这个数组加就可以了!
static void show_pending(const sigset_t& pending)
{
       std::string show;
       for(int i = MAX_SIGNUM;i>=1;i--)
       {
           if(sigismember(&pending,i) == 1)
               show +='1';
           else 
               show +='0';
       }
       std::cout << show << std::endl;
}
int main()
{
       //首先尝试屏蔽2号信号!
       sigset_t set,oset,pending;
       //1.1初始化!
       sigemptyset(&set);//全部设为0
       sigemptyset(&oset);
       sigemptyset(&pending);
       //1.2添加要屏蔽的信号
       for(auto e:sigv)
       {
           sigaddset(&set, e); // 在set信号集将e号信号色设置为1
       }
       //前面这一堆动作是没有影响该进程信号屏蔽字的!——只是在用户层构建一个信号集!
       //1.3屏蔽信号!
       sigprocmask(SIG_BLOCK,&set,&oset);//将信号集设置进内核!

       //遍历打印所有的pending信号集!
       while(true)
       {
           //2.1获取
           sigpending(&pending);//获取该进程的pending表
           //2.2打印pending表!
           show_pending(pending);
           sleep(1);
       }
       return 0;
}

image-20230804165111603

==我们发现当我们使用ctrl+c发送2号信号后,2号信号就被阻塞!然后pending表里面的2更好信号位置就变了1!==

sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

和signal一样这也是一个信号捕抓函数!功能也是一样的!即对特定信号设定特定的回调方法!——但是sigaction有更详细的选项设置

==首先我们就要认识一下struct sigaction这个结构体==

//The sigaction structure is defined as something like:
struct sigaction {
    void     (*sa_handler)(int);//设置普通信号的回调函数
    void     (*sa_sigaction)(int, siginfo_t *, void *);//设置实时信号的回调函数
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

上面我们说过,信号处理的时候是不能去屏蔽同信号(因为会被自动的接触!)那么如果我们想要在处理本信号的时候,顺便屏蔽其他类型的信号呢?——可以的,我们可以添加进==sa_mask==里面,这样就是这个成员变量的作用!

#include<signal.h>
#include<iostream>
#include<unistd.h>
#include<cstdio>
using namespace std;
void Count(int cnt)
{
    while(cnt)
    {
        printf("cnt:%d",cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
    printf("\n");
}
void handler(int signo)
{
    cout << "get a signo :" << signo << "is running"<< endl;
    Count(20);
}
int main()
{
    struct sigaction act,oact;
    act.sa_flags = 0;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);    
    //我们直接向
    sigaddset(&act.sa_mask,3);//将三号信号设置进mask里面!
    //这样子在除了屏蔽2号信号本身,还会将3号信号给屏蔽!
    sigaction(SIGINT,&act,&oact);

    while(true)
    {
        sleep(1);
    }
    return 0;
}

image-20230807112442518

在处理2号信号的时候,2,3号信号就都被屏蔽了!

image-20230807112544560

==只有处理完2号信号,操作系统才会将2号信号和3号信号都解除屏蔽!——此时3号信号就可以抵达了!这样子进程就被终止了!==

总结

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。

如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字

可重入函数

假设有一个链表,我们要对其进行头插!

image-20230807161922843

正常看起来是这样没错!

==但是假如出现了一些情况!——插入的第一步的时候因为某些原因触发了信号!而信号handler是我们的自定义行为,里面一个也有插入会发生什么呢?==

image-20230807163435618

==我们发现,虽然都执行成功了!——但是插入顺序却出现问题了!导致了我们内存节点丢失!==

之所以会出现这个问题,是因为==main执行流和信号捕抓执行流,两套不同的执行流,重复进入了同一个函数!==导致了代码结果出现了未定义或者出错的情况!——我们将这种函数称之为==不可重入函数!==

volatile

这是一个C语言的的关键字!——不过我们一般很少用!

==这个关键字的作用是保持内存的可见性!——这个说法听起来很抽象!==

我们下面的例子来说明这个

#include <stdio.h>
#include<signal.h>
int quit = 0;

void handler(int signo)
{
    printf("%d 号信号,正在被捕抓\n",signo);
    printf("quit : %d",quit);
    quit = 1;
    printf("-> %d\n", quit);
}
int main()
{
    signal(2,handler);    
    while(!quit);
    printf("注意!该进程是正常退出的!\n");
    return 0;
}

image-20230807204655656

代码运行的结果不出我们所料!

SIGCHLD

这个信号是和进程等待有关系

如果一个子进程退出了!——那么这个子进程就会变成僵尸状态,然后让父进程读取它,获取子进程的退出码和退出信号!

如果一个子进程没有退出——那么父进程就要阻塞或者非阻塞的等待子进程!

==当子进程退出的时候,它不会直接就退出,然后进入僵尸状态,而是它在进入僵尸状态的时候!它会告诉父进程它的状态!==

==那么它是怎么告诉父进程的呢?——通过发送信号的方式!发送SIGCHLD信号!(17号信号)==

image-20230808095753533

我们可以看到SIGCHLD的行为是Ign(ignore the signal)忽略该信号!这个忽略是内核级别的忽略

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
void Count(int cnt)
{
while(cnt)
{
printf("cnt:%2d\r",cnt);
fflush(stdout);
cnt--;
sleep(1);
}
printf("\n");
}
void handler(int signo)
{
printf("pid: %d , %d 号信号,正在被捕抓\n",getpid(),signo);
}
int main()
{
signal(SIGCHLD,handler);
printf("我是父进程!pid : %d,ppid: %d\n", getpid(), getppid());

pid_t id = fork();
while(id == 0)
{
printf("我是子进程!我要退出了!pid : %d,ppid: %d\n",getpid(),getppid());
Count(5);
exit(1);
} 
while(1) sleep(1);

return 0;
}

image-20230808101930323

==用上面断点我们可以看出来!——子进程退出之后!父进程确实会捕抓子进程发出的信号!==

那么如果知道这一点的话有什么用处呢?

如果不知道这一点,我们想要知道子进程退出什么时候退出!我们只能主动的去调用waitpid和wait这样的函数!——无论是阻塞等待还是非阻塞等待

==但是现在我们知道了!我们其实可以不用去主动关心子进程!等子进程退出了!那么它就会自己给父进程发送信号!这时候父进程再去回收子进程!==

void handler(int signo)
{
//我们是可以在handler里面直接调用wait或者waitpid的!
}

但是这样子代码的健壮性不好!

==情况1:假如我们有很多的子进程!(例如:10个)在同一时刻同时退出==

那么就会向父进程同时发送10个SIGCHLD信号!==但是上面我们讲过当正在处理一个信号的时候,操作系统会自动屏蔽同类型的信号!而且因为pending表只有一份!后续的9个信号也只能被保存一份!==——所以在handler里面直接调用wait或者waitpid是不好的!

所以最好的方式是通过循环等待!——因为我们并不知道有几个进程退出了!所以只能while(1) --> waipid式的等待

举报

相关推荐

0 条评论