【Linux篇】信号从哪来?到哪去?—— Linux信号的产生方式与保存机制

【Linux篇】信号从哪来?到哪去?—— Linux信号的产生方式与保存机制
📌 个人主页:孙同学_
🔧 文章专栏:Liunx
💡 关注我,分享经验,助你少走弯路!

文章目录

一. 认识信号

首先我们应该知道信号和信号量之间没有任何关系。
生活中的信号比如说:闹钟,红绿灯,上课铃声,狼烟,电话铃声,肚子叫,脸色等。
闹钟响了我们就得起床,上课铃声响了我们就得进教室进行上课,狼烟起来了士兵们就知道要打仗了,电话铃声响了我们就知道来电话了,肚子叫我们就知道我们的肚子饿了,一个人的脸色不好,我们就能直到他可能生气了。
什么是信号?中断我们人正在做的事情,是一种事件的异步通知机制

其实进程就相当于我们的人,当进程收到信号时,进程就要中断进程正在做的事情,这种方式就叫做信号,所以信号是给进程发送的,用来进行事件异步通知的机制

  • 信号的产生相对于进程的运行是异步的
  • 信号是发给进程的

基本结论:

  1. 信号处理在信号没有产生时就知道该如何处理了
  2. 信号的处理不是立即处理,可以等一会再处理,在合适的时候进行处理
  3. 人能识别信号前提是被“教育”过的(红灯亮了要等一等),进程也是如此,OS程序员设计的进程,进程早已经内置的对于信号的识别和处理方式!
  4. 信号源是非常多的,给进程产生信号的信号源也非常多

二. 产生信号的方式

2.1 键盘产生信号

我们先编写一段代码testSig.cc

#include<iostream>#include<unistd.h>intmain(){while(true){ std::cout <<"Hello world"<< std::endl;sleep(1);}return0;}
在这里插入图片描述


当这段代码运行起来后我么想终止,我们可以使用ctrl + c,但为什么ctrl + c可以终止呢?
因为ctrl + c是给目标进程发送信号的。相当一部分信号的处理动作就是让进程自己终止。

查看linux中的信号列表
指令:kill -l

在这里插入图片描述


在我们计算机中,信号就是一个整数的数字,我们未来想要用进程的话,可以用该数字向目标进程发送信号,但是数字的可读性不是很好,未来我们在使用的时候,会把数字定义成为大写的字母,该宏对应的值就是前面匹配的数字。
Linux系统里一共有62和信号,从34到64这部分信号称为实时信号,这种信号往往一旦产生就需要立即处理。

而我们上面的ctrl + c就是通过键盘给目标进程发送2号信号。

进程收到信号的处理方式:
1.默认动作处理
2.自定义动作处理
3.忽略处理!

更改一个信号的默认处理动作:signal

在这里插入图片描述
#include<signal.h>typedefvoid(*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
  • 参数
    • signum:信号对应的数字
    • handler:返回值为void,参数为int的函数

信号不再执行默认的方法,反而执行自定义的函数中指针指向的方法。

案例:将2号信号的默认处理改成打印:"获得了一个信号:" << sig

#include<iostream>#include<unistd.h>#include<signal.h>voidheadlerSig(int sig){ std::cout <<"获得了一个信号:"<< sig << std::endl;}intmain(){signal(SIGINT,headlerSig);int cnt =0;while(true){ std::cout <<"Hello world: "<< cnt++<< std::endl;sleep(1);}return0;}

当我们ctrl + c的时候就会发现,此时的ctrl + c不再是终止,而是打印出这段话。

在这里插入图片描述


此时我们想终止的话可以用ctrl + \3号信号来终止。

查看所有信号的额默认处理:man 7 signal

在这里插入图片描述

我们上面说过,信号是发送给目标进程的,可是目标进程是谁呢?

前台进程和后台进程

当我们在命令行直接./XXX运行时,这种进程叫做前台进程;当我们直接在命令行./XXX &,这叫做后台进程。
当我们没有启动任何程序的时候,我们的系统里有一个进程一直在给我们提供的服务,这个进程叫做命令行shell进程,命令行shell进程就是在前台的。

当我们的进程是后台进程时,我们ctrl + c无法处理我们的信号,所以键盘产生的信号只能发送给前台进程。

