从零开始学习Linux(12)---进程间通信(信号量与信号)

1.信号量

信号量是计算机科学中用于同步和互斥的一种抽象数据类型。在并发编程中,当多个进程或线程需要访问共享资源时,信号量用来确保资源在同一时刻只被一个进程或线程访问,从而避免竞争条件。

信号量通常具有以下特性:

  1. 整数值:信号量是一个非负整数,用来表示可用的资源的数量。

  2. 两个原子操作

    • P操作(Proberen测试):如果信号量的值大于零,则将其减一;否则,进程或线程会被阻塞,直到信号量值变为正。

    • V操作(Verhogen增加):增加信号量的值,并唤醒等待的进程或线程。

根据信号量的取值,可以分为以下两种:

  • 二进制信号量:其值只能是0或1,通常用于互斥。

  • 计数信号量:其值可以是任何非负整数,用于表示资源的可用数量。

可用ipcs -s查看信号量:

2.信号

信号是操作系统提供的让用户(进程)给其他进程发送异步信息的一种方式,Linux定义了一系列的信号,每个信号都有一个唯一的编号和一个默认的行为。输入kill -l可以列出所有的信号,以下是一些常见的信号类型:

进程可以针对信号执行以下操作:

  • 忽略:进程可以选择忽略某些信号。
  • 捕获:进程可以提供一个信号处理函数(信号处理程序),当信号发生时,该函数将被调用。
  • 默认行为:如果进程没有指定信号的处理方式,那么将执行信号的默认行为。

下面是简单示例代码:

cpp 复制代码
#include<iostream>           // 引入标准输入输出流库,用于打印信息到标准输出
#include<unistd.h>           // 引入unistd.h头文件,提供对POSIX操作系统API的访问,如sleep()函数
#include<signal.h>           // 引入signal.h头文件,提供信号处理的函数和宏定义
#include<sys/types.h>        // 引入sys/types.h头文件,提供系统调用所需的类型定义

// 定义信号处理函数,参数signo为接收到的信号编号
void handler(int signo) {
    std::cout<<"get a sig,number is:"<<signo<<std::endl; // 打印接收到的信号编号
    exit(100); // 使用exit函数退出程序,返回状态码100
}

int main() {
    signal(SIGINT,handler); // 将SIGINT信号(通常由Ctrl+C产生)的处理函数设置为handler
    while(true) {           // 创建一个无限循环
        std::cout<<"I am activing...,pid:"<<getpid()<<std::endl; // 打印活动信息,包括进程ID
        sleep(1);           // 调用sleep函数,使程序暂停执行1秒
    }
    return 0;               // 程序正常结束,返回0
}

使用kill函数发送信号给指定进程,返回0表示成功,-1表示失败:

cpp 复制代码
#include<iostream>
#include<cerrno>
#include<cstring>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>

using namespace std;

int main(int argc, char *argv[]) {
    // 检查命令行参数的数量是否为3(包括程序名称本身)
    if (argc != 3) {
        std::cout << "Usage:" << argv[0] << " -signumber pid" << std::endl; // 打印使用说明
        return 1; // 参数数量不正确,返回错误码1
    }

    // 从第一个命令行参数中提取信号编号,跳过前导的'-'字符
    int signumber = std::stoi(argv[1] + 1);
    // 从第二个命令行参数中提取进程ID
    int pid = std::stoi(argv[2]);

    // 使用kill函数发送信号给指定进程,返回0表示成功,-1表示失败
    int n = kill(pid, signumber);
    // 检查kill函数的返回值
    if (n < 0) {
        // 如果返回值小于0,说明发送信号失败,打印错误信息
        std::cerr << "kill error," << strerror(errno) << std::endl;
    }
    // 如果没有错误,程序正常结束,返回0
    return 0;
}

使用raise函数发送信号给自己:

cpp 复制代码
#include<iostream>
#include<cerrno>
#include<cstring>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>

using namespace std;

void handler(int signumber)
{
    std::cout<<"get a sinal,number is:"<<signumber<<std::endl;
}

int main()
{
    signal(2,handler);
    int cnt=0;
    while (true)
    {
        cout<<"cnt:"<<cnt++<<endl;
        sleep(1);
        if(cnt%5==0)
        {
            cout<<"send 2 to caller"<<endl;
            raise(2);
        }
    } 
}

下面是**alarm**函数的原型和基本用法:

  • seconds:指定定时器超时的时间,以秒为单位。
  • **返回值:**返回之前设置的定时器剩余的秒数,如果没有设置定时器,则返回0。
cpp 复制代码
int g_cnt=0;

void handler(int sig)
{
    cout<<"get a sig:"<<sig<<"g_cnt:"<<g_cnt<<endl;
    exit(0);
}

int main()
{
    signal(SIGALRM,handler);
    alarm(1);
    while (true)
    {
        g_cnt++;
    }
    // int cnt=0;
    // while (true)
    // {
    //     cout<<"cnt:"<<cnt++<<endl;
    // }
}

