【Linux系统】万字解析,进程间的信号

前言:

        上文我们讲到了,进程间通信的命名管道与共享内存:【Linux系统】命名管道与共享内存-CSDN博客​​​​​​

        本文我们来讲一讲,进程的信号问题

        点个关注!


信号概念

        信号是OS发送给进程的异步机制!所谓异步指的是,发送信息这个动作,并不会干扰进程本身!

对于信号的基本认识:

        1.什么样的信号应该如何处理,是在信号产生之前早就得知了的
        2.信号的处理并不是立即处理,而是等待合适的时间去处理
        3.对于进程来说,其内部是以及内置了对于信号的识别以及处理方式
        4.产生信号的方式很多,也就是说信号源非常多

信号的产生

信号的产生有很多方式

1.键盘产生信号

        之前我们常见的:Ctrl + c就是信号,用于终止进程!

        信号都有那些:

        其中,我们只需要关注信号1~31(普通信号),信号的名字本身是,其真正的值就是前面的编号。

处理信号

        进程收到信号之后,进程会在合适的时候,进程处理!其中处理的方式有三种

1.执行默认的处理动作!(相当一部分的信号默认动作都是终止进程)
2.执行自定义动作!
3.忽略信号,继续做自己的事!

自定义处理
 #include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
#include <signal.h>
#include <iostream>
using namespace std;void sighandler(int i)
{cout << "收到一个信号:" << i << endl;
}int main()
{// 将1~31的信号全部自定义for (int i = 0; i < 32; i++){signal(i, sighandler);}while (1){}
}
hyc@hyc-alicloud:~/linux/进程信号$ ./test
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2
^C收到一个信号:2

        可以看到,我们发送了多次的Ctrl + c信号,可见Ctrl + c信号发送的就是2号信号:SIGINT

        当然并不是说有的信号都可以被自定义,那不然进程就无法停止了!

前后台

        当我们运行可执行程序时,我们发现Linux指令不起作用了?!这就是前后台的问题了

在OS中,进程分为:前台进程、后台进程
前台进程:有且仅有一个!并且只有前台进程才能接收输入的数据!
后台进程:可以有多个!
虽然输入的数据只有前台进程可以接收,但是输出的数据可以由前后台共同进行的!

        所以,当我们运行我们的程序时,当前这个程序就处于前台了!那么负责接收解析指令的shell程序就会退出前台!而后台程序是不能接收输入进来的数据的,所以这才导致我们输入的指令没有反应!

        

发送信号的本质

        信号发送给进程后,进程需要在合适的时间再进行处理!那么这就意味着进程需要先将信号保存下来!后续再读取执行。

        那么保存在哪里呢?答案是保存在task_struct的sigs变量中!其中sigs采用的是位图结构比特位的位置表示信号的编号比特位的内容(1表示收到、0表示没有收到)表示是否收到

        所以,发送信号的本质就是,向目标进程写信号 -> 修改位图!

        但是task_struct中的数据属于OS内核数据!所以想要修改其数据,就只能让OS自己来修改!所以信号只能让OS来发送!

2.系统调用产生信号

kill接口
#include <signal.h>
int kill(pid_t pid, int sig);作用:向指定的进程发送信号!pid 参数:
pid > 0:向指定进程 ID 的进程发送信号
pid = 0:向与调用进程同进程组的所有进程发送信号sig 参数:
代表信号编号

看看效果:

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;void sighandler(int i)
{cout << "收到一个信号:" << i << endl;
}int main()
{// 将1~31的信号全部自定义for (int i = 0; i < 32; i++){signal(i, sighandler);}kill(getpid(), 2);
}

也可以通过kill来验证一下,上面说的“并不是所有信号都可以被自定义!”

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
using namespace std;void sighandler(int i)
{cout << "收到一个信号:" << i << endl;
}int main()
{// 将1~31的信号全部自定义for (int i = 0; i < 32; i++){signal(i, sighandler);}for (int i = 1; i < 32; i++){kill(getpid(), i);}
}

        可见,信号9并不能被“自定义”!当然不仅仅编号9,还有其他信号也不能被自定义。

