linux信号的产生和保存

发布于:2025-07-24 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

信号

信号说明

前台进程与后台进程

产生信号

系统调用产生信号

软件条件产生信号

硬件异常产生信号

除0操作

野指针操作

Core Dump

信号的保存

sigset_t

信号集操作函数

sigemptyset

sigprocmask

sigpending

sigaddset

sigismember


信号

讲进程信号之前,我们需要先做一个预备工作。讲一下信号基本结论。

1.

进程为什么能识别信号?处理信号?

OS程序员在设计进程的时候,早已内置了信号的识别和处理方式。也就是说在进程在没有信号产生的时候,早就知道信号如何处理了。

2.

进程是怎么处理信号的呢?

进程在收到信号,可能不会立即处理,也可能等一会再处理,只会在合适的时候处理信号。

处理信号的方式有三种:默认处理动作,自定义信号处理动作,忽略处理。其中自定义处理信号也叫信号捕捉。

一个简单样例:

Ctrl+c本质是给前台进程发送2号信号,前台进程收到信号,从而进程退出。

信号说明

man 7 signal中都有详细说明!

注意:1~31是普通信号,34~64是实时信号。

每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,信号的本质其实就是一个数字!

自定义信号处理函数:

再看样例:

执行:

要注意的是,signal函数仅仅是设置了特定信号的捕捉⾏为处理⽅式,并不是直接调⽤处 理动作。如果后续特定信号没有产⽣,设置的捕捉函数永远也不会被调⽤!!

前台进程与后台进程

1.

不管是前台进程还是后台进程,都可以向标准输出上打印,而前台进程本质就是要从键盘获取数据的。命令行shell进程就是一个典型的前台进程,⼀个命令后⾯加个&可以放到后台运⾏。

2.

我们知道,父进程fork一个子进程,父进程先退出了,子进程会自动变成后台进程,后台进程只能使用kill命令才能收到信号。

3.

前台进程是不会能被暂停的,当使用ctrl+z(SIGSTOP,19号信号)暂停,这个进程就会被提到后台。

信号是进程之间事件异步通知的⼀种⽅式,属于软中断。

下面通过三个步骤详细说明信号:信号的产生,信号的保存,信号的处理。

产生信号

产生信号的方式有多种,上述是键盘产生信号,还有系统调用产生信号,也可以使用kill命令来向进程发送信号,当然也可以异常发送信号。

注意:不管哪种方式产生信号,本质上都是OS给进程发送信号的。

系统调用产生信号

一个函数:kill函数

使用:

void hander(int signumber)
{
    printf("我是进程,pid:%d,我获得了信号:%d\n",getpid(),signumber);
}
int main()
{
    signal(SIGINT,hander);
    while(true)
    {
        sleep(1);
        printf("我是进程,pid:%d\n",getpid());
        printf("发送了一个信号\n");
        kill(getpid(),SIGINT);//发送信号
    }
    return 0;
}

我们可以模拟一个kill,实现自己的kill命令:

//testsig.cc
void hander(int signumber)
{
    printf("我是进程,pid:%d,我获得了信号:%d\n", getpid(), signumber);
}
int main()
{
    signal(SIGINT,hander);
    while(true)
    {
        sleep(1);
        printf("我是进程,pid:%d\n",getpid());
    }
}
//mykill.cc
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        std::cout<<"输入错误"<<std::endl;
        return 1;
    }
    pid_t target=std::stoi(argv[1]);
    int signum=std::stoi(argv[2]);
    kill(target,signum);
    return 0;
}

一个函数:raise函数

可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)。

使用:

void hander(int signumber)
{
    printf("我是进程,pid:%d,我获得了信号:%d\n",getpid(),signumber);
}
int main()
{
    signal(SIGINT,hander);
    while(true)
    {
        sleep(1);
        printf("我是进程,pid:%d\n",getpid());
        raise(2);//不断发送2号信号
    }
    return 0;
}

