Liunx:信号

发布于:2025-02-10 ⋅ 阅读:(36) ⋅ 点赞:(0)

        信号是进程的内置属性之一,即一个进程可以识别信号,也可以对信号有相应的响应能力。除了进程之间传递消息属于进程间的通信,进程之间相互协调配合工作也属于通信,通过信号,来发出和处理响应,完成进程之间的协调配合。

1-31是普通信号。9号 19号信号不可被捕捉。剩余的为实时信号,也就是当信号发生时需要立即做出响应的信号

 


自定义信号处理 

signal()库函数。传入信号编号,以及相应的处理函数.SIG_DFL为默认处理动作,SIG_IGN为忽略信号,不做任何处理。两个宏被定义为0和1,进而被强转为void (*)(int) 的函数指针类型。对应着PCB中信号处理方法表中的相应信号处理函数。

#include <iostream>
#include<signal.h>
#include<unistd.h>

using namespace std;

void myhander(int sig)  //自定义处理函数
{
    cout<< "收到信号:"<<sig<<endl;
}

int main()
{

    signal(SIGINT,myhander); //捕捉信号
    while(true)
    {

        cout<<"hello\n"<<endl;
        sleep(1);
    }
    return 0;

}

 

        signal系统调用在代码中设置一次就会全局有效 。自定义函数的sig参数用处在于,可以将所有信号的自定义处理都写在一个函数中,根据sig参数对不同的信号分别做处理。

        硬件之间的交互包括协调工作和数据的传递。一个问题在于硬件之间如何知道何时以及从哪个外设读取数据?一个硬件向另一个硬件发送通知,这是硬件中断完成的任务。当代的计算机系统中,当外设中存在数据时,就会向CPU发送硬件中断,以此让操作系统完成数据的拷贝。当外设数量较多时,给每个外设分配一个中断号,硬件中断时将自己的中断号发送给CPU。这就解决了硬件之间的数据何时读取,从哪读取。

        在软件层面上,每次OS开机启动时会构建一张中断向量表,表中保存着直接访问外设的方法地址。这些地址指向OS中的某个地址,该地址对应着某个方法,这些方法是OS本身包含的。当硬件向CPU发送中断号时,OS把该中断号作为索引,从向量表中找到对应的方法来实现对硬件的操作。这个方法的执行才是真正的从外设中拷贝数据到内存。

        各种外设都有针脚集成在电路板上,这些针脚与CPU的针脚间接相连,硬件通过这些相连的针脚可以向CPU发送硬件中断。这是在硬件层面上完成的此种功能,而信号就是在软件层面上模拟这种功能。

        从键盘上输入一些组合控制键,首先键盘会产生中断信号,OS会判断这些组合键,转而变为软件层面的信号发送给进程。硬件之间的交互通过产生中断来引起相应程序的执行,而如见之间的交互,诸如一个进程告知另一个进程数据读取完毕等,是通过信号来完成。硬件中断和信号实际上就是实现软件或者硬件的协调工作。 

        信号的产生和代码执行是异步的,属于软中断。

 

信号产生

信号产生的方式:1.键盘组合键 (如ctrl + z 19号信号暂停进程)2.kill 指令 3. 系统调用 kill()、库函数 raise()、abort() 4.异常 软件中断

系统调用

        raise() 向它的调用者发送指定信号。相当于kill(getpid(),signo)。

         引起一个进程终止。谁调用abort,该函数就向谁发送6号信号。该函数发出的6号信号后只能执行默认的信号处理动作。

#include <iostream>
#include<signal.h>
#include<unistd.h>

using namespace std;

void myhander(int sig)  //自定义处理函数
{
    cout<< "收到信号:SIGABRT "<<sig<<endl;   
}

int main()
{
    int cnt=0;
    signal(SIGABRT,myhander); //捕捉信号
    while(true)
    {
       cout<<"pid::"<<getpid()<<" ";

        if(cnt%2==0)
        {
            abort();
        }
        
        cnt++;
        sleep(1);
    }
    return 0;

}

软件中断

#include<iostream>

using namespace std;

int main()
{
    int a=10;
    a/=0;
    cout<<"after dive"<<endl;
    return 0;
}

         进程产生了8 号信号。代码运行到a/0后不再继续执行。OS会识别到程序运行的错误,从而向软件发送信号,软件中断发出的信号可以被捕捉然后执行自定义的处理方法。本质上还是硬件触发的信号。

由软件条件产生信号

       返回上次设定的闹钟发出信号的剩余时间。向进程发送14号信号。

#include<iostream>
#include<unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

//alarm 