我们再来谈谈前后台的问题:
当我们登陆Linux系统,不管我们是拿桌面登录还是拿Xshell登陆,Linux系统首先会帮我们创建一个进程,这个进程叫做shell,在ubuntu下这个进程叫做bash进程,这个bash进程首先会输出命令行,然后等待用户输入。当我们./xxx它就会创建一个子进程,这个进程是前台进程,前台进程能从标准输入(键盘)中获取内容,后台进程是相反的。但是不管是前台进程还是后台进程,它们两个都能向标准输出上打印。

前台进程只能有一个,后台进程可以有多个。
前台进程的本质是从键盘上获取数据的。

我们以前的父进程退出,子进程ctrl + c就杀不掉了,原因是父进程退出了子进程就变成了孤儿进程,被自动提到后台了,所以只能用kill杀掉。

jobs查看所有的后台任务
fg + 任务号:表示把特定的进程提到前台
ctrl + z:暂停进程
前台进程不能被暂停,因为前台进程永远要接收用户输入
所以一个进程一旦被暂停,就会被自动提到后台
让暂停的进程运行起来:bg + 任务号,但是此时运行起来的进程是后台进程

什么叫做给进程发送信号?
信号产生之后并不是立即处理的,所以进程要把接收到的信号记录下来。记录的目的是为了在合适的时候处理。
记录在哪里?
进程的struct task_struct
如何记录?
在进程的pcb里维护一个整数,位图结构,用比特位的位置表示信号编号,用内容表示是否收到信号。
发送信号的本质是向目标进程写信号,本质上是修改位图(需要两个东西,一个是进程的pid,一个是信号的编号)
task_struct舒徐操作系统内的数据结构对象,所以修改位图的本质是修改内核的数据。
不管信号怎么产生,发送信号在底层必须让给OS发送。所以操作系统必须提供发送信号的系统调用。kill命令是用c语言写的,它调的就是操作系统的系统调用接口,来完成对目标进程发信号。

在这里插入图片描述

信号 VS 通信IPC
狭义上讲,通信IPC和信号不一样

2.2 系统调用

给目标进程发送信号的系统调用:kill

在这里插入图片描述

原型

#include<sys/types.h>#include<signal.h>intkill(pid_t pid,int sig);

代码示例:
Makefile

.PHONY:all all:testsig mykill testsig:testSig.cc g++-o $@ $^-std=c++11 mykill:mykill.cc g++-o $@ $^-std=c++11.PHONY:clean clean: rm -f testsig mykill 

mykill.cc

#include<iostream>#include<sys/types.h>#include<signal.h>//./mykill signumber pidintmain(int argc,char*argv[])//命令行参数{if(argc !=3){ std::cout <<"./mykilee signumber pid"<< std::endl;return1;//退出码设置为1}int signum = std::stoi(argv[1]);//给进程发送几号信号 pid_t target = std::stoi(argv[2]);//给哪个进程发送信号int n =kill(target,signum);//给哪个进程发,发什么if(n ==0)//发送成功{ std::cout <<"send"<< signum <<"to"<< target <<"success";return0;}return0;}

testSig.cc

#include<iostream>#include<unistd.h>#include<signal.h>#include<sys/types.h>voidheadlerSig(int sig){ std::cout <<"获得了一个信号:"<< sig << std::endl;}intmain(){signal(SIGINT,headlerSig);int cnt =0;while(true){ std::cout <<"Hello world: "<< cnt++<<",pid"<<getpid()<< std::endl;sleep(1);}return0;}

我们的信号大部分都是可以自定义捕捉的,但是有一个信号不能自定义捕捉,这个信号是9号信号SIGKILL,所以9号信号不能被自定义捕捉。

产生信号的第二个系统调用:raise

在这里插入图片描述
#include<signal.h>intraise(int sig);

raise表示自己给自己发送信号,raise可以指明自己给自己发送哪一个信号

#include<iostream>#include<unistd.h>#include<signal.h>#include<sys/types.h>voidheadlerSig(int sig){ std::cout <<"获得了一个信号:"<< sig << std::endl;}intmain(){for(int i =1; i <32; i++)signal(i, headlerSig);// 捕捉所有信号for(int i =1; i <32; i++){//每隔1秒自己给自己发一个信号sleep(1);raise(i);}int cnt =0;while(true){ std::cout <<"Hello world: "<< cnt++<<",pid"<<getpid()<< std::endl;sleep(1);}return0;}
在这里插入图片描述


到9号信号停了下来原因是9号信号不能被捕捉。

第三个系统调用:abort(其实不属于系统调用)
abort也是自己给自己发送信号,这个信号要求进程必须处理,它是用来终止进程的

2.3 硬件异常产生信号

