【Linux】24.进程信号(1)

news2025/2/4 7:55:11

文章目录

  • 1. 信号入门
    • 1.1 进程与信号的相关知识
    • 1.2 技术应用角度的信号
    • 1.3 注意
    • 1.4 信号概念
    • 1.5 信号处理常见方式概览
  • 2. 产生信号
    • 2.1 通过终端按键产生信号
    • 2.2 调用系统函数向进程发信号
    • 2.3 由软件条件产生信号
    • 2.4 硬件异常产生信号
    • 2.5 信号保存
  • 3. 阻塞信号
    • 3.1 信号其他相关常见概念
    • 3.2 在内核中的表示
    • 3.3 sigset_t
    • 3.4 信号集操作函数
      • sigprocmask
      • sigpending


1. 信号入门

1.1 进程与信号的相关知识

  1. 进程 必须 识别+能够处理信号(信号没有产生,也要具备处理信号的能力)信号的处理能力,属于进程内置功能的一部分

  2. 进程即便是没有收到信号,也能知道哪些信号该怎么处理

  3. 当进程真的收到了一个具体的信号的时候,进程可能并不会立即处理这个信号,需要等到合适的时候

  4. 一个进程,当信号产生到信号开始被处理,就一定会有时间窗口,进程具有临时保存哪些信号已经发生了的能力


1.2 技术应用角度的信号

用户输入命令,在Shell下启动一个前台进程。

用户按下Ctrl+C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程

前台进程因为收到信号,进而引起进程退出

ctrl+c为什么能够杀掉我们前台进程呢?

Linux中,一次登陆中,一个终端一般会配上一个bash,每一个登陆,只允许一个进程是前台进程,可以允许多个进程是后台进程。

键盘输入首先是被前台进程收到的。(这是前台进程和后台进程的本质区别)

ctrl +c本质是被进程解释成为收到了信号。ctrl+c 会触发SIGINT信号(信号编号2),然后终端驱动程序捕获这个按键组合,将SIGINT信号发送给前台进程组的所有进程。

  1. 前台进程特性

    • 与终端关联

    • 能够接收终端输入

    • 属于当前终端的前台进程组

  2. 只能终止前台进程的原因

    • 终端只与前台进程组关联

    • 后台进程组收不到终端产生的信号

关键点:ctrl+c 本质是通过信号机制来终止进程的,而不是直接"杀死"进程。

b1cca7f77b86b1a13f67caf132fd63bf

1-31是普通信号,34-64是实时信号。

信号的处理方式:

  1. 默认动作

  2. 忽略

  3. 自定义动作(信号的捕捉)

例如红灯亮了就等绿灯是默认动作,不管红灯闯红灯就是忽略,红灯了唱歌跳舞就是自定义动作。

进程收到2号信号的默认动作,就是终止自己。

不是所有的信号都是可以被signal捕捉的,比如:9,19。

但是无论信号如何产生,最终一定是谁发送给进程的?

OS,因为OS是进程的管理者。


1.3 注意

  1. Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

1.4 信号概念

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


1.5 信号处理常见方式概览

(sigaction函数稍后详细介绍),可选的处理动作有以下三种:

  1. 忽略此信号。
  2. 执行该信号的默认处理动作。
  3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

2. 产生信号

2.1 通过终端按键产生信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。

Core Dump

首先解释什么是Core Dump。当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。

进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。

默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。


2.2 调用系统函数向进程发信号

首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号。

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。

raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1

abort函数使当前进程接收到信号而异常终止。

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

2.3 由软件条件产生信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

2.4 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。以下是几种常见的硬件异常及其对应的信号:

  1. 除零异常(SIGFPE):
    当程序执行除以0的操作时,CPU的算术逻辑单元会检测到这个异常。例如:
int a = 1;
int b = 0;
int c = a / b;  // 触发SIGFPE信号
  1. 段错误(SIGSEGV):
    当程序访问了非法内存地址时,内存管理单元(MMU)会产生异常。例如:
int *p = NULL;
*p = 1;         // 访问空指针,触发SIGSEGV信号

int arr[10];
arr[10000] = 1; // 数组越界,可能触发SIGSEGV信号
  1. 非法指令(SIGILL):
    当CPU执行了非法指令时产生此信号:
void (*bad_func_ptr)() = (void (*)())0x12345678;
bad_func_ptr();  // 执行非法地址的代码,触发SIGILL信号
  1. 总线错误(SIGBUS):
    当访问未对齐的内存地址时可能产生此信号:
char *ptr = (char *)0x12345;
int *iptr = (int *)ptr;
*iptr = 1;      // 可能触发SIGBUS信号

在系统层面,这些硬件异常的处理流程是:

  1. 硬件检测到异常
  2. 触发CPU中断
  3. CPU切换到内核态
  4. 内核将硬件异常转换为相应的信号
  5. 内核向进程发送信号
  6. 如果进程注册了信号处理函数,则执行该函数
  7. 如果没有注册处理函数,则执行信号的默认处理动作(通常是终止进程)

这就是为什么C/C++中的很多运行时错误(如除零、空指针解引用、数组越界等)最终都表现为进程收到信号并终止。这种机制让操作系统能够及时发现并处理程序中的严重错误,防止错误程序继续运行可能造成的更大危害。


2.5 信号保存

为什么要信号保存?

进程收到信号之后,可能不会立即处理这个信号。信号不会被处理,就要有一个时间窗口。


3. 阻塞信号

3.1 信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)

  • 信号从产生到递达之间的状态,称为信号未决(Pending)。

  • 进程可以选择阻塞 (Block )某个信号。

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

  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。


3.2 在内核中的表示

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

fca6ee5b6f978ae7d0db9dec37b27c60

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。


3.3 sigset_t

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。


3.4 信号集操作函数

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

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo); 
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

  • 注意:在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

sigprocmask

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1

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

b5469757cf1306db6a8ca96d07d949ea

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。


sigpending

#include <signal.h>
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1

代码:

void printsigset(sigset_t *set)
{
    // 打印信号集中的信号状态,用1表示信号在集合中,0表示不在
    for(int i=1; i<32; i++) {
        if (sigismember(set, i)) {  // 判断信号i是否在信号集set中
            putchar('1');
        } else {
            putchar('0');
        }
    }
    puts("");
}

int main()
{
    sigset_t s, p;
    sigemptyset(&s);        // 初始化信号集s为空集
    sigaddset(&s, SIGINT);  // 将SIGINT信号添加到信号集s中,Ctrl+C
    sigprocmask(SIG_BLOCK, &s, NULL);  // 设置信号屏蔽字,阻塞SIGINT信号
    
    while(1) {
        sigpending(&p);     // 获取未决信号集
        printsigset(&p);    // 打印未决信号集
        sleep(1);
    }
    return 0;
}

程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl+C将会使SIGINT信号处于未决状态。按Ctrl+\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

代码:

// 打印未决信号集的函数
void PrintPending(sigset_t &pending)
{
    // 从31号信号到1号信号逐个检查
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
            cout << "1";  // 信号处于未决状态
        else
            cout << "0";  // 信号不在未决集中
    }
    cout << "\n\n";
}

// 信号处理函数
void handler(int signo)
{
    cout << "catch a signo: " << signo << endl;
}