一个函数:abort函数

abort函数是给当前进程发送SIGABRT信号(6号信号),虽然会捕捉,但是还是会退出。

使用:

void hander(int signumber)
{
    printf("我是进程,pid:%d,我获得了信号:%d\n", getpid(), signumber);
}
int main()
{
    signal(SIGABRT, hander);
    abort();//发送6号信号
    return 0;
}

我们看到虽然自定义捕捉了,但是还是会退出!

以上例子就是系统调用可以产生信号。

软件条件产生信号

之前学习管道的时候,就已经接触过了软件条件产生信号。

当一个进程向管道写入,另一个进程向管道读数据,当写端进程不写了,或者被杀掉了,读就没有意义了,OS就会向读端进程发送SIGPIPE信号(13号信号),终止掉这个进程。

一个函数:alarm函数

使用:我们可以利用这个函数体会IO效率问题。

IO多:

int main()
{
    int count = 0;
    alarm(1);
    while (true)
    {
        count++;
        std::cout << "count:" << count << std::endl;
    }
    return 0;
}

在这1s内,count会一直往显示器上打印,我们使用的是云服务器,运行的count值需要通过网络传到本地,然后IO到本地显示器,所以才只有count值只有3w。

IO少:

int count = 0;
void hander(int signumber)
{
    std::cout << "count:" << count << std::endl;
    exit(1);
}
int main()
{
    signal(SIGALRM, hander);
    alarm(1);
    while (true)
        count++;
    return 0;
}

在这1s内,count一直运算,最后才会从网络传到本地,再IO到显示器,最后才会打印,比较快,所以count有4亿多。

一个函数:pause函数

使用:

void hander(int signumber)
{
    std::cout<<"signumber:"<<signumber<<std::endl;
    alarm(1);
}
int main()
{
    signal(SIGALRM,hander);
    alarm(1);
    while(true)
        pause();
    return 0;
}

这段代码可以每隔1s就发送一个信号,完成一些任务。

硬件异常产生信号

硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前 进程执⾏了除以0的指令,CPU的运算单元会产⽣异常,内核将这个异常解释为SIGFPE信号发送给进 程。再⽐如当前进程访问了⾮法内存地址,MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送 给进程。

除0操作

void hander(int signumber)
{
    printf("我是进程,pid:%d,我获得了信号:%d\n", getpid(), signumber);
    sleep(1);
}
int main()
{
    signal(SIGFPE,hander);
    //Floating point exception 表示浮点数异常
    sleep(1);
    int a=1;
    a/=0;//浮点数异常信号,8号信号
    return 0;
}

野指针操作

void hander(int signumber)
{
    printf("我是进程,pid:%d,我获得了信号:%d\n", getpid(), signumber);
    sleep(1);
}
int main()
{
    signal(SIGSEGV,hander);
    //Segmentation fault,表示段错误 
    int* p=nullptr;
    *p=100;//野指针,OS发送段错误信号,11号信号
    return 0;
}

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层⾯上,是被当成信号处理的。

为什么叫硬件异常?

cpu在调度进程时,会将进程的变量,存放在cpu的寄存器上,当数据出现除0(其实也是溢出)或野指针(cpu在利用虚拟地址通过页表访问其物理地址时,没有物理地址),OS就会给进程发送对应的信号。这就是典型的硬件中断触发的信号。

