进程的信号

生活中的信号

例如:红绿灯,闹钟,转向灯,狼烟

什么是Linux信号?

本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理。

结合进程,得出信号结论:

a. 进程要处理信号,必须具备信号"识别"能力(看到+处理动作)。

b. 为什么进程能识别信号?--- 程序员

c. 信号产生是随机的,进程这时候可能正在执行任务,所以进程的后续处理,可能不是立即处理。

d. 信号会临时的记录下对应的信号,方便后续(合适的时候)进行处理。

e. 一般而言,信号的产生相对于进程而言是异步的。

信号处理的常见方式

a. 默认(进程自带的,程序员写好的逻辑)

b. 忽略

c. 自定义捕捉

常见的信号

【1 - 31】:  普通信号    【34 - 64】:实时信号

Term:终止  Core:核心转储终止  Ign:忽略  Cont:继续   Stop:暂停

如何理解组合键ctrl+c变成信号呢?

答:键盘的工作方式是通过中断方式进行的,os找到与组合键ctrl+c对应的信号即可。

如何理解信号被进程保存?

答:什么信号(位图中哪一个比特位),是否产生(1,产生 0,没有)---进程中的位图结构,os只要将信号写入到进程pcb内部的位图结构中。

如何理解信号发送的本质?

答:os向目标进程写信号,os直接修改进程pcb中指定的位图结构,完成发送信号过程。

产生信号

a. 终端键盘产生信号

#include <signal.h>

typedef void (*sighandler_t)(int);//函数指针

自定义捕捉系统调用方法
sighandler_t signal(int signum, sighandler_t handler);
返回值:返回老的捕捉方法的函数地址
参数1:信号的值/宏
参数2:通过回调函数的方式,修改对应的信号捕捉方法

代码实现:

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
//typedef void(*sighandler_t)(int);

void handler(int sig)
{
    cout<<"接受到一个信号....pid: "<<getpid()<<endl;
}

int main()
{
    signal(SIGINT,handler);
    while(1)
    {
        sleep(1);
    }
    return 0;
}
可使用ctrl+c:2号信号捕捉,也可以使用kill -2 进程pid 捕捉,原理是一样的

核心转储

关闭时

打开时

当进程出现某种异常的时候,由os将当前进程在内存中的相关核心数据,转储到磁盘中。

核心转储作用和功能:当进程出现某种异常的时候,是否由os将当前进程在内存中的相关核心数据,转储到磁盘中,主要是为了调试。

验证进程等待中的信号信息和core dump标记位

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
//typedef void(*sighandler_t)(int);

int main()
{
    pid_t id=fork();
    if(id==0)
    {
        int a=100;
        a/=0;
        cout<<"run here...."<<endl;
    }

    int status;
    wait(&status);
    cout<<"信号信息:"<<(status&0x7F)<<" core dump:"<<(status>>7&1)<<endl;
    return 0;
}
//结果:信号信息:8 是核心转储:1

b. 系统调用接口产生信号

本质:用户调用系统接口 -> 执行os对应的系统调用代码 -> os提取参数,或者设置特定的数值 -> os向目标进程写消息 -> 修改对应进程的信号标记位-> 进程后续会处理信号-> 执行对应的处理动作

a. 第一种
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
返回值:succeed:0  fail:-1并且errno被设置
参数:pid:进程pid   sig:信号


b. 第二种
#include <signal.h>

int raise(int sig);
返回值:succeed:0  fail:!0
参数:sig:信号

c. 第三种
#include <stdlib.h>
//发送6号信号,终止进程
void abort(void);

c. 软件条件产生信号

1.软件中管道通信

代码验证:

1.创建匿名管道 2.让父进程读取,子进程写入 3.父子进程通信一段时间时间 4.让父进程 关闭读端&&waitpid(),子进程一直写入 5.子进程退出,父进程拿到子进程退出码status 6.提取退出信号

#include<iostream>
#include<fcntl.h>
#include<unistd.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>
#include<cstring>