int main()
{
    // 4. 屏蔽所有可屏蔽信号
    sigset_t bset, oset;
    sigemptyset(&bset);    // 清空信号集
    sigemptyset(&oset);    // 清空旧信号集
    for (int i = 1; i <= 31; i++)
    {
        sigaddset(&bset, i); // 将所有信号添加到屏蔽集
    }
    sigprocmask(SIG_SETMASK, &bset, &oset); // 设置信号屏蔽字

    // 循环检测未决信号
    sigset_t pending;
    while (true)
    {
        int n = sigpending(&pending);  // 获取未决信号集
        if (n < 0)
            continue;
        PrintPending(pending);         // 打印未决信号集
        sleep(1);
    }


    // // 0. 对2号信号进行自定义捕捉
    // signal(2, handler);

    // // 1. 先对2号信号进行屏蔽 --- 数据预备
    // sigset_t bset, oset; // 在哪里开辟的空间???用户栈上的,属于用户区
    // sigemptyset(&bset);
    // sigemptyset(&oset);
    // sigaddset(&bset, 2); // 我们已经把2好信号屏蔽了吗?并没有设置进入到你的进程的task_struct
    // // 1.2 调用系统调用,将数据设置进内核
    // sigprocmask(SIG_SETMASK, &bset, &oset); // 我们已经把2好信号屏蔽了吗?ok

    // // 2. 重复打印当前进程的pending 0000000000000000000000000
    // sigset_t pending;
    // int cnt = 0;
    // while (true)
    // {
    //     // 2.1 获取
    //     int n = sigpending(&pending);
    //     if (n < 0)
    //         continue;
    //     // 2.2 打印
    //     PrintPending(pending);

    //     sleep(1);
    //     cnt++;
    //     // 2.3 解除阻塞
    //     if(cnt == 20)
    //     {
    //         cout << "unblock 2 signo" << endl;
    //         sigprocmask(SIG_SETMASK, &oset, nullptr); // 我们已经把2好信号屏蔽了吗?ok
    //     }
    // }
    // // 3 发送2号 0000000000000000000000010

    return 0;
}

被注释的代码:

// 0. 设置2号信号(SIGINT)的处理函数
signal(2, handler);

// 1. 先对2号信号进行屏蔽 --- 数据预备
sigset_t bset, oset;    // 在用户栈上创建信号集
sigemptyset(&bset);     // 初始化为空集
sigemptyset(&oset);     // 保存旧的信号屏蔽字
sigaddset(&bset, 2);    // 只添加2号信号到屏蔽集
// 调用系统调用,将数据设置进内核
sigprocmask(SIG_SETMASK, &bset, &oset);

// 2. 监控未决信号状态
// 重复打印当前进程的pending 0000000000000000000000000
sigset_t pending;
int cnt = 0;
while (true)
{
    int n = sigpending(&pending);  // 获取未决信号
    if (n < 0)
        continue;
    PrintPending(pending);         // 打印未决信号状态

    sleep(1);
    cnt++;
    // 20秒后解除2号信号的屏蔽
    if(cnt == 20)
    {
        cout << "unblock 2 signo" << endl;
        // 恢复原来的信号屏蔽字,即解除屏蔽
        sigprocmask(SIG_SETMASK, &oset, nullptr);
    }
}
// 3 发送2号 0000000000000000000000010

两个场景的区别:

  1. 当前执行的代码:
    • 屏蔽所有可屏蔽信号
    • 持续监控所有信号的未决状态
    • 信号会一直保持在未决状态
  2. 注释掉的代码:
    • 只屏蔽SIGINT(2号)信号
    • 设置了SIGINT的自定义处理函数
    • 20秒后解除屏蔽,让信号能够被处理
    • 可以观察到SIGINT信号从未决变为已处理的过程

注释中的重要说明:

  • task_struct:进程描述符,在内核中保存进程的信号屏蔽字
  • 信号集虽然在用户栈上定义,但实际的屏蔽操作是在内核中完成
  • 通过注释分步骤展示了信号屏蔽、监控和解除屏蔽的完整流程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2291687.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

股票入门知识

股票入门&#xff08;更适合中国宝宝体制&#xff09; 股市基础知识 本文介绍了股票的基础知识&#xff0c;股票的分类&#xff0c;各板块发行上市条件&#xff0c;股票代码&#xff0c;交易时间&#xff0c;交易规则&#xff0c;炒股术语&#xff0c;影响股价的因素&#xf…