异常信号:

cpp 复制代码
void handler(int sig)
{
    cout<<"get a sig:"<<sig<<endl;
    exit(1);
}

int main()
{
    signal(SIGFPE,handler);
    int a=10;
    a/=0;
    while(true) sleep(1);
    return 0;
}

1.core功能

通过ulimit -a [大小]打开Linux的core功能

在Linux系统中,**core**功能通常指的是核心转储(core dump)功能,这是操作系统在进程崩溃或接收到特定信号时生成的一种文件,其中包含了进程内存和寄存器状态的快照。核心转储文件对于调试和故障分析非常有用,因为它可以帮助开发者或系统管理员了解导致进程崩溃的原因。

2.信号集

信号集(signal set)是Linux操作系统中用于管理信号的一种数据结构,它用于指定一组信号的集合,以便进行信号的阻塞、解除阻塞和查询操作。信号集是信号处理机制的一部分,用于处理信号的发送和接收。

信号集在内核中由**sigset_t**类型表示,这是一个无符号整数类型的数组,其中每个元素代表一个信号编号。

** sigprocmask**函数是 Linux 操作系统中用于信号屏蔽操作的系统调用。这个函数允许进程设置或查询当前进程的信号屏蔽字(signal mask),以决定哪些信号可以被进程接收到,哪些信号将被阻塞。

cpp 复制代码
#include <signal.h>

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
  • **how:**指定如何操作信号屏蔽字。它可以是以下值之一:

    • SIG_BLOCK: 添加**set**中指定的信号到当前进程的信号屏蔽字中。
    • SIG_UNBLOCK: 从当前进程的信号屏蔽字中移除**set**中指定的信号。
    • SIG_SETMASK: 设置当前进程的信号屏蔽字为**set**中指定的信号集合。
  • set: 指向一个**sigset_t**类型的指针,其中包含了要添加到或从信号屏蔽字中移除的信号集合。

  • oset: 指向一个**sigset_t** 类型的指针,如果**how** 参数是**SIG_BLOCK** 或**SIG_UNBLOCK** ,则这个指针指向当前进程的信号屏蔽字,sigprocmask 函数会根据**how** 参数的操作更新这个屏蔽字。如果**how** 参数是**SIG_SETMASK** ,则这个指针可以设置为**NULL**,因为新的信号屏蔽字将直接设置到当前进程的屏蔽字中。

sigprocmask 函数返回0表示成功,返回-1表示失败,并设置**errno**以指示错误类型。

cpp 复制代码
void PrintSig(sigset_t &pending)
{
    // 打印当前进程的挂起信号位图
    cout << "Pending bitmap:";
    // 遍历信号编号,从31(最大的信号编号)到1
    for (int signo = 31; signo > 0; signo--)
    {
        // 使用sigismember函数检查信号是否在挂起信号集中
        if (sigismember(&pending, signo))
        {
            // 如果信号在挂起信号集中,打印1
            cout << "1";
        }
        else
        {
            // 如果信号不在挂起信号集中,打印0
            cout << "0";
        }
    }
    // 换行
    cout << endl;
}

int main()
{
    // 定义两个信号集变量,用于屏蔽和挂起信号
    sigset_t block, oblock;
    // 初始化block信号集为空集
    sigemptyset(&block);
    // 将信号2添加到block信号集中
    sigaddset(&block, 2);
    // 遍历从1到31的所有信号,并将它们添加到block信号集中
    for (int signo = 1; signo <= 31; signo++)
    {
        sigaddset(&block, signo);
    }
    // 使用sigprocmask函数设置block信号集为当前进程的信号屏蔽字
    int n = sigprocmask(SIG_SETMASK, &block, &oblock);
    // 断言n的值为0,如果不是0,则程序会退出
    assert(n == 0);
    // 打印设置信号屏蔽字成功的消息
    cout << "block 2 signal success" << endl;

    // 创建一个无限循环,用于持续检查挂起信号
    while (true)
    {
        // 初始化pending信号集为空集
        sigset_t pending;
        sigemptyset(&pending);
        // 使用sigpending函数获取当前进程的挂起信号集
        n = sigpending(&pending);
        // 断言n的值为0,如果不是0,则程序会退出
        assert(n == 0);
        // 调用PrintSig函数打印挂起信号位图
        PrintSig(pending);
        // 暂停进程执行1秒
        sleep(1);
    }
}

对应于**SIGKILL** 和**SIGSTOP**信号,这两个信号是保留的,不能被阻塞或忽略

3.内核态和用户态

在计算机操作系统中,用户态(User Mode)和内核态(Kernel Mode)是两种不同的运行模式,它们在操作系统中扮演着不同的角色,并且具有不同的权限和功能。

用户态(User Mode)