int main()
{
    int pipefd[2]={0};
    int m=pipe(pipefd);
    assert(m!=-1);
    (void)m;
    pid_t id=fork();
    //子进程
    if(id==0)
    {
        //关闭读端
        close(pipefd[0]);
        //写数据
        int count=0;
        char buffer[1024]="我是子进程,我正在给你发消息: ";
        while(1)
        {
            char buffer1[1024]={0};
            count++;
            snprintf(buffer1,sizeof(buffer1),"%s count:%d",buffer,count);
            ssize_t s =write(pipefd[1],buffer1,strlen(buffer1));
            sleep(1);
        }
        exit(1);
    }
    //父进程
    char buffer2[1024];
    close(pipefd[1]);
    int n=0;
    while(1)
    {
        //sleep(1);
        ssize_t s=read(pipefd[0],buffer2,sizeof(buffer2)-1);
        if(s>0)
        {
            buffer2[s]='\0';
        }
        std::cout<<buffer2<<std::endl;
        n++;
        if(n==10)
        {
            close(pipefd[0]);
            break;
        }
    }
    //close(pipefd[0]);
    int status=0;
    wait(&status);
    std::cout<<(status&0x7F)<<std::endl;
    return 0;
}

2. 闹钟问题

先看一份代码:

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

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

最终结果:95927Alarm clock
为什么cpu计算这么慢:IO+网络的原因
改进,没有IO影响
#include<iostream>
#include<unistd.h>

void showCount(int signum)
{
    cout<<"final count: "<<count<<endl;
}

int count=0;
int main()
{
    signal(SIGALRM,showCount);
    alarm(1);
    while(1) cout++;
    return 0;
}
结果:final count: 582527911

勘探定时器功能原理

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<functional>
#include<vector>
using namespace std;
typedef function<void ()> func;
uint64_t count=0;
vector<func> callbacks;

void showCount()
{
    cout<<"final count: "<<count<<endl;
}
void logUser()
{
    if(fork()==0)
    {
        execl("/usr/bin/who","who");
        exit(1);
    }
    wait(nullptr);
}
void showLog()
{
    cout<<"这个时日志功能"<<endl;
}

void catchSig(int signum)
{
    for(auto& f:callbacks)
    {
        f();
    }
    alarm(1);
}

int main()
{
    alarm(1);
    signal(SIGALRM,catchSig);
    callbacks.push_back(showCount);
    callbacks.push_back(logUser);
    callbacks.push_back(showLog);
    while(1) sleep(1);
    return 0;
}

问:如何理解软件条件给进程发送消息?

答:a. os先识别到某种软件条件触发不满足 b. os构建信号,发送在指定的进程

d. 硬件异常产生信号

除0代码如下:

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


void handler(int signo)
{
    std::cout<<"除0错误"<<std::endl;
    sleep(1);
}

int main()
{
    signal(SIGFPE,handler);
    int m=100;
    m/=0;
    while(1) sleep(1);
    return 0;
}
输出结果:死循环打印:除0错误

问:如何理解除0问题?

答:1. 进行计算的是软件cpu 2. cpu内部是有寄存器的,状态寄存器(位图),有对应的状态标记位(溢出标记位),os会自动进行计算完毕之后的检测,如果溢出标记位是1. os识别到有溢出问题,立即找到当前运行进程,提取pid,os完成信号发送的过程,进程在适当的时候进行处理 3. 一旦出现硬件异常,进程一定会退出嘛,不一定,一般默认处理方式是退出 4. 为什么会死循环?--寄存器中的异常一直没有被解决

指针越界访问代码如下:

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

void catcher(int signo)
{
    std::cout<<"越界访问"<<std::endl;
    sleep(1);
}

int main()
{
    signal(SIGSEGV,catcher);
    int* arr=nullptr;
    *arr=6;
    return 0;
}

问:如何理解野指针或者越界问题?

答:1. 都必须通过地址,找到目标位置 2. 我们语言上面的地址,全都是虚拟地址 3. 将虚拟地址转化为物理地址 4. 页表+(页表将数据导入)MMU中找到虚拟地址对应的物理内存(Memory Manager Unit,硬件,内部也有寄存器)5.野指针,越界- > 非法地址 - > MMU转化的时候,一定会报错。

注意:所有的信号,有他的来源,但最终全部都是被os识别,解释,并且发送的。

阻塞信号

信号其他相关概念

a. 实际执行信号的处理动作叫做信号递达

b. 信号从产生到递达之间的状态称为信号未决

c. 进程可以选择阻塞某个信号

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

e. 注意阻塞和忽略是不同的,前者处于阻塞,没有被递达,后者完成递达,但是不做处理。

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

每个信号都有俩个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作,信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

sigset_t:每个信号只有一个bit的未决标志,非0即,不记录该信号产生了多少次,阻塞标志也是这样表示。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(signal Mask),这里"屏蔽"应该理解为阻塞而表示忽略。

