当前位置: 首页 > news >正文

深度剖析 Linux 信号:从基础概念到高级应用,全面解析其在进程管理与系统交互中的核心作用与底层运行机制

文章目录

  • 一.对信号的认识
    • 1.1信号的本质
    • 1.2对于信号还需要以下认知:
    • 1.3常见信号及含义:
    • 1.4对signal函数认识:
  • 二.如何产生信号
    • 2.1键盘产生信号
    • 2.2系统指令
    • 2.3系统调用
      • 2.3.1 kill:
      • 2.3.2 raise:
      • 2.3.3 abort:
      • 程序测试:
    • 2.4软件条件
      • 程序测试:
    • 2.5硬件异常
      • 程序测试:
  • 三.如何保存信号
    • 3.1准备工作:
    • 3.2进程如何管理信号
      • 3.2.1相关系统调用:
        • sigprocmask(对block位图进行操作)
        • sigpending(对pending位图进行操作)
      • 程序测试:
  • 四.如何捕捉信号
  • 五.如何处理信号

一.对信号的认识

在 Linux 系统中,信号(Signal) 是进程间通信的一种轻量级方式,用于通知进程发生了某种事件(如错误、用户操作、系统状态变化等)。信号可以被内核、其他进程或进程自身发送,接收信号的进程会根据预设的处理方式做出响应(如终止、暂停、忽略等)

1.1信号的本质

信号是异步事件:进程不需要主动等待信号,当信号发生时,内核会中断进程的正常执行流程,转而去处理信号。

异步与同步事件是什么?
同步是指任务按顺序执行,前一个操作完成后,才能开始下一个操作,整个过程中调用方会一直等待结果返回,不会被其他任务打断。示例:日常场景:打电话时,你说一句话,必须等对方回应后才能说下一句,这就是同步。

异步是指任务无需等待前一个操作完成即可开始,调用方不会被阻塞,而是继续执行其他任务,当被调用的操作完成后,会通过回调、事件通知等方式告知调用方结果。示例:日常场景:发邮件时,你点击 “发送” 后无需等待对方收到,就可以继续写其他邮件,这就是异步。

1.2对于信号还需要以下认知:

1.进程必须识别+能够处理信号,即使信号没有产生,也要有处理信号的能力,这属于进程内置功能的一部分;
2.当进程收到一个具体的信号,可能不会立即处理,可以进行延时处理;(信号处理三种方式:默认动作/忽略/自定义动作)
3.一个进程从产生到处理,就一定会有时间窗口,进程具有临时保存已经收到的信号的能力
4.kill -l有这些信号:信号编号范围从 1 到 64,其中 1 - 31 号是普通信号,34 - 64 号信号是实时信号,目前只考虑1-31普通信号

1.3常见信号及含义:

查看Linux中的信号可以使用:

kill -l

在这里插入图片描述
Linux 定义了几十种信号(如 SIGINT、SIGKILL 等),每种信号对应一种特定事件,用整数编号(1-64,其中 1-31 为传统信号,34-64 为实时信号),本篇只介绍1-31传统信号。这些信号默认的处理动作是在signal(7)中都有详细说明:

man 7 signal 