用Python实现K均值聚类算法

在数据挖掘和机器学习领域&#xff0c;聚类是一种常见的无监督学习方法&#xff0c;用于将数据点划分为不同的组或簇。K均值聚类算法是其中一种简单而有效的聚类算法。今天&#xff0c;我将通过一个具体的Python代码示例&#xff0c;向大家展示如何实现K均值聚类算法&#xff0…

Flask代码审计实战

文章目录 Flask代码审计SQL注入命令/代码执行反序列化文件操作XXESSRFXSS其他 审计实战后记reference Flask代码审计 SQL注入 1、正确的使用直白一点就是&#xff1a;使用”逗号”&#xff0c;而不是”百分号” stmt "SELECT * FROM table WHERE id?" connectio…

Unity 2D实战小游戏开发跳跳鸟 - 跳跳鸟碰撞障碍物逻辑

在有了之前创建的可移动障碍物之后,就可以开始进行跳跳鸟碰撞到障碍物后死亡的逻辑,死亡后会产生一个对应的效果。 跳跳鸟碰撞逻辑 创建Obstacle Tag 首先跳跳鸟在碰撞到障碍物时,我们需要判定碰撞到的是障碍物,可以给障碍物的Prefab预制体添加一个Tag为Obstacle,添加步…

【玩转 Postman 接口测试与开发2_015】第12章:模拟服务器(Mock servers)在 Postman 中的创建与用法(含完整实测效果图)

《API Testing and Development with Postman》最新第二版封面 文章目录 第十二章 模拟服务器&#xff08;Mock servers&#xff09;在 Postman 中的创建与用法1 模拟服务器的概念2 模拟服务器的创建2.1 开启侧边栏2.2 模拟服务器的两种创建方式2.3 私有模拟器的 API 秘钥的用法…

mysql操作语句与事务

数据库设计范式 数据库设计的三大范式 ‌第一范式&#xff08;1NF&#xff09;‌&#xff1a;要求数据库表的每一列都是不可分割的原子数据项&#xff0c;即列中的每个值都应该是单一的、不可分割的实体。例如&#xff0c;如果一个表中的“地址”列包含了省、市、区等多个信息…

基于SpringBoot电脑组装系统平台系统功能实现五

一、前言介绍&#xff1a; 1.1 项目摘要 随着科技的进步&#xff0c;计算机硬件技术日新月异&#xff0c;包括处理器&#xff08;CPU&#xff09;、主板、内存、显卡等关键部件的性能不断提升&#xff0c;为电脑组装提供了更多的选择和可能性。不同的硬件组合可以构建出不同类…

【智力测试——二分、前缀和、乘法逆元、组合计数】

题目 代码 #include <bits/stdc.h> using namespace std; using ll long long; const int mod 1e9 7; const int N 1e5 10; int r[N], c[N], f[2 * N]; int nr[N], nc[N], nn, nm; int cntr[N], cntc[N]; int n, m, t;void init(int n) {f[0] f[1] 1;for (int i …

玉米苗和杂草识别分割数据集labelme格式1997张3类别

数据集格式&#xff1a;labelme格式(不包含mask文件&#xff0c;仅仅包含jpg图片和对应的json文件) 图片数量(jpg文件个数)&#xff1a;1997 标注数量(json文件个数)&#xff1a;1997 标注类别数&#xff1a;3 标注类别名称:["corn","weed","Bean…

string例题

一、字符串最后一个单词长度 题目解析&#xff1a;由题输入一段字符串或一句话找最后一个单词的长度&#xff0c;也就是找最后一个空格后的单词长度。1.既然有空格那用我们常规的cin就不行了&#xff0c;我们这里使用getline,2.读取空格既然是最后一个空格后的单词&#xff0c;…

设计模式 - 行为模式_Template Method Pattern模板方法模式在数据处理中的应用