用户态是操作系统中为普通应用程序提供的一种运行模式。在用户态中,进程只能访问自己的地址空间和有限的系统资源。用户态的主要特点是:

  • 进程只能访问自己分配的内存空间,不能直接访问其他进程或内核的内存空间。
  • 进程的执行受到一定的限制,不能直接执行某些操作,如访问硬件设备或修改系统配置。
  • 用户态的进程通常通过系统调用来请求内核服务,如文件操作、网络通信等。

内核态(Kernel Mode)

内核态是操作系统为内核提供的一种运行模式。在内核态中,内核可以访问所有的系统资源,包括硬件设备、内存空间和文件系统。内核态的主要特点是:

  • 内核可以访问所有的系统资源,包括硬件设备、内存空间和文件系统。
  • 内核可以执行所有类型的操作,包括直接访问硬件设备、修改系统配置等。
  • 内核态的进程通常是通过中断或系统调用从用户态切换到内核态。

信号捕捉的过程如下:

sigaction 是一个系统调用,用于设置信号的行为,包括信号处理函数、信号掩码和信号动作。这个系统调用允许用户程序对信号进行更精细的控制,包括设置信号的处理方式、屏蔽信号、设置信号动作等。

cpp 复制代码
#include <signal.h>

int sigaction(int signum, const struct sigaction *restrict new_action, struct sigaction *restrict old_action);
  • signum: 要设置行为的信号编号。
  • new_action: 指向**struct sigaction**的指针,其中包含了新的信号行为设置。这个结构体定义了信号处理函数、信号掩码和信号动作。
  • old_action: 指向**struct sigaction**的指针,用于保存旧的信号行为设置,如果需要的话。
cpp 复制代码
struct sigaction {
    void (*sa_handler)(int);           /* 信号处理函数 */
    void (*sa_sigaction)(int, siginfo_t *, void *); /* 信号处理函数,可以获取信号的附加信息 */
    sigset_t sa_mask;                 /* 信号掩码,用于阻塞信号 */
    int sa_flags;                     /* 信号行为的标志 */
    void (*sa_restorer)(void);        /* 恢复函数,通常设置为NULL */
};

使用代码如下:

cpp 复制代码
void Print(sigset_t &pending)
{
    // 打印当前进程的挂起信号位图
    cout << "curr process pending:";
    // 遍历信号编号,从31(最大的信号编号)到1
    for (int sig = 31; sig >= 1; sig--)
    {
        // 使用sigismember函数检查信号是否在挂起信号集中
        if (sigismember(&pending, sig))
        {
            // 如果信号在挂起信号集中,打印1
            cout << "1";
        }
        else
        {
            // 如果信号不在挂起信号集中,打印0
            cout << "0";
        }
    }
    // 换行
    cout << endl;
}

void handler(int signo)
{
    // 信号处理函数
    cout << "signal:" << signo << endl;
    // 创建一个信号集,用于存储挂起的信号
    sigset_t pending;
    // 初始化信号集为空集
    sigemptyset(&pending);
    // 创建一个无限循环,不断检查是否有信号被挂起
    while (true)
    {
        // 使用sigpending函数获取当前进程的挂起信号集
        sigpending(&pending);
        // 调用Print函数打印挂起信号位图
        Print(pending);
        // 暂停进程执行1秒
        sleep(1);
    }
}

int main()
{
    // 定义信号处理结构体
    struct sigaction act, oact;
    // 设置信号处理函数为handler
    act.sa_handler = handler;
    // 设置信号行为的标志为0
    act.sa_flags = 0;
    // 初始化信号掩码为空集
    sigemptyset(&act.sa_mask);
    // 使用sigaction函数设置SIGINT信号的处理方式
    sigaction(2, &act, &oact);
    // 创建一个无限循环,不断执行
    while (true)
    {
        // 暂停进程执行1秒
        sleep(1);
    }
    // 程序正常结束
    return 0;
}

4.volatile关键字

在C和C++编程语言中,volatile 是一个关键字,用于修饰变量或类型,以指示编译器在优化代码时不要假设该变量是常量。当一个变量被声明为**volatile**时,编译器不会对它的读写操作进行优化,这意味着每次访问该变量时,编译器都会直接从内存中读取其值,而不是从寄存器或缓存中读取。

相关推荐
秃头佛爷2 分钟前
Python学习大纲总结及注意事项
开发语言·python·学习
九河云1 小时前
AWS账号注册费用详解:新用户是否需要付费?
服务器·云计算·aws
Lary_Rock1 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
幺零九零零2 小时前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
dayouziei2 小时前
java的类加载机制的学习
java·学习
云飞云共享云桌面3 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮4 小时前
Linux 使用中的问题
linux·运维
dsywws5 小时前
Linux学习笔记之vim入门
linux·笔记·学习
晨曦_子画6 小时前
3种最难学习和最容易学习的 3 种编程语言
学习