信号集操作函数

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

对信号集本身操作:

#include <signal.h>

//初始化set所指信号集,将所有信号bit置0,表示信号全部无效
int sigemptyset(sigset_t *set);
//初始化set所指信号集,将所有信号bit置1,表示信号全部有效
int sigfillset(sigset_t *set);
//将信号集中signo信号改成有效信号
int sigaddset (sigset_t *set, int signo);
//将信号集中signo信号改成无效信号
int sigdelset(sigset_t *set, int signo);
//以上返回值全部相同,succeed:0  fail:-1

//检测一个信号在set所指信号集是否存在,存在返回0,表示真,不存在返回-1,表示假
int sigismember(const sigset_t *set, int signo); 

对进程task_struct中block和pending进行操作:

#include <signal.h>
读取当前进程的信号集,通过set参数传出,返回0表示成功,-1表示失败
int sigpending(sigset_t *set);

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:how   SIG_BLOCK  SIG_UNBLOCK  SIG_SETMASK
SIG_BLOCK:表示了我们要将set信号传入到进程block信号屏蔽字,相当于mask=mask|set
SIG_UNBLOCK:表示我们要使用set对应的信号集取消进程对应的block信号屏蔽字,
相当于mask=mask&(~set)
SIG_SETMASK:设置当前信号屏蔽字为set所指向的值
参数oldset:表示传入set信号集之前,进程的block信号屏蔽字
返回值:0表示成功,-1表示失败

问:当我们对所有的信号都进行了自定义捕捉,这个进程是不是就意味着不能被信号杀死?

答:不是,9号信号不能被自定义捕捉,9号信号可以强制杀死一切进程。

问:当我们将2号信号block,并且不断获取并打印当前进程的pending信号集,如果我们突然发送一个2号信号,我们就可不可以看到pending信号集中,有一个比特位0->1?

代码验证如下:

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

void handler(int signo)
{
    std::cout<<"捕捉到一个信号: "<<signo<<std::endl;
}

void showSignal(sigset_t* pending)
{
    for (int i = 1; i <= 31; i++)
        {
            int m = sigismember(pending, i);
            if (m == 1)
                std::cout << "1";
            else
                std::cout << "0";
        }
        std::cout << std::endl;
}

int main()
{
    signal(SIGINT,handler);
    //1.创建俩个sigset_t变量
    sigset_t bset, obset,pending;
    //2.初始化信号集
    sigemptyset(&bset);
    sigemptyset(&obset);
    sigemptyset(&pending);
    //3.向bset信号集中加入2号信号
    sigaddset(&bset, SIGINT);
    //4.将2号信号进行阻塞
    sigprocmask(SIG_BLOCK, &bset, &obset);
    int count=0;
    while(1)
    {
        //5.获取pending信号集
        sigpending(&pending);
        //6.打印全部的pending信号集中所有的信号信息
        showSignal(&pending);
        sleep(1);
        if(count++==20) break;
    }
    return 0;
}

问:当我们对所有的信号都进行block,我们是不是就写了一个不会被异常或者用户杀掉的进程?

答:不是,9号信号和19号信号不会被block,可以杀掉进程,前者是杀掉进程,后者是暂停进程,20号信号默认行为是忽略,本意是在终端停止输入,也不会被block。

问:信号产生之后,可能无法被立即处理,会在合适的时候处理,这个合适的时候是什么时候?

答:由于信号存在于进程的pcb的信号位图中,所以要处理信号,必须在内核态中,从内核态返回用户态的时候,操作系统会进行信号检测和处理。

问:进程为什么会进入内核态中?

答:进行系统调用,缺陷陷阱异常等,汇编指令int 80是一种中断指令,会内置在系统调用函数中,当调用系统接口时,在cpu中进行身份认证,cpu中存在俩套寄存器,一套给自己用,其中有一个寄存器CR3,表示当前执行权限,相当于位图,1表示内核态,3表示用户态。

注意:页表有俩种,内核级页表,用户级页表,所以操作系统就可以实现在进程地址空间上,俩种状态的转换,0-3G用户空间,3-4G表示内核空间,这个内核级页表是被所有进程地址空间共享,用户态是一个受管控的状态,内核状态是操作系统执行自己代码的一个状态,具有非常高的优先级

问:进程凭什么有权执行os的代码?

答:凭的是处于内核态还是用户态?

捕捉信号

1. 信号在pcb中是如何被os检测和处理流程?(图解)

