【Linux后端服务器开发】信号量与信号

目录

一、信号量概述

二、信号概述

三、信号产生

1、终端按键产生信号

2、调用系统函数产生信号

3、硬件异常产生信号

4、软件条件

四、信号保存

1、信号阻塞

2、信号捕捉流程

五、信号递达


一、信号量概述

  • 信号量:一个计数器,通常用来表示公共资源中,资源数量的多少
  • 公共资源:能够被多个进程同时访问的资源
  • 访问没有被保护的公共资源:数据不一致
  • 被保护的公共资源:临界资源
  • 资源(内存、文件、网络......)是要被使用的,一定有该进程对应的代码来访问这部分临界资源(临界区),不访问临界资源的代码是非临界区
  • 如何保护公共资源:互斥 && 同步
  • 所有进程在访问公共资源之前,都需要先申请sem信号量 ---> 信号量本身就是一个公共资源
  • 公共资源的使用:1. 作为一个整体使用;2. 划分为一个个子资源使用
  • 进程预定公共资源,信号量sem--(P操作);进程释放公共资源,信号量sem++(V操作);若信号量sem == 0,进程无法预定公共资源
  • 信号量的 --、++ 操作是原子性的(要么不执行,要么执行完)

申请信号量

控制信号量

设置信号量(PV操作)

二、信号概述

信号与信号量的关系,就如同老婆和老婆饼的关系,没有任何关系。

[1, 31]是普通信号,[34, 64]是实时信号

  • 进程本身是被程序员编写的属性和逻辑的集合 ------ 程序员编码完成
  • 当进程收到信号的时候,进程可能正在执行更重要的代码,信号不一定会被立即处理
  • 进程本身要有对应信号的保存能力
  • 进程对信号的三种处理方式:默认、自定义、忽视 ------ 信号被捕捉
  • 进程对信号的保存:task_struct中用位图结构保存
  • 发送信号的本质:修改pcb中的信号位图
  • pcb是内核维护的数据结构对象,由os管理
  • 所有发送信号的方式,本质都是os向目标进程发信号: os向用户提供发送信号的系统调用

三、信号产生

1、终端按键产生信号

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