abort接口
#include <stdlib.h>
void abort(void);作用:强制终止当前的进程!

看看效果:

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;void sighandler(int i)
{cout << "收到一个信号:" << i << endl;
}int main()
{// 将1~31的信号全部自定义for (int i = 0; i < 32; i++){signal(i, sighandler);}abort();for (int i = 1; i < 32; i++){kill(getpid(), i);}
}

        显然,进程并没有执行for循环!这说明:即使自定义了处理函数,abort 最终仍会强制终止进程!

alarm接口
#include <unistd.h>
unsigned int alarm(unsigned int seconds);作用:向当前进程发送SIGLRM信号参数 seconds:指定定时器的超时时间(单位:秒)
若 seconds > 0:内核会在 seconds 秒后向当前进程发送 SIGALRM 信号
若 seconds = 0:取消当前进程中已设置的所有 alarm 定时器(如果存在)返回值:
若之前已设置过 alarm 定时器且未超时:返回剩余的秒数(即距离上次设置的超时时间还剩多久)
若之前未设置过 alarm 或已超时:返回 0

看看效果:

void sighandler(int i)
{cout << "收到一个信号:" << i << endl;
}int main()
{// 将1~31的信号全部自定义for (int i = 0; i < 32; i++){signal(i, sighandler);}alarm(5);sleep(5);
}

        可见,确实发送了信号!

3.命令产生信号

killall -9 chrome  # 发送SIGKILL(9),强制终止所有chrome进程

        很简单就不过多说明了

4.异常产生信号

程序异常:出现“除0错误”“野指针” 等错误!

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <iostream>
using namespace std;void sighandler(int i)
{cout << "收到一个信号:" << i << endl;
}int main()
{// 将1~31的信号全部自定义for (int i = 0; i < 32; i++){signal(i, sighandler);}int a = 1;a /= 0;
}

        除零错误,发送信号8!

void sighandler(int i)
{cout << "收到一个信号:" << i << endl;
}int main()
{// 将1~31的信号全部自定义for (int i = 0; i < 32; i++){signal(i, sighandler);}int *p = nullptr;*p = 10;
}

        同样的,野指针也发送了信号!

        上面我们说到所有的信号都是由OS来进行发送的!那OS是如何得知程序出现错误的呢?

        因为,OS是所有软硬资源的管理者!通过硬件协作自身监控机制,实时捕获程序运行中的异常状态

信号的保存

信号的产生我们知道了,下面我们来看信号是如何保存的

核心概念

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

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

        被阻塞的信号会处于未决状态,直到进程解除对该信号的阻塞才会执行递达动作

        信号递达后分别有3个动作:默认动作、自定义动作、忽略!

        注:忽略是递达后的动作!而阻塞是未递达的动作!

保存

        task_sturct中存在三张表,信号由三张表负责保存。

        handler表,保存信号的处理方法,其本质的函数指针数组。SIG_DFL表示默认方法,SIG_IGN表示忽略,使用接口sighandler(int sigon)表示自定义方法!(SIG_DFL本质是宏,其内容是被强转的整数0:(_sighandler_t) 0。)

        pending表,保存信号是否被接收,其本质是位图。0表示没有接收到,1表示接收到了。

        block表,保存信号是否被阻塞,其本质是位图。0表示没有被阻塞,1表示被阻塞了。

        一行信息才是一个信号的完整信息!从上往下,依次表示信号1~31!

sigset_t

        sigset_t是一个数据类型,表示信号集!用于记录每个信号的“有效”“无效”状态。

        从上图来看,我们发现每个信号的block、pending都只使用一个bit位来表示!而并不记录这个信号产生了多少次!所以这两个信号都用sigset_t来存储,分别叫做未决信号集阻塞信号集(屏蔽信号集)

信号集操作函数

sigprocmask

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

参数:

      sigprocmask的行为由how决定,set和oldset分别用于指定新的信号集和保存旧的信号集