先说问题:我们在执行C/C++代码时,我们的程序有时会崩掉,一种叫做除0,一种叫做野指针。
我们的代码里要是有除0错误代码就会崩掉是因为进程收到了8号信号(SIGFPE)。
我们的代码中要是有野指针,就会收到11号信号(SIGSEGV)。段错误

信号全部都是由操作系统发送的,我们的程序出错了,操作系统会识别我们的进程犯错了并且分析犯错类型,然后向我们的目标进程发送信号,整个过程其实和我们的用户是没有关系的。

现在的核心问题是操作系统怎么知道我们的进程犯错了???

  • 操作系统怎么知道我们的程序除0了?
    除0运算本质是在CPU上运行的,CPU上有各种的寄存器,比如标志寄存器(EFLAGS)这个寄存器是若干个比特位,比如000100,有一个比特位表示我们CPU当前计算时是否出现溢出。换句话说我们的操作系统怎么知道CPU计算出问题了呢?
    答案是我们的CPU属于硬件,操作系统是软硬件资源的管理者,当我们的程序一旦出错,操作系统是能识别到是硬件上出错了的,然后它这个计算是溢出了的,而我们的CPU寄存器保存的是当前进程的上下文,当然也包括当前进程的task_struct,它的硬件上出错了,所以操作系统就能识别到哪一个进程出错了,操作系统转而给我们的目标进程发送对应的信号。
  • 操作系统怎么知道野指针(段错误)的问题?
    我们在拿野指针访问0号地址,则进程的虚拟地址就是0,而0号地址在我们的页表中并不存在映射关系,所以无法映射到我们的内存当中,CPU中存在的地址都是虚拟地址,在我们的CPU内部存在着一个寄存器叫做CR3寄存器,这个寄存器会记录下当前页表的起始地址。在CPU内部继承了一种硬件单元,这个硬件单元叫做MMU,将来CPU寻址是将虚拟地址交给MMU,把CR3寄存器里的内容也交给MMU,此时虚拟地址有了,页表有了,MMU在硬件层面上拿着对应的页表完成虚拟地址到物理地址的转化。
    MMU在转化时也有可能转化失败,此时再往0号地址去写就会发生硬件报错。操作系统作为软硬件资源的管理者,硬件报错了,当前运行的依旧是当前进程的上下文,并且知道当前错误是虚拟到物理转化失败了,并且还要写入,所以就会给当前进程发送11号信号,让进程直接终止。

2.4 软件条件产生信号

存在一个系统调用,为当前进程设置闹钟:alarm
当闹钟时间到了的时候,它就会为当前进程推送一个信号。
比如alarm(5)定一个5秒的闹钟,5秒之后它就会为当前进程发送一个信号。alarm(0)表示取消闹钟。

在这里插入图片描述
#include<unistd.h>unsignedintalarm(unsignedint seconds);

调用 alarm函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发SIGALRM 信号,该信号的默认处理动作是终止当前进程。

  • 返回值
    这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。

我们来设定一种闹钟,这个闹钟1秒钟发送一个闹钟信号,此时进程就执行某种任务。以闹钟为驱动力,让进程每隔1秒做一次,每隔1秒做一次。

#include<iostream>#include<unistd.h>#include<signal.h>#include<sys/types.h>voidhandlerSig(int sig){ std::cout<<"获得了一个信号"<< sig <<"pid:"<<getpid()<< std::endl;alarm(1);}intmain(){signal(SIGALRM,handlerSig);alarm(1);while(true){ std::cout <<".,"<<"pid:"<<getpid()<< std::endl;sleep(1);}return0;}

等待一个信号的接口:pause

在这里插入图片描述
#include<unistd.h>intpause(void);

pause的作用是等待一个信号,也就是没有信号时pause就暂停了,只有当我们的信号被捕捉,并且从信号捕捉函数返回的时候它才会直接返回。出错的时候会返回-1,否则就会暂停起来。

我们让我们的进程每隔一秒完成一些任务

让我们的进程一直处于暂停状态,受外部信号的驱动每隔一秒发送一个信号,信号到来时它会执行对应的任务,这就是操作系统的本质