问:os能做到帮进程执行对应的handler方法嘛?为什么os不在内核态执行用户代码呢?

答:可以,但是os不想,因为用户的代码可能会存在安全隐患。

2. 内核如何实现信号的捕捉(文字解释)

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

3. sigaction(读取和修改与指定信号相关的处理动作)

#include <signal.h>

int sigaction(int signo, const struct sigaction *act,
              struct sigaction *oldact);
//sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo
是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传
出该信号原来的处理动作。
//act和oact指向sigaction结构体:
struct sigaction
{
   void (*sa_handler)(int);//处理方法的函数指针,普通信号
   void (*sa_sigcation)(int,siginfo_t*,void*);//实时信号
   sigset_t sa_mask;//传入需要屏蔽的信号集
   int sa_flags;//默认为0,不考虑
   void (*sa_restorer)(void);//不考虑
}

注意:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

问:处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,os如何处理?

代码如下:

#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;

//7.打印pending信号集
void showSignal(sigset_t* pending)
{
    for(int i=1;i<=31;i++)
    {
        if(sigismember(pending,i)==1) cout<<"1";
        else cout<<"0";
    }
    cout<<endl;
}
void handler(int signo)
{
    cout<<"获取一个信号: "<<signo<<endl;
    sigset_t pending;
    sigemptyset(&pending);
    int count=10;
    while(count--)
    {
        //6.验证捕捉方法期间,如果进程收到多个同样的信号,会将block中对应的信号设置为1,
        //等捕捉方法完成,自动将block中对应的信号设置为0
        sigpending(&pending);
        showSignal(&pending);
        sleep(1);
    }
}

int main()
{
    //1.设置结构体
    struct sigaction act,oact;
    //2.自定义捕捉方法
    signal(SIGINT,handler);
    //3.初始化结构体内的变量
    act.sa_flags=0;
    act.sa_handler=handler;
    //4.设置要屏蔽的信号
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);
    sigaddset(&act.sa_mask,6);
    //5.调用捕捉方法
    sigaction(SIGINT,&act,&oact);

    while(1) true;
    return 0;
}

可重入函数

如果一个函数符合以下条件之一,则是不可重入的:

a. 调用了malloc或free,因为malloc也是全局链表来管理堆的

b.调用了标准I/O库函数,标准I/O库的很多实现以不可重入的方式使用全局数据结构

volatile

代码如下:

#include<iostream>
#include<signal.h>
using namespace std;

int flag=0;
void changeFlag(int signum)
{
    (void)signum;
    cout<<"change flag: "<<flag;
    flag=1;
    cout<< "->" <<flag<<endl;
}

int main()
{
    signal(2,changeFlag);
    while(!flag);
    cout<<"进程正常退出后:"<<flag<<endl;
    return 0;
}
//编译时优化:+-O3,一直运行不退出
//不优化:打印结果退出

问:为什么进程一直运行不退出?

答:cpu将flag的值保存到寄存器中,不从内存中读取flag的内容,解决方案,volatile int flag=0;volatile作用:保持内存的可见性。

SIGCHLD信号(子进程退出向父进程发送的信号)

验证父进程是否会收到子进程退出的信号码17:

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;

void handler(int signo)
{
    cout<<"收到信号:"<<signo<<endl;
    pid_t id;
    while(id=wait(nullptr))
    {
        cout<<"wait pid: "<<id<<endl;
        break;
    }
}

int main()
{
    signal(SIGCHLD,handler);
    pid_t id=fork();
    if(id==0)
    {
        cout<<"child pid: "<<getpid()<<endl;
        sleep(3);
        exit(1);
    }
    while(1)
    {
        cout<<"father process pid: "<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

如果我们不想得到子进程的退出信息(退出码,退出信号)代码如下:

  #include<iostream>
  #include<signal.h>
  #include<unistd.h>                                                                                                                                                                      
  #include<sys/types.h>
  #include<sys/wait.h>
  using namespace std;
   
  int main()
  {
      signal(SIGCHLD,SIG_IGN);
      if(fork()==0)
      {
          cout<<"child: "<<getpid()<<endl;
          sleep(5);
          exit(0);
      }      
      while(true)                                                                                
      {                                                
          cout<<"parent: "<<getpid()<<" 执行我自己的任务 "<<endl;
          sleep(1); 
      }                                   
      return 0;                        
} 

结果如下:可见父进程创建的子进程会被os自动回收,不会出现僵尸进程的状态。

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯狂的小码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值