void myhander(int signo)
{
    cout<<"Capture signal : "<<signo<<endl;
    
    cout<<"work... "<<endl; //设计work()函数

    alarm(5);//每间隔五秒响一次
}
int main()
{
    //捕捉信号
    signal(SIGALRM,myhander);
    int n=alarm(5);
    int cnt=5;
    while(true)
    {
        cout<<"process runing...\n"<<endl;

        sleep(1);
    }
    return 0;
}

        在myhander函数中增加一个函数用于执行任务,然后重新设置一个闹钟,可以实现一个基于信号的定期执行的任务。 

core dump 标志

        信号发生以后,OS对不同的信号的处理方式进行的分类,core或者term:

        在子进程因core类型的信号而异常退出时,子进程退出状态中的core dump标志位会被修改为1:

int main()
{

    signal(SIGALRM,myhander);//捕捉信号

    pid_t pid=fork();
    if(pid==-1)
    {
        cout<<"fork error \n"<<endl;
        exit(1);
    }
    else
    {
        cout<<" fork successfully\n";
    }

    if(pid==0)
    {
        //child;
        int cnt=5;
       while(true)
       { 
        cout<<"child_process is runing,pid::  "<<getpid()<<"  "<<cnt++<<"\n"<<endl;
        int a=10;
        a/=0;
        sleep(1);
       }
       cout<<"child process exit()\n"<<endl;

       exit(0);

    }
    int statu=0;
    //父进程等待

    if(waitpid(pid,&statu,0)==-1)
    {
        cout<<"waitpid error\n\n";

    }
    if(WIFEXITED(statu))
    {
        cout<<"main:: 进程正常退出... 退出码:: "<<WEXITSTATUS(statu)<<"\n\n";

    }
    else{
        cout<<"main:: 进程异常退出... core dump:  "<<((statu>>7)&1)<<"退出信号:: "<<(statu&0x7f)<<endl;
    }


    return 0;
}

        当子进程因程序异常退出时,子进程的返回状态中的core dump标志会被设置

        dump为转储,也就是OS会将程序在内存中的运行信息 保存在一个core文件中。通过 ulimit -c size 可以开启core dump服务并设置core文件的大小:

         开启core dump 服务运行异常程序,程序终止后会在当前目录产生一个core文件:

         可以通过core dump文件去定位程序运行过程中的错误。g++编译时添加-g选项,运行编译后的文件生成core dump文件,然后用gdb工具 core-file指令打开生成的core文件,可以直接解析出程序的错误。

信号的保存

        OS向进程发送信号实际上就是修改进程的PC中的一个成员,信号标志位,用位图结构存储。该结构每次只能保存一个信号,若当前信号未处理,OS继续向该进程发送信号,会导致信号的丢失。所以需要保存信号。当OS向进程发送多个实时信号时,会在PCB中维护一个队列,将实时信号存储。

信号的保存

        实际执行信号的处理动作叫做信号信号递答,从信号产生到信号递答之间的动作叫做信号未决。两种状态都有相应的数据结构对应。信号递答对应着PCB结构体中保存信号的位图结构完成相应的设置。信号未决意味着PCB中有相应的结构体保存未递答的信号。同时PCB中有相应的信号处理方法表,也就是一个函数指针数组。来支持signal()系统调用的实现。

        进程可以屏蔽掉某些信号,在解除屏蔽之前,这些信号不会递答。在PCB中新增一个block表可实现上述功能。

        有了阻塞表,信号未决表,函数处理方法表,通过三张表就能完成对普通信号的记录保存等工作。

        语言中对于信号的管理功能的实现,始终绕不开这三张表。 


        OS向应用层提供了一个信号集的数据类型,方便我们对信号未决表(pending 表)和block表做操作。 用户在用户层定义sigset_t类型的值,然后通过函数对其内容进行修改。

        功能分别为:清空信号集、将信号集所有值置1,设置信号集中的某个信号,删除信号集中的信号

signalprocmsk() 阻塞信号 

        signalprocmask()用于更改信号屏蔽字。

        通过how传入下面三个参数之一,来更改或者读取信号屏蔽字: 

      第二个参数传入你要修改的block表中的字段,也就是你事先定义并设置好的sigset_t类型的变量,第三个参数为输出型参数,用于修改之前的block表中的值。

sigpending() 读取当前进程的pending表中的内容

        


        信号概念在OS中的实体对应着PCB中的block,pending和hander三张表,两个位图一个函数指针数组。signal(),sigprocmask(),pending(), 以及sigemptyset五个函数,分别支持了三张表的修改和读取操作。

        测试代码:

#include<iostream>
#include<unistd.h>
#include<signal.h>