在这里插入图片描述

  • Term(Terminate,终止进程)
    行为:收到这类信号时,进程会直接终止(退出)。
    实例:SIGINT(用户按 Ctrl+C)、SIGTERM(默认 kill 命令发的终止信号 )。
  • Ign(Ignore,忽略信号)
    行为:收到这类信号时,进程啥都不做,直接忽略。
    实例:SIGCHLD(子进程退出时给父进程发的信号,默认忽略,父进程可主动捕获它回收子进程 )、SIGURG(套接字紧急数据通知,很多程序默认不用就忽略 )。
  • Core(终止进程并生成核心转储)
    行为:进程终止,同时生成 core dump 文件(记录进程崩溃瞬间的内存、寄存器等信息 ),方便事后用调试工具(如 gdb )分析崩溃原因。
    实例:SIGSEGV(非法内存访问,比如空指针、数组越界 )、SIGABRT(进程主动调用 abort() 触发 )。
    注意:生成 core 文件需要系统开启相关配置(默认可能关闭,需用 ulimit -c 核心转储文件大小 命令打开限制 )。
  • Stop(暂停进程)
    行为:进程会被暂停(暂停后不执行代码,但保留资源 ),可以用 SIGCONT 恢复。
    实例:SIGSTOP(强制暂停,不能被捕获 / 忽略 )、SIGTSTP(用户按 Ctrl+Z 触发,可被捕获 / 忽略 )。
    特点:SIGSTOP 是 “强制暂停”,进程无法拒绝;而 SIGTSTP 是 “友好暂停”,程序可以自定义处理(比如暂停前保存进度 )。
  • Cont(恢复暂停的进程)
    行为:如果进程当前是 “暂停状态”(比如被 SIGSTOP、SIGTSTP 暂停 ),收到 SIGCONT 会恢复执行;如果进程没暂停,这个信号会被忽略。
    典型场景:常用 fg/bg 命令(本质是给进程发 SIGCONT ),把后台暂停的任务调回前台 / 让它继续在后台运行。
    典型场景:常用 fg/bg 命令(本质是给进程发 SIGCONT ),把后台暂停的任务调回前台 / 让它继续在后台运行。

1.4对signal函数认识:

在这里插入图片描述
1.typedef void (*sighandler_t)(int);//函数指针
2.sighandler_t signal(int signum, sighandler_t handler);//设置信号动作,signum为kill中的信号名,后为自定义的动作函数),设置完毕后处理信号将切换为自定义动作,只需要在一开始设置,往后都有效;

eg.设置自定义动作/忽略/默认动作的三种方式:
signal(SIGINT, signalHandler); //signal(SIGINT, NULL); 空指针等于没有更改处理方式,还是默认动作
void signalHandler(int sig);是自定义动作函数,sig是要修改的信号名,方便收到信号后找到
signal(SIGINT,SIG_IGN); // 忽略SIGINT信号
signal(SIGINT,SIG_DFL); // 恢复SIGINT信号的默认处理方式

二.如何产生信号

2.1键盘产生信号

当我们输入指令时,可以向指定进程发送指定信号,比如:

ctrl c:代表二号信号 2 SIGINT 终止进程
ctrl z:代表二十号信号 20 SIGTSTP:可以发送停止信号,将当前前台进程挂起到后台
ctrl \ : 代表三号信号 3 SIGQUIT :使得进程退出产生core文件和coredump标记

这里需要了解前后台的相关知识:

键盘输入会被前台进程收到,一个终端对应一个会话对应一个bash对应一个前台进程对应多个后台进程,ctrl C实际上代表2号信号(SIGINT),可以使用jobs查看后台任务,fg 后台任务号可以将后台任务切到前台 ,ctrlZ暂停进程后会切换到后台,bg 后台任务号是让当前处于 “暂停状态” 的进程在后台继续执行
在这里插入图片描述

#此外
./xxx.exe #代表启动可执行程序,默认在前台运行,接收键盘输入
./yyy.exe & #代表在后台启动可执行程序,接收不到键盘输入,但可以正常输出

键盘是如何与操作系统关联起来的?
1.键盘通过封装读写函数与struct device形成虚拟文件系统,内存可以直接从键盘中读取数据;
2.中断针脚:CPU 有专门的针脚。当键盘需要 CPU 处理事务时,会通过中断单元向 CPU 的中断针脚发送中断号。硬件中断:当键盘有数据需要传输时,它会先将请求发送给中断单元,中断单元向 CPU 发送中断信号。CPU根据高低电流脉冲将中断号转换为0/1数据储存在寄存器中;CPU 响应中断:CPU 在每个指令周期结束时,会检查中断针脚是否有信号。如果检测到中断请求信号,并且 CPU 当前允许响应中断,CPU 会暂停当前正在执行的指令,保存当前的执行状态,然后根据中断号中断向量表(中断向量表存储了每个中断对应方法的地址 )找到对应的读写方法,进行数据的读写,当输入ctrl+C时,OS会判断数据将其转换为2号信号发送给进程
在这里插入图片描述
3.键盘与显示器独自拥有各自的缓冲区,互相不影响