int main() 
{
    while (1) 
    {
        cout << "this is a process, pid = " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

键盘按键 ctrl C ---> os将ctrl+C解释为2号信号(SIGINT) ---> 进程终止

自定义捕捉方法

signal函数的调用,并不是自定义捕捉方法函数的直接调用

仅仅设置了对目标信号的捕捉方法,该方法并不一定会被调用

这个自定义捕捉方法只有收到了捕捉信号的时候才会被调用

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

void Handler(int signo) 
{
    cout << "进程捕捉到了一个信号,信号编号是:" << signo << endl;
}

int main() 
{
    signal(2, Handler);

    while (1) 
    {
        cout << "this is a process, pid = " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

当自定义2号信号(SIGINT)的捕捉方法之后,进程收到2号信号并不会终止,而是执行自定义捕捉方法的打印函数

2、调用系统函数产生信号

  • kill() 可以向任意进程发送任意信号
  • raise() 可以向自身进程发送任意信号 ----- kill(getpid(), signal)
  • abort() 给自己发送指定的信号SIGABRT ----- kill(getpid(), SIGABRT)

mykill.cc

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

static void Usage(const string& proc) 
{
    cout << "\nUsage: " << proc << "pid signo\n" << endl;
}

void Handler(int signo) 
{
    cout << "进程捕捉到了一个信号,信号编号是:" << signo << endl;
}

int main(int argc, char* argv[]) 
{
    if (argc != 3) 
    {
        Usage(argv[0]);
        exit(1);
    }
    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);
    int n = kill(pid, signo);
    if (n != 0) 
        perror("kill");

    return 0;
}

test.cc 一个死循环进程

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

int main() 
{
    while (true) 
    {
        cout << "我是一个正在运行的进程,pid:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

myraise.cc

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

int main() 
{
    int cnt = 0;
    while (true) 
    {
        printf("cnt: %d\n", cnt++);
        sleep(1);
        if (cnt >= 5) 
        {
            raise(9);
            //abort();
        }
    }
    return 0;
}

5s后进程被自己发送的信号杀死(或放弃)

3、硬件异常产生信号

SIGFPE

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

int main() 
{
    while (true) 
    {
        cout << "我在运行中..." << endl;
        sleep(1);
        int a = 10;
        a /= 0;         
    }

    return 0;
}

除0 浮点数错误SIGFPE

获取信号编号

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

void CatchSig(int signo) 
{
    cout << "获取一个信号,信号编号是:" << signo << endl;
    sleep(1);
}

int main() 
{
    signal(SIGFPE, CatchSig);

    while (true) 
    {
        cout << "我在运行中..." << endl;
        sleep(1);
        int a = 10;
        a /= 0;         //除0 浮点数报错
    }

    return 0;
}

  • os通过状态寄存器当中的溢出标记位判断cpu是否发生运算异常,若产生异常,os发送信号
  • 进程收到信号,不一定会立即执行
  • cpu每次切换进程时进行上下文保存和恢复,os都会检测一次异常

4、软件条件

设置闹钟,进程1s之后结束,统计cnt累加次数

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

int main() 
{
    alarm(1);
    int cnt = 0;
    while (1) 
    {
        cout << "cnt: " << cnt++ << endl;
    }

    return 0;
}

将cnt定义为全局重新,用捕捉信号重新定义

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

int cnt = 0;

void CatchSig(int signo) 
{
    cout << "cnt: " << cnt << endl;
}

int main(int argc, char* argv[]) 
{
    signal(SIGALRM, CatchSig);
    alarm(1);
    while (1) 
    {
        cnt++;
    }

    return 0;
}

由此可见计算机将数据从内存打印到外设时间损耗巨大,IO效率很低

闹钟是一次性闹钟,执行之后不再执行

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

static int cnt = 0;

void CatchSig(int signo) 
{
    cout << "cnt: " << cnt << endl;
    alarm(1);
}

int main(int argc, char* argv[]) 
{
    signal(SIGALRM, CatchSig);
    alarm(1);
    while (1) 
        cnt++;

    return 0;
}

任何一个进程都可以通过alarm()系统调用设置闹钟,操作系统如何管理闹钟?先描述,再组织

核心转储

部分服务器默认关闭了核心转储

#include <iostream>
using namespace std;

int main(int argc, char* argv[]) 
{
    //核心转储
    while (1) 
    {
        int a[10];
        a[100000] = 100;
    }

    return 0;
}

越界访问,不仅报了段错误,还生成了core.30202文件

以Term退出的没有核心转储

核心转储:当进程出现异常的时候,将进程在对应的时刻,在内存中的数据转储到磁盘中

核心转储意义:支持调试(事后调试)

对所有信号做自定义捕捉测试

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

void CatchSig(int signo) 
{
    cout << "收到一个信号,信号编号是:" << signo << endl;
}

int main(int argc, char* argv[]) 
{
    for (int signo = 31; signo >= 1; --signo) 
        signal(signo, CatchSig);

    while (1) 
    {
        cout << "我在运行, pid = " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

只有9号信号可杀死进程(9号进程由os直接控制,禁止对9号信号做捕捉)

四、信号保存

  • 执行信号的处理动作称之为信号递达
  • 信号从产生到递达之间的状态叫做信号未决
  • 进程可以选择阻塞某个信号
  • 被阻塞的信号永远不会递达,除非进程接触阻塞
  • 阻塞和忽视是有不同的,忽视是递达之后的一种处理动作

1、信号阻塞

每个信号都有两个标志位表示阻塞(block)未决(pending),还有一个函数指针表示处理动作

如果一个信号没有产生,可以预先将它设为阻塞状态

2、信号捕捉流程

信号产生的时候,不会被立即处理,而是在从内核态返回用户态的时候进行处理

用户想要访问内核或硬件资源,必须通过系统调用完成访问

CPU中存在CR3表态寄存器,表示当前运行的运行级别:0表示内核态,3表示用户态

每一个进程都有自己的地址空间(用户空间独占)、内核空间(被映射到每一个进程的3~4G)

进程要访问OS的接口,只需要在自己的地址空间上进行跳转

每一个进程都会共享一个内核级页表,无论进程如何切换,不会更改内核空间

系统调用接口,起始位置会将我们的用户权限更改为内核态(陷入内核)

特定的身份调用特定的代码,内核态也无法直接调用用户态状态

信号捕捉流程图

五、信号递达

sigprocmask读取或更改进程的信号屏蔽字

sigpending获取进程的pending位图

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

static vector<int> sigarr = {2, 3};

void ShowPending(const sigset_t& pending) 
{
    for (int signo = 31; signo >= 1; signo--) {
        if (sigismember(&pending, signo)) 
            cout << "1";
        else 
            cout << "0";
    }
    cout << endl;
}

void MyHandler(int signo) 
{
    cout << signo << " 号信号已经被递达\n" << endl;
}

int main(int argc, char* argv[]) 
{
    for (const auto& e : sigarr) 
        signal(e, MyHandler);

    // 1. 屏蔽指定的信号
    sigset_t block, oblock, pending;
    // 1.1 初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    // 1.2 添加要屏蔽的信号
    for (const auto& e : sigarr) 
        sigaddset(&block, e);   // 2号信号
    // 1.3 开始屏蔽
    sigprocmask(SIG_SETMASK, &block, &oblock);

    // 2. 遍历打印pending信号集
    int cnt = 10;
    while (1) 
    {
        sigemptyset(&pending);
        sigpending(&pending);
        ShowPending(pending);
        sleep(1);
        if (cnt-- == 0) 
        {
            cout << "恢复对信号的屏蔽\n" << endl;
            sigprocmask(SIG_SETMASK, &oblock, &block);
        }
    }

    return 0;
}

将2号、3号信号屏蔽

ctrl C + ctrl \ 发送2号信号和3号信号,信号未决,pending位图2号和3号bit位置1

10s后重置对信号的屏蔽,os直接将重置的信号递达,捕捉到2号、3号信号

sigaction

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

void ShowPending(const sigset_t& pending) 
{
    for (int signo = 31; signo >= 1; signo--) 
    {
        if (sigismember(&pending, signo)) 
            cout << "1";
        else 
            cout << "0";
    }
    cout << endl;
}

void MyHandler(int signo) 
{
    cout << "get a signo: " << signo << endl;
}

int main(int argc, char* argv[]) 
{
    struct sigaction act, oact;
    act.sa_handler = MyHandler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGINT, &act, &oact);

    while (true);

    return 0;
}

进程串行处理同类信号

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

void Count(int cnt) 
{
    while (cnt) 
    {
        printf("cnt: %d\n", cnt);
        fflush(stdout);
        cnt--;
        sleep(1);
    }
}

void MyHandler(int signo) 
{
    cout << "get a signo: " << signo << endl;
    Count(10);
}

int main(int argc, char* argv[]) 
{
    struct sigaction act, oact;
    act.sa_handler = MyHandler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);     //在2号信号捕捉期间,屏蔽3号信号
    sigaction(SIGINT, &act, &oact);

    while (true) sleep(1);

    return 0;
}

  • 当进程正在递达某一个信号期间,同类信号无法被递达
  • 当当前信号正在被捕捉,系统自动将当前信号加入到进程的信号屏蔽字,当捕捉完成,系统又自动解除对该信号的屏蔽
  • 一般一个信号被解除屏蔽的话,会自动递达当前屏蔽信号
  • 进程处理信号的原则是串行处理同类信号,不允许递归处理

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

AllinTome

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

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

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

打赏作者

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

抵扣说明:

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

余额充值