using namespace std;

void Printpending(sigset_t* pending)
{
     for(int signo=1;signo<=31;signo++)
        {
            if(sigismember(pending,signo)) //不能直接对其进行位操作
            {
                cout<<"1 ";
            }
            else
            {
                cout<<"0 ";
            }
            
        }
        cout<<"  pid:: "<<getpid()<<endl;
        
}

void myhander(int signo)
{
    cout<<"捕捉到"<<signo<<"号信号 "<<endl;
}

int main()
{

    signal(2,myhander);
    //先屏蔽二号信号
    sigset_t set; //对应的结构 
                        //     typedef struct
                        // {
                        //   unsigned long int __val[_SIGSET_NWORDS];
                        // } __sigset_t;
        //对该信号集做清空必须通过对应的函数
        sigemptyset(&set);
        //对二号信号屏蔽
        sigaddset(&set,SIGINT); //先设置好信号集的值,这是后续调用sigprocmask时需要传递的必要参数,以便设置进入PCB中 
        //调用系统调用设置block表
        sigset_t oset;
        sigemptyset(&oset);
        sigprocmask(SIG_BLOCK,&set,&oset);
    
    //重复打印信号表
    int cnt=0;
    while(true)
    {
        sigset_t pending;
        sigpending(&pending);//pending取出表
        //打印
        
        Printpending(&pending);
       
       cnt++;
         //打印一次后解除信号
        if(cnt==5)
        {
            sigdelset(&set,2);
            cout<<"set 被设置:";
            Printpending(&set);
            sigprocmask(SIG_SETMASK,&set,nullptr);
            cout<<"解除2号信号阻塞"<<endl;          
        }
        sleep(1);
    }
    //发送2号信号

    return 0;
}

       通过对进程的信号屏蔽,pending会保存该信号但不会对其进行处理,然后解除屏蔽,就实现了信号暂时保存的功能。


信号捕捉

        OS修改进程PCB中的两个位图结构,意味着信号的产生,从信号产生到信号执行,中间还有一个必须的动作,就是进程检测自己PCB中的两个位图结构中相应的信号被设置。这个检测自己相应结构中数据是否改变的动作,在进程从内核态切换到用户态时发生。也就是说,每次进程从内核态切换到用户态时,对信号结构检测完后,才会对信号产生响应。

        signal()是语言层面的信号捕捉函数,sigaction()是系统调用。

        

        通过定义sigaction结构体,初始化成员变量sa_handler自定义信号处理函数,实现的功能与signal()类似,都是自定义信号处理函数。

        在由内核态切换到用户态处理信号时,在执行信号处理函数之前,OS会先修改pending表。即信号产生时pending表相应的信号位为1,在回到上层执行handler函数时,会先将pending表相应信号位由1置0。测试代码:

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<string.h>


using namespace std;

void PrintPending(sigset_t& pending)
{
    for(int i=1;i<=31;i++)
    {
        if(sigismember(&pending,i))
        {
            cout<<"1 ";
        }
        else
        {
            cout<<"0 ";
        }
    }
    cout<<"\n";
}
void myhander(int signo)
{
    cout<<"捕捉到"<<signo<<"号信号"<<endl;
    //取出并打印pending表
    sigset_t set;
    sigemptyset(&set);
    sigpending(&set);

    PrintPending(set);

    sleep(1);
}


int main()
{
    //测试代码 测试信号递答时的pending表的变化 即信号产生后,OS会先修改pending表,将该信号的值从1置0 然后调用handler函数
    sigset_t set;
    struct sigaction act;

    memset(&act,0,sizeof(act));

    //系统调用捕捉2号信号
    act.sa_handler=myhander;
    sigaction(2,&act,nullptr);

    
    sigemptyset(&set);

    while(true)
    {
        cout<<"pid:: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

         执行某信号的hanlder函数期间,OS会暂时自动阻塞该信号。即信号递答期间,再次产生该信号,系统会自动保存。

        原因是,若handler函数执行完毕,进程存在一个从内核态转向用户态的过程,若在handler函数执行期间再次产生该信号,OS会再次执行该handler函数,导致hanlder的深层调用。测试代码:


//修改handler函数,在进程处理信号时,再次向进程发出2号信号,获取pending表
void myhander(int signo)
{
    cout<<"捕捉到"<<signo<<"号信号"<<endl;
    //取出并打印pending表
    sigset_t set;
    sigemptyset(&set);
    /
    while(true)
    {
        sigpending(&set);
         PrintPending(set);
         sleep(1);
    }

    sleep(1);
}

 


网站公告

今日签到

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