2.2系统指令

除了在键盘输入ctrl+组合键,还可以使用命令行的方式来发送信号:

kill -信号 + pid

kill 是命令本身,信号 表示要发送的信号类型,信号可以通过信号编号(如 9 代表 SIGKILL) 或者信号名称(如 SIGTERM) 来指定。当省略信号时,默认发送 SIGTERM(15 号信号),pid 是目标进程的进程 ID。

pkill -信号 + 文件名字

pkill 是一个基于名称、进程属性等条件来查找并向匹配的进程发送信号的命令,信号 的含义与 kill 命令中的一样,用于指定要发送的信号类型,文件名字 指的是目标进程对应的可执行文件名称(比如 a.out 是一个 C 语言程序编译后的可执行文件),pkill 会根据这个名字去查找相关进程。此外,pkill 还支持通过其他条件(如进程所属用户 -u、进程所在终端 -t 等)来筛选进程。

在这里插入图片描述

2.3系统调用

2.3.1 kill:

向指定进程发送信号:
在这里插入图片描述
返回值:给指定进程发信号;成功就返回0;否则返回-1.

2.3.2 raise:

向本进程发送信号:
在这里插入图片描述
返回值:成功返回0失败返回非0。

2.3.3 abort:

向本进程发6号信号;并发生核心转储:
在这里插入图片描述
如果 SIGABRT 信号被忽略,或者被一个会返回的处理函数捕获,abort() 函数仍然会终止进程。它会通过恢复 SIGABRT 的默认处置方式,然后再次发送该信号来实现这一点。

补充: 部分系统中 abort() 会直接绕过信号处理机制: 某些 Linux 系统的 abort() 实现中,即使注册了 SIGABRT 的处理函数,abort() 也可能直接调用内核接口强制终止进程(如使用 _exit()),完全不触发自定义处理函数。这是因为 SIGABRT 设计用于处理 “不可恢复的致命错误”,系统不允许程序通过自定义处理函数逃避终止。

程序测试:

void signalHandler(int sig)
{cout << "Signal " << sig << " received." << endl;
}void test1()
{signal(SIGINT, signalHandler); //signal(SIGINT, NULL); 空指针等于没有更改处理方式,还是默认动作signal(SIGABRT, signalHandler); // 注册SIGABRT信号处理函数kill(getpid(), SIGINT); // 发送SIGINT信号给自己raise(SIGINT); // 发送SIGINT信号给自己abort(); // 触发SIGABRT信号,虽然自定义了SIGABRT的处理函数,但abort()还是会导致程序终止while(1){cout<<"Running... Press Ctrl+C to send SIGINT." << endl;sleep(1);}
}int main()
{test1();return 0;
}

在这里插入图片描述

2.4软件条件

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。什么是软件条件?

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进⾏相应的处理。简⽽⾔之,软件条件是因操作系统内部或外部软件操作⽽触发的信号产⽣。

这里主要介绍alarm函数 和SIGALRM信号:
在这里插入图片描述
unsigned int alarm(unsigned int seconds);会设定一个seconds秒后的闹钟,到时间后向当前进程发送SIGALRM信号。返回值:alarm() 函数返回距离之前已设置的闹钟预定触发时间剩余的秒数;若之前未设置任何闹钟,则返回零。闹钟是一次性的,也就是说只能设定触发一次,操作系统会将其描述为struct alarm的结构体通过堆进行组织:

struct alarm {struct list_head    list;        /* 闹钟链表节点,用于将所有闹钟链接到进程的alarm_list */unsigned int        pending;     /* 标记闹钟是否已生效(1)或未生效(0) */ktime_t             expiry;      /* 闹钟到期时间(基于ktime_t时间类型) */struct task_struct  *task;       /* 指向拥有该闹钟的进程描述符 */unsigned int        interval;    /* 周期性闹钟的间隔时间(单位:jiffies) */void                (*function)(unsigned long); /* 闹钟到期时调用的函数 */unsigned long       data;        /* 传递给function函数的参数 */struct rcu_head     rcu;         /* RCU(Read-Copy-Update)机制相关字段 */
};