Core Dump

  • ⾸先解释什么是CoreDump。当⼀个进程要异常终⽌时,可以选择把进程的⽤⼾空间内存数据全部 保存到磁盘上,⽂件名通常是core,这叫做CoreDump。
  • 进程异常终⽌通常是因为有Bug,⽐如⾮法内存访问导致段错误,事后可以⽤调试器检查core⽂件以 查清错误原因,这叫做 Post-mortem Debug (事后调试)。
  • ⼀个进程允许产⽣多⼤的 core ⽂件取决于进程的 中)。默认是不允许产⽣ core ⽂件的,因为 Resource Limit (这个信息保存在PCB core ⽂件中可能包含⽤⼾密码等敏感信息,不安全。
  • 在开发调试阶段可以⽤ 改变 S ulimit 命令改变这个限制,允许产生core文件

主要用途就是调试。

core和term区别:

例子:

当子进程异常退出的时候,父进程等待子进程,有个status,core dump标志。

int main()
{
    if (fork() == 0)
    {
        sleep(1);
        int a = 10;
        a /= 0;
        exit(0);
    }
    int status = 0;
    waitpid(-1, &status, 0);
    printf("exit signal: %d, core dump: %d\n", status & 0x7F, (status >> 7) & 1);
    return 0;
}

信号的保存

  • 实际执⾏信号的处理动作称为信号递达(Delivery)
  • 信号从产⽣到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动 作。

理解:

1.

信号的编号表示三张表的下标,前两张表都可以看成位图,而handler表存放的是函数指针,block表示信号阻塞(阻塞为1),pending表示信号是否收到(收到为1),handler表示信号的动作(忽略,默认,自定义),其实也就是函数。

2.

handler表中,SIG_DFL和SIG_IGN都是宏,分别表示默认(0),忽略(1).

3.

举个例子:当有一个信号到来时,pending表就会将对应的信号比特位变成1,如果这个信号没有被阻塞,就会执行对应的信号动作,在执行动作之前,会先将pending表对应信号比特位由1变0,再去执行。

sigset_t

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

信号集操作函数

sigset_t类型对于每种信号⽤⼀个bit表⽰“有效”或“⽆效”状态,⾄于这个类型内部如何存储这些 bit则依赖于系统实现,从使⽤者的⻆度是不必关⼼的,使⽤者只能调⽤以下函数来操作sigset_t变量, ⽽不应该对它的内部数据做任何解释,⽐如⽤printf直接打印sigset_t变量是没有意义的。

  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表⽰该信号集不包含 任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表⽰该信号集的有效信号 包括系统⽀持的所有信号。
  • 注意,在使⽤sigset_t类型的变量之前,⼀定要调⽤sigemptyset或sigfillset做初始化,使信号集处于 确定的状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删 除某种有效信号。
  • 这四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信 号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

sigemptyset

sigprocmask

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

如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是⾮空指针,则更改 进程的信号屏蔽字,参数how指⽰如何更改。如果oset和set都是⾮空指针,则先将原来的信号屏蔽字 备份到oset⾥,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了 how参数的可选值。

如果调⽤sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀ 个信号递达。

sigpending

sigaddset

sigismember

使用:

void PrintPending(sigset_t &pending)
{
    printf("我是一个进程(%d), pending: ", getpid());
    for (int signo = 31; signo >= 1; signo--)
    {   //查询某个信号是否在pending表中
        if (sigismember(&pending, signo))
            std::cout << "1";//在打印1
        else
            std::cout << "0";//不在打印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); 
    std::cout << "#######################" << std::endl;
}
int main()
{
    signal(SIGINT, handler);
    // 1. 屏蔽2号信号
    sigset_t block, oblock;
    //初始化
    sigemptyset(&block);
    //初始化
    sigemptyset(&oblock);
    // 将2号信号添加进block中
    sigaddset(&block, SIGINT); 
    //将block覆盖原有的block表,oblock就是原有的
    //oblock参数是一个输出型参数
    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. 打印pending
        PrintPending(pending);
        if (cnt == 10)
        {
            // 5. 恢复对2号信号的block情况
            std::cout << "解除对2号的屏蔽" << std::endl;
            sigprocmask(SIG_SETMASK, &oblock, nullptr);
        }
        sleep(1);
        cnt++;
    }
    return 0;
}

我们下期见!!!


网站公告

今日签到

点亮在社区的每一天
去签到