文章目录 概述1. 核心思想2. 结构3. 示例代码4. 优点5. 缺点6. 适用场景7. 案例&#xff1a;模板方法模式在数据处理中的应用案例背景UML搭建抽象基类 - 数据处理的 “总指挥”子类定制 - 适配不同供应商供应商 A 的数据处理器供应商 B 的数据处理器 在业务代码中整合运用 8. 总…

基于脉冲响应不变法的IIR滤波器设计与MATLAB实现

一、设计原理 脉冲响应不变法是一种将模拟滤波器转换为数字滤波器的经典方法。其核心思想是通过对模拟滤波器的冲激响应进行等间隔采样来获得数字滤波器的单位脉冲响应。 设计步骤&#xff1a; 确定数字滤波器性能指标 将数字指标转换为等效的模拟滤波器指标 设计对应的模拟…

RabbitMQ快速上手及入门

概念 概念&#xff1a; publisher&#xff1a;生产者&#xff0c;也就是发送消息的一方 consumer&#xff1a;消费者&#xff0c;也就是消费消息的一方 queue&#xff1a;队列&#xff0c;存储消息。生产者投递的消息会暂存在消息队列中&#xff0c;等待消费者处理 exchang…

自动化构建-make/Makefile 【Linux基础开发工具】

文章目录 一、背景二、Makefile编译过程三、变量四、变量赋值1、""是最普通的等号2、“:” 表示直接赋值3、“?” 表示如果该变量没有被赋值&#xff0c;4、""和写代码是一样的&#xff0c; 五、预定义变量六、函数**通配符** 七、伪目标 .PHONY八、其他常…

响应式编程与协程

响应式编程与协程的比较 响应式编程的弊端虚拟线程Java线程内核线程的局限性传统线程池的demo虚拟线程的demo 响应式编程的弊端 前面用了几篇文章介绍了响应式编程&#xff0c;它更多的使用少量线程实现线程间解耦和异步的作用&#xff0c;如线程的Reactor模型&#xff0c;主要…

智能小区物业管理系统推动数字化转型与提升用户居住体验

内容概要 在当今快速发展的社会中&#xff0c;智能小区物业管理系统的出现正在改变传统的物业管理方式。这种系统不仅仅是一种工具&#xff0c;更是一种推动数字化转型的重要力量。它通过高效的技术手段&#xff0c;将物业管理与用户居住体验紧密结合&#xff0c;无疑为社区带…

从Proxmox VE开始:安装与配置指南

前言 Proxmox Virtual Environment (Proxmox VE) 是一个开源的虚拟化平台&#xff0c;基于Debian Linux&#xff0c;支持KVM虚拟机和LXC容器。它提供了一个强大的Web管理界面&#xff0c;方便用户管理虚拟机、存储、网络等资源。Proxmox VE广泛应用于企业级虚拟化、云计算和开…

【C++】B2115 密码翻译

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 &#x1f4af;前言&#x1f4af;题目解析&#x1f4af;1. 老师的做法代码实现&#xff1a;思路解析&#xff1a; &#x1f4af;2. 我的做法代码实现&#xff1a;思路分析&#xff1a; &#x1f4af;3. 老师…

Leetcode—598. 区间加法 II【简单】

2025每日刷题&#xff08;206&#xff09; Leetcode—598. 区间加法 II 实现代码 class Solution { public:int maxCount(int m, int n, vector<vector<int>>& ops) {int ans m * n;int x ops.size();if(ops.empty()) {return ans;}int xm ops[0][0], ym …

OpenCV:图像轮廓

目录 简述 1. 什么是图像轮廓&#xff1f; 2. 查找图像轮廓 2.1 接口定义 2.2 参数说明 2.3 代码示例 2.4 运行结果 3. 绘制图像轮廓 3.1 接口定义 3.2 参数说明 3.3 代码示例 3.4 运行结果 4. 计算轮廓周长 5. 计算轮廓面积 6. 示例&#xff1a;计算图像轮廓的面…