how控制信号屏蔽字的修改方式,仅支持 3 个预定义值(核心参数)。
set(输入型参数)指向新的信号集:
  若 how 非 0,此参数指定要操作的信号集;
  若为 NULL,表示不修改屏蔽字(仅用于获取旧屏蔽字)。
oldset(输出型参数)用于保存修改前的旧信号屏蔽字:
  若为 NULL,表示不保存旧屏蔽字。

how的取值:

sigpending

#include <signal.h>
int sigpending(sigset_t *set);作用:获取当前进程未决信号集。参数:set是指向sigset_t类型的指针,用于存储未决信号集合。
返回值:成功时返回0;失败时返回-1,并设置errno。可能的错误包括EFAULT(set指向非法地址)。

演示:

#include <signal.h>
#include <iostream>
using namespace std;void Print(sigset_t pending)
{cout << "当前进程:" << getpid() << "pending:" << endl;for (int i = 31; i >= 1; i--){// 检查信号编号i是否在pending中if (sigismember(&pending, i)){cout << 1;}else{cout << 0;}}cout << endl;
}void handler(int signo)
{cout << "信号递达!" << endl;sigset_t pending;sigpending(&pending);Print(pending);
}int main()
{// 捕捉2号信号signal(2, handler);// 屏蔽2号信号sigset_t set, oldset;sigemptyset(&set);sigemptyset(&oldset);    // 将信号集初始化为空!sigaddset(&set, SIGINT); // 向指定信号集,添加信号// 向进程阻塞信号集添加信息,让SIGINT信号被阻塞sigprocmask(SIG_BLOCK, &set, &oldset);int cnt = 10;while (true){// 获取当前进程的pending信号集sigset_t pending;sigpending(&pending);// 打印pending信号集Print(pending);cnt--;// 解除对2号信号的阻塞if (cnt == 0){cout << "解除对2号信号的阻塞\n";sigprocmask(SIG_SETMASK, &oldset, &set);}sleep(1);}
}

        我们可以看到:在还没有解除2号信号阻塞时,信号确实接收到了!但没有递达。当信号阻塞解除时,信号立马递达了

注意:

        当信号准备抵达时,会先将pending表中信号对应的1修改为0!避免同一个信号被反复递达。

        

补充:

      在Linux中信号中止的方式有两种:CoreTerm

      其唯一区别就在于是否会“核心转储”!  Core:在进程异常退出时,会在当前路径下形成一个文件,将进程的核心数据拷贝至文件中,然后将进程退出!

        而Term则会直接进行进程退出!核心转储的目的是为了实现debug!开启core dump,程序运行崩溃时,gdb core-file core,可以直接帮我们定位到错误的地方!

        但在云服务器上 core dump功能是被禁用掉的,因为云服务器是部署端,不是生产端。

        当然也可以通过 ulimit -a 查看,ulimit -c打开core dump功能。

信号处理

信号的保存我们知道了是如何进行的,下面我们来讲一讲信号的如何处理的

        上面我们讲到了信号的处理,进程收到了信号不是立即处理!而是在合适的时间进行处理。

先直接给出结论:

        适合的时间:进程从内核态,返回至用户态的时候。此时会进行信号检查(检查spending若发现接收到了信号,则再去检查block,若block没有显示阻塞,则去执行信号对应的方法!反之不满足任何一点)

        我们在执行自定义方法时,OS也必须进行用户身份的转化!:用户态与内核态的转化。(因为用户身份是无法访问操作系统的内核数据的)。当然仅执行默认动作(完全由内核态完成,这是系统预定义好的)或忽略动作是不需要的用户身份转化的!

举例说明理解上图:

        用户创建信号SIGQUIT的自定义执行函数sighandler。

        当前正在执行main函数,发生了中断、异常或系统调用,切换至内核态处理中断、异常或系统调用。

        当在内核态处理完中断、异常或系统调用后,返回main函数之前。要对信号进行检查!当前检查到信号SIGQUIT需要递达

        然后内核决定返回用户态去执行sighandler函数,而不是返回到用户态的main函数。sighandler函数与main函数使用不同的堆栈空间,它们之间互不影响!

        sighandler函数返回后,自动执行特殊的系统调用:sigreturn,再次进入内核态

        此时没有新的信号需要递达,那么会再次返回用户态,并且是返回的main函数。恢复main函数的上下文继续执行。

处理流程总结如下图:

        一次流程会进行4次身份切换。

值得一提:

        我们自己的程序会进入内核吗?当然会!因为自己写的程序执行后也是一个进程,而只要是进程就会被OS调度!所以自己写的程序会进行内核!

sigaction

补充一个接口:自定义信号处理方式

        上面我们讲到了接口:signal。sigaction的功能与signal类似,同样可以进行对信号进行自定义处理。不过signal的功能更多。

#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);signum:目标信号
act:结构体,自定义捕捉方法(输入型参数)
oldact:结构体,保存之前的方法(输出型参数)
返回值:成功返回 0,失败返回 -1 并设置 errno结构体:
struct sigaction {void     (*sa_handler)(int);                        //自定义捕捉方法void     (*sa_sigaction)(int, siginfo_t*, void*);   //暂不考虑sigset_t   sa_mask;                                 //信号集int        sa_flags;                                //暂不考虑void     (*sa_restorer)(void);                      //暂不考虑
};

具体功能介绍:

        1.当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。

        2.如果想不仅仅让当前的信号被屏蔽,可以在sa_mask中添加想要额外屏蔽的信号

演示:

#include <signal.h>
#include <iostream>
using namespace std;void Print()
{sigset_t pending;sigpending(&pending); // 获取当前信号的pending表for (int i = 31; i >= 1; i--){if (sigismember(&pending, i))cout << 1;elsecout << 0;}cout << endl;sleep(1);
}void handler(int sigon)
{cout << "获取到信号:2" << endl;while (true){Print();}
}int main()
{struct sigaction act, oldact;act.sa_handler = handler;sigaction(2, &act, &oldact);while (true){Print();}
}

        运行结果如上图。

        我们可以看到当我们第一次发送2号信号时,获取到信号立刻递达!pending表为空。但当我们第二次发送pending表时,可以看到没有显示获取到信号的信息,pending出现内容表示未决。

        验证了上面所说的特性:sigaction函数会自动的屏蔽当前获取到的信号,知道这个信号处理完,才会解除屏蔽。当有相同的信号发送时,会自动的阻塞。

也可以通过向sa_mask添加想要屏蔽的信号:

例如:捕捉2号信号、屏蔽3,4号信号

#include <signal.h>
#include <iostream>
using namespace std;void Print()
{sigset_t pending;sigpending(&pending); // 获取当前信号的pending表cout << getpid() << ":";for (int i = 31; i >= 1; i--){if (sigismember(&pending, i))cout << 1;elsecout << 0;}cout << endl;sleep(5);
}void handler(int sigon)
{cout << "获取到信号:" << sigon << endl;while (true){Print();}
}int main()
{struct sigaction act, oldact;act.sa_handler = handler;// 向sa_mask添加想要屏蔽的信号sigaddset(&act.sa_mask, 3);sigaddset(&act.sa_mask, 4);sigaction(2, &act, &oldact);while (true){Print();}
}

穿插了解-OS是如何运行的

硬件中断

理解操作系统必不可少一个概念

        由外设触发的,中断系统运行流程,叫做硬件中断。

        作用:当外设准备好的时候,会直接中断控制器发送中断,中断控制器会通知CPU:已经有外设准备好了!CPU得知后会向中断控制器获取准备好的外设的中断号,随后在中断向量表中,根据中断号,找到对应的中断服务,CPU执行对应服务。

        所以CPU并不关心外设是否准备好,而是外设会主动的告诉CPU!

        中断向量表:是操作系统的一部分,在操作系统启动时就加载到内存中了。其本质可以理解为:函数指针数组,中断号就是其下标!

        补充:对于寄存器的概念,其实不仅仅CPU有,外设也有寄存器!

了解完中断,不知你有莫名的熟悉感吗?

        发送中断-----发送信号?

        获取中断号------保存信号?

        中断号------信号编号

        处理中断-----处理信号?自定义捕捉?

        是的!信号是纯软件,其本质是模拟硬件的中断!

时钟中断

现在我们知道了当有硬件中断时,会根据中断号,去执行对应的中断服务!

        而当没有没有中断时,OS此时在干什么呢?OS是暂停的!

        是的!你没有看错,没有中断的OS是暂停的!也就是说OS是依靠中断来驱动的!

        OS是不可能暂停不运行的!

        于是有了一个时钟源,时钟源会以一个固定的频率不断的向OS发送特殊中断!获取到这个中断号,并执行进程调度任务!

        于是OS就在时钟的驱动下运行起来了!

        时钟频率:既是我们常说的主频!根据时钟的频率+时间戳,就可以让我们的计算机在离线状态下也可以知晓正确的时间!

        同样的,一个进程的时间片是否耗尽也是通过时钟频率判断的!每进行一次进程调度,就会让目前正在被运行的进程的时间片 --,减去后再次判断时间片是否耗尽,若耗尽OS将会剥离进程,没有耗尽则进行执行!

死循环

        OS的本质是一个死循环!

        OS本身是躺平的!OS本身并不干什么事情。需要做什么事情,就向中断向量表钟添加对应的方法即可!

软中断

软件也能触发中断吗?当然可以!

通过软件的错误逻辑让硬件发生异常进行中断:

        如:除0错误、野指针、等等。

        例如除0错误,引发EFLAGS寄存器硬件,发生CPU内部的中断!

不通过硬件,仅仅通过软件进行中断:

        隆重介绍两个系统调用:

        int 0x80

        syscall

     (这是通过指令实现的系统调用,其本质是指令集!其实C/C++代码编译后:就是指令集+数据,考虑汇编就很好理解)

        调用这两个系统调用,即可让CPU自动进行软中断。

        中断后进行中断服务。而软中断的中断服务是去系统调用函数指针表中查询并执行对应的系统调用方法

        其中我们查询调用方法是依靠数组下标去查询的,而这个数组下标我们叫做:系统调用号

        其中系统调用号由OS提供!

        请注意!之前我们讲到过系统调用是由OS提供的。其实OS只提供系统调用号,不提供任何接口!!!我们所使用的系统调用接口其实是由glibc封装系统调用号实现的。系统调用号存放在头文件<unistd_64.h>中,通过寄存器exa传递给OS

        所以系统调用的简单流程就是:先通过int 0X80或syscall触发软中断,陷入内核、然后执行中断服务、进入系统调用函数指针数组、根据系统调用号查找并执行对应的系统调用方法。

用户态与内核态

先给出结论:

        用户态:就是在虚拟内存中,执行用户区[0-3]GB时的状态。        

        内核态:就是在虚拟内存中,执行内核区[3-4]GB时的状态。        

关于页表:

        对于用户页表,每一个进程都有一个独立的用户页表!因为进程具有独立性,每一个进程都有其独有的数据与代码。

        对于内核页表,仅有一个内核页表!因为OS的固定的。

        于是,不论是那一个进程执行系统调用,都可以陷入内核区,访问内核的方法与数据!

CPL标志位:

       我们知道用户区与内核区都在虚拟地址空间中,访问用户区还是内核区都是通过虚拟地址来访问的!

        那如果在用户区中,通过内核区的代码直接访问内核岂不是出错了??因为我们说过OS是不能被任何人直接访问的!只能通过系统调用间接访问!

        于是便有了CPL(Current Privilege Level:当前权限级别CPL在CPU中Cs寄存器的低两位

        CPL:3表示当前处于用户态,0表示当前处于内核态。系统会自动维护CPL。

        所以OS会根据CPL标志位,来判断当前操作是否合法,以此来避免不必要的错误。

可重入函数

简单理解:

        重入:就是同时被多个执行流使用、调用。

        可重入函数:可以同时被多个执行流调用,且不会影响函数执行结果的函数

        反之,会影响的就叫做不可重入函数。

        符合一下条件之一,便是不可重入函数:

1.调用了mallco或free,因为mallco是用全局链表管管理的。
2.调用了标准IO库,因为大部分的标准IO库都是不可重入的,其使用了全局变量。

        

volatile

这是一个修饰符

先直接来看一段代码:

#include <signal.h>
#include <stdio.h>
#include <iostream>
using namespace std;int n = 0;
void handler(int sigon)
{cout << "make 0 -> 1" << endl;n = 1;
}int main()
{signal(2, handler);while (n == 0){cout << "循环中" << endl;sleep(2);}cout << "解除循环!" << endl;
}hyc@hyc-alicloud:~/linux/volatile$ ./test
循环中
循环中
循环中
循环中
循环中
^Cmake 0 -> 1
循环中
循环中
循环中
循环中

        我们会发现很奇怪的一点:明明n由0变成1了,为什么循环还不停止呢??

        这与编译器的优化有关!在main函数中n这个变量不进行任何修改,仅仅作为判断条件。于是编译器就将n直接保存至寄存器中,没必要每判断一次就从内存中读取一次,效率太低了!

        于是我们信号自定义的执行方法中即使修改了n的值,但也只是内存数据的修改,CPU并不会区读取内存了,而是直接访问寄存器!

        寄存器覆盖进程变量的真实情况,我们叫做:内存不可见!

        于是我们将会用到:volatile,来保证此变量的内存可见性!

#include <signal.h>
#include <stdio.h>
#include <iostream>
using namespace std;//保证内存可见性!
volatile int n = 0;void handler(int sigon)
{cout << "make 0 -> 1" << endl;n = 1;
}int main()
{signal(2, handler);while (n == 0){cout << "循环中" << endl;sleep(2);}cout << "解除循环!" << endl;
}hyc@hyc-alicloud:~/linux/volatile$ ./test
循环中
循环中
循环中
循环中
^Cmake 0 -> 1
解除循环!

SIGCHLD信号

        SIGCHLD信号,其实是子进程在终止时向父进程发送的信号。不过这个给信号的默认执行动作一般都是忽略,起到一个通知的作用

        我们可以通过SIGCHLD信号,自定义处理动作来让父进程回收子进程

演示:

#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <iostream>
using namespace std;void handler(int sigon)
{cout << "收到信号:" << sigon << endl;int re = waitpid(0, NULL, WNOHANG); // 进行非阻塞等待cout << "完成等待" << endl;
}int main()
{pid_t pid = fork();if (pid == 0){// 子进程cout << "子进程退出!\n";exit(1);}// 父进程signal(SIGCHLD, handler);while (true){// 即使有多个子进程也可以全部等待(非阻塞等待)sleep(2);cout << "父进程running\n";}
}

补充:我们等待子进程往往是想得知子进程执行的结果怎么样,但如果我们并不关心,可以让SIGCHLD信号的默认动作设为忽略。这样子进程结束后就会自动清理资源,不会形成僵尸进程!

#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <iostream>
using namespace std;int main()
{pid_t pid = fork();if (pid == 0){// 子进程cout << "子进程退出!\n";exit(1);}// 父进程signal(SIGCHLD, SIG_IGN);while (true){// 即使有多个子进程也可以全部等待(非阻塞等待)sleep(2);cout << "父进程running\n";}
}

注意区别:

        SIGCHLD信号的默认动作是忽略,用于传达消息通知父进程子进程已经终止,会形成僵尸进程,需要父进程进行等待操作,回收子进程!(默认忽略,被动行为)

        将SIGCHLD信号的处理动作自定义为忽略,表示我们不关心子进程的执行情况,子进程终止时就会自动的清理其空间,不会形成僵尸进程!(显示忽略,主动告知)

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

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

相关文章

AI时代SEO关键词实战解析

内容概要 随着人工智能技术深度融入搜索引擎的运行机制&#xff0c;传统的SEO关键词研究方法正经历着根本性的变革。本文聚焦于AI时代背景下&#xff0c;如何利用智能化的策略精准定位目标用户&#xff0c;实现搜索可见度的实质性跃升。我们将深入探讨AI技术如何革新关键词研究…

Spring Boot + Spring MVC 项目结构

下面一个既能返回 JSP 页面&#xff0c;又能提供 JSON API 的 Spring Boot Spring MVC 项目结构&#xff0c;这样你就能同时用到 Controller 和 RestController 的优势。 &#x1f3d7; 项目结构 springboot-mvc-mixed/ ├── src/main/java/com/example/demo/ │ ├── …

通俗易懂的讲解下Ceph的存储原理

Ceph存储原理解析 要理解 Ceph 的存储原理&#xff0c;我们可以用一个 “分布式仓库” 的比喻来拆解 —— 把 Ceph 想象成一个由多个 “仓库管理员”&#xff08;硬件节点&#xff09;共同打理的大型仓库&#xff0c;能高效存储、管理海量货物&#xff08;数据&#xff09;&…

软件测试小结(1)

一、什么是测试&#xff1f;1.1 生活中常见的测试例如去商场买衣服&#xff1a;①、选择一件符合审美的衣服 -> 外观测试&#xff1b;②、穿上身上试试是否合身 -> 试穿测试&#xff1b;③、 看看衣服的材料是否纯棉 -> 材料测试&#xff1b;④、 询问衣服的价格 ->…

Python未来3-5年技术发展趋势分析:从AI到Web的全方位演进

Python作为全球最流行的编程语言之一&#xff0c;在开发者社区中占据核心地位。其简洁语法、丰富库生态和跨领域适用性&#xff0c;使其在AI、Web开发、数据科学等领域持续领先。本文基于当前技术演进趋势&#xff08;如2023-2024年的开源项目、社区讨论和行业报告&#xff09;…

【ComfyUI】SDXL Turbo一步完成高速高效的图像生成

今天演示的案例是一个基于 ComfyUI 与 Stable Diffusion XL Turbo 的图生图工作流。整体流程通过加载轻量化的 Turbo 版本模型&#xff0c;在文本编码与调度器的配合下&#xff0c;以极快的推理速度完成从提示词到高质量图像的生成。 配合演示图可以直观感受到&#xff0c;简洁…

基于 GPT-OSS 的在线编程课 AI 助教追问式对话 API 开发全记录

本文记录了如何在 3 天内使用 GPT-OSS 开源权重搭建一个 在线编程课 AI 助教追问式对话 API&#xff0c;从需求分析、数据准备到微调与部署全流程实战。 1️⃣ 需求与指标 回答准确率 ≥ 95%响应延迟 < 1 秒支持多学生并发提问 2️⃣ 数据准备 收集课程问答对清理无效数据…

YOLO v11 目标检测+关键点检测 实战记录

流水账记录一下yolo目标检测 1.搭建pytorch 不做解释 看以往博客或网上搜都行 2.下载yolo源码 &#xff1a; https://github.com/ultralytics/ultralytics 3.样本标注工具&#xff1a;labelme 自己下载 4.准备数据集 4.1 新建一个放置数据集的路径4.2 构建训练集和测试集 运行以…

uniApp 混合开发全指南:原生与跨端的协同方案

uniApp 作为跨端框架&#xff0c;虽能覆盖多数场景&#xff0c;但在需要调用原生能力&#xff08;如蓝牙、传感器&#xff09;、集成第三方原生 SDK&#xff08;如支付、地图&#xff09; 或在现有原生 App 中嵌入 uniApp 页面时&#xff0c;需采用「混合开发」模式。本文将系统…

【大模型】使用MLC-LLM转换和部署Qwen2.5 0.5B模型

目录 ■准备工作 下载模型 安装依赖 安装基础依赖 安装mlc-llm ■权重转换 ■生成配置文件 ■模型编译 GPU版本编译 CPU版本编译 ■启动服务 启动GPU服务 启动CPU服务 ■服务测试 ■扩展 优化量化版本(可选,节省内存) INT4量化版本 调整窗口大小以节省内存…

云计算学习100天-第43天-cobbler

目录 Cobbler 基本概念 命令 搭建cobbler 网络架构 Cobbler 基本概念 Cobbler是一款快速的网络系统部署工具&#xff0c;比PXE配置简单 集中管理所需服务&#xff08;DHCP、DNS、TFTP、WEB&#xff09; 内部集成了一个镜像版本仓库 内部集成了一个ks应答文件仓库 提供…

接口测试:如何定位BUG的产生原因

1小时postman接口测试从入门到精通教程我们从在日常功能测试过程中对UI的每一次操作说白了就是对一个或者多个接口的一次调用&#xff0c;接口的返回的内容(移动端一般为json)经过前端代码的处理最终展示在页面上。http接口是离我们最近的一层接口&#xff0c;web端和移动端所展…

GPIO的8种工作方式

GPIO的8种工作方式&#xff1a;一、4 种输入模式1.1 Floating Input 浮空输入1.2 Pull-up Input 上拉输入1.3 Pull-down Input 下拉输入1.4 Analog Input 模拟输入二、4种输出模式2.1 General Push-Pull Output 推挽输出2.2 General Open-Drain Output 开漏输出2.3…

LeetCode算法日记 - Day 29: 重排链表、合并 K 个升序链表

目录 1. 重排链表 1.1 题目解析 1.2 解法 1.3 代码实现 2. 合并 K 个升序链表 2.1 题目解析 2.2 解法 2.3 代码实现 1. 重排链表 143. 重排链表 - 力扣&#xff08;LeetCode&#xff09; 给定一个单链表 L 的头节点 head &#xff0c;单链表 L 表示为&#xff1a; L…

算法模板(Java版)_前缀和与差分

ZZHow(ZZHow1024) &#x1f4a1; 差分是前缀和的逆运算。 前缀和 &#x1f4a1; 前缀和作用&#xff1a;快速求出 [l, r] 区间的和。 一维前缀和 例题&#xff1a;AcWing 795. 前缀和 import java.util.Scanner;public class Main {public static void main(String[] args)…

openssl使用SM2进行数据加密和数据解密

一、准备工作 1. 安装依赖 sudo apt-get update sudo apt-get install libssl-dev2. 确认 OpenSSL 版本 openssl version如果是 1.1.1 或 3.0&#xff0c;就支持 SM2/SM3/SM4。二、C 语言示例代码 这个程序会&#xff1a; 生成 SM2 密钥对使用公钥加密一段明文使用私钥解密恢复…

用滑动窗口与线性回归将音频信号转换为“Token”序列:一种简单的音频特征编码方法

在深度学习和语音处理领域&#xff0c;如何将原始音频信号有效地表示为离散的“Token”序列&#xff0c;是语音识别、音频生成等任务中的关键问题。常见的方法如Mel频谱图向量量化&#xff08;VQ&#xff09;、wav2vec等已经非常成熟&#xff0c;但这些模型通常依赖复杂的神经网…

Vue开发准备

vs code VSCode的下载地址https://code.visualstudio.com/Download Node.js node.js的下载地址 https://nodejs.org/zh-cn/download 注意&#xff1a;nodejs安装路径不要和vscode安装到同一个文件夹&#xff0c;两个应用分别装到两个不同的文件夹 npm config set cache &q…

QT6(QFileSystemModel和QTreeView)

QT6QFileSystemModel和QTreeView QFileSystemModel为本机的文件系统提供一个模型&#xff0c;QFileSystemModelt和QTreeView结合使用&#xff0c;可以用目录树的形式显示本机的文件系统&#xff0c;如同Windows的资源管理器一样使用QFileSystemModel提供的接口函数&#xff0c;…

【开题答辩全过程】以 基于Spring Boot的房屋租赁系统的设计与实现为例,包含答辩的问题和答案

个人简介一名14年经验的资深毕设内行人&#xff0c;语言擅长Java、php、微信小程序、Python、Golang、安卓Android等开发项目包括大数据、深度学习、网站、小程序、安卓、算法。平常会做一些项目定制化开发、代码讲解、答辩教学、文档编写、也懂一些降重方面的技巧。感谢大家的…