程序测试:

void signalHandler(int sig)
{int n=alarm(5);if (n == 0){cout << "Alarm was not set." << endl;}else{cout << "Alarm was set to go off in " << n << " seconds." << endl;}cout << "Signal " << sig << " received." << endl;
}void test2()
{alarm(5);signal(SIGALRM, signalHandler); // 注册SIGALRM信号处理函数while(1){cout<<"Running... Waiting for SIGALRM." << endl;sleep(1);}
}int main()
{test2();return 0;
}

在这里插入图片描述

2.5硬件异常

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。

为什么除零错误会产生段错误?操作系统如何得知?
1.代码中发生/0时,CPU上的状态寄存器会标记溢出标志位(由0->1),由于OS和CPU密切关联,OS会向进程发送信号,进程可以选择处理方式;当前进程相关联的寄存器上的数据都是当前进程的上下文,当前进程发生异常,不会影响到操纵系统或者其他进程的运行,当进程没有退出时,CPU一直检测到溢出标志位的错误,操作系统也会一直向进程发送信号;
2.当发生野指针错误时,保存在内存上的页表和MMU地址转换失败,虚拟地址到物理地址转换失败,会将失败地址放在CPU的一个寄存器中,同样会被操作系统识别到,向进程发送信号

程序测试:

void test3()
{pid_t pid = fork();if(pid < 0){cout << "Fork failed." << endl;return;}else if(pid == 0) // 子进程{cout<<"Child process started with PID: " << getpid() << endl;int a=10;a/=0; // 故意制造一个除零错误,触发SIGFPE信号exit(2);}else // 父进程{int status=0;waitpid(pid, &status, 0); // 等待子进程结束cout<<"exit_code:"<<((status>>8)&0xFF)<<" exit_signal:"<<(status&0x7F) << " core_dump:"<<((status>>7)&1) << endl;//这里将退出码,收到的信号以及coredump标志位 使用位运算提取出来看看!}
}int main()
{test3();return 0;
}

在这里插入图片描述
这时可以看到/0错误被检测出,但是core_dump标志位为0,这需要我们手动设置ulimit参数:
手动ulimit -c 大小 去开启这一功能,形成core.pid后-g使用gdb调试:core-file core.pid可以查看错误来源

在这里插入图片描述
这样就可以看到设置的coredump文件大小为10240字节,再次运行看看:
在这里插入图片描述
core_dump为1,代表已经生成核心转储文件,可以使用GDB来事后调试查看错误出处:
在这里插入图片描述
可以看到,根据核心转储,我们可以发现代码的错误在78行,除零错误!

三.如何保存信号

3.1准备工作:

普及下有关信号的相关背景:
1.操作系统将信号发送给进程,保存在task_struct中的int signal:0000 0000 0000 0000 0000 0000 0000 0000 32个比特位,第0位不用,剩下31个比特位对应31个普通信号;
信号位图将指定的信号对应比特位0->1;如果是实时信号,会将信号存入结构体用双向链表管理起来
2.实际执⾏信号的处理动作称为信号递达(Delivery);信号从产⽣到递达之间的状态,称为信号未决(Pending)
在task_struct中,存在block、pending、handler三个位图,block位图用1表示当前比特位对应的信号被屏蔽,pending位图用1表示收到当前比特位对应的信号,handler函数指针数组,下标对应信号对应着信号处理方法。
3.进程可以选择阻塞(Block)某个信号。
(被阻塞的信号在接受到之后只会被标记,不会被执行)

4.被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
注意: 阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作;而且当pending已经标记后;当阻塞解除;它会立即执行对应的信号(先清空对应pending表的信号然后再去执行)
5.如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理? Linux是这样实现的 : 常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

3.2进程如何管理信号

来看看流程图:
在这里插入图片描述

从上图来看,每个信号只有一个比特位的未决标志,非0即1,不记录该信号产生了多少次。阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储, sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。后面详细介绍——

3.2.1相关系统调用:

前文已经知道了sigset_t是一个位图结构,下面介绍操作:

#include <signal.h>
int sigemptyset(sigset_t *set);//把这个信号集每一位都初始化为0
int sigfillset(sigset_t *set);//把这个信号集每一位都变成1
int sigaddset(sigset_t *set, int signo);//添加 0->1
int sigdelset(sigset_t *set, int signo);//删除 1->0
int sigismember(const sigset_t *set, int signo);//判断信号是否存在(存在返回1不存在0出错-1)

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

sigprocmask(对block位图进行操作)

在这里插入图片描述
返回值:若成功则为 0,若出错则为 -1(9&&19号信号无法被block)

  • 如果 oldset 是非空指针,则读取进程的当前信号屏蔽字通过 oset 参数传出。
  • 如果 set 是非空指针,则更改进程的信号屏蔽字,参数 how 指示如何更改。
  • 如果 oset 和 set 都是非空指针,则先将原来的信号屏蔽字备份到 oset 里,然后根据 set 和 how 参数更改信号屏蔽字。
    在这里插入图片描述

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

操作符说明等效表达式
SIG_BLOCKset 包含了我们希望添加到当前信号屏蔽字的信号mask = mask
SIG_UNBLOCKset 包含了我们希望从当前信号屏蔽字中解除阻塞的信号mask = mask & ~set
SIG_SETMASK 设置当前信号屏蔽字为 set 所指向的值mask = set
sigpending(对pending位图进行操作)

在这里插入图片描述
拿出pending位图,成功返回0,失败返回-1

程序测试:

void print(sigset_t *set)
{for(int i=32;i>=1;i--){if(i%4==0) cout<<" ";if(sigismember(set, i)) cout<<1;else cout<<0;}cout << endl;
}
void signalhandler(int sig)
{cout << "Signal " << sig << " received." << endl;
}
void test2()
{sigset_t set,oset,pset;//练习使用各种信号集操作函数signal(SIGINT, signalhandler); sigemptyset(&set); // 初始化信号集为空sigemptyset(&oset); // 初始化另一个信号集为空sigemptyset(&pset); // 初始化第三个信号集为空sigaddset(&set, 2); // 将SIGINT信号添加到信号集中int n=sigprocmask(SIG_SETMASK, &set, &oset); // 将set信号集中的信号阻塞,并将之前的阻塞信号集保存到oset中if(n == -1){perror("sigprocmask");return;}int cnt=0;while(1){int n=sigpending(&pset); // 获取当前进程的未决信号集if(n == -1){perror("sigpending");return;}print(&pset); // 打印未决信号集cnt++;if(cnt==10){int m=sigprocmask(SIG_SETMASK, &oset, nullptr); // 恢复之前的阻塞信号集if(m == -1){perror("sigprocmask");return;}}sleep(1);}
}int main()
{test2();return 0;  
}

在这里插入图片描述
可以看到,当ctrl+C时,pending表收到2号信号,0->1,但由于block表中2号信号被设置为阻塞,因此不处理,当cnt==10时,block表取消对2号信号的阻塞,pending表中1->0,对应自定义处理函数生效!

四.如何捕捉信号

虚拟地址空间的补充:
在这里插入图片描述

虚拟地址中0-3G留给用户,3G-4G留给OS,这部分内存通过内核级页表映射到物理内存,(一般是映射到物理内存最低部分),虽然用户级页表随着进程增多而增多,但内核级页表永远只有一份,并且由于操作系统内容是不变的,在执行代码时,跳转到内核级虚拟内存上就可以执行系统调用

操作系统的时钟中断:

操作系统本质是基于时间中断的一个死循环,CMOS 芯片内置了一个实时时钟(Real-Time Clock,RTC),CMOS芯片每隔很短时间,通过针脚向计算机通过时钟中断发送中断号,CPU通过中断号查找中断向量表使OS继续执行重复任务

用户态与内核态:

用户态 : 用户态是用户程序运行时的状态,其核心特点是权限受限,目的是防止用户程序误操作或恶意破坏系统资源,以用户身份只能访问[0,3GB]的虚拟地址空间
内核态 : 内核态是操作系统内核运行时的状态,拥有最高权限,负责管理系统的核心资源和执行关键操作,以内核的身份可以访问[3,4GB]的虚拟地址空间