在这里插入图片描述
#include<iostream>#include<vector>#include<functional>#include<unistd.h>#include<signal.h>#include<sys/types.h>///////func/////////voidSche(){ std::cout <<"我是进程调度"<< std::endl;}voidMemManger(){ std::cout <<"我是周期性的内存管理,正在检查有没有内存问题"<< std::endl;}voidFflush(){ std::cout <<"我是刷新程序,我在定期刷新内存数据"<< std::endl;}////////////////////using func_t = std::function<void()>;// 用来存储没有返回值且没有参数的函数 std::vector<func_t> funcs;// 这个容器中包含了一堆方法// 每隔一秒,完成一些任务voidhandlerSig(int sig){ std::cout <<"#########################"<< std::endl;for(auto f : funcs){f();} std::cout <<"#########################"<< std::endl;alarm(1);}intmain(){ funcs.push_back(Sche); funcs.push_back(MemManger); funcs.push_back(Fflush);signal(SIGALRM, handlerSig);alarm(1);while(true)//操作系统的本质! {pause();}return0;}

快速理解闹钟:
OS内可能同时存在很多闹钟,所以操作系统就要对闹钟进行管理。所以所谓的闹钟就是一种数据结构,创建闹钟就是创建一种闹钟的结构体对象。
下面这就是一个闹钟的描述结构:

structtimer_list{structlist_head entry;unsignedlong expires;//闹钟自己的过期时间void(*function)(unsignedlong);//闹钟要执行的方法unsignedlong data;structtvec_t_base_s*base;};

我们之前学过数据结构中的堆,我们设置的闹钟是以最小堆的方式,即堆顶元素为最短超时的闹钟,通过和时间戳做比较,如果堆顶元素的时间小于了我们的时间戳,则堆顶的闹钟出堆,向目标进程发送信号,执行对应的SIGALRM

所以闹钟超时之后,向目标进程发送信号的这种方式,叫做软件条件产生信号。

总结:上面产生信号的五中方式当中,不论哪一种方式,都是由OS进行信号的发送的。所谓的信号发送其实是向目标进程修改比特位。

三. 信号的保存

我们上面就已经说过了,进程接受到信号不一定立即处理,所以前提就先得保存信号。

3.1 信号其他的相关常见概念

  • 实际执行信号的处理动作称为信号抵达(Delivery)。
  • 信号从产生到信号抵达中间的的状态称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号,我们把阻塞信号也叫做屏蔽信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行抵达的动作。
  • 注意,忽略和信号阻塞是不同的,忽略是信号抵达状态时一种可选择的执行信号的方式。

3.2 这些概念在内核中的表现

在这里插入图片描述

在进程的PCB中不仅仅有位图,其实进程当中与普通信号相关的有三张表,blockpendinghandler表。

pending:(unsigned int pending)表其实就是保存我们信号收到的位图,也叫做未决图。

  • 比特位的位置:表示的是第几个信号
  • 比特位的内容:是否收到信号,0表示未收到该信号,1表示收到了该信号

block:(unsigned int block)也是位图。

  • 比特位的位置:表示的是第几个信号
  • 比特位的内容:是否阻塞,0表示可以直接抵达,1表示不能直接抵达
在这里插入图片描述


pending&(~block):表示可以直接抵达的信号

handler:我们之前用的系统调用signal,它里面的一个参数就是handler

在这里插入图片描述


这个表是一个函数指针数组。这个表的下标表示的就是信号编号。
所以我们的signal函数的本质就是在系统调用的内部,拿着我们传进去的信号编号作为该数组的索引,找到特定的位置,把我们自己设置的函数的地址传进去。

所以当前有没有信号,有没有被阻塞,怎么处理我们就知道了。这三张表合起来,共同承担了进程怎么识别信号。

回到最开始我们说的,信号再没抵达时我们就知道该信号如何处理了,即便是没有block,pending,我们也知道它的handlerSIG_DFL默认处理动作,进程未来处理信号的方式本质上就是修改handler表

所以我们应该如何看待信号的处理呢?

在这里插入图片描述


我们应该横着来看这张表,从上往下对应了31组描述信号的关系。这3这表支撑了进程对信号的识别。

3.3 sigset_t

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

3.4 信号集操作函数

sigset_t类型对于每种信号用一个比特位表示“有效”或者“无效”状态,至于内部如何存储这些比特位依赖于系统的实现,从使用者的角度是不关心的,使用者只需要调用下面的函数来操作sigset_t变量,而不需要对它的内部做任何解释。

#include<signal.h>intsigemptyset(sigset_t *set);//表示把其中一个信号集清空intsigfillset(sigset_t *set);//把位图全部置1intsigaddset(sigset_t *set,int signo);//把一个信号添加到集合里(这个信号是几,就把其比特位置1)intsigdelset(sigset_t *set,int signo);//把一个信号删掉(比特位置0)intsigismember(const sigset_t *set,int signo);//判断一个信号是否在集合里(其实就是判断第几个比特位是否为1)
  • sigemptyset:函数sigemptyset用来初始化set所指向的信号集,使其中所有信号的对应比特位清零,表示该信号集不包含任何有效信号。
  • sigfillset:函数sigfillset用来初始化set所指向的信号集,使其中所有信号的对应比特位全部置1,表示该信号集包含所有信号。
  • 注意:在使用sigset_t变量之前,一定要调用sigemptyset或者sigfillset初始化,使信号集处于确定状态。初始化sigset_t后,就可以调用sigaddsetsigdelset在信号集中添加或者删除某种有效信号了。
3.4.1 sigprocmask

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

哪一个进程调用这个函数就是在设置或者更新自己的block

在这里插入图片描述
在这里插入图片描述
  • 参数
    • 第二个参数是未来我们用户自己传入的sigset_t类型(输入型)
    • 第三个参数是一个输出型参数,在我们改block之前把旧的block带出去,这样如果我们就可以恢复到之前的block
  • 返回值
    设置成功0被返回,否则-1被返回

how:表示你想做什么操作

在这里插入图片描述


sig_block:表示把传进来的设置为1的信号新增到block表中,如果已经屏蔽了就继续屏蔽,没有屏蔽的信号设置成屏蔽
sig_unblock:表示把已经屏蔽的信号设置为未屏蔽的
sig_setmask:表示未来自己定义的信号集,把整个block表全部更新一遍

3.4.2 sigpending
在这里插入图片描述
#include<signal.h>intsigpending(sigset_t *set);

这个函数是用来获取当前进程的pending信号集的,这个参数是一个输出型参数

demo代码: 先将2号信号屏蔽,然后不断获取pending表,不断打印,再向目标进程发送2号信号,因为2号信号不会被立即抵达(执行),只是pending表由0变1,不执行对应的操作,所以我们才能看到信号由0变为1的过程。

#include<iostream>#include<cstdio>#include<vector>#include<functional>#include<unistd.h>#include<signal.h>#include<sys/types.h>voidPrintPending(sigset_t &pending){printf("我是一个进程(%d),pending:",getpid());// 遍历所有信号for(int signo =31; signo >=1; signo--)// 先打印出来的第一个比特位是最高位{// 判断指定信号是否在当前集合里if(sigismember(&pending, signo)){ std::cout <<"1";}else{ std::cout <<"0";}} std::cout << std::endl;}voidhandler(int sig){ std::cout <<"抵达"<< sig <<"信号!"<< std::endl;}intmain(){signal(SIGINT, handler);// 1.屏蔽2号信号 sigset_t block, oblock;sigemptyset(&block);// 将信号集清空sigemptyset(&oblock);// 将2号信号添加到信号集中sigaddset(&block, SIGINT);// 将32个信号全部添加到信号集当中// for(int i = 1; i < 32; i++)// sigaddset(&block,i);int n =sigprocmask(SIG_SETMASK,&block,&oblock);(void)n;// 防止出现n只有定义但并没有被使用// 4. 重复获取打印过程int cnt =10;while(true){// 2.获取pending信号集 sigset_t pending;int m =sigpending(&pending);// 3.打印PrintPending(pending);if(cnt ==0){// 5.恢复对2号信号的block情况 std::cout <<"解除对2号信号的屏蔽"<< std::endl;sigprocmask(SIG_SETMASK,&oblock,nullptr);}sleep(1); cnt--;}return0;}

在这个过程中我们会发现一个现象,我们要是把所有信号都屏蔽的话,那么这个进程就有了“金刚不坏之身了”,这个进程要是病毒的话那岂不是完蛋了!但是由实验现象可以知道,9号信号是不能被屏蔽的。所以9号信号不可被捕捉,不可被阻塞!!!

pending表中的由1变0,是在handler执行之前变的还是handler执行之后变的。
结论是:当我们准备抵达的时候,首先要清空pending信号集中的对应的位图 1 -> 0。(在抵达之前由1变0)

🌵补充细节问题: 我们保存信号用的是pending位图,pending位图用一个比特位来表示是否收到,那如果将来有一种信号产生很多次该怎么办呢?
比如说今天向目标进程发了一百个2号信号,因为进程不一定是对信号做立即处理的,此时的普通信号该怎么处理呢?Linux中是这样设计的,常规信号在抵达之前产生多次只记录一次,而实时信号在抵达之前产生多次,可以一次放在一个队列里。

🍒信号终止进程的方式有两种,core,term
core 🆚 term
core是核心的意思,如果一个进程以core退出,往往会在当前路径下形成一个文件,进程异常退出的时候,进程在内存中的核心数据会从内存拷贝在磁盘,形成一个文件,称为核心转储,只要是为了支持debug调试的。
term是进程因为异常而退出,但是term不做任何转储功能,直接退出

注意:在我们的云服务器上 core dump功能是被禁止掉的。
为什么要禁止掉?
未来如果我们的程序发生错误,core dump是要在当前路径下形成一个文件的,如果我们的程序挂一次,它形成一个文件,挂一次,形成一个文件就很容易把我们的磁盘打满。
如何查看?
ulimit -a ,其中有一个core file size为0表示关闭
如何打开?
ulimit -c (临时方案)

为什么要进行核心转储?
答案是:为了支持debug

如果我们未来在找bug找不到,我们可以开启core dump,直接让程序运行崩溃,gdb,core-file core,直接帮助我们来定位到出错行。(事后调试)

在这里插入图片描述


我们之前的进程等待,次低8位代表的是进程退出时的退出码,低7位表示的是该进程退出时的退出信号,一个进程在退出的时候如果信号为0表示正常退出的(因为信号是从1-31的,没有0号信号)。除了次低8位和低7位,还有一个标志位就是core dump标志位,表示是否core dump

在这里插入图片描述
在这里插入图片描述

👍 如果对你有帮助,欢迎:

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔

Read more

企微“虚拟同事“智能机器人实践:基于 Java+Dify AI构建可视化工作流接入方案

企微“虚拟同事“智能机器人实践:基于 Java+Dify AI构建可视化工作流接入方案

最开始 今天,分享在企业微信“智能机器人”新功能上的实践案例,侧重流程,省略更多的接入和调试细节,实现通过 API 模式接入自己的AI应用。 企业微信在 2025 新品发布会上推出的“智能机器人”,相比传统 Webhook 机器人,强势融入了 AI 还能用上 RAG(Retrieval-Augmented Generation)能力,支持联系人搜索、群聊@问答、私聊交互,支持流式返回内容,并支持markdown格式内容的渲染 相比 Webhook 机器人主动的推送消息,智能机器人更像是AI员工。 本文将基于企业微信配置 + Java Spring Boot 中后台服务 + Dify AI 应用,通过 API 模式接入企业自定义 AI 服务,实现用户通过智能机器人到 Dify 可视化

By Ne0inhk
大模型开发 - AgentScope Java v1.0 深度解读

大模型开发 - AgentScope Java v1.0 深度解读

文章目录 * 概述 * 一、 范式重构:从“硬编码工作流”到“ReAct 概率计算” * 1.1 三层职责划分:寻找自治与控制的平衡点 * 二、 工具生态:如何管理成百上千个“AI 接口”? * 2.1 结构化工具管理:Group 与 Meta-Tool * 2.2 异步与并行:解决“等风来”的性能瓶颈 * 三、 企业级基建:安全、记忆与协议化集成 * 3.1 安全沙箱:给“破坏王”穿上拘束衣 * 3.2 上下文工程:RAG + Memory 即基础设施 * 3.3 A2A 与

By Ne0inhk
【Java】反射详解

【Java】反射详解

Java 反射详解 运行时动态获取类信息、创建对象、调用方法完整教程 目录 * 一、反射概述 * 二、获取Class对象 * 三、构造方法反射 * 四、字段反射 * 五、方法反射 * 六、实战案例 一、反射概述 1.1 什么是反射 反射(Reflection)是Java提供的一种机制,允许程序在运行时检查和操作类的结构(类、方法、字段等)。 反射的核心功能: * 运行时获取类的信息 * 动态创建对象 * 动态调用方法 * 动态访问和修改字段 1.2 反射的应用场景 * 框架开发:Spring、Hibernate等框架大量使用反射 * 动态代理:AOP面向切面编程 * 注解处理:运行时处理注解 * 插件系统:动态加载类 * 序列化/反序列化:JSON、

By Ne0inhk

项目hbuilder运行报错加载报错Failed to load module script: Expected a JavaScript-or-Wasm module script but the

加载报错Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of “”. Strict MIME type checking is enforced for module scripts per HTML spec., 这个 MIME type 错误是 HBuilder 真机运行的典型问题。 vite.config.js 将base设置为./ base: "./", // HBuilder 真机必须用相对路径 builder:{ assetsInlineLimit: 4096, // 小于

By Ne0inhk