OS如何区分内核态和用户态?

通过CPU上的ecs寄存器的后两位数据,00对应着内核态,11对应着用户态操作系统会通过int 80汇编命令转换为内核态——陷入内核

下面介绍信号的捕捉与处理 :

在这里插入图片描述
用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。信号接收到不会立即处理,在内核态返回到用户态时,进行信号的检测与处理,具体流程类似于循环,详见图解!

在这里插入图片描述四个交点代表内核态和用户态之间的相互转化,中间的交点代表进行信号的检测与处理!在例子中,这一次循环只进行了一次信号检测与处理,何时会出现 “同一循环多次处理信号”?
只有一种情况:在第一次信号处理完成后,第二次切换回主流程前,又产生了新的未决信号 。例如:

  • 主流程 → 内核态(因系统调用)→ 检测到信号 A → 切用户态处理 A → 处理中又触发信号 B → 处理完 A 切回内核 → 第二次切用户态前检测到 B → 再次处理 B。

五.如何处理信号

sigaction系统调用:

在这里插入图片描述
sigaction可以对signo信号的处理方式进行设定,类似于signal,可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
//singo是信号序号,act和oact是两个结构体:
struct sigaction {void     (*sa_handler)(int);//函数指针void     (*sa_sigaction)(int, siginfo_t *, void *);sigset_t   sa_mask;//在处理指定信号时,(该信号被屏蔽)设定还想屏蔽的其他信号int        sa_flags;void     (*sa_restorer)(void);};

当信号未决时,pending表中为1,当信号递达时,先将pending表中由1->0,再调用信号的处理函数,内核自动将当前信号加入进程的信号屏蔽字,防止信号捕捉被嵌套调用。当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前信号处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段设置这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

http://www.lryc.cn/news/617424.html

相关文章:

  • 电力仿真系统:技术革新与市场格局的深度解析
  • 【CV 目标检测】①——目标检测概述
  • 【Oracle】如何使用DBCA工具删除数据库?
  • 低延迟RTSP|RTMP视频链路在AI驱动无人机与机器人操控中的架构实践与性能优化
  • 排序与查找,简略版
  • 简单清晰的讲解一下RNN神经网络
  • 常用设计模式系列(十九)- 状态模式
  • EI检索-学术会议 | 人工智能、虚拟现实、可视化
  • 揭开内容分发网络(CDN)的神秘面纱:互联网的隐形加速器
  • 武汉火影数字|VR大空间是什么?如何打造VR大空间项目
  • 【线性基】 P3857 [TJOI2008] 彩灯|省选-
  • 第16届蓝桥杯Python青少组中/高级组选拔赛(STEMA)2024年10月20日真题
  • 【14-模型训练细节】
  • 基于Android的小区车辆管理系统
  • 让AI应用开发更简单——蚂蚁集团推出企业级AI集成解决方案
  • 论文中PDF的公式如何提取-公式提取
  • 闸机控制系统从设计到实现全解析:第 5 篇:RabbitMQ 消息队列与闸机通信设计
  • 覆盖近 1.5 万个物种,谷歌 DeepMind 发布 Perch 2.0,刷新生物声学分类检测 SOTA
  • 国内 Mac 开启 Apple Intelligence 教程
  • 【C++】哈希表的实现(unordered_map和unordered_set的底层)
  • Redis实现排行榜
  • 2025年渗透测试面试题总结-14(题目+回答)
  • 【MySQL基础篇】:MySQL索引——提升数据库查询性能的关键
  • 简单的身份验证中间件Tinyauth
  • 如何使用 Watchtower 实现定时更新 docker 中的镜像并自动更新容器(附 schedule 的参数详细解释)
  • 京东商品评论API秘籍!轻松获取商品评论数据
  • Go 语言三大核心数据结构深度解析:数组、切片(Slice)与映射(Map)
  • 【JSON】通俗易懂的JSON介绍
  • LangChain 框架 Parser 讲解
  • Spring Framework源码解